no-constant-condition.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. /**
  2. * @fileoverview Rule to flag use constant conditions
  3. * @author Christian Schulz <http://rndm.de>
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Helpers
  8. //------------------------------------------------------------------------------
  9. //------------------------------------------------------------------------------
  10. // Rule Definition
  11. //------------------------------------------------------------------------------
  12. module.exports = {
  13. meta: {
  14. type: "problem",
  15. docs: {
  16. description: "disallow constant expressions in conditions",
  17. category: "Possible Errors",
  18. recommended: true,
  19. url: "https://eslint.org/docs/rules/no-constant-condition"
  20. },
  21. schema: [
  22. {
  23. type: "object",
  24. properties: {
  25. checkLoops: {
  26. type: "boolean",
  27. default: true
  28. }
  29. },
  30. additionalProperties: false
  31. }
  32. ],
  33. messages: {
  34. unexpected: "Unexpected constant condition."
  35. }
  36. },
  37. create(context) {
  38. const options = context.options[0] || {},
  39. checkLoops = options.checkLoops !== false,
  40. loopSetStack = [];
  41. let loopsInCurrentScope = new Set();
  42. //--------------------------------------------------------------------------
  43. // Helpers
  44. //--------------------------------------------------------------------------
  45. /**
  46. * Returns literal's value converted to the Boolean type
  47. * @param {ASTNode} node any `Literal` node
  48. * @returns {boolean | null} `true` when node is truthy, `false` when node is falsy,
  49. * `null` when it cannot be determined.
  50. */
  51. function getBooleanValue(node) {
  52. if (node.value === null) {
  53. /*
  54. * it might be a null literal or bigint/regex literal in unsupported environments .
  55. * https://github.com/estree/estree/blob/14df8a024956ea289bd55b9c2226a1d5b8a473ee/es5.md#regexpliteral
  56. * https://github.com/estree/estree/blob/14df8a024956ea289bd55b9c2226a1d5b8a473ee/es2020.md#bigintliteral
  57. */
  58. if (node.raw === "null") {
  59. return false;
  60. }
  61. // regex is always truthy
  62. if (typeof node.regex === "object") {
  63. return true;
  64. }
  65. return null;
  66. }
  67. return !!node.value;
  68. }
  69. /**
  70. * Checks if a branch node of LogicalExpression short circuits the whole condition
  71. * @param {ASTNode} node The branch of main condition which needs to be checked
  72. * @param {string} operator The operator of the main LogicalExpression.
  73. * @returns {boolean} true when condition short circuits whole condition
  74. */
  75. function isLogicalIdentity(node, operator) {
  76. switch (node.type) {
  77. case "Literal":
  78. return (operator === "||" && getBooleanValue(node) === true) ||
  79. (operator === "&&" && getBooleanValue(node) === false);
  80. case "UnaryExpression":
  81. return (operator === "&&" && node.operator === "void");
  82. case "LogicalExpression":
  83. /*
  84. * handles `a && false || b`
  85. * `false` is an identity element of `&&` but not `||`
  86. */
  87. return operator === node.operator &&
  88. (
  89. isLogicalIdentity(node.left, operator) ||
  90. isLogicalIdentity(node.right, operator)
  91. );
  92. case "AssignmentExpression":
  93. return ["||=", "&&="].includes(node.operator) &&
  94. operator === node.operator.slice(0, -1) &&
  95. isLogicalIdentity(node.right, operator);
  96. // no default
  97. }
  98. return false;
  99. }
  100. /**
  101. * Checks if a node has a constant truthiness value.
  102. * @param {ASTNode} node The AST node to check.
  103. * @param {boolean} inBooleanPosition `false` if checking branch of a condition.
  104. * `true` in all other cases
  105. * @returns {Bool} true when node's truthiness is constant
  106. * @private
  107. */
  108. function isConstant(node, inBooleanPosition) {
  109. // node.elements can return null values in the case of sparse arrays ex. [,]
  110. if (!node) {
  111. return true;
  112. }
  113. switch (node.type) {
  114. case "Literal":
  115. case "ArrowFunctionExpression":
  116. case "FunctionExpression":
  117. case "ObjectExpression":
  118. return true;
  119. case "TemplateLiteral":
  120. return (inBooleanPosition && node.quasis.some(quasi => quasi.value.cooked.length)) ||
  121. node.expressions.every(exp => isConstant(exp, inBooleanPosition));
  122. case "ArrayExpression": {
  123. if (node.parent.type === "BinaryExpression" && node.parent.operator === "+") {
  124. return node.elements.every(element => isConstant(element, false));
  125. }
  126. return true;
  127. }
  128. case "UnaryExpression":
  129. if (
  130. node.operator === "void" ||
  131. node.operator === "typeof" && inBooleanPosition
  132. ) {
  133. return true;
  134. }
  135. if (node.operator === "!") {
  136. return isConstant(node.argument, true);
  137. }
  138. return isConstant(node.argument, false);
  139. case "BinaryExpression":
  140. return isConstant(node.left, false) &&
  141. isConstant(node.right, false) &&
  142. node.operator !== "in";
  143. case "LogicalExpression": {
  144. const isLeftConstant = isConstant(node.left, inBooleanPosition);
  145. const isRightConstant = isConstant(node.right, inBooleanPosition);
  146. const isLeftShortCircuit = (isLeftConstant && isLogicalIdentity(node.left, node.operator));
  147. const isRightShortCircuit = (inBooleanPosition && isRightConstant && isLogicalIdentity(node.right, node.operator));
  148. return (isLeftConstant && isRightConstant) ||
  149. isLeftShortCircuit ||
  150. isRightShortCircuit;
  151. }
  152. case "AssignmentExpression":
  153. if (node.operator === "=") {
  154. return isConstant(node.right, inBooleanPosition);
  155. }
  156. if (["||=", "&&="].includes(node.operator) && inBooleanPosition) {
  157. return isLogicalIdentity(node.right, node.operator.slice(0, -1));
  158. }
  159. return false;
  160. case "SequenceExpression":
  161. return isConstant(node.expressions[node.expressions.length - 1], inBooleanPosition);
  162. // no default
  163. }
  164. return false;
  165. }
  166. /**
  167. * Tracks when the given node contains a constant condition.
  168. * @param {ASTNode} node The AST node to check.
  169. * @returns {void}
  170. * @private
  171. */
  172. function trackConstantConditionLoop(node) {
  173. if (node.test && isConstant(node.test, true)) {
  174. loopsInCurrentScope.add(node);
  175. }
  176. }
  177. /**
  178. * Reports when the set contains the given constant condition node
  179. * @param {ASTNode} node The AST node to check.
  180. * @returns {void}
  181. * @private
  182. */
  183. function checkConstantConditionLoopInSet(node) {
  184. if (loopsInCurrentScope.has(node)) {
  185. loopsInCurrentScope.delete(node);
  186. context.report({ node: node.test, messageId: "unexpected" });
  187. }
  188. }
  189. /**
  190. * Reports when the given node contains a constant condition.
  191. * @param {ASTNode} node The AST node to check.
  192. * @returns {void}
  193. * @private
  194. */
  195. function reportIfConstant(node) {
  196. if (node.test && isConstant(node.test, true)) {
  197. context.report({ node: node.test, messageId: "unexpected" });
  198. }
  199. }
  200. /**
  201. * Stores current set of constant loops in loopSetStack temporarily
  202. * and uses a new set to track constant loops
  203. * @returns {void}
  204. * @private
  205. */
  206. function enterFunction() {
  207. loopSetStack.push(loopsInCurrentScope);
  208. loopsInCurrentScope = new Set();
  209. }
  210. /**
  211. * Reports when the set still contains stored constant conditions
  212. * @returns {void}
  213. * @private
  214. */
  215. function exitFunction() {
  216. loopsInCurrentScope = loopSetStack.pop();
  217. }
  218. /**
  219. * Checks node when checkLoops option is enabled
  220. * @param {ASTNode} node The AST node to check.
  221. * @returns {void}
  222. * @private
  223. */
  224. function checkLoop(node) {
  225. if (checkLoops) {
  226. trackConstantConditionLoop(node);
  227. }
  228. }
  229. //--------------------------------------------------------------------------
  230. // Public
  231. //--------------------------------------------------------------------------
  232. return {
  233. ConditionalExpression: reportIfConstant,
  234. IfStatement: reportIfConstant,
  235. WhileStatement: checkLoop,
  236. "WhileStatement:exit": checkConstantConditionLoopInSet,
  237. DoWhileStatement: checkLoop,
  238. "DoWhileStatement:exit": checkConstantConditionLoopInSet,
  239. ForStatement: checkLoop,
  240. "ForStatement > .test": node => checkLoop(node.parent),
  241. "ForStatement:exit": checkConstantConditionLoopInSet,
  242. FunctionDeclaration: enterFunction,
  243. "FunctionDeclaration:exit": exitFunction,
  244. FunctionExpression: enterFunction,
  245. "FunctionExpression:exit": exitFunction,
  246. YieldExpression: () => loopsInCurrentScope.clear()
  247. };
  248. }
  249. };