400 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			400 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * @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);
 | |
|             }
 | |
|         };
 | |
|     }
 | |
| };
 |