| /** | |
|  * @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/latest/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 || []; | |
|         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; | |
|             } | |
| 
 | |
|             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)) { | |
|                         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" || 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); | |
|             } | |
|         }; | |
|     } | |
| };
 |