[d565449] | 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 | };
|
---|