no-constant-binary-expression.js 18 KB

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