/** * @fileoverview Rule to flag non-camelcased identifiers * @author Nicholas C. Zakas */ "use strict"; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const astUtils = require("./utils/ast-utils"); //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ /** @type {import('../shared/types').Rule} */ module.exports = { meta: { type: "suggestion", docs: { description: "enforce camelcase naming convention", recommended: false, url: "https://eslint.org/docs/rules/camelcase" }, schema: [ { type: "object", properties: { ignoreDestructuring: { type: "boolean", default: false }, ignoreImports: { type: "boolean", default: false }, ignoreGlobals: { type: "boolean", default: false }, properties: { enum: ["always", "never"] }, allow: { type: "array", items: [ { type: "string" } ], minItems: 0, uniqueItems: true } }, additionalProperties: false } ], messages: { notCamelCase: "Identifier '{{name}}' is not in camel case.", notCamelCasePrivate: "#{{name}} is not in camel case." } }, create(context) { const options = context.options[0] || {}; const properties = options.properties === "never" ? "never" : "always"; const ignoreDestructuring = options.ignoreDestructuring; const ignoreImports = options.ignoreImports; const ignoreGlobals = options.ignoreGlobals; const allow = options.allow || []; //-------------------------------------------------------------------------- // Helpers //-------------------------------------------------------------------------- // contains reported nodes to avoid reporting twice on destructuring with shorthand notation const reported = new Set(); /** * Checks if a string contains an underscore and isn't all upper-case * @param {string} name The string to check. * @returns {boolean} if the string is underscored * @private */ function isUnderscored(name) { const nameBody = name.replace(/^_+|_+$/gu, ""); // if there's an underscore, it might be A_CONSTANT, which is okay return nameBody.includes("_") && nameBody !== nameBody.toUpperCase(); } /** * Checks if a string match the ignore list * @param {string} name The string to check. * @returns {boolean} if the string is ignored * @private */ function isAllowed(name) { return allow.some( entry => name === entry || name.match(new RegExp(entry, "u")) ); } /** * Checks if a given name is good or not. * @param {string} name The name to check. * @returns {boolean} `true` if the name is good. * @private */ function isGoodName(name) { return !isUnderscored(name) || isAllowed(name); } /** * Checks if a given identifier reference or member expression is an assignment * target. * @param {ASTNode} node The node to check. * @returns {boolean} `true` if the node is an assignment target. */ function isAssignmentTarget(node) { const parent = node.parent; switch (parent.type) { case "AssignmentExpression": case "AssignmentPattern": return parent.left === node; case "Property": return ( parent.parent.type === "ObjectPattern" && parent.value === node ); case "ArrayPattern": case "RestElement": return true; default: return false; } } /** * Checks if a given binding identifier uses the original name as-is. * - If it's in object destructuring or object expression, the original name is its property name. * - If it's in import declaration, the original name is its exported name. * @param {ASTNode} node The `Identifier` node to check. * @returns {boolean} `true` if the identifier uses the original name as-is. */ function equalsToOriginalName(node) { const localName = node.name; const valueNode = node.parent.type === "AssignmentPattern" ? node.parent : node; const parent = valueNode.parent; switch (parent.type) { case "Property": return ( (parent.parent.type === "ObjectPattern" || parent.parent.type === "ObjectExpression") && parent.value === valueNode && !parent.computed && parent.key.type === "Identifier" && parent.key.name === localName ); case "ImportSpecifier": return ( parent.local === node && astUtils.getModuleExportName(parent.imported) === localName ); default: return false; } } /** * Reports an AST node as a rule violation. * @param {ASTNode} node The node to report. * @returns {void} * @private */ function report(node) { if (reported.has(node.range[0])) { return; } reported.add(node.range[0]); // Report it. context.report({ node, messageId: node.type === "PrivateIdentifier" ? "notCamelCasePrivate" : "notCamelCase", data: { name: node.name } }); } /** * Reports an identifier reference or a binding identifier. * @param {ASTNode} node The `Identifier` node to report. * @returns {void} */ function reportReferenceId(node) { /* * For backward compatibility, if it's in callings then ignore it. * Not sure why it is. */ if ( node.parent.type === "CallExpression" || node.parent.type === "NewExpression" ) { return; } /* * For backward compatibility, if it's a default value of * destructuring/parameters then ignore it. * Not sure why it is. */ if ( node.parent.type === "AssignmentPattern" && node.parent.right === node ) { return; } /* * The `ignoreDestructuring` flag skips the identifiers that uses * the property name as-is. */ if (ignoreDestructuring && equalsToOriginalName(node)) { return; } report(node); } return { // Report camelcase of global variable references ------------------ Program() { const scope = context.getScope(); if (!ignoreGlobals) { // Defined globals in config files or directive comments. for (const variable of scope.variables) { if ( variable.identifiers.length > 0 || isGoodName(variable.name) ) { continue; } for (const reference of variable.references) { /* * For backward compatibility, this rule reports read-only * references as well. */ reportReferenceId(reference.identifier); } } } // Undefined globals. for (const reference of scope.through) { const id = reference.identifier; if (isGoodName(id.name)) { continue; } /* * For backward compatibility, this rule reports read-only * references as well. */ reportReferenceId(id); } }, // Report camelcase of declared variables -------------------------- [[ "VariableDeclaration", "FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression", "ClassDeclaration", "ClassExpression", "CatchClause" ]](node) { for (const variable of context.getDeclaredVariables(node)) { if (isGoodName(variable.name)) { continue; } const id = variable.identifiers[0]; // Report declaration. if (!(ignoreDestructuring && equalsToOriginalName(id))) { report(id); } /* * For backward compatibility, report references as well. * It looks unnecessary because declarations are reported. */ for (const reference of variable.references) { if (reference.init) { continue; // Skip the write references of initializers. } reportReferenceId(reference.identifier); } } }, // Report camelcase in properties ---------------------------------- [[ "ObjectExpression > Property[computed!=true] > Identifier.key", "MethodDefinition[computed!=true] > Identifier.key", "PropertyDefinition[computed!=true] > Identifier.key", "MethodDefinition > PrivateIdentifier.key", "PropertyDefinition > PrivateIdentifier.key" ]](node) { if (properties === "never" || isGoodName(node.name)) { return; } report(node); }, "MemberExpression[computed!=true] > Identifier.property"(node) { if ( properties === "never" || !isAssignmentTarget(node.parent) || // ← ignore read-only references. isGoodName(node.name) ) { return; } report(node); }, // Report camelcase in import -------------------------------------- ImportDeclaration(node) { for (const variable of context.getDeclaredVariables(node)) { if (isGoodName(variable.name)) { continue; } const id = variable.identifiers[0]; // Report declaration. if (!(ignoreImports && equalsToOriginalName(id))) { report(id); } /* * For backward compatibility, report references as well. * It looks unnecessary because declarations are reported. */ for (const reference of variable.references) { reportReferenceId(reference.identifier); } } }, // Report camelcase in re-export ----------------------------------- [[ "ExportAllDeclaration > Identifier.exported", "ExportSpecifier > Identifier.exported" ]](node) { if (isGoodName(node.name)) { return; } report(node); }, // Report camelcase in labels -------------------------------------- [[ "LabeledStatement > Identifier.label", /* * For backward compatibility, report references as well. * It looks unnecessary because declarations are reported. */ "BreakStatement > Identifier.label", "ContinueStatement > Identifier.label" ]](node) { if (isGoodName(node.name)) { return; } report(node); } }; } };