| /** |
| * @fileoverview Rule to disallow use of unmodified expressions in loop conditions |
| * @author Toru Nagashima |
| */ |
| |
| "use strict"; |
| |
| //------------------------------------------------------------------------------ |
| // Requirements |
| //------------------------------------------------------------------------------ |
| |
| const Traverser = require("../shared/traverser"), |
| astUtils = require("./utils/ast-utils"); |
| |
| //------------------------------------------------------------------------------ |
| // Helpers |
| //------------------------------------------------------------------------------ |
| |
| const SENTINEL_PATTERN = /(?:(?:Call|Class|Function|Member|New|Yield)Expression|Statement|Declaration)$/u; |
| const LOOP_PATTERN = /^(?:DoWhile|For|While)Statement$/u; // for-in/of statements don't have `test` property. |
| const GROUP_PATTERN = /^(?:BinaryExpression|ConditionalExpression)$/u; |
| const SKIP_PATTERN = /^(?:ArrowFunction|Class|Function)Expression$/u; |
| const DYNAMIC_PATTERN = /^(?:Call|Member|New|TaggedTemplate|Yield)Expression$/u; |
| |
| /** |
| * @typedef {Object} LoopConditionInfo |
| * @property {eslint-scope.Reference} reference - The reference. |
| * @property {ASTNode} group - BinaryExpression or ConditionalExpression nodes |
| * that the reference is belonging to. |
| * @property {Function} isInLoop - The predicate which checks a given reference |
| * is in this loop. |
| * @property {boolean} modified - The flag that the reference is modified in |
| * this loop. |
| */ |
| |
| /** |
| * Checks whether or not a given reference is a write reference. |
| * |
| * @param {eslint-scope.Reference} reference - A reference to check. |
| * @returns {boolean} `true` if the reference is a write reference. |
| */ |
| function isWriteReference(reference) { |
| if (reference.init) { |
| const def = reference.resolved && reference.resolved.defs[0]; |
| |
| if (!def || def.type !== "Variable" || def.parent.kind !== "var") { |
| return false; |
| } |
| } |
| return reference.isWrite(); |
| } |
| |
| /** |
| * Checks whether or not a given loop condition info does not have the modified |
| * flag. |
| * |
| * @param {LoopConditionInfo} condition - A loop condition info to check. |
| * @returns {boolean} `true` if the loop condition info is "unmodified". |
| */ |
| function isUnmodified(condition) { |
| return !condition.modified; |
| } |
| |
| /** |
| * Checks whether or not a given loop condition info does not have the modified |
| * flag and does not have the group this condition belongs to. |
| * |
| * @param {LoopConditionInfo} condition - A loop condition info to check. |
| * @returns {boolean} `true` if the loop condition info is "unmodified". |
| */ |
| function isUnmodifiedAndNotBelongToGroup(condition) { |
| return !(condition.modified || condition.group); |
| } |
| |
| /** |
| * Checks whether or not a given reference is inside of a given node. |
| * |
| * @param {ASTNode} node - A node to check. |
| * @param {eslint-scope.Reference} reference - A reference to check. |
| * @returns {boolean} `true` if the reference is inside of the node. |
| */ |
| function isInRange(node, reference) { |
| const or = node.range; |
| const ir = reference.identifier.range; |
| |
| return or[0] <= ir[0] && ir[1] <= or[1]; |
| } |
| |
| /** |
| * Checks whether or not a given reference is inside of a loop node's condition. |
| * |
| * @param {ASTNode} node - A node to check. |
| * @param {eslint-scope.Reference} reference - A reference to check. |
| * @returns {boolean} `true` if the reference is inside of the loop node's |
| * condition. |
| */ |
| const isInLoop = { |
| WhileStatement: isInRange, |
| DoWhileStatement: isInRange, |
| ForStatement(node, reference) { |
| return ( |
| isInRange(node, reference) && |
| !(node.init && isInRange(node.init, reference)) |
| ); |
| } |
| }; |
| |
| /** |
| * Gets the function which encloses a given reference. |
| * This supports only FunctionDeclaration. |
| * |
| * @param {eslint-scope.Reference} reference - A reference to get. |
| * @returns {ASTNode|null} The function node or null. |
| */ |
| function getEncloseFunctionDeclaration(reference) { |
| let node = reference.identifier; |
| |
| while (node) { |
| if (node.type === "FunctionDeclaration") { |
| return node.id ? node : null; |
| } |
| |
| node = node.parent; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Updates the "modified" flags of given loop conditions with given modifiers. |
| * |
| * @param {LoopConditionInfo[]} conditions - The loop conditions to be updated. |
| * @param {eslint-scope.Reference[]} modifiers - The references to update. |
| * @returns {void} |
| */ |
| function updateModifiedFlag(conditions, modifiers) { |
| |
| for (let i = 0; i < conditions.length; ++i) { |
| const condition = conditions[i]; |
| |
| for (let j = 0; !condition.modified && j < modifiers.length; ++j) { |
| const modifier = modifiers[j]; |
| let funcNode, funcVar; |
| |
| /* |
| * Besides checking for the condition being in the loop, we want to |
| * check the function that this modifier is belonging to is called |
| * in the loop. |
| * FIXME: This should probably be extracted to a function. |
| */ |
| const inLoop = condition.isInLoop(modifier) || Boolean( |
| (funcNode = getEncloseFunctionDeclaration(modifier)) && |
| (funcVar = astUtils.getVariableByName(modifier.from.upper, funcNode.id.name)) && |
| funcVar.references.some(condition.isInLoop) |
| ); |
| |
| condition.modified = inLoop; |
| } |
| } |
| } |
| |
| //------------------------------------------------------------------------------ |
| // Rule Definition |
| //------------------------------------------------------------------------------ |
| |
| module.exports = { |
| meta: { |
| type: "problem", |
| |
| docs: { |
| description: "disallow unmodified loop conditions", |
| category: "Best Practices", |
| recommended: false, |
| url: "https://eslint.org/docs/rules/no-unmodified-loop-condition" |
| }, |
| |
| schema: [] |
| }, |
| |
| create(context) { |
| const sourceCode = context.getSourceCode(); |
| let groupMap = null; |
| |
| /** |
| * Reports a given condition info. |
| * |
| * @param {LoopConditionInfo} condition - A loop condition info to report. |
| * @returns {void} |
| */ |
| function report(condition) { |
| const node = condition.reference.identifier; |
| |
| context.report({ |
| node, |
| message: "'{{name}}' is not modified in this loop.", |
| data: node |
| }); |
| } |
| |
| /** |
| * Registers given conditions to the group the condition belongs to. |
| * |
| * @param {LoopConditionInfo[]} conditions - A loop condition info to |
| * register. |
| * @returns {void} |
| */ |
| function registerConditionsToGroup(conditions) { |
| for (let i = 0; i < conditions.length; ++i) { |
| const condition = conditions[i]; |
| |
| if (condition.group) { |
| let group = groupMap.get(condition.group); |
| |
| if (!group) { |
| group = []; |
| groupMap.set(condition.group, group); |
| } |
| group.push(condition); |
| } |
| } |
| } |
| |
| /** |
| * Reports references which are inside of unmodified groups. |
| * |
| * @param {LoopConditionInfo[]} conditions - A loop condition info to report. |
| * @returns {void} |
| */ |
| function checkConditionsInGroup(conditions) { |
| if (conditions.every(isUnmodified)) { |
| conditions.forEach(report); |
| } |
| } |
| |
| /** |
| * Checks whether or not a given group node has any dynamic elements. |
| * |
| * @param {ASTNode} root - A node to check. |
| * This node is one of BinaryExpression or ConditionalExpression. |
| * @returns {boolean} `true` if the node is dynamic. |
| */ |
| function hasDynamicExpressions(root) { |
| let retv = false; |
| |
| Traverser.traverse(root, { |
| visitorKeys: sourceCode.visitorKeys, |
| enter(node) { |
| if (DYNAMIC_PATTERN.test(node.type)) { |
| retv = true; |
| this.break(); |
| } else if (SKIP_PATTERN.test(node.type)) { |
| this.skip(); |
| } |
| } |
| }); |
| |
| return retv; |
| } |
| |
| /** |
| * Creates the loop condition information from a given reference. |
| * |
| * @param {eslint-scope.Reference} reference - A reference to create. |
| * @returns {LoopConditionInfo|null} Created loop condition info, or null. |
| */ |
| function toLoopCondition(reference) { |
| if (reference.init) { |
| return null; |
| } |
| |
| let group = null; |
| let child = reference.identifier; |
| let node = child.parent; |
| |
| while (node) { |
| if (SENTINEL_PATTERN.test(node.type)) { |
| if (LOOP_PATTERN.test(node.type) && node.test === child) { |
| |
| // This reference is inside of a loop condition. |
| return { |
| reference, |
| group, |
| isInLoop: isInLoop[node.type].bind(null, node), |
| modified: false |
| }; |
| } |
| |
| // This reference is outside of a loop condition. |
| break; |
| } |
| |
| /* |
| * If it's inside of a group, OK if either operand is modified. |
| * So stores the group this reference belongs to. |
| */ |
| if (GROUP_PATTERN.test(node.type)) { |
| |
| // If this expression is dynamic, no need to check. |
| if (hasDynamicExpressions(node)) { |
| break; |
| } else { |
| group = node; |
| } |
| } |
| |
| child = node; |
| node = node.parent; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Finds unmodified references which are inside of a loop condition. |
| * Then reports the references which are outside of groups. |
| * |
| * @param {eslint-scope.Variable} variable - A variable to report. |
| * @returns {void} |
| */ |
| function checkReferences(variable) { |
| |
| // Gets references that exist in loop conditions. |
| const conditions = variable |
| .references |
| .map(toLoopCondition) |
| .filter(Boolean); |
| |
| if (conditions.length === 0) { |
| return; |
| } |
| |
| // Registers the conditions to belonging groups. |
| registerConditionsToGroup(conditions); |
| |
| // Check the conditions are modified. |
| const modifiers = variable.references.filter(isWriteReference); |
| |
| if (modifiers.length > 0) { |
| updateModifiedFlag(conditions, modifiers); |
| } |
| |
| /* |
| * Reports the conditions which are not belonging to groups. |
| * Others will be reported after all variables are done. |
| */ |
| conditions |
| .filter(isUnmodifiedAndNotBelongToGroup) |
| .forEach(report); |
| } |
| |
| return { |
| "Program:exit"() { |
| const queue = [context.getScope()]; |
| |
| groupMap = new Map(); |
| |
| let scope; |
| |
| while ((scope = queue.pop())) { |
| queue.push(...scope.childScopes); |
| scope.variables.forEach(checkReferences); |
| } |
| |
| groupMap.forEach(checkConditionsInGroup); |
| groupMap = null; |
| } |
| }; |
| } |
| }; |