1/** 2 * @fileoverview Rule to flag non-camelcased identifiers 3 * @author Nicholas C. Zakas 4 */ 5 6"use strict"; 7 8//------------------------------------------------------------------------------ 9// Rule Definition 10//------------------------------------------------------------------------------ 11 12module.exports = { 13 meta: { 14 type: "suggestion", 15 16 docs: { 17 description: "enforce camelcase naming convention", 18 category: "Stylistic Issues", 19 recommended: false, 20 url: "https://eslint.org/docs/rules/camelcase" 21 }, 22 23 schema: [ 24 { 25 type: "object", 26 properties: { 27 ignoreDestructuring: { 28 type: "boolean", 29 default: false 30 }, 31 ignoreImports: { 32 type: "boolean", 33 default: false 34 }, 35 properties: { 36 enum: ["always", "never"] 37 }, 38 allow: { 39 type: "array", 40 items: [ 41 { 42 type: "string" 43 } 44 ], 45 minItems: 0, 46 uniqueItems: true 47 } 48 }, 49 additionalProperties: false 50 } 51 ], 52 53 messages: { 54 notCamelCase: "Identifier '{{name}}' is not in camel case." 55 } 56 }, 57 58 create(context) { 59 60 const options = context.options[0] || {}; 61 let properties = options.properties || ""; 62 const ignoreDestructuring = options.ignoreDestructuring; 63 const ignoreImports = options.ignoreImports; 64 const allow = options.allow || []; 65 66 if (properties !== "always" && properties !== "never") { 67 properties = "always"; 68 } 69 70 //-------------------------------------------------------------------------- 71 // Helpers 72 //-------------------------------------------------------------------------- 73 74 // contains reported nodes to avoid reporting twice on destructuring with shorthand notation 75 const reported = []; 76 const ALLOWED_PARENT_TYPES = new Set(["CallExpression", "NewExpression"]); 77 78 /** 79 * Checks if a string contains an underscore and isn't all upper-case 80 * @param {string} name The string to check. 81 * @returns {boolean} if the string is underscored 82 * @private 83 */ 84 function isUnderscored(name) { 85 86 // if there's an underscore, it might be A_CONSTANT, which is okay 87 return name.includes("_") && name !== name.toUpperCase(); 88 } 89 90 /** 91 * Checks if a string match the ignore list 92 * @param {string} name The string to check. 93 * @returns {boolean} if the string is ignored 94 * @private 95 */ 96 function isAllowed(name) { 97 return allow.some( 98 entry => name === entry || name.match(new RegExp(entry, "u")) 99 ); 100 } 101 102 /** 103 * Checks if a parent of a node is an ObjectPattern. 104 * @param {ASTNode} node The node to check. 105 * @returns {boolean} if the node is inside an ObjectPattern 106 * @private 107 */ 108 function isInsideObjectPattern(node) { 109 let current = node; 110 111 while (current) { 112 const parent = current.parent; 113 114 if (parent && parent.type === "Property" && parent.computed && parent.key === current) { 115 return false; 116 } 117 118 if (current.type === "ObjectPattern") { 119 return true; 120 } 121 122 current = parent; 123 } 124 125 return false; 126 } 127 128 /** 129 * Checks whether the given node represents assignment target property in destructuring. 130 * 131 * For examples: 132 * ({a: b.foo} = c); // => true for `foo` 133 * ([a.foo] = b); // => true for `foo` 134 * ([a.foo = 1] = b); // => true for `foo` 135 * ({...a.foo} = b); // => true for `foo` 136 * @param {ASTNode} node An Identifier node to check 137 * @returns {boolean} True if the node is an assignment target property in destructuring. 138 */ 139 function isAssignmentTargetPropertyInDestructuring(node) { 140 if ( 141 node.parent.type === "MemberExpression" && 142 node.parent.property === node && 143 !node.parent.computed 144 ) { 145 const effectiveParent = node.parent.parent; 146 147 return ( 148 effectiveParent.type === "Property" && 149 effectiveParent.value === node.parent && 150 effectiveParent.parent.type === "ObjectPattern" || 151 effectiveParent.type === "ArrayPattern" || 152 effectiveParent.type === "RestElement" || 153 ( 154 effectiveParent.type === "AssignmentPattern" && 155 effectiveParent.left === node.parent 156 ) 157 ); 158 } 159 return false; 160 } 161 162 /** 163 * Reports an AST node as a rule violation. 164 * @param {ASTNode} node The node to report. 165 * @returns {void} 166 * @private 167 */ 168 function report(node) { 169 if (!reported.includes(node)) { 170 reported.push(node); 171 context.report({ node, messageId: "notCamelCase", data: { name: node.name } }); 172 } 173 } 174 175 return { 176 177 Identifier(node) { 178 179 /* 180 * Leading and trailing underscores are commonly used to flag 181 * private/protected identifiers, strip them before checking if underscored 182 */ 183 const name = node.name, 184 nameIsUnderscored = isUnderscored(name.replace(/^_+|_+$/gu, "")), 185 effectiveParent = (node.parent.type === "MemberExpression") ? node.parent.parent : node.parent; 186 187 // First, we ignore the node if it match the ignore list 188 if (isAllowed(name)) { 189 return; 190 } 191 192 // MemberExpressions get special rules 193 if (node.parent.type === "MemberExpression") { 194 195 // "never" check properties 196 if (properties === "never") { 197 return; 198 } 199 200 // Always report underscored object names 201 if (node.parent.object.type === "Identifier" && node.parent.object.name === node.name && nameIsUnderscored) { 202 report(node); 203 204 // Report AssignmentExpressions only if they are the left side of the assignment 205 } else if (effectiveParent.type === "AssignmentExpression" && nameIsUnderscored && (effectiveParent.right.type !== "MemberExpression" || effectiveParent.left.type === "MemberExpression" && effectiveParent.left.property.name === node.name)) { 206 report(node); 207 208 } else if (isAssignmentTargetPropertyInDestructuring(node) && nameIsUnderscored) { 209 report(node); 210 } 211 212 /* 213 * Properties have their own rules, and 214 * AssignmentPattern nodes can be treated like Properties: 215 * e.g.: const { no_camelcased = false } = bar; 216 */ 217 } else if (node.parent.type === "Property" || node.parent.type === "AssignmentPattern") { 218 219 if (node.parent.parent && node.parent.parent.type === "ObjectPattern") { 220 if (node.parent.shorthand && node.parent.value.left && nameIsUnderscored) { 221 report(node); 222 } 223 224 const assignmentKeyEqualsValue = node.parent.key.name === node.parent.value.name; 225 226 if (nameIsUnderscored && node.parent.computed) { 227 report(node); 228 } 229 230 // prevent checking righthand side of destructured object 231 if (node.parent.key === node && node.parent.value !== node) { 232 return; 233 } 234 235 const valueIsUnderscored = node.parent.value.name && nameIsUnderscored; 236 237 // ignore destructuring if the option is set, unless a new identifier is created 238 if (valueIsUnderscored && !(assignmentKeyEqualsValue && ignoreDestructuring)) { 239 report(node); 240 } 241 } 242 243 // "never" check properties or always ignore destructuring 244 if (properties === "never" || (ignoreDestructuring && isInsideObjectPattern(node))) { 245 return; 246 } 247 248 // don't check right hand side of AssignmentExpression to prevent duplicate warnings 249 if (nameIsUnderscored && !ALLOWED_PARENT_TYPES.has(effectiveParent.type) && !(node.parent.right === node)) { 250 report(node); 251 } 252 253 // Check if it's an import specifier 254 } else if (["ImportSpecifier", "ImportNamespaceSpecifier", "ImportDefaultSpecifier"].includes(node.parent.type)) { 255 256 if (node.parent.type === "ImportSpecifier" && ignoreImports) { 257 return; 258 } 259 260 // Report only if the local imported identifier is underscored 261 if ( 262 node.parent.local && 263 node.parent.local.name === node.name && 264 nameIsUnderscored 265 ) { 266 report(node); 267 } 268 269 // Report anything that is underscored that isn't a CallExpression 270 } else if (nameIsUnderscored && !ALLOWED_PARENT_TYPES.has(effectiveParent.type)) { 271 report(node); 272 } 273 } 274 275 }; 276 277 } 278}; 279