Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 

368 rindas
12 KiB

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