| [9af201e] | 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 | * 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 | */
|
|---|
| 26 | function 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 | */
|
|---|
| 44 | function 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 | */
|
|---|
| 130 | function 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 | */
|
|---|
| 158 | function 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 | */
|
|---|
| 273 | function 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 | */
|
|---|
| 358 | function 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 | */
|
|---|
| 414 | function 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} */
|
|---|
| 438 | module.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 | };
|
|---|