Permalink
Cannot retrieve contributors at this time
| /** | |
| * @fileoverview Rule to flag use of constructors without capital letters | |
| * @author Nicholas C. Zakas | |
| */ | |
| "use strict"; | |
| //------------------------------------------------------------------------------ | |
| // Requirements | |
| //------------------------------------------------------------------------------ | |
| //------------------------------------------------------------------------------ | |
| // Helpers | |
| //------------------------------------------------------------------------------ | |
| const CAPS_ALLOWED = [ | |
| "Array", | |
| "Boolean", | |
| "Date", | |
| "Error", | |
| "Function", | |
| "Number", | |
| "Object", | |
| "RegExp", | |
| "String", | |
| "Symbol", | |
| "BigInt" | |
| ]; | |
| /** | |
| * Ensure that if the key is provided, it must be an array. | |
| * @param {Object} obj Object to check with `key`. | |
| * @param {string} key Object key to check on `obj`. | |
| * @param {*} fallback If obj[key] is not present, this will be returned. | |
| * @returns {string[]} Returns obj[key] if it's an Array, otherwise `fallback` | |
| */ | |
| function checkArray(obj, key, fallback) { | |
| /* istanbul ignore if */ | |
| if (Object.prototype.hasOwnProperty.call(obj, key) && !Array.isArray(obj[key])) { | |
| throw new TypeError(`${key}, if provided, must be an Array`); | |
| } | |
| return obj[key] || fallback; | |
| } | |
| /** | |
| * A reducer function to invert an array to an Object mapping the string form of the key, to `true`. | |
| * @param {Object} map Accumulator object for the reduce. | |
| * @param {string} key Object key to set to `true`. | |
| * @returns {Object} Returns the updated Object for further reduction. | |
| */ | |
| function invert(map, key) { | |
| map[key] = true; | |
| return map; | |
| } | |
| /** | |
| * Creates an object with the cap is new exceptions as its keys and true as their values. | |
| * @param {Object} config Rule configuration | |
| * @returns {Object} Object with cap is new exceptions. | |
| */ | |
| function calculateCapIsNewExceptions(config) { | |
| let capIsNewExceptions = checkArray(config, "capIsNewExceptions", CAPS_ALLOWED); | |
| if (capIsNewExceptions !== CAPS_ALLOWED) { | |
| capIsNewExceptions = capIsNewExceptions.concat(CAPS_ALLOWED); | |
| } | |
| return capIsNewExceptions.reduce(invert, {}); | |
| } | |
| //------------------------------------------------------------------------------ | |
| // Rule Definition | |
| //------------------------------------------------------------------------------ | |
| module.exports = { | |
| meta: { | |
| type: "suggestion", | |
| docs: { | |
| description: "require constructor names to begin with a capital letter", | |
| category: "Stylistic Issues", | |
| recommended: false, | |
| url: "https://eslint.org/docs/rules/new-cap" | |
| }, | |
| schema: [ | |
| { | |
| type: "object", | |
| properties: { | |
| newIsCap: { | |
| type: "boolean", | |
| default: true | |
| }, | |
| capIsNew: { | |
| type: "boolean", | |
| default: true | |
| }, | |
| newIsCapExceptions: { | |
| type: "array", | |
| items: { | |
| type: "string" | |
| } | |
| }, | |
| newIsCapExceptionPattern: { | |
| type: "string" | |
| }, | |
| capIsNewExceptions: { | |
| type: "array", | |
| items: { | |
| type: "string" | |
| } | |
| }, | |
| capIsNewExceptionPattern: { | |
| type: "string" | |
| }, | |
| properties: { | |
| type: "boolean", | |
| default: true | |
| } | |
| }, | |
| additionalProperties: false | |
| } | |
| ], | |
| messages: { | |
| upper: "A function with a name starting with an uppercase letter should only be used as a constructor.", | |
| lower: "A constructor name should not start with a lowercase letter." | |
| } | |
| }, | |
| create(context) { | |
| const config = Object.assign({}, context.options[0]); | |
| config.newIsCap = config.newIsCap !== false; | |
| config.capIsNew = config.capIsNew !== false; | |
| const skipProperties = config.properties === false; | |
| const newIsCapExceptions = checkArray(config, "newIsCapExceptions", []).reduce(invert, {}); | |
| const newIsCapExceptionPattern = config.newIsCapExceptionPattern ? new RegExp(config.newIsCapExceptionPattern, "u") : null; | |
| const capIsNewExceptions = calculateCapIsNewExceptions(config); | |
| const capIsNewExceptionPattern = config.capIsNewExceptionPattern ? new RegExp(config.capIsNewExceptionPattern, "u") : null; | |
| const listeners = {}; | |
| const sourceCode = context.getSourceCode(); | |
| //-------------------------------------------------------------------------- | |
| // Helpers | |
| //-------------------------------------------------------------------------- | |
| /** | |
| * Get exact callee name from expression | |
| * @param {ASTNode} node CallExpression or NewExpression node | |
| * @returns {string} name | |
| */ | |
| function extractNameFromExpression(node) { | |
| let name = ""; | |
| if (node.callee.type === "MemberExpression") { | |
| const property = node.callee.property; | |
| if (property.type === "Literal" && (typeof property.value === "string")) { | |
| name = property.value; | |
| } else if (property.type === "Identifier" && !node.callee.computed) { | |
| name = property.name; | |
| } | |
| } else { | |
| name = node.callee.name; | |
| } | |
| return name; | |
| } | |
| /** | |
| * Returns the capitalization state of the string - | |
| * Whether the first character is uppercase, lowercase, or non-alphabetic | |
| * @param {string} str String | |
| * @returns {string} capitalization state: "non-alpha", "lower", or "upper" | |
| */ | |
| function getCap(str) { | |
| const firstChar = str.charAt(0); | |
| const firstCharLower = firstChar.toLowerCase(); | |
| const firstCharUpper = firstChar.toUpperCase(); | |
| if (firstCharLower === firstCharUpper) { | |
| // char has no uppercase variant, so it's non-alphabetic | |
| return "non-alpha"; | |
| } | |
| if (firstChar === firstCharLower) { | |
| return "lower"; | |
| } | |
| return "upper"; | |
| } | |
| /** | |
| * Check if capitalization is allowed for a CallExpression | |
| * @param {Object} allowedMap Object mapping calleeName to a Boolean | |
| * @param {ASTNode} node CallExpression node | |
| * @param {string} calleeName Capitalized callee name from a CallExpression | |
| * @param {Object} pattern RegExp object from options pattern | |
| * @returns {boolean} Returns true if the callee may be capitalized | |
| */ | |
| function isCapAllowed(allowedMap, node, calleeName, pattern) { | |
| const sourceText = sourceCode.getText(node.callee); | |
| if (allowedMap[calleeName] || allowedMap[sourceText]) { | |
| return true; | |
| } | |
| if (pattern && pattern.test(sourceText)) { | |
| return true; | |
| } | |
| if (calleeName === "UTC" && node.callee.type === "MemberExpression") { | |
| // allow if callee is Date.UTC | |
| return node.callee.object.type === "Identifier" && | |
| node.callee.object.name === "Date"; | |
| } | |
| return skipProperties && node.callee.type === "MemberExpression"; | |
| } | |
| /** | |
| * Reports the given messageId for the given node. The location will be the start of the property or the callee. | |
| * @param {ASTNode} node CallExpression or NewExpression node. | |
| * @param {string} messageId The messageId to report. | |
| * @returns {void} | |
| */ | |
| function report(node, messageId) { | |
| let callee = node.callee; | |
| if (callee.type === "MemberExpression") { | |
| callee = callee.property; | |
| } | |
| context.report({ node, loc: callee.loc.start, messageId }); | |
| } | |
| //-------------------------------------------------------------------------- | |
| // Public | |
| //-------------------------------------------------------------------------- | |
| if (config.newIsCap) { | |
| listeners.NewExpression = function(node) { | |
| const constructorName = extractNameFromExpression(node); | |
| if (constructorName) { | |
| const capitalization = getCap(constructorName); | |
| const isAllowed = capitalization !== "lower" || isCapAllowed(newIsCapExceptions, node, constructorName, newIsCapExceptionPattern); | |
| if (!isAllowed) { | |
| report(node, "lower"); | |
| } | |
| } | |
| }; | |
| } | |
| if (config.capIsNew) { | |
| listeners.CallExpression = function(node) { | |
| const calleeName = extractNameFromExpression(node); | |
| if (calleeName) { | |
| const capitalization = getCap(calleeName); | |
| const isAllowed = capitalization !== "upper" || isCapAllowed(capIsNewExceptions, node, calleeName, capIsNewExceptionPattern); | |
| if (!isAllowed) { | |
| report(node, "upper"); | |
| } | |
| } | |
| }; | |
| } | |
| return listeners; | |
| } | |
| }; |