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);
 | 
						|
            }
 | 
						|
        };
 | 
						|
    }
 | 
						|
};
 |