source: imaps-frontend/node_modules/eslint/lib/rules/no-constant-binary-expression.js

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

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