UNPKG

12.5 kBJavaScriptView Raw
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 ignoreGlobals: {
36 type: "boolean",
37 default: false
38 },
39 properties: {
40 enum: ["always", "never"]
41 },
42 allow: {
43 type: "array",
44 items: [
45 {
46 type: "string"
47 }
48 ],
49 minItems: 0,
50 uniqueItems: true
51 }
52 },
53 additionalProperties: false
54 }
55 ],
56
57 messages: {
58 notCamelCase: "Identifier '{{name}}' is not in camel case."
59 }
60 },
61
62 create(context) {
63
64 const options = context.options[0] || {};
65 let properties = options.properties || "";
66 const ignoreDestructuring = options.ignoreDestructuring;
67 const ignoreImports = options.ignoreImports;
68 const ignoreGlobals = options.ignoreGlobals;
69 const allow = options.allow || [];
70
71 let globalScope;
72
73 if (properties !== "always" && properties !== "never") {
74 properties = "always";
75 }
76
77 //--------------------------------------------------------------------------
78 // Helpers
79 //--------------------------------------------------------------------------
80
81 // contains reported nodes to avoid reporting twice on destructuring with shorthand notation
82 const reported = [];
83 const ALLOWED_PARENT_TYPES = new Set(["CallExpression", "NewExpression"]);
84
85 /**
86 * Checks if a string contains an underscore and isn't all upper-case
87 * @param {string} name The string to check.
88 * @returns {boolean} if the string is underscored
89 * @private
90 */
91 function isUnderscored(name) {
92
93 // if there's an underscore, it might be A_CONSTANT, which is okay
94 return name.includes("_") && name !== name.toUpperCase();
95 }
96
97 /**
98 * Checks if a string match the ignore list
99 * @param {string} name The string to check.
100 * @returns {boolean} if the string is ignored
101 * @private
102 */
103 function isAllowed(name) {
104 return allow.some(
105 entry => name === entry || name.match(new RegExp(entry, "u"))
106 );
107 }
108
109 /**
110 * Checks if a parent of a node is an ObjectPattern.
111 * @param {ASTNode} node The node to check.
112 * @returns {boolean} if the node is inside an ObjectPattern
113 * @private
114 */
115 function isInsideObjectPattern(node) {
116 let current = node;
117
118 while (current) {
119 const parent = current.parent;
120
121 if (parent && parent.type === "Property" && parent.computed && parent.key === current) {
122 return false;
123 }
124
125 if (current.type === "ObjectPattern") {
126 return true;
127 }
128
129 current = parent;
130 }
131
132 return false;
133 }
134
135 /**
136 * Checks whether the given node represents assignment target property in destructuring.
137 *
138 * For examples:
139 * ({a: b.foo} = c); // => true for `foo`
140 * ([a.foo] = b); // => true for `foo`
141 * ([a.foo = 1] = b); // => true for `foo`
142 * ({...a.foo} = b); // => true for `foo`
143 * @param {ASTNode} node An Identifier node to check
144 * @returns {boolean} True if the node is an assignment target property in destructuring.
145 */
146 function isAssignmentTargetPropertyInDestructuring(node) {
147 if (
148 node.parent.type === "MemberExpression" &&
149 node.parent.property === node &&
150 !node.parent.computed
151 ) {
152 const effectiveParent = node.parent.parent;
153
154 return (
155 effectiveParent.type === "Property" &&
156 effectiveParent.value === node.parent &&
157 effectiveParent.parent.type === "ObjectPattern" ||
158 effectiveParent.type === "ArrayPattern" ||
159 effectiveParent.type === "RestElement" ||
160 (
161 effectiveParent.type === "AssignmentPattern" &&
162 effectiveParent.left === node.parent
163 )
164 );
165 }
166 return false;
167 }
168
169 /**
170 * Checks whether the given node represents a reference to a global variable that is not declared in the source code.
171 * These identifiers will be allowed, as it is assumed that user has no control over the names of external global variables.
172 * @param {ASTNode} node `Identifier` node to check.
173 * @returns {boolean} `true` if the node is a reference to a global variable.
174 */
175 function isReferenceToGlobalVariable(node) {
176 const variable = globalScope.set.get(node.name);
177
178 return variable && variable.defs.length === 0 &&
179 variable.references.some(ref => ref.identifier === node);
180 }
181
182 /**
183 * Checks whether the given node represents a reference to a property of an object in an object literal expression.
184 * This allows to differentiate between a global variable that is allowed to be used as a reference, and the key
185 * of the expressed object (which shouldn't be allowed).
186 * @param {ASTNode} node `Identifier` node to check.
187 * @returns {boolean} `true` if the node is a property name of an object literal expression
188 */
189 function isPropertyNameInObjectLiteral(node) {
190 const parent = node.parent;
191
192 return (
193 parent.type === "Property" &&
194 parent.parent.type === "ObjectExpression" &&
195 !parent.computed &&
196 parent.key === node
197 );
198 }
199
200 /**
201 * Reports an AST node as a rule violation.
202 * @param {ASTNode} node The node to report.
203 * @returns {void}
204 * @private
205 */
206 function report(node) {
207 if (!reported.includes(node)) {
208 reported.push(node);
209 context.report({ node, messageId: "notCamelCase", data: { name: node.name } });
210 }
211 }
212
213 return {
214
215 Program() {
216 globalScope = context.getScope();
217 },
218
219 Identifier(node) {
220
221 /*
222 * Leading and trailing underscores are commonly used to flag
223 * private/protected identifiers, strip them before checking if underscored
224 */
225 const name = node.name,
226 nameIsUnderscored = isUnderscored(name.replace(/^_+|_+$/gu, "")),
227 effectiveParent = (node.parent.type === "MemberExpression") ? node.parent.parent : node.parent;
228
229 // First, we ignore the node if it match the ignore list
230 if (isAllowed(name)) {
231 return;
232 }
233
234 // Check if it's a global variable
235 if (ignoreGlobals && isReferenceToGlobalVariable(node) && !isPropertyNameInObjectLiteral(node)) {
236 return;
237 }
238
239 // MemberExpressions get special rules
240 if (node.parent.type === "MemberExpression") {
241
242 // "never" check properties
243 if (properties === "never") {
244 return;
245 }
246
247 // Always report underscored object names
248 if (node.parent.object.type === "Identifier" && node.parent.object.name === node.name && nameIsUnderscored) {
249 report(node);
250
251 // Report AssignmentExpressions only if they are the left side of the assignment
252 } else if (effectiveParent.type === "AssignmentExpression" && nameIsUnderscored && (effectiveParent.right.type !== "MemberExpression" || effectiveParent.left.type === "MemberExpression" && effectiveParent.left.property.name === node.name)) {
253 report(node);
254
255 } else if (isAssignmentTargetPropertyInDestructuring(node) && nameIsUnderscored) {
256 report(node);
257 }
258
259 /*
260 * Properties have their own rules, and
261 * AssignmentPattern nodes can be treated like Properties:
262 * e.g.: const { no_camelcased = false } = bar;
263 */
264 } else if (node.parent.type === "Property" || node.parent.type === "AssignmentPattern") {
265
266 if (node.parent.parent && node.parent.parent.type === "ObjectPattern") {
267 if (node.parent.shorthand && node.parent.value.left && nameIsUnderscored) {
268 report(node);
269 }
270
271 const assignmentKeyEqualsValue = node.parent.key.name === node.parent.value.name;
272
273 if (nameIsUnderscored && node.parent.computed) {
274 report(node);
275 }
276
277 // prevent checking righthand side of destructured object
278 if (node.parent.key === node && node.parent.value !== node) {
279 return;
280 }
281
282 const valueIsUnderscored = node.parent.value.name && nameIsUnderscored;
283
284 // ignore destructuring if the option is set, unless a new identifier is created
285 if (valueIsUnderscored && !(assignmentKeyEqualsValue && ignoreDestructuring)) {
286 report(node);
287 }
288 }
289
290 // "never" check properties or always ignore destructuring
291 if (properties === "never" || (ignoreDestructuring && isInsideObjectPattern(node))) {
292 return;
293 }
294
295 // don't check right hand side of AssignmentExpression to prevent duplicate warnings
296 if (nameIsUnderscored && !ALLOWED_PARENT_TYPES.has(effectiveParent.type) && !(node.parent.right === node)) {
297 report(node);
298 }
299
300 // Check if it's an import specifier
301 } else if (["ImportSpecifier", "ImportNamespaceSpecifier", "ImportDefaultSpecifier"].includes(node.parent.type)) {
302
303 if (node.parent.type === "ImportSpecifier" && ignoreImports) {
304 return;
305 }
306
307 // Report only if the local imported identifier is underscored
308 if (
309 node.parent.local &&
310 node.parent.local.name === node.name &&
311 nameIsUnderscored
312 ) {
313 report(node);
314 }
315
316 // Report anything that is underscored that isn't a CallExpression
317 } else if (nameIsUnderscored && !ALLOWED_PARENT_TYPES.has(effectiveParent.type)) {
318 report(node);
319 }
320 }
321
322 };
323
324 }
325};