blob: 85292d136ec532c4be54730ff17e2b6569339702 [file] [log] [blame]
/**
* @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;
}
};
}
};