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