• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * @fileoverview Rule to flag non-matching identifiers
3 * @author Matthieu Larcher
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Rule Definition
10//------------------------------------------------------------------------------
11
12module.exports = {
13    meta: {
14        type: "suggestion",
15
16        docs: {
17            description: "require identifiers to match a specified regular expression",
18            category: "Stylistic Issues",
19            recommended: false,
20            url: "https://eslint.org/docs/rules/id-match"
21        },
22
23        schema: [
24            {
25                type: "string"
26            },
27            {
28                type: "object",
29                properties: {
30                    properties: {
31                        type: "boolean",
32                        default: false
33                    },
34                    onlyDeclarations: {
35                        type: "boolean",
36                        default: false
37                    },
38                    ignoreDestructuring: {
39                        type: "boolean",
40                        default: false
41                    }
42                }
43            }
44        ],
45        messages: {
46            notMatch: "Identifier '{{name}}' does not match the pattern '{{pattern}}'."
47        }
48    },
49
50    create(context) {
51
52        //--------------------------------------------------------------------------
53        // Options
54        //--------------------------------------------------------------------------
55        const pattern = context.options[0] || "^.+$",
56            regexp = new RegExp(pattern, "u");
57
58        const options = context.options[1] || {},
59            properties = !!options.properties,
60            onlyDeclarations = !!options.onlyDeclarations,
61            ignoreDestructuring = !!options.ignoreDestructuring;
62
63        //--------------------------------------------------------------------------
64        // Helpers
65        //--------------------------------------------------------------------------
66
67        // contains reported nodes to avoid reporting twice on destructuring with shorthand notation
68        const reported = new Map();
69        const ALLOWED_PARENT_TYPES = new Set(["CallExpression", "NewExpression"]);
70        const DECLARATION_TYPES = new Set(["FunctionDeclaration", "VariableDeclarator"]);
71        const IMPORT_TYPES = new Set(["ImportSpecifier", "ImportNamespaceSpecifier", "ImportDefaultSpecifier"]);
72
73        /**
74         * Checks if a string matches the provided pattern
75         * @param {string} name The string to check.
76         * @returns {boolean} if the string is a match
77         * @private
78         */
79        function isInvalid(name) {
80            return !regexp.test(name);
81        }
82
83        /**
84         * Checks if a parent of a node is an ObjectPattern.
85         * @param {ASTNode} node The node to check.
86         * @returns {boolean} if the node is inside an ObjectPattern
87         * @private
88         */
89        function isInsideObjectPattern(node) {
90            let { parent } = node;
91
92            while (parent) {
93                if (parent.type === "ObjectPattern") {
94                    return true;
95                }
96
97                parent = parent.parent;
98            }
99
100            return false;
101        }
102
103        /**
104         * Verifies if we should report an error or not based on the effective
105         * parent node and the identifier name.
106         * @param {ASTNode} effectiveParent The effective parent node of the node to be reported
107         * @param {string} name The identifier name of the identifier node
108         * @returns {boolean} whether an error should be reported or not
109         */
110        function shouldReport(effectiveParent, name) {
111            return (!onlyDeclarations || DECLARATION_TYPES.has(effectiveParent.type)) &&
112                !ALLOWED_PARENT_TYPES.has(effectiveParent.type) && isInvalid(name);
113        }
114
115        /**
116         * Reports an AST node as a rule violation.
117         * @param {ASTNode} node The node to report.
118         * @returns {void}
119         * @private
120         */
121        function report(node) {
122            if (!reported.has(node)) {
123                context.report({
124                    node,
125                    messageId: "notMatch",
126                    data: {
127                        name: node.name,
128                        pattern
129                    }
130                });
131                reported.set(node, true);
132            }
133        }
134
135        return {
136
137            Identifier(node) {
138                const name = node.name,
139                    parent = node.parent,
140                    effectiveParent = (parent.type === "MemberExpression") ? parent.parent : parent;
141
142                if (parent.type === "MemberExpression") {
143
144                    if (!properties) {
145                        return;
146                    }
147
148                    // Always check object names
149                    if (parent.object.type === "Identifier" &&
150                        parent.object.name === name) {
151                        if (isInvalid(name)) {
152                            report(node);
153                        }
154
155                    // Report AssignmentExpressions left side's assigned variable id
156                    } else if (effectiveParent.type === "AssignmentExpression" &&
157                        effectiveParent.left.type === "MemberExpression" &&
158                        effectiveParent.left.property.name === node.name) {
159                        if (isInvalid(name)) {
160                            report(node);
161                        }
162
163                    // Report AssignmentExpressions only if they are the left side of the assignment
164                    } else if (effectiveParent.type === "AssignmentExpression" && effectiveParent.right.type !== "MemberExpression") {
165                        if (isInvalid(name)) {
166                            report(node);
167                        }
168                    }
169
170                /*
171                 * Properties have their own rules, and
172                 * AssignmentPattern nodes can be treated like Properties:
173                 * e.g.: const { no_camelcased = false } = bar;
174                 */
175                } else if (parent.type === "Property" || parent.type === "AssignmentPattern") {
176
177                    if (parent.parent && parent.parent.type === "ObjectPattern") {
178                        if (parent.shorthand && parent.value.left && isInvalid(name)) {
179
180                            report(node);
181                        }
182
183                        const assignmentKeyEqualsValue = parent.key.name === parent.value.name;
184
185                        // prevent checking righthand side of destructured object
186                        if (!assignmentKeyEqualsValue && parent.key === node) {
187                            return;
188                        }
189
190                        const valueIsInvalid = parent.value.name && isInvalid(name);
191
192                        // ignore destructuring if the option is set, unless a new identifier is created
193                        if (valueIsInvalid && !(assignmentKeyEqualsValue && ignoreDestructuring)) {
194                            report(node);
195                        }
196                    }
197
198                    // never check properties or always ignore destructuring
199                    if (!properties || (ignoreDestructuring && isInsideObjectPattern(node))) {
200                        return;
201                    }
202
203                    // don't check right hand side of AssignmentExpression to prevent duplicate warnings
204                    if (parent.right !== node && shouldReport(effectiveParent, name)) {
205                        report(node);
206                    }
207
208                // Check if it's an import specifier
209                } else if (IMPORT_TYPES.has(parent.type)) {
210
211                    // Report only if the local imported identifier is invalid
212                    if (parent.local && parent.local.name === node.name && isInvalid(name)) {
213                        report(node);
214                    }
215
216                // Report anything that is invalid that isn't a CallExpression
217                } else if (shouldReport(effectiveParent, name)) {
218                    report(node);
219                }
220            }
221
222        };
223
224    }
225};
226