1/** 2 * @fileoverview Rule to flag use of constructors without capital letters 3 * @author Nicholas C. Zakas 4 */ 5 6"use strict"; 7 8//------------------------------------------------------------------------------ 9// Requirements 10//------------------------------------------------------------------------------ 11 12const astUtils = require("./utils/ast-utils"); 13 14//------------------------------------------------------------------------------ 15// Helpers 16//------------------------------------------------------------------------------ 17 18const CAPS_ALLOWED = [ 19 "Array", 20 "Boolean", 21 "Date", 22 "Error", 23 "Function", 24 "Number", 25 "Object", 26 "RegExp", 27 "String", 28 "Symbol", 29 "BigInt" 30]; 31 32/** 33 * Ensure that if the key is provided, it must be an array. 34 * @param {Object} obj Object to check with `key`. 35 * @param {string} key Object key to check on `obj`. 36 * @param {*} fallback If obj[key] is not present, this will be returned. 37 * @returns {string[]} Returns obj[key] if it's an Array, otherwise `fallback` 38 */ 39function checkArray(obj, key, fallback) { 40 41 /* istanbul ignore if */ 42 if (Object.prototype.hasOwnProperty.call(obj, key) && !Array.isArray(obj[key])) { 43 throw new TypeError(`${key}, if provided, must be an Array`); 44 } 45 return obj[key] || fallback; 46} 47 48/** 49 * A reducer function to invert an array to an Object mapping the string form of the key, to `true`. 50 * @param {Object} map Accumulator object for the reduce. 51 * @param {string} key Object key to set to `true`. 52 * @returns {Object} Returns the updated Object for further reduction. 53 */ 54function invert(map, key) { 55 map[key] = true; 56 return map; 57} 58 59/** 60 * Creates an object with the cap is new exceptions as its keys and true as their values. 61 * @param {Object} config Rule configuration 62 * @returns {Object} Object with cap is new exceptions. 63 */ 64function calculateCapIsNewExceptions(config) { 65 let capIsNewExceptions = checkArray(config, "capIsNewExceptions", CAPS_ALLOWED); 66 67 if (capIsNewExceptions !== CAPS_ALLOWED) { 68 capIsNewExceptions = capIsNewExceptions.concat(CAPS_ALLOWED); 69 } 70 71 return capIsNewExceptions.reduce(invert, {}); 72} 73 74//------------------------------------------------------------------------------ 75// Rule Definition 76//------------------------------------------------------------------------------ 77 78module.exports = { 79 meta: { 80 type: "suggestion", 81 82 docs: { 83 description: "require constructor names to begin with a capital letter", 84 category: "Stylistic Issues", 85 recommended: false, 86 url: "https://eslint.org/docs/rules/new-cap" 87 }, 88 89 schema: [ 90 { 91 type: "object", 92 properties: { 93 newIsCap: { 94 type: "boolean", 95 default: true 96 }, 97 capIsNew: { 98 type: "boolean", 99 default: true 100 }, 101 newIsCapExceptions: { 102 type: "array", 103 items: { 104 type: "string" 105 } 106 }, 107 newIsCapExceptionPattern: { 108 type: "string" 109 }, 110 capIsNewExceptions: { 111 type: "array", 112 items: { 113 type: "string" 114 } 115 }, 116 capIsNewExceptionPattern: { 117 type: "string" 118 }, 119 properties: { 120 type: "boolean", 121 default: true 122 } 123 }, 124 additionalProperties: false 125 } 126 ], 127 messages: { 128 upper: "A function with a name starting with an uppercase letter should only be used as a constructor.", 129 lower: "A constructor name should not start with a lowercase letter." 130 } 131 }, 132 133 create(context) { 134 135 const config = Object.assign({}, context.options[0]); 136 137 config.newIsCap = config.newIsCap !== false; 138 config.capIsNew = config.capIsNew !== false; 139 const skipProperties = config.properties === false; 140 141 const newIsCapExceptions = checkArray(config, "newIsCapExceptions", []).reduce(invert, {}); 142 const newIsCapExceptionPattern = config.newIsCapExceptionPattern ? new RegExp(config.newIsCapExceptionPattern, "u") : null; 143 144 const capIsNewExceptions = calculateCapIsNewExceptions(config); 145 const capIsNewExceptionPattern = config.capIsNewExceptionPattern ? new RegExp(config.capIsNewExceptionPattern, "u") : null; 146 147 const listeners = {}; 148 149 const sourceCode = context.getSourceCode(); 150 151 //-------------------------------------------------------------------------- 152 // Helpers 153 //-------------------------------------------------------------------------- 154 155 /** 156 * Get exact callee name from expression 157 * @param {ASTNode} node CallExpression or NewExpression node 158 * @returns {string} name 159 */ 160 function extractNameFromExpression(node) { 161 return node.callee.type === "Identifier" 162 ? node.callee.name 163 : astUtils.getStaticPropertyName(node.callee) || ""; 164 } 165 166 /** 167 * Returns the capitalization state of the string - 168 * Whether the first character is uppercase, lowercase, or non-alphabetic 169 * @param {string} str String 170 * @returns {string} capitalization state: "non-alpha", "lower", or "upper" 171 */ 172 function getCap(str) { 173 const firstChar = str.charAt(0); 174 175 const firstCharLower = firstChar.toLowerCase(); 176 const firstCharUpper = firstChar.toUpperCase(); 177 178 if (firstCharLower === firstCharUpper) { 179 180 // char has no uppercase variant, so it's non-alphabetic 181 return "non-alpha"; 182 } 183 if (firstChar === firstCharLower) { 184 return "lower"; 185 } 186 return "upper"; 187 188 } 189 190 /** 191 * Check if capitalization is allowed for a CallExpression 192 * @param {Object} allowedMap Object mapping calleeName to a Boolean 193 * @param {ASTNode} node CallExpression node 194 * @param {string} calleeName Capitalized callee name from a CallExpression 195 * @param {Object} pattern RegExp object from options pattern 196 * @returns {boolean} Returns true if the callee may be capitalized 197 */ 198 function isCapAllowed(allowedMap, node, calleeName, pattern) { 199 const sourceText = sourceCode.getText(node.callee); 200 201 if (allowedMap[calleeName] || allowedMap[sourceText]) { 202 return true; 203 } 204 205 if (pattern && pattern.test(sourceText)) { 206 return true; 207 } 208 209 const callee = astUtils.skipChainExpression(node.callee); 210 211 if (calleeName === "UTC" && callee.type === "MemberExpression") { 212 213 // allow if callee is Date.UTC 214 return callee.object.type === "Identifier" && 215 callee.object.name === "Date"; 216 } 217 218 return skipProperties && callee.type === "MemberExpression"; 219 } 220 221 /** 222 * Reports the given messageId for the given node. The location will be the start of the property or the callee. 223 * @param {ASTNode} node CallExpression or NewExpression node. 224 * @param {string} messageId The messageId to report. 225 * @returns {void} 226 */ 227 function report(node, messageId) { 228 let callee = astUtils.skipChainExpression(node.callee); 229 230 if (callee.type === "MemberExpression") { 231 callee = callee.property; 232 } 233 234 context.report({ node, loc: callee.loc, messageId }); 235 } 236 237 //-------------------------------------------------------------------------- 238 // Public 239 //-------------------------------------------------------------------------- 240 241 if (config.newIsCap) { 242 listeners.NewExpression = function(node) { 243 244 const constructorName = extractNameFromExpression(node); 245 246 if (constructorName) { 247 const capitalization = getCap(constructorName); 248 const isAllowed = capitalization !== "lower" || isCapAllowed(newIsCapExceptions, node, constructorName, newIsCapExceptionPattern); 249 250 if (!isAllowed) { 251 report(node, "lower"); 252 } 253 } 254 }; 255 } 256 257 if (config.capIsNew) { 258 listeners.CallExpression = function(node) { 259 260 const calleeName = extractNameFromExpression(node); 261 262 if (calleeName) { 263 const capitalization = getCap(calleeName); 264 const isAllowed = capitalization !== "upper" || isCapAllowed(capIsNewExceptions, node, calleeName, capIsNewExceptionPattern); 265 266 if (!isAllowed) { 267 report(node, "upper"); 268 } 269 } 270 }; 271 } 272 273 return listeners; 274 } 275}; 276