eslint 8.13.0 → 8.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +17 -12
  2. package/lib/cli-engine/cli-engine.js +2 -4
  3. package/lib/cli-engine/lint-result-cache.js +1 -1
  4. package/lib/linter/code-path-analysis/code-path-segment.js +1 -1
  5. package/lib/linter/code-path-analysis/code-path-state.js +1 -1
  6. package/lib/linter/code-path-analysis/code-path.js +1 -1
  7. package/lib/rules/accessor-pairs.js +4 -4
  8. package/lib/rules/callback-return.js +2 -2
  9. package/lib/rules/capitalized-comments.js +1 -1
  10. package/lib/rules/consistent-this.js +1 -1
  11. package/lib/rules/dot-notation.js +2 -2
  12. package/lib/rules/function-paren-newline.js +7 -4
  13. package/lib/rules/global-require.js +3 -3
  14. package/lib/rules/indent-legacy.js +2 -2
  15. package/lib/rules/indent.js +1 -1
  16. package/lib/rules/index.js +1 -0
  17. package/lib/rules/jsx-quotes.js +1 -1
  18. package/lib/rules/lines-around-comment.js +3 -3
  19. package/lib/rules/max-lines.js +2 -2
  20. package/lib/rules/newline-before-return.js +1 -1
  21. package/lib/rules/no-bitwise.js +2 -2
  22. package/lib/rules/no-console.js +1 -1
  23. package/lib/rules/no-constant-binary-expression.js +500 -0
  24. package/lib/rules/no-constant-condition.js +4 -197
  25. package/lib/rules/no-control-regex.js +23 -10
  26. package/lib/rules/no-empty-function.js +1 -1
  27. package/lib/rules/no-extra-boolean-cast.js +3 -3
  28. package/lib/rules/no-extra-semi.js +1 -1
  29. package/lib/rules/no-global-assign.js +1 -1
  30. package/lib/rules/no-implicit-coercion.js +6 -6
  31. package/lib/rules/no-magic-numbers.js +3 -3
  32. package/lib/rules/no-misleading-character-class.js +90 -17
  33. package/lib/rules/no-mixed-operators.js +1 -1
  34. package/lib/rules/no-mixed-requires.js +1 -1
  35. package/lib/rules/no-multi-spaces.js +1 -1
  36. package/lib/rules/no-native-reassign.js +1 -1
  37. package/lib/rules/no-new-wrappers.js +1 -1
  38. package/lib/rules/no-prototype-builtins.js +3 -3
  39. package/lib/rules/no-shadow.js +5 -5
  40. package/lib/rules/no-sparse-arrays.js +1 -1
  41. package/lib/rules/no-underscore-dangle.js +31 -2
  42. package/lib/rules/no-unused-expressions.js +1 -1
  43. package/lib/rules/no-unused-vars.js +1 -1
  44. package/lib/rules/operator-assignment.js +2 -2
  45. package/lib/rules/prefer-const.js +1 -1
  46. package/lib/rules/prefer-reflect.js +2 -2
  47. package/lib/rules/prefer-regex-literals.js +3 -3
  48. package/lib/rules/quote-props.js +2 -2
  49. package/lib/rules/quotes.js +1 -1
  50. package/lib/rules/spaced-comment.js +1 -1
  51. package/lib/rules/utils/ast-utils.js +203 -7
  52. package/lib/rules/valid-jsdoc.js +1 -1
  53. package/lib/rules/valid-typeof.js +4 -4
  54. package/lib/rules/yoda.js +1 -1
  55. package/package.json +23 -8
@@ -0,0 +1,500 @@
1
+ /**
2
+ * @fileoverview Rule to flag constant comparisons and logical expressions that always/never short circuit
3
+ * @author Jordan Eldredge <https://jordaneldredge.com>
4
+ */
5
+
6
+ "use strict";
7
+
8
+ const globals = require("globals");
9
+ const { isNullLiteral, isConstant, isReferenceToGlobalVariable, isLogicalAssignmentOperator } = require("./utils/ast-utils");
10
+
11
+ const NUMERIC_OR_STRING_BINARY_OPERATORS = new Set(["+", "-", "*", "/", "%", "|", "^", "&", "**", "<<", ">>", ">>>"]);
12
+
13
+ //------------------------------------------------------------------------------
14
+ // Helpers
15
+ //------------------------------------------------------------------------------
16
+
17
+ /**
18
+ * Test if an AST node has a statically knowable constant nullishness. Meaning,
19
+ * it will always resolve to a constant value of either: `null`, `undefined`
20
+ * or not `null` _or_ `undefined`. An expression that can vary between those
21
+ * three states at runtime would return `false`.
22
+ * @param {Scope} scope The scope in which the node was found.
23
+ * @param {ASTNode} node The AST node being tested.
24
+ * @returns {boolean} Does `node` have constant nullishness?
25
+ */
26
+ function hasConstantNullishness(scope, node) {
27
+ switch (node.type) {
28
+ case "ObjectExpression": // Objects are never nullish
29
+ case "ArrayExpression": // Arrays are never nullish
30
+ case "ArrowFunctionExpression": // Functions never nullish
31
+ case "FunctionExpression": // Functions are never nullish
32
+ case "ClassExpression": // Classes are never nullish
33
+ case "NewExpression": // Objects are never nullish
34
+ case "Literal": // Nullish, or non-nullish, literals never change
35
+ case "TemplateLiteral": // A string is never nullish
36
+ case "UpdateExpression": // Numbers are never nullish
37
+ case "BinaryExpression": // Numbers, strings, or booleans are never nullish
38
+ return true;
39
+ case "CallExpression": {
40
+ if (node.callee.type !== "Identifier") {
41
+ return false;
42
+ }
43
+ const functionName = node.callee.name;
44
+
45
+ return (functionName === "Boolean" || functionName === "String" || functionName === "Number") &&
46
+ isReferenceToGlobalVariable(scope, node.callee);
47
+ }
48
+ case "AssignmentExpression":
49
+ if (node.operator === "=") {
50
+ return hasConstantNullishness(scope, node.right);
51
+ }
52
+
53
+ /*
54
+ * Handling short-circuiting assignment operators would require
55
+ * walking the scope. We won't attempt that (for now...) /
56
+ */
57
+ if (isLogicalAssignmentOperator(node.operator)) {
58
+ return false;
59
+ }
60
+
61
+ /*
62
+ * The remaining assignment expressions all result in a numeric or
63
+ * string (non-nullish) value:
64
+ * "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "|=", "^=", "&="
65
+ */
66
+
67
+ return true;
68
+ case "UnaryExpression":
69
+
70
+ /*
71
+ * "void" Always returns `undefined`
72
+ * "typeof" All types are strings, and thus non-nullish
73
+ * "!" Boolean is never nullish
74
+ * "delete" Returns a boolean, which is never nullish
75
+ * Math operators always return numbers or strings, neither of which
76
+ * are non-nullish "+", "-", "~"
77
+ */
78
+
79
+ return true;
80
+ case "SequenceExpression": {
81
+ const last = node.expressions[node.expressions.length - 1];
82
+
83
+ return hasConstantNullishness(scope, last);
84
+ }
85
+ case "Identifier":
86
+ return node.name === "undefined" && isReferenceToGlobalVariable(scope, node);
87
+ case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
88
+ case "JSXFragment":
89
+ return false;
90
+ default:
91
+ return false;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Test if an AST node is a boolean value that never changes. Specifically we
97
+ * test for:
98
+ * 1. Literal booleans (`true` or `false`)
99
+ * 2. Unary `!` expressions with a constant value
100
+ * 3. Constant booleans created via the `Boolean` global function
101
+ * @param {Scope} scope The scope in which the node was found.
102
+ * @param {ASTNode} node The node to test
103
+ * @returns {boolean} Is `node` guaranteed to be a boolean?
104
+ */
105
+ function isStaticBoolean(scope, node) {
106
+ switch (node.type) {
107
+ case "Literal":
108
+ return typeof node.value === "boolean";
109
+ case "CallExpression":
110
+ return node.callee.type === "Identifier" && node.callee.name === "Boolean" &&
111
+ isReferenceToGlobalVariable(scope, node.callee) &&
112
+ (node.arguments.length === 0 || isConstant(scope, node.arguments[0], true));
113
+ case "UnaryExpression":
114
+ return node.operator === "!" && isConstant(scope, node.argument, true);
115
+ default:
116
+ return false;
117
+ }
118
+ }
119
+
120
+
121
+ /**
122
+ * Test if an AST node will always give the same result when compared to a
123
+ * bolean value. Note that comparison to boolean values is different than
124
+ * truthiness.
125
+ * https://262.ecma-international.org/5.1/#sec-11.9.3
126
+ *
127
+ * Javascript `==` operator works by converting the boolean to `1` (true) or
128
+ * `+0` (false) and then checks the values `==` equality to that number.
129
+ * @param {Scope} scope The scope in which node was found.
130
+ * @param {ASTNode} node The node to test.
131
+ * @returns {boolean} Will `node` always coerce to the same boolean value?
132
+ */
133
+ function hasConstantLooseBooleanComparison(scope, node) {
134
+ switch (node.type) {
135
+ case "ObjectExpression":
136
+ case "ClassExpression":
137
+
138
+ /**
139
+ * In theory objects like:
140
+ *
141
+ * `{toString: () => a}`
142
+ * `{valueOf: () => a}`
143
+ *
144
+ * Or a classes like:
145
+ *
146
+ * `class { static toString() { return a } }`
147
+ * `class { static valueOf() { return a } }`
148
+ *
149
+ * Are not constant verifiably when `inBooleanPosition` is
150
+ * false, but it's an edge case we've opted not to handle.
151
+ */
152
+ return true;
153
+ case "ArrayExpression": {
154
+ const nonSpreadElements = node.elements.filter(e =>
155
+
156
+ // Elements can be `null` in sparse arrays: `[,,]`;
157
+ e !== null && e.type !== "SpreadElement");
158
+
159
+
160
+ /*
161
+ * Possible future direction if needed: We could check if the
162
+ * single value would result in variable boolean comparison.
163
+ * For now we will err on the side of caution since `[x]` could
164
+ * evaluate to `[0]` or `[1]`.
165
+ */
166
+ return node.elements.length === 0 || nonSpreadElements.length > 1;
167
+ }
168
+ case "ArrowFunctionExpression":
169
+ case "FunctionExpression":
170
+ return true;
171
+ case "UnaryExpression":
172
+ if (node.operator === "void" || // Always returns `undefined`
173
+ node.operator === "typeof" // All `typeof` strings, when coerced to number, are not 0 or 1.
174
+ ) {
175
+ return true;
176
+ }
177
+ if (node.operator === "!") {
178
+ return isConstant(scope, node.argument, true);
179
+ }
180
+
181
+ /*
182
+ * We won't try to reason about +, -, ~, or delete
183
+ * In theory, for the mathematical operators, we could look at the
184
+ * argument and try to determine if it coerces to a constant numeric
185
+ * value.
186
+ */
187
+ return false;
188
+ case "NewExpression": // Objects might have custom `.valueOf` or `.toString`.
189
+ return false;
190
+ case "CallExpression": {
191
+ if (node.callee.type === "Identifier" &&
192
+ node.callee.name === "Boolean" &&
193
+ isReferenceToGlobalVariable(scope, node.callee)
194
+ ) {
195
+ return node.arguments.length === 0 || isConstant(scope, node.arguments[0], true);
196
+ }
197
+ return false;
198
+ }
199
+ case "Literal": // True or false, literals never change
200
+ return true;
201
+ case "Identifier":
202
+ return node.name === "undefined" && isReferenceToGlobalVariable(scope, node);
203
+ case "TemplateLiteral":
204
+
205
+ /*
206
+ * In theory we could try to check if the quasi are sufficient to
207
+ * prove that the expression will always be true, but it would be
208
+ * tricky to get right. For example: `000.${foo}000`
209
+ */
210
+ return node.expressions.length === 0;
211
+ case "AssignmentExpression":
212
+ if (node.operator === "=") {
213
+ return hasConstantLooseBooleanComparison(scope, node.right);
214
+ }
215
+
216
+ /*
217
+ * Handling short-circuiting assignment operators would require
218
+ * walking the scope. We won't attempt that (for now...)
219
+ *
220
+ * The remaining assignment expressions all result in a numeric or
221
+ * string (non-nullish) values which could be truthy or falsy:
222
+ * "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "|=", "^=", "&="
223
+ */
224
+ return false;
225
+ case "SequenceExpression": {
226
+ const last = node.expressions[node.expressions.length - 1];
227
+
228
+ return hasConstantLooseBooleanComparison(scope, last);
229
+ }
230
+ case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
231
+ case "JSXFragment":
232
+ return false;
233
+ default:
234
+ return false;
235
+ }
236
+ }
237
+
238
+
239
+ /**
240
+ * Test if an AST node will always give the same result when _strictly_ compared
241
+ * to a bolean value. This can happen if the expression can never be boolean, or
242
+ * if it is always the same boolean value.
243
+ * @param {Scope} scope The scope in which the node was found.
244
+ * @param {ASTNode} node The node to test
245
+ * @returns {boolean} Will `node` always give the same result when compared to a
246
+ * static boolean value?
247
+ */
248
+ function hasConstantStrictBooleanComparison(scope, node) {
249
+ switch (node.type) {
250
+ case "ObjectExpression": // Objects are not booleans
251
+ case "ArrayExpression": // Arrays are not booleans
252
+ case "ArrowFunctionExpression": // Functions are not booleans
253
+ case "FunctionExpression":
254
+ case "ClassExpression": // Classes are not booleans
255
+ case "NewExpression": // Objects are not booleans
256
+ case "TemplateLiteral": // Strings are not booleans
257
+ case "Literal": // True, false, or not boolean, literals never change.
258
+ case "UpdateExpression": // Numbers are not booleans
259
+ return true;
260
+ case "BinaryExpression":
261
+ return NUMERIC_OR_STRING_BINARY_OPERATORS.has(node.operator);
262
+ case "UnaryExpression": {
263
+ if (node.operator === "delete") {
264
+ return false;
265
+ }
266
+ if (node.operator === "!") {
267
+ return isConstant(scope, node.argument, true);
268
+ }
269
+
270
+ /*
271
+ * The remaining operators return either strings or numbers, neither
272
+ * of which are boolean.
273
+ */
274
+ return true;
275
+ }
276
+ case "SequenceExpression": {
277
+ const last = node.expressions[node.expressions.length - 1];
278
+
279
+ return hasConstantStrictBooleanComparison(scope, last);
280
+ }
281
+ case "Identifier":
282
+ return node.name === "undefined" && isReferenceToGlobalVariable(scope, node);
283
+ case "AssignmentExpression":
284
+ if (node.operator === "=") {
285
+ return hasConstantStrictBooleanComparison(scope, node.right);
286
+ }
287
+
288
+ /*
289
+ * Handling short-circuiting assignment operators would require
290
+ * walking the scope. We won't attempt that (for now...)
291
+ */
292
+ if (isLogicalAssignmentOperator(node.operator)) {
293
+ return false;
294
+ }
295
+
296
+ /*
297
+ * The remaining assignment expressions all result in either a number
298
+ * or a string, neither of which can ever be boolean.
299
+ */
300
+ return true;
301
+ case "CallExpression": {
302
+ if (node.callee.type !== "Identifier") {
303
+ return false;
304
+ }
305
+ const functionName = node.callee.name;
306
+
307
+ if (
308
+ (functionName === "String" || functionName === "Number") &&
309
+ isReferenceToGlobalVariable(scope, node.callee)
310
+ ) {
311
+ return true;
312
+ }
313
+ if (functionName === "Boolean" && isReferenceToGlobalVariable(scope, node.callee)) {
314
+ return (
315
+ node.arguments.length === 0 || isConstant(scope, node.arguments[0], true));
316
+ }
317
+ return false;
318
+ }
319
+ case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
320
+ case "JSXFragment":
321
+ return false;
322
+ default:
323
+ return false;
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Test if an AST node will always result in a newly constructed object
329
+ * @param {Scope} scope The scope in which the node was found.
330
+ * @param {ASTNode} node The node to test
331
+ * @returns {boolean} Will `node` always be new?
332
+ */
333
+ function isAlwaysNew(scope, node) {
334
+ switch (node.type) {
335
+ case "ObjectExpression":
336
+ case "ArrayExpression":
337
+ case "ArrowFunctionExpression":
338
+ case "FunctionExpression":
339
+ case "ClassExpression":
340
+ return true;
341
+ case "NewExpression": {
342
+ if (node.callee.type !== "Identifier") {
343
+ return false;
344
+ }
345
+
346
+ /*
347
+ * All the built-in constructors are always new, but
348
+ * user-defined constructors could return a sentinel
349
+ * object.
350
+ *
351
+ * Catching these is especially useful for primitive constructures
352
+ * which return boxed values, a surprising gotcha' in JavaScript.
353
+ */
354
+ return Object.hasOwnProperty.call(globals.builtin, node.callee.name) &&
355
+ isReferenceToGlobalVariable(scope, node.callee);
356
+ }
357
+ case "Literal":
358
+
359
+ // Regular expressions are objects, and thus always new
360
+ return typeof node.regex === "object";
361
+ case "SequenceExpression": {
362
+ const last = node.expressions[node.expressions.length - 1];
363
+
364
+ return isAlwaysNew(scope, last);
365
+ }
366
+ case "AssignmentExpression":
367
+ if (node.operator === "=") {
368
+ return isAlwaysNew(scope, node.right);
369
+ }
370
+ return false;
371
+ case "ConditionalExpression":
372
+ return isAlwaysNew(scope, node.consequent) && isAlwaysNew(scope, node.alternate);
373
+ case "JSXElement": // ESLint has a policy of not assuming any specific JSX behavior.
374
+ case "JSXFragment":
375
+ return false;
376
+ default:
377
+ return false;
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Checks whether or not a node is `null` or `undefined`. Similar to the one
383
+ * found in ast-utils.js, but this one correctly handles the edge case that
384
+ * `undefined` has been redefined.
385
+ * @param {Scope} scope Scope in which the expression was found.
386
+ * @param {ASTNode} node A node to check.
387
+ * @returns {boolean} Whether or not the node is a `null` or `undefined`.
388
+ * @public
389
+ */
390
+ function isNullOrUndefined(scope, node) {
391
+ return (
392
+ isNullLiteral(node) ||
393
+ (node.type === "Identifier" && node.name === "undefined" && isReferenceToGlobalVariable(scope, node)) ||
394
+ (node.type === "UnaryExpression" && node.operator === "void")
395
+ );
396
+ }
397
+
398
+
399
+ /**
400
+ * Checks if one operand will cause the result to be constant.
401
+ * @param {Scope} scope Scope in which the expression was found.
402
+ * @param {ASTNode} a One side of the expression
403
+ * @param {ASTNode} b The other side of the expression
404
+ * @param {string} operator The binary expression operator
405
+ * @returns {ASTNode | null} The node which will cause the expression to have a constant result.
406
+ */
407
+ function findBinaryExpressionConstantOperand(scope, a, b, operator) {
408
+ if (operator === "==" || operator === "!=") {
409
+ if (
410
+ (isNullOrUndefined(scope, a) && hasConstantNullishness(scope, b)) ||
411
+ (isStaticBoolean(scope, a) && hasConstantLooseBooleanComparison(scope, b))
412
+ ) {
413
+ return b;
414
+ }
415
+ } else if (operator === "===" || operator === "!==") {
416
+ if (
417
+ (isNullOrUndefined(scope, a) && hasConstantNullishness(scope, b)) ||
418
+ (isStaticBoolean(scope, a) && hasConstantStrictBooleanComparison(scope, b))
419
+ ) {
420
+ return b;
421
+ }
422
+ }
423
+ return null;
424
+ }
425
+
426
+ //------------------------------------------------------------------------------
427
+ // Rule Definition
428
+ //------------------------------------------------------------------------------
429
+
430
+ /** @type {import('../shared/types').Rule} */
431
+ module.exports = {
432
+ meta: {
433
+ type: "problem",
434
+ docs: {
435
+ description: "disallow expressions where the operation doesn't affect the value",
436
+ recommended: false,
437
+ url: "https://eslint.org/docs/rules/no-constant-binary-expression"
438
+ },
439
+ schema: [],
440
+ messages: {
441
+ constantBinaryOperand: "Unexpected constant binary expression. Compares constantly with the {{otherSide}}-hand side of the `{{operator}}`.",
442
+ constantShortCircuit: "Unexpected constant {{property}} on the left-hand side of a `{{operator}}` expression.",
443
+ alwaysNew: "Unexpected comparison to newly constructed object. These two values can never be equal.",
444
+ bothAlwaysNew: "Unexpected comparison of two newly constructed objects. These two values can never be equal."
445
+ }
446
+ },
447
+
448
+ create(context) {
449
+ return {
450
+ LogicalExpression(node) {
451
+ const { operator, left } = node;
452
+ const scope = context.getScope();
453
+
454
+ if ((operator === "&&" || operator === "||") && isConstant(scope, left, true)) {
455
+ context.report({ node: left, messageId: "constantShortCircuit", data: { property: "truthiness", operator } });
456
+ } else if (operator === "??" && hasConstantNullishness(scope, left)) {
457
+ context.report({ node: left, messageId: "constantShortCircuit", data: { property: "nullishness", operator } });
458
+ }
459
+ },
460
+ BinaryExpression(node) {
461
+ const scope = context.getScope();
462
+ const { right, left, operator } = node;
463
+ const rightConstantOperand = findBinaryExpressionConstantOperand(scope, left, right, operator);
464
+ const leftConstantOperand = findBinaryExpressionConstantOperand(scope, right, left, operator);
465
+
466
+ if (rightConstantOperand) {
467
+ context.report({ node: rightConstantOperand, messageId: "constantBinaryOperand", data: { operator, otherSide: "left" } });
468
+ } else if (leftConstantOperand) {
469
+ context.report({ node: leftConstantOperand, messageId: "constantBinaryOperand", data: { operator, otherSide: "right" } });
470
+ } else if (operator === "===" || operator === "!==") {
471
+ if (isAlwaysNew(scope, left)) {
472
+ context.report({ node: left, messageId: "alwaysNew" });
473
+ } else if (isAlwaysNew(scope, right)) {
474
+ context.report({ node: right, messageId: "alwaysNew" });
475
+ }
476
+ } else if (operator === "==" || operator === "!=") {
477
+
478
+ /*
479
+ * If both sides are "new", then both sides are objects and
480
+ * therefore they will be compared by reference even with `==`
481
+ * equality.
482
+ */
483
+ if (isAlwaysNew(scope, left) && isAlwaysNew(scope, right)) {
484
+ context.report({ node: left, messageId: "bothAlwaysNew" });
485
+ }
486
+ }
487
+
488
+ }
489
+
490
+ /*
491
+ * In theory we could handle short circuting assignment operators,
492
+ * for some constant values, but that would require walking the
493
+ * scope to find the value of the variable being assigned. This is
494
+ * dependant on https://github.com/eslint/eslint/issues/13776
495
+ *
496
+ * AssignmentExpression() {},
497
+ */
498
+ };
499
+ }
500
+ };