no-unmodified-loop-condition.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. /**
  2. * @fileoverview Rule to disallow use of unmodified expressions in loop conditions
  3. * @author Toru Nagashima
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const Traverser = require("../shared/traverser"),
  10. astUtils = require("./utils/ast-utils");
  11. //------------------------------------------------------------------------------
  12. // Helpers
  13. //------------------------------------------------------------------------------
  14. const SENTINEL_PATTERN =
  15. /(?:(?:Call|Class|Function|Member|New|Yield)Expression|Statement|Declaration)$/u;
  16. const LOOP_PATTERN = /^(?:DoWhile|For|While)Statement$/u; // for-in/of statements don't have `test` property.
  17. const GROUP_PATTERN = /^(?:BinaryExpression|ConditionalExpression)$/u;
  18. const SKIP_PATTERN = /^(?:ArrowFunction|Class|Function)Expression$/u;
  19. const DYNAMIC_PATTERN = /^(?:Call|Member|New|TaggedTemplate|Yield)Expression$/u;
  20. /**
  21. * @typedef {Object} LoopConditionInfo
  22. * @property {eslint-scope.Reference} reference - The reference.
  23. * @property {ASTNode} group - BinaryExpression or ConditionalExpression nodes
  24. * that the reference is belonging to.
  25. * @property {Function} isInLoop - The predicate which checks a given reference
  26. * is in this loop.
  27. * @property {boolean} modified - The flag that the reference is modified in
  28. * this loop.
  29. */
  30. /**
  31. * Checks whether or not a given reference is a write reference.
  32. * @param {eslint-scope.Reference} reference A reference to check.
  33. * @returns {boolean} `true` if the reference is a write reference.
  34. */
  35. function isWriteReference(reference) {
  36. if (reference.init) {
  37. const def = reference.resolved && reference.resolved.defs[0];
  38. if (!def || def.type !== "Variable" || def.parent.kind !== "var") {
  39. return false;
  40. }
  41. }
  42. return reference.isWrite();
  43. }
  44. /**
  45. * Checks whether or not a given loop condition info does not have the modified
  46. * flag.
  47. * @param {LoopConditionInfo} condition A loop condition info to check.
  48. * @returns {boolean} `true` if the loop condition info is "unmodified".
  49. */
  50. function isUnmodified(condition) {
  51. return !condition.modified;
  52. }
  53. /**
  54. * Checks whether or not a given loop condition info does not have the modified
  55. * flag and does not have the group this condition belongs to.
  56. * @param {LoopConditionInfo} condition A loop condition info to check.
  57. * @returns {boolean} `true` if the loop condition info is "unmodified".
  58. */
  59. function isUnmodifiedAndNotBelongToGroup(condition) {
  60. return !(condition.modified || condition.group);
  61. }
  62. /**
  63. * Checks whether or not a given reference is inside of a given node.
  64. * @param {ASTNode} node A node to check.
  65. * @param {eslint-scope.Reference} reference A reference to check.
  66. * @returns {boolean} `true` if the reference is inside of the node.
  67. */
  68. function isInRange(node, reference) {
  69. const or = node.range;
  70. const ir = reference.identifier.range;
  71. return or[0] <= ir[0] && ir[1] <= or[1];
  72. }
  73. /**
  74. * Checks whether or not a given reference is inside of a loop node's condition.
  75. * @param {ASTNode} node A node to check.
  76. * @param {eslint-scope.Reference} reference A reference to check.
  77. * @returns {boolean} `true` if the reference is inside of the loop node's
  78. * condition.
  79. */
  80. const isInLoop = {
  81. WhileStatement: isInRange,
  82. DoWhileStatement: isInRange,
  83. ForStatement(node, reference) {
  84. return (
  85. isInRange(node, reference) &&
  86. !(node.init && isInRange(node.init, reference))
  87. );
  88. },
  89. };
  90. /**
  91. * Gets the function which encloses a given reference.
  92. * This supports only FunctionDeclaration.
  93. * @param {eslint-scope.Reference} reference A reference to get.
  94. * @returns {ASTNode|null} The function node or null.
  95. */
  96. function getEncloseFunctionDeclaration(reference) {
  97. let node = reference.identifier;
  98. while (node) {
  99. if (node.type === "FunctionDeclaration") {
  100. return node.id ? node : null;
  101. }
  102. node = node.parent;
  103. }
  104. return null;
  105. }
  106. /**
  107. * Updates the "modified" flags of given loop conditions with given modifiers.
  108. * @param {LoopConditionInfo[]} conditions The loop conditions to be updated.
  109. * @param {eslint-scope.Reference[]} modifiers The references to update.
  110. * @returns {void}
  111. */
  112. function updateModifiedFlag(conditions, modifiers) {
  113. for (let i = 0; i < conditions.length; ++i) {
  114. const condition = conditions[i];
  115. for (let j = 0; !condition.modified && j < modifiers.length; ++j) {
  116. const modifier = modifiers[j];
  117. let funcNode, funcVar;
  118. /*
  119. * Besides checking for the condition being in the loop, we want to
  120. * check the function that this modifier is belonging to is called
  121. * in the loop.
  122. * FIXME: This should probably be extracted to a function.
  123. */
  124. const inLoop =
  125. condition.isInLoop(modifier) ||
  126. Boolean(
  127. (funcNode = getEncloseFunctionDeclaration(modifier)) &&
  128. (funcVar = astUtils.getVariableByName(
  129. modifier.from.upper,
  130. funcNode.id.name,
  131. )) &&
  132. funcVar.references.some(condition.isInLoop),
  133. );
  134. condition.modified = inLoop;
  135. }
  136. }
  137. }
  138. //------------------------------------------------------------------------------
  139. // Rule Definition
  140. //------------------------------------------------------------------------------
  141. /** @type {import('../types').Rule.RuleModule} */
  142. module.exports = {
  143. meta: {
  144. type: "problem",
  145. docs: {
  146. description: "Disallow unmodified loop conditions",
  147. recommended: false,
  148. url: "https://eslint.org/docs/latest/rules/no-unmodified-loop-condition",
  149. },
  150. schema: [],
  151. messages: {
  152. loopConditionNotModified:
  153. "'{{name}}' is not modified in this loop.",
  154. },
  155. },
  156. create(context) {
  157. const sourceCode = context.sourceCode;
  158. let groupMap = null;
  159. /**
  160. * Reports a given condition info.
  161. * @param {LoopConditionInfo} condition A loop condition info to report.
  162. * @returns {void}
  163. */
  164. function report(condition) {
  165. const node = condition.reference.identifier;
  166. context.report({
  167. node,
  168. messageId: "loopConditionNotModified",
  169. data: node,
  170. });
  171. }
  172. /**
  173. * Registers given conditions to the group the condition belongs to.
  174. * @param {LoopConditionInfo[]} conditions A loop condition info to
  175. * register.
  176. * @returns {void}
  177. */
  178. function registerConditionsToGroup(conditions) {
  179. for (let i = 0; i < conditions.length; ++i) {
  180. const condition = conditions[i];
  181. if (condition.group) {
  182. let group = groupMap.get(condition.group);
  183. if (!group) {
  184. group = [];
  185. groupMap.set(condition.group, group);
  186. }
  187. group.push(condition);
  188. }
  189. }
  190. }
  191. /**
  192. * Reports references which are inside of unmodified groups.
  193. * @param {LoopConditionInfo[]} conditions A loop condition info to report.
  194. * @returns {void}
  195. */
  196. function checkConditionsInGroup(conditions) {
  197. if (conditions.every(isUnmodified)) {
  198. conditions.forEach(report);
  199. }
  200. }
  201. /**
  202. * Checks whether or not a given group node has any dynamic elements.
  203. * @param {ASTNode} root A node to check.
  204. * This node is one of BinaryExpression or ConditionalExpression.
  205. * @returns {boolean} `true` if the node is dynamic.
  206. */
  207. function hasDynamicExpressions(root) {
  208. let retv = false;
  209. Traverser.traverse(root, {
  210. visitorKeys: sourceCode.visitorKeys,
  211. enter(node) {
  212. if (DYNAMIC_PATTERN.test(node.type)) {
  213. retv = true;
  214. this.break();
  215. } else if (SKIP_PATTERN.test(node.type)) {
  216. this.skip();
  217. }
  218. },
  219. });
  220. return retv;
  221. }
  222. /**
  223. * Creates the loop condition information from a given reference.
  224. * @param {eslint-scope.Reference} reference A reference to create.
  225. * @returns {LoopConditionInfo|null} Created loop condition info, or null.
  226. */
  227. function toLoopCondition(reference) {
  228. if (reference.init) {
  229. return null;
  230. }
  231. let group = null;
  232. let child = reference.identifier;
  233. let node = child.parent;
  234. while (node) {
  235. if (SENTINEL_PATTERN.test(node.type)) {
  236. if (LOOP_PATTERN.test(node.type) && node.test === child) {
  237. // This reference is inside of a loop condition.
  238. return {
  239. reference,
  240. group,
  241. isInLoop: isInLoop[node.type].bind(null, node),
  242. modified: false,
  243. };
  244. }
  245. // This reference is outside of a loop condition.
  246. break;
  247. }
  248. /*
  249. * If it's inside of a group, OK if either operand is modified.
  250. * So stores the group this reference belongs to.
  251. */
  252. if (GROUP_PATTERN.test(node.type)) {
  253. // If this expression is dynamic, no need to check.
  254. if (hasDynamicExpressions(node)) {
  255. break;
  256. } else {
  257. group = node;
  258. }
  259. }
  260. child = node;
  261. node = node.parent;
  262. }
  263. return null;
  264. }
  265. /**
  266. * Finds unmodified references which are inside of a loop condition.
  267. * Then reports the references which are outside of groups.
  268. * @param {eslint-scope.Variable} variable A variable to report.
  269. * @returns {void}
  270. */
  271. function checkReferences(variable) {
  272. // Gets references that exist in loop conditions.
  273. const conditions = variable.references
  274. .map(toLoopCondition)
  275. .filter(Boolean);
  276. if (conditions.length === 0) {
  277. return;
  278. }
  279. // Registers the conditions to belonging groups.
  280. registerConditionsToGroup(conditions);
  281. // Check the conditions are modified.
  282. const modifiers = variable.references.filter(isWriteReference);
  283. if (modifiers.length > 0) {
  284. updateModifiedFlag(conditions, modifiers);
  285. }
  286. /*
  287. * Reports the conditions which are not belonging to groups.
  288. * Others will be reported after all variables are done.
  289. */
  290. conditions.filter(isUnmodifiedAndNotBelongToGroup).forEach(report);
  291. }
  292. return {
  293. "Program:exit"(node) {
  294. const queue = [sourceCode.getScope(node)];
  295. groupMap = new Map();
  296. let scope;
  297. while ((scope = queue.pop())) {
  298. queue.push(...scope.childScopes);
  299. scope.variables.forEach(checkReferences);
  300. }
  301. groupMap.forEach(checkConditionsInGroup);
  302. groupMap = null;
  303. },
  304. };
  305. },
  306. };