summaryrefslogtreecommitdiff
path: root/tools/node_modules/eslint/lib/rules/no-unmodified-loop-condition.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/node_modules/eslint/lib/rules/no-unmodified-loop-condition.js')
-rw-r--r--tools/node_modules/eslint/lib/rules/no-unmodified-loop-condition.js366
1 files changed, 366 insertions, 0 deletions
diff --git a/tools/node_modules/eslint/lib/rules/no-unmodified-loop-condition.js b/tools/node_modules/eslint/lib/rules/no-unmodified-loop-condition.js
new file mode 100644
index 0000000000..49dff0d0ce
--- /dev/null
+++ b/tools/node_modules/eslint/lib/rules/no-unmodified-loop-condition.js
@@ -0,0 +1,366 @@
+/**
+ * @fileoverview Rule to disallow use of unmodified expressions in loop conditions
+ * @author Toru Nagashima
+ */
+
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const Traverser = require("../util/traverser"),
+ astUtils = require("../ast-utils");
+
+//------------------------------------------------------------------------------
+// Helpers
+//------------------------------------------------------------------------------
+
+const pushAll = Function.apply.bind(Array.prototype.push);
+const SENTINEL_PATTERN = /(?:(?:Call|Class|Function|Member|New|Yield)Expression|Statement|Declaration)$/;
+const LOOP_PATTERN = /^(?:DoWhile|For|While)Statement$/; // for-in/of statements don't have `test` property.
+const GROUP_PATTERN = /^(?:BinaryExpression|ConditionalExpression)$/;
+const SKIP_PATTERN = /^(?:ArrowFunction|Class|Function)Expression$/;
+const DYNAMIC_PATTERN = /^(?:Call|Member|New|TaggedTemplate|Yield)Expression$/;
+
+/**
+ * @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))
+ );
+ }
+};
+
+/**
+ * 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;
+ const traverser = new Traverser();
+
+ traverser.traverse(root, {
+ 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;
+}
+
+/**
+ * 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: {
+ docs: {
+ description: "disallow unmodified loop conditions",
+ category: "Best Practices",
+ recommended: false
+ },
+
+ schema: []
+ },
+
+ create(context) {
+ 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);
+ }
+ }
+
+ /**
+ * 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())) {
+ pushAll(queue, scope.childScopes);
+ scope.variables.forEach(checkReferences);
+ }
+
+ groupMap.forEach(checkConditionsInGroup);
+ groupMap = null;
+ }
+ };
+ }
+};