| /** |
| * @fileoverview Rule to flag non-camelcased identifiers |
| * @author Nicholas C. Zakas |
| */ |
| |
| "use strict"; |
| |
| //------------------------------------------------------------------------------ |
| // Requirements |
| //------------------------------------------------------------------------------ |
| |
| const astUtils = require("./utils/ast-utils"); |
| |
| //------------------------------------------------------------------------------ |
| // Rule Definition |
| //------------------------------------------------------------------------------ |
| |
| /** @type {import('../types').Rule.RuleModule} */ |
| module.exports = { |
| meta: { |
| type: "suggestion", |
| |
| defaultOptions: [ |
| { |
| allow: [], |
| ignoreDestructuring: false, |
| ignoreGlobals: false, |
| ignoreImports: false, |
| properties: "always", |
| }, |
| ], |
| |
| docs: { |
| description: "Enforce camelcase naming convention", |
| recommended: false, |
| frozen: true, |
| url: "https://eslint.org/docs/latest/rules/camelcase", |
| }, |
| |
| schema: [ |
| { |
| type: "object", |
| properties: { |
| ignoreDestructuring: { |
| type: "boolean", |
| }, |
| ignoreImports: { |
| type: "boolean", |
| }, |
| ignoreGlobals: { |
| type: "boolean", |
| }, |
| 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 [ |
| { |
| allow, |
| ignoreDestructuring, |
| ignoreGlobals, |
| ignoreImports, |
| properties, |
| }, |
| ] = context.options; |
| const sourceCode = context.sourceCode; |
| |
| //-------------------------------------------------------------------------- |
| // 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; |
| } |
| |
| /* |
| * Import attribute keys are always ignored |
| */ |
| if (astUtils.isImportAttributeKey(node)) { |
| return; |
| } |
| |
| report(node); |
| } |
| |
| return { |
| // Report camelcase of global variable references ------------------ |
| Program(node) { |
| const scope = sourceCode.getScope(node); |
| |
| 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) || |
| astUtils.isImportAttributeKey(id) |
| ) { |
| 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 sourceCode.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" || |
| astUtils.isImportAttributeKey(node) || |
| 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 sourceCode.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); |
| }, |
| }; |
| }, |
| }; |