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.
package/lib/rules/index.js
CHANGED
@@ -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.
|
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.
|
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.
|
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",
|