eslint 8.13.0 → 8.14.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.
@@ -103,6 +103,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({
103
103
  "no-confusing-arrow": () => require("./no-confusing-arrow"),
104
104
  "no-console": () => require("./no-console"),
105
105
  "no-const-assign": () => require("./no-const-assign"),
106
+ "no-constant-binary-expression": () => require("./no-constant-binary-expression"),
106
107
  "no-constant-condition": () => require("./no-constant-condition"),
107
108
  "no-constructor-return": () => require("./no-constructor-return"),
108
109
  "no-continue": () => require("./no-continue"),
@@ -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
+ };
@@ -5,6 +5,8 @@
5
5
 
6
6
  "use strict";
7
7
 
8
+ const { isConstant } = require("./utils/ast-utils");
9
+
8
10
  //------------------------------------------------------------------------------
9
11
  // Helpers
10
12
  //------------------------------------------------------------------------------
@@ -53,201 +55,6 @@ module.exports = {
53
55
  // Helpers
54
56
  //--------------------------------------------------------------------------
55
57
 
56
- /**
57
- * Returns literal's value converted to the Boolean type
58
- * @param {ASTNode} node any `Literal` node
59
- * @returns {boolean | null} `true` when node is truthy, `false` when node is falsy,
60
- * `null` when it cannot be determined.
61
- */
62
- function getBooleanValue(node) {
63
- if (node.value === null) {
64
-
65
- /*
66
- * it might be a null literal or bigint/regex literal in unsupported environments .
67
- * https://github.com/estree/estree/blob/14df8a024956ea289bd55b9c2226a1d5b8a473ee/es5.md#regexpliteral
68
- * https://github.com/estree/estree/blob/14df8a024956ea289bd55b9c2226a1d5b8a473ee/es2020.md#bigintliteral
69
- */
70
-
71
- if (node.raw === "null") {
72
- return false;
73
- }
74
-
75
- // regex is always truthy
76
- if (typeof node.regex === "object") {
77
- return true;
78
- }
79
-
80
- return null;
81
- }
82
-
83
- return !!node.value;
84
- }
85
-
86
- /**
87
- * Checks if a branch node of LogicalExpression short circuits the whole condition
88
- * @param {ASTNode} node The branch of main condition which needs to be checked
89
- * @param {string} operator The operator of the main LogicalExpression.
90
- * @returns {boolean} true when condition short circuits whole condition
91
- */
92
- function isLogicalIdentity(node, operator) {
93
- switch (node.type) {
94
- case "Literal":
95
- return (operator === "||" && getBooleanValue(node) === true) ||
96
- (operator === "&&" && getBooleanValue(node) === false);
97
-
98
- case "UnaryExpression":
99
- return (operator === "&&" && node.operator === "void");
100
-
101
- case "LogicalExpression":
102
-
103
- /*
104
- * handles `a && false || b`
105
- * `false` is an identity element of `&&` but not `||`
106
- */
107
- return operator === node.operator &&
108
- (
109
- isLogicalIdentity(node.left, operator) ||
110
- isLogicalIdentity(node.right, operator)
111
- );
112
-
113
- case "AssignmentExpression":
114
- return ["||=", "&&="].includes(node.operator) &&
115
- operator === node.operator.slice(0, -1) &&
116
- isLogicalIdentity(node.right, operator);
117
-
118
- // no default
119
- }
120
- return false;
121
- }
122
-
123
- /**
124
- * Checks if an identifier is a reference to a global variable.
125
- * @param {ASTNode} node An identifier node to check.
126
- * @returns {boolean} `true` if the identifier is a reference to a global variable.
127
- */
128
- function isReferenceToGlobalVariable(node) {
129
- const scope = context.getScope();
130
- const reference = scope.references.find(ref => ref.identifier === node);
131
-
132
- return Boolean(
133
- reference &&
134
- reference.resolved &&
135
- reference.resolved.scope.type === "global" &&
136
- reference.resolved.defs.length === 0
137
- );
138
- }
139
-
140
- /**
141
- * Checks if a node has a constant truthiness value.
142
- * @param {ASTNode} node The AST node to check.
143
- * @param {boolean} inBooleanPosition `true` if checking the test of a
144
- * condition. `false` in all other cases. When `false`, checks if -- for
145
- * both string and number -- if coerced to that type, the value will
146
- * be constant.
147
- * @returns {Bool} true when node's truthiness is constant
148
- * @private
149
- */
150
- function isConstant(node, inBooleanPosition) {
151
-
152
- // node.elements can return null values in the case of sparse arrays ex. [,]
153
- if (!node) {
154
- return true;
155
- }
156
- switch (node.type) {
157
- case "Literal":
158
- case "ArrowFunctionExpression":
159
- case "FunctionExpression":
160
- return true;
161
- case "ClassExpression":
162
- case "ObjectExpression":
163
-
164
- /**
165
- * In theory objects like:
166
- *
167
- * `{toString: () => a}`
168
- * `{valueOf: () => a}`
169
- *
170
- * Or a classes like:
171
- *
172
- * `class { static toString() { return a } }`
173
- * `class { static valueOf() { return a } }`
174
- *
175
- * Are not constant verifiably when `inBooleanPosition` is
176
- * false, but it's an edge case we've opted not to handle.
177
- */
178
- return true;
179
- case "TemplateLiteral":
180
- return (inBooleanPosition && node.quasis.some(quasi => quasi.value.cooked.length)) ||
181
- node.expressions.every(exp => isConstant(exp, false));
182
-
183
- case "ArrayExpression": {
184
- if (!inBooleanPosition) {
185
- return node.elements.every(element => isConstant(element, false));
186
- }
187
- return true;
188
- }
189
-
190
- case "UnaryExpression":
191
- if (
192
- node.operator === "void" ||
193
- node.operator === "typeof" && inBooleanPosition
194
- ) {
195
- return true;
196
- }
197
-
198
- if (node.operator === "!") {
199
- return isConstant(node.argument, true);
200
- }
201
-
202
- return isConstant(node.argument, false);
203
-
204
- case "BinaryExpression":
205
- return isConstant(node.left, false) &&
206
- isConstant(node.right, false) &&
207
- node.operator !== "in";
208
-
209
- case "LogicalExpression": {
210
- const isLeftConstant = isConstant(node.left, inBooleanPosition);
211
- const isRightConstant = isConstant(node.right, inBooleanPosition);
212
- const isLeftShortCircuit = (isLeftConstant && isLogicalIdentity(node.left, node.operator));
213
- const isRightShortCircuit = (inBooleanPosition && isRightConstant && isLogicalIdentity(node.right, node.operator));
214
-
215
- return (isLeftConstant && isRightConstant) ||
216
- isLeftShortCircuit ||
217
- isRightShortCircuit;
218
- }
219
- case "NewExpression":
220
- return inBooleanPosition;
221
- case "AssignmentExpression":
222
- if (node.operator === "=") {
223
- return isConstant(node.right, inBooleanPosition);
224
- }
225
-
226
- if (["||=", "&&="].includes(node.operator) && inBooleanPosition) {
227
- return isLogicalIdentity(node.right, node.operator.slice(0, -1));
228
- }
229
-
230
- return false;
231
-
232
- case "SequenceExpression":
233
- return isConstant(node.expressions[node.expressions.length - 1], inBooleanPosition);
234
- case "SpreadElement":
235
- return isConstant(node.argument, inBooleanPosition);
236
- case "CallExpression":
237
- if (node.callee.type === "Identifier" && node.callee.name === "Boolean") {
238
- if (node.arguments.length === 0 || isConstant(node.arguments[0], true)) {
239
- return isReferenceToGlobalVariable(node.callee);
240
- }
241
- }
242
- return false;
243
- case "Identifier":
244
- return node.name === "undefined" && isReferenceToGlobalVariable(node);
245
-
246
- // no default
247
- }
248
- return false;
249
- }
250
-
251
58
  /**
252
59
  * Tracks when the given node contains a constant condition.
253
60
  * @param {ASTNode} node The AST node to check.
@@ -255,7 +62,7 @@ module.exports = {
255
62
  * @private
256
63
  */
257
64
  function trackConstantConditionLoop(node) {
258
- if (node.test && isConstant(node.test, true)) {
65
+ if (node.test && isConstant(context.getScope(), node.test, true)) {
259
66
  loopsInCurrentScope.add(node);
260
67
  }
261
68
  }
@@ -280,7 +87,7 @@ module.exports = {
280
87
  * @private
281
88
  */
282
89
  function reportIfConstant(node) {
283
- if (node.test && isConstant(node.test, true)) {
90
+ if (node.test && isConstant(context.getScope(), node.test, true)) {
284
91
  context.report({ node: node.test, messageId: "unexpected" });
285
92
  }
286
93
  }
@@ -32,6 +32,7 @@ const thisTagPattern = /^[\s*]*@this/mu;
32
32
 
33
33
 
34
34
  const COMMENTS_IGNORE_PATTERN = /^\s*(?:eslint|jshint\s+|jslint\s+|istanbul\s+|globals?\s+|exported\s+|jscs)/u;
35
+ const ESLINT_DIRECTIVE_PATTERN = /^(?:eslint[- ]|(?:globals?|exported) )/u;
35
36
  const LINEBREAKS = new Set(["\r\n", "\r", "\n", "\u2028", "\u2029"]);
36
37
 
37
38
  // A set of node types that can contain a list of statements
@@ -788,6 +789,203 @@ function getModuleExportName(node) {
788
789
  return node.value;
789
790
  }
790
791
 
792
+ /**
793
+ * Returns literal's value converted to the Boolean type
794
+ * @param {ASTNode} node any `Literal` node
795
+ * @returns {boolean | null} `true` when node is truthy, `false` when node is falsy,
796
+ * `null` when it cannot be determined.
797
+ */
798
+ function getBooleanValue(node) {
799
+ if (node.value === null) {
800
+
801
+ /*
802
+ * it might be a null literal or bigint/regex literal in unsupported environments .
803
+ * https://github.com/estree/estree/blob/14df8a024956ea289bd55b9c2226a1d5b8a473ee/es5.md#regexpliteral
804
+ * https://github.com/estree/estree/blob/14df8a024956ea289bd55b9c2226a1d5b8a473ee/es2020.md#bigintliteral
805
+ */
806
+
807
+ if (node.raw === "null") {
808
+ return false;
809
+ }
810
+
811
+ // regex is always truthy
812
+ if (typeof node.regex === "object") {
813
+ return true;
814
+ }
815
+
816
+ return null;
817
+ }
818
+
819
+ return !!node.value;
820
+ }
821
+
822
+ /**
823
+ * Checks if a branch node of LogicalExpression short circuits the whole condition
824
+ * @param {ASTNode} node The branch of main condition which needs to be checked
825
+ * @param {string} operator The operator of the main LogicalExpression.
826
+ * @returns {boolean} true when condition short circuits whole condition
827
+ */
828
+ function isLogicalIdentity(node, operator) {
829
+ switch (node.type) {
830
+ case "Literal":
831
+ return (operator === "||" && getBooleanValue(node) === true) ||
832
+ (operator === "&&" && getBooleanValue(node) === false);
833
+
834
+ case "UnaryExpression":
835
+ return (operator === "&&" && node.operator === "void");
836
+
837
+ case "LogicalExpression":
838
+
839
+ /*
840
+ * handles `a && false || b`
841
+ * `false` is an identity element of `&&` but not `||`
842
+ */
843
+ return operator === node.operator &&
844
+ (
845
+ isLogicalIdentity(node.left, operator) ||
846
+ isLogicalIdentity(node.right, operator)
847
+ );
848
+
849
+ case "AssignmentExpression":
850
+ return ["||=", "&&="].includes(node.operator) &&
851
+ operator === node.operator.slice(0, -1) &&
852
+ isLogicalIdentity(node.right, operator);
853
+
854
+ // no default
855
+ }
856
+ return false;
857
+ }
858
+
859
+ /**
860
+ * Checks if an identifier is a reference to a global variable.
861
+ * @param {Scope} scope The scope in which the identifier is referenced.
862
+ * @param {ASTNode} node An identifier node to check.
863
+ * @returns {boolean} `true` if the identifier is a reference to a global variable.
864
+ */
865
+ function isReferenceToGlobalVariable(scope, node) {
866
+ const reference = scope.references.find(ref => ref.identifier === node);
867
+
868
+ return Boolean(
869
+ reference &&
870
+ reference.resolved &&
871
+ reference.resolved.scope.type === "global" &&
872
+ reference.resolved.defs.length === 0
873
+ );
874
+ }
875
+
876
+
877
+ /**
878
+ * Checks if a node has a constant truthiness value.
879
+ * @param {Scope} scope Scope in which the node appears.
880
+ * @param {ASTNode} node The AST node to check.
881
+ * @param {boolean} inBooleanPosition `true` if checking the test of a
882
+ * condition. `false` in all other cases. When `false`, checks if -- for
883
+ * both string and number -- if coerced to that type, the value will
884
+ * be constant.
885
+ * @returns {boolean} true when node's truthiness is constant
886
+ * @private
887
+ */
888
+ function isConstant(scope, node, inBooleanPosition) {
889
+
890
+ // node.elements can return null values in the case of sparse arrays ex. [,]
891
+ if (!node) {
892
+ return true;
893
+ }
894
+ switch (node.type) {
895
+ case "Literal":
896
+ case "ArrowFunctionExpression":
897
+ case "FunctionExpression":
898
+ return true;
899
+ case "ClassExpression":
900
+ case "ObjectExpression":
901
+
902
+ /**
903
+ * In theory objects like:
904
+ *
905
+ * `{toString: () => a}`
906
+ * `{valueOf: () => a}`
907
+ *
908
+ * Or a classes like:
909
+ *
910
+ * `class { static toString() { return a } }`
911
+ * `class { static valueOf() { return a } }`
912
+ *
913
+ * Are not constant verifiably when `inBooleanPosition` is
914
+ * false, but it's an edge case we've opted not to handle.
915
+ */
916
+ return true;
917
+ case "TemplateLiteral":
918
+ return (inBooleanPosition && node.quasis.some(quasi => quasi.value.cooked.length)) ||
919
+ node.expressions.every(exp => isConstant(scope, exp, false));
920
+
921
+ case "ArrayExpression": {
922
+ if (!inBooleanPosition) {
923
+ return node.elements.every(element => isConstant(scope, element, false));
924
+ }
925
+ return true;
926
+ }
927
+
928
+ case "UnaryExpression":
929
+ if (
930
+ node.operator === "void" ||
931
+ node.operator === "typeof" && inBooleanPosition
932
+ ) {
933
+ return true;
934
+ }
935
+
936
+ if (node.operator === "!") {
937
+ return isConstant(scope, node.argument, true);
938
+ }
939
+
940
+ return isConstant(scope, node.argument, false);
941
+
942
+ case "BinaryExpression":
943
+ return isConstant(scope, node.left, false) &&
944
+ isConstant(scope, node.right, false) &&
945
+ node.operator !== "in";
946
+
947
+ case "LogicalExpression": {
948
+ const isLeftConstant = isConstant(scope, node.left, inBooleanPosition);
949
+ const isRightConstant = isConstant(scope, node.right, inBooleanPosition);
950
+ const isLeftShortCircuit = (isLeftConstant && isLogicalIdentity(node.left, node.operator));
951
+ const isRightShortCircuit = (inBooleanPosition && isRightConstant && isLogicalIdentity(node.right, node.operator));
952
+
953
+ return (isLeftConstant && isRightConstant) ||
954
+ isLeftShortCircuit ||
955
+ isRightShortCircuit;
956
+ }
957
+ case "NewExpression":
958
+ return inBooleanPosition;
959
+ case "AssignmentExpression":
960
+ if (node.operator === "=") {
961
+ return isConstant(scope, node.right, inBooleanPosition);
962
+ }
963
+
964
+ if (["||=", "&&="].includes(node.operator) && inBooleanPosition) {
965
+ return isLogicalIdentity(node.right, node.operator.slice(0, -1));
966
+ }
967
+
968
+ return false;
969
+
970
+ case "SequenceExpression":
971
+ return isConstant(scope, node.expressions[node.expressions.length - 1], inBooleanPosition);
972
+ case "SpreadElement":
973
+ return isConstant(scope, node.argument, inBooleanPosition);
974
+ case "CallExpression":
975
+ if (node.callee.type === "Identifier" && node.callee.name === "Boolean") {
976
+ if (node.arguments.length === 0 || isConstant(scope, node.arguments[0], true)) {
977
+ return isReferenceToGlobalVariable(scope, node.callee);
978
+ }
979
+ }
980
+ return false;
981
+ case "Identifier":
982
+ return node.name === "undefined" && isReferenceToGlobalVariable(scope, node);
983
+
984
+ // no default
985
+ }
986
+ return false;
987
+ }
988
+
791
989
  //------------------------------------------------------------------------------
792
990
  // Public Interface
793
991
  //------------------------------------------------------------------------------
@@ -908,12 +1106,8 @@ module.exports = {
908
1106
  const comment = node.value.trim();
909
1107
 
910
1108
  return (
911
- node.type === "Line" && comment.indexOf("eslint-") === 0 ||
912
- node.type === "Block" && (
913
- comment.indexOf("global ") === 0 ||
914
- comment.indexOf("eslint ") === 0 ||
915
- comment.indexOf("eslint-") === 0
916
- )
1109
+ node.type === "Line" && comment.startsWith("eslint-") ||
1110
+ node.type === "Block" && ESLINT_DIRECTIVE_PATTERN.test(comment)
917
1111
  );
918
1112
  },
919
1113
 
@@ -1905,6 +2099,7 @@ module.exports = {
1905
2099
  return OCTAL_OR_NON_OCTAL_DECIMAL_ESCAPE_PATTERN.test(rawString);
1906
2100
  },
1907
2101
 
2102
+ isReferenceToGlobalVariable,
1908
2103
  isLogicalExpression,
1909
2104
  isCoalesceExpression,
1910
2105
  isMixedLogicalAndCoalesceExpressions,
@@ -1918,5 +2113,6 @@ module.exports = {
1918
2113
  isSameReference,
1919
2114
  isLogicalAssignmentOperator,
1920
2115
  getSwitchCaseColonToken,
1921
- getModuleExportName
2116
+ getModuleExportName,
2117
+ isConstant
1922
2118
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint",
3
- "version": "8.13.0",
3
+ "version": "8.14.0",
4
4
  "author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>",
5
5
  "description": "An AST-based pattern checker for JavaScript.",
6
6
  "bin": {
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "lint-staged": {
34
34
  "*.js": "eslint --fix",
35
- "*.md": "markdownlint"
35
+ "*.md": "markdownlint --fix"
36
36
  },
37
37
  "files": [
38
38
  "LICENSE",
@@ -47,7 +47,7 @@
47
47
  "homepage": "https://eslint.org",
48
48
  "bugs": "https://github.com/eslint/eslint/issues/",
49
49
  "dependencies": {
50
- "@eslint/eslintrc": "^1.2.1",
50
+ "@eslint/eslintrc": "^1.2.2",
51
51
  "@humanwhocodes/config-array": "^0.9.2",
52
52
  "ajv": "^6.10.0",
53
53
  "chalk": "^4.0.0",