no-constant-condition.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  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, node.operator) ||
  90. isLogicalIdentity(node.right, node.operator)
  91. );
  92. // no default
  93. }
  94. return false;
  95. }
  96. /**
  97. * Checks if a node has a constant truthiness value.
  98. * @param {ASTNode} node The AST node to check.
  99. * @param {boolean} inBooleanPosition `false` if checking branch of a condition.
  100. * `true` in all other cases
  101. * @returns {Bool} true when node's truthiness is constant
  102. * @private
  103. */
  104. function isConstant(node, inBooleanPosition) {
  105. // node.elements can return null values in the case of sparse arrays ex. [,]
  106. if (!node) {
  107. return true;
  108. }
  109. switch (node.type) {
  110. case "Literal":
  111. case "ArrowFunctionExpression":
  112. case "FunctionExpression":
  113. case "ObjectExpression":
  114. return true;
  115. case "TemplateLiteral":
  116. return (inBooleanPosition && node.quasis.some(quasi => quasi.value.cooked.length)) ||
  117. node.expressions.every(exp => isConstant(exp, inBooleanPosition));
  118. case "ArrayExpression": {
  119. if (node.parent.type === "BinaryExpression" && node.parent.operator === "+") {
  120. return node.elements.every(element => isConstant(element, false));
  121. }
  122. return true;
  123. }
  124. case "UnaryExpression":
  125. if (node.operator === "void") {
  126. return true;
  127. }
  128. return (node.operator === "typeof" && inBooleanPosition) ||
  129. isConstant(node.argument, true);
  130. case "BinaryExpression":
  131. return isConstant(node.left, false) &&
  132. isConstant(node.right, false) &&
  133. node.operator !== "in";
  134. case "LogicalExpression": {
  135. const isLeftConstant = isConstant(node.left, inBooleanPosition);
  136. const isRightConstant = isConstant(node.right, inBooleanPosition);
  137. const isLeftShortCircuit = (isLeftConstant && isLogicalIdentity(node.left, node.operator));
  138. const isRightShortCircuit = (inBooleanPosition && isRightConstant && isLogicalIdentity(node.right, node.operator));
  139. return (isLeftConstant && isRightConstant) ||
  140. isLeftShortCircuit ||
  141. isRightShortCircuit;
  142. }
  143. case "AssignmentExpression":
  144. return (node.operator === "=") && isConstant(node.right, inBooleanPosition);
  145. case "SequenceExpression":
  146. return isConstant(node.expressions[node.expressions.length - 1], inBooleanPosition);
  147. // no default
  148. }
  149. return false;
  150. }
  151. /**
  152. * Tracks when the given node contains a constant condition.
  153. * @param {ASTNode} node The AST node to check.
  154. * @returns {void}
  155. * @private
  156. */
  157. function trackConstantConditionLoop(node) {
  158. if (node.test && isConstant(node.test, true)) {
  159. loopsInCurrentScope.add(node);
  160. }
  161. }
  162. /**
  163. * Reports when the set contains the given constant condition node
  164. * @param {ASTNode} node The AST node to check.
  165. * @returns {void}
  166. * @private
  167. */
  168. function checkConstantConditionLoopInSet(node) {
  169. if (loopsInCurrentScope.has(node)) {
  170. loopsInCurrentScope.delete(node);
  171. context.report({ node: node.test, messageId: "unexpected" });
  172. }
  173. }
  174. /**
  175. * Reports when the given node contains a constant condition.
  176. * @param {ASTNode} node The AST node to check.
  177. * @returns {void}
  178. * @private
  179. */
  180. function reportIfConstant(node) {
  181. if (node.test && isConstant(node.test, true)) {
  182. context.report({ node: node.test, messageId: "unexpected" });
  183. }
  184. }
  185. /**
  186. * Stores current set of constant loops in loopSetStack temporarily
  187. * and uses a new set to track constant loops
  188. * @returns {void}
  189. * @private
  190. */
  191. function enterFunction() {
  192. loopSetStack.push(loopsInCurrentScope);
  193. loopsInCurrentScope = new Set();
  194. }
  195. /**
  196. * Reports when the set still contains stored constant conditions
  197. * @returns {void}
  198. * @private
  199. */
  200. function exitFunction() {
  201. loopsInCurrentScope = loopSetStack.pop();
  202. }
  203. /**
  204. * Checks node when checkLoops option is enabled
  205. * @param {ASTNode} node The AST node to check.
  206. * @returns {void}
  207. * @private
  208. */
  209. function checkLoop(node) {
  210. if (checkLoops) {
  211. trackConstantConditionLoop(node);
  212. }
  213. }
  214. //--------------------------------------------------------------------------
  215. // Public
  216. //--------------------------------------------------------------------------
  217. return {
  218. ConditionalExpression: reportIfConstant,
  219. IfStatement: reportIfConstant,
  220. WhileStatement: checkLoop,
  221. "WhileStatement:exit": checkConstantConditionLoopInSet,
  222. DoWhileStatement: checkLoop,
  223. "DoWhileStatement:exit": checkConstantConditionLoopInSet,
  224. ForStatement: checkLoop,
  225. "ForStatement > .test": node => checkLoop(node.parent),
  226. "ForStatement:exit": checkConstantConditionLoopInSet,
  227. FunctionDeclaration: enterFunction,
  228. "FunctionDeclaration:exit": exitFunction,
  229. FunctionExpression: enterFunction,
  230. "FunctionExpression:exit": exitFunction,
  231. YieldExpression: () => loopsInCurrentScope.clear()
  232. };
  233. }
  234. };