You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
150 lines
5.0 KiB
150 lines
5.0 KiB
/** |
|
* @fileoverview Rule to disallow loops with a body that allows only one iteration |
|
* @author Milos Djermanovic |
|
*/ |
|
|
|
"use strict"; |
|
|
|
//------------------------------------------------------------------------------ |
|
// Helpers |
|
//------------------------------------------------------------------------------ |
|
|
|
const allLoopTypes = ["WhileStatement", "DoWhileStatement", "ForStatement", "ForInStatement", "ForOfStatement"]; |
|
|
|
/** |
|
* Determines whether the given node is the first node in the code path to which a loop statement |
|
* 'loops' for the next iteration. |
|
* @param {ASTNode} node The node to check. |
|
* @returns {boolean} `true` if the node is a looping target. |
|
*/ |
|
function isLoopingTarget(node) { |
|
const parent = node.parent; |
|
|
|
if (parent) { |
|
switch (parent.type) { |
|
case "WhileStatement": |
|
return node === parent.test; |
|
case "DoWhileStatement": |
|
return node === parent.body; |
|
case "ForStatement": |
|
return node === (parent.update || parent.test || parent.body); |
|
case "ForInStatement": |
|
case "ForOfStatement": |
|
return node === parent.left; |
|
|
|
// no default |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
/** |
|
* Creates an array with elements from the first given array that are not included in the second given array. |
|
* @param {Array} arrA The array to compare from. |
|
* @param {Array} arrB The array to compare against. |
|
* @returns {Array} a new array that represents `arrA \ arrB`. |
|
*/ |
|
function getDifference(arrA, arrB) { |
|
return arrA.filter(a => !arrB.includes(a)); |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Rule Definition |
|
//------------------------------------------------------------------------------ |
|
|
|
/** @type {import('../shared/types').Rule} */ |
|
module.exports = { |
|
meta: { |
|
type: "problem", |
|
|
|
docs: { |
|
description: "Disallow loops with a body that allows only one iteration", |
|
recommended: false, |
|
url: "https://eslint.org/docs/rules/no-unreachable-loop" |
|
}, |
|
|
|
schema: [{ |
|
type: "object", |
|
properties: { |
|
ignore: { |
|
type: "array", |
|
items: { |
|
enum: allLoopTypes |
|
}, |
|
uniqueItems: true |
|
} |
|
}, |
|
additionalProperties: false |
|
}], |
|
|
|
messages: { |
|
invalid: "Invalid loop. Its body allows only one iteration." |
|
} |
|
}, |
|
|
|
create(context) { |
|
const ignoredLoopTypes = context.options[0] && context.options[0].ignore || [], |
|
loopTypesToCheck = getDifference(allLoopTypes, ignoredLoopTypes), |
|
loopSelector = loopTypesToCheck.join(","), |
|
loopsByTargetSegments = new Map(), |
|
loopsToReport = new Set(); |
|
|
|
let currentCodePath = null; |
|
|
|
return { |
|
onCodePathStart(codePath) { |
|
currentCodePath = codePath; |
|
}, |
|
|
|
onCodePathEnd() { |
|
currentCodePath = currentCodePath.upper; |
|
}, |
|
|
|
[loopSelector](node) { |
|
|
|
/** |
|
* Ignore unreachable loop statements to avoid unnecessary complexity in the implementation, or false positives otherwise. |
|
* For unreachable segments, the code path analysis does not raise events required for this implementation. |
|
*/ |
|
if (currentCodePath.currentSegments.some(segment => segment.reachable)) { |
|
loopsToReport.add(node); |
|
} |
|
}, |
|
|
|
onCodePathSegmentStart(segment, node) { |
|
if (isLoopingTarget(node)) { |
|
const loop = node.parent; |
|
|
|
loopsByTargetSegments.set(segment, loop); |
|
} |
|
}, |
|
|
|
onCodePathSegmentLoop(_, toSegment, node) { |
|
const loop = loopsByTargetSegments.get(toSegment); |
|
|
|
/** |
|
* The second iteration is reachable, meaning that the loop is valid by the logic of this rule, |
|
* only if there is at least one loop event with the appropriate target (which has been already |
|
* determined in the `loopsByTargetSegments` map), raised from either: |
|
* |
|
* - the end of the loop's body (in which case `node === loop`) |
|
* - a `continue` statement |
|
* |
|
* This condition skips loop events raised from `ForInStatement > .right` and `ForOfStatement > .right` nodes. |
|
*/ |
|
if (node === loop || node.type === "ContinueStatement") { |
|
|
|
// Removes loop if it exists in the set. Otherwise, `Set#delete` has no effect and doesn't throw. |
|
loopsToReport.delete(loop); |
|
} |
|
}, |
|
|
|
"Program:exit"() { |
|
loopsToReport.forEach( |
|
node => context.report({ node, messageId: "invalid" }) |
|
); |
|
} |
|
}; |
|
} |
|
};
|
|
|