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