| /** |
| * @fileoverview Rule to flag use of constructors without capital letters |
| * @author Nicholas C. Zakas |
| */ |
| |
| "use strict"; |
| |
| //------------------------------------------------------------------------------ |
| // Requirements |
| //------------------------------------------------------------------------------ |
| |
| const astUtils = require("./utils/ast-utils"); |
| |
| //------------------------------------------------------------------------------ |
| // Helpers |
| //------------------------------------------------------------------------------ |
| |
| const CAPS_ALLOWED = [ |
| "Array", |
| "Boolean", |
| "Date", |
| "Error", |
| "Function", |
| "Number", |
| "Object", |
| "RegExp", |
| "String", |
| "Symbol", |
| "BigInt", |
| ]; |
| |
| /** |
| * 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) { |
| const capIsNewExceptions = Array.from( |
| new Set([...config.capIsNewExceptions, ...CAPS_ALLOWED]), |
| ); |
| |
| return capIsNewExceptions.reduce(invert, {}); |
| } |
| |
| //------------------------------------------------------------------------------ |
| // Rule Definition |
| //------------------------------------------------------------------------------ |
| |
| /** @type {import('../types').Rule.RuleModule} */ |
| module.exports = { |
| meta: { |
| type: "suggestion", |
| |
| docs: { |
| description: |
| "Require constructor names to begin with a capital letter", |
| recommended: false, |
| url: "https://eslint.org/docs/latest/rules/new-cap", |
| }, |
| |
| schema: [ |
| { |
| type: "object", |
| properties: { |
| newIsCap: { |
| type: "boolean", |
| }, |
| capIsNew: { |
| type: "boolean", |
| }, |
| newIsCapExceptions: { |
| type: "array", |
| items: { |
| type: "string", |
| }, |
| }, |
| newIsCapExceptionPattern: { |
| type: "string", |
| }, |
| capIsNewExceptions: { |
| type: "array", |
| items: { |
| type: "string", |
| }, |
| }, |
| capIsNewExceptionPattern: { |
| type: "string", |
| }, |
| properties: { |
| type: "boolean", |
| }, |
| }, |
| additionalProperties: false, |
| }, |
| ], |
| |
| defaultOptions: [ |
| { |
| capIsNew: true, |
| capIsNewExceptions: CAPS_ALLOWED, |
| newIsCap: true, |
| newIsCapExceptions: [], |
| properties: true, |
| }, |
| ], |
| |
| 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] = context.options; |
| const skipProperties = !config.properties; |
| |
| const newIsCapExceptions = 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.sourceCode; |
| |
| //-------------------------------------------------------------------------- |
| // Helpers |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * Get exact callee name from expression |
| * @param {ASTNode} node CallExpression or NewExpression node |
| * @returns {string} name |
| */ |
| function extractNameFromExpression(node) { |
| return node.callee.type === "Identifier" |
| ? node.callee.name |
| : astUtils.getStaticPropertyName(node.callee) || ""; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| const callee = astUtils.skipChainExpression(node.callee); |
| |
| if (calleeName === "UTC" && callee.type === "MemberExpression") { |
| // allow if callee is Date.UTC |
| return ( |
| callee.object.type === "Identifier" && |
| callee.object.name === "Date" |
| ); |
| } |
| |
| return skipProperties && 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 = astUtils.skipChainExpression(node.callee); |
| |
| if (callee.type === "MemberExpression") { |
| callee = callee.property; |
| } |
| |
| context.report({ node, loc: callee.loc, 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; |
| }, |
| }; |