[d565449] | 1 | /**
|
---|
| 2 | * @fileoverview Rule to flag creation of function inside a loop
|
---|
| 3 | * @author Ilya Volodin
|
---|
| 4 | */
|
---|
| 5 |
|
---|
| 6 | "use strict";
|
---|
| 7 |
|
---|
| 8 | //------------------------------------------------------------------------------
|
---|
| 9 | // Helpers
|
---|
| 10 | //------------------------------------------------------------------------------
|
---|
| 11 |
|
---|
| 12 | /**
|
---|
| 13 | * Gets the containing loop node of a specified node.
|
---|
| 14 | *
|
---|
| 15 | * We don't need to check nested functions, so this ignores those.
|
---|
| 16 | * `Scope.through` contains references of nested functions.
|
---|
| 17 | * @param {ASTNode} node An AST node to get.
|
---|
| 18 | * @returns {ASTNode|null} The containing loop node of the specified node, or
|
---|
| 19 | * `null`.
|
---|
| 20 | */
|
---|
| 21 | function getContainingLoopNode(node) {
|
---|
| 22 | for (let currentNode = node; currentNode.parent; currentNode = currentNode.parent) {
|
---|
| 23 | const parent = currentNode.parent;
|
---|
| 24 |
|
---|
| 25 | switch (parent.type) {
|
---|
| 26 | case "WhileStatement":
|
---|
| 27 | case "DoWhileStatement":
|
---|
| 28 | return parent;
|
---|
| 29 |
|
---|
| 30 | case "ForStatement":
|
---|
| 31 |
|
---|
| 32 | // `init` is outside of the loop.
|
---|
| 33 | if (parent.init !== currentNode) {
|
---|
| 34 | return parent;
|
---|
| 35 | }
|
---|
| 36 | break;
|
---|
| 37 |
|
---|
| 38 | case "ForInStatement":
|
---|
| 39 | case "ForOfStatement":
|
---|
| 40 |
|
---|
| 41 | // `right` is outside of the loop.
|
---|
| 42 | if (parent.right !== currentNode) {
|
---|
| 43 | return parent;
|
---|
| 44 | }
|
---|
| 45 | break;
|
---|
| 46 |
|
---|
| 47 | case "ArrowFunctionExpression":
|
---|
| 48 | case "FunctionExpression":
|
---|
| 49 | case "FunctionDeclaration":
|
---|
| 50 |
|
---|
| 51 | // We don't need to check nested functions.
|
---|
| 52 | return null;
|
---|
| 53 |
|
---|
| 54 | default:
|
---|
| 55 | break;
|
---|
| 56 | }
|
---|
| 57 | }
|
---|
| 58 |
|
---|
| 59 | return null;
|
---|
| 60 | }
|
---|
| 61 |
|
---|
| 62 | /**
|
---|
| 63 | * Gets the containing loop node of a given node.
|
---|
| 64 | * If the loop was nested, this returns the most outer loop.
|
---|
| 65 | * @param {ASTNode} node A node to get. This is a loop node.
|
---|
| 66 | * @param {ASTNode|null} excludedNode A node that the result node should not
|
---|
| 67 | * include.
|
---|
| 68 | * @returns {ASTNode} The most outer loop node.
|
---|
| 69 | */
|
---|
| 70 | function getTopLoopNode(node, excludedNode) {
|
---|
| 71 | const border = excludedNode ? excludedNode.range[1] : 0;
|
---|
| 72 | let retv = node;
|
---|
| 73 | let containingLoopNode = node;
|
---|
| 74 |
|
---|
| 75 | while (containingLoopNode && containingLoopNode.range[0] >= border) {
|
---|
| 76 | retv = containingLoopNode;
|
---|
| 77 | containingLoopNode = getContainingLoopNode(containingLoopNode);
|
---|
| 78 | }
|
---|
| 79 |
|
---|
| 80 | return retv;
|
---|
| 81 | }
|
---|
| 82 |
|
---|
| 83 | /**
|
---|
| 84 | * Checks whether a given reference which refers to an upper scope's variable is
|
---|
| 85 | * safe or not.
|
---|
| 86 | * @param {ASTNode} loopNode A containing loop node.
|
---|
| 87 | * @param {eslint-scope.Reference} reference A reference to check.
|
---|
| 88 | * @returns {boolean} `true` if the reference is safe or not.
|
---|
| 89 | */
|
---|
| 90 | function isSafe(loopNode, reference) {
|
---|
| 91 | const variable = reference.resolved;
|
---|
| 92 | const definition = variable && variable.defs[0];
|
---|
| 93 | const declaration = definition && definition.parent;
|
---|
| 94 | const kind = (declaration && declaration.type === "VariableDeclaration")
|
---|
| 95 | ? declaration.kind
|
---|
| 96 | : "";
|
---|
| 97 |
|
---|
| 98 | // Variables which are declared by `const` is safe.
|
---|
| 99 | if (kind === "const") {
|
---|
| 100 | return true;
|
---|
| 101 | }
|
---|
| 102 |
|
---|
| 103 | /*
|
---|
| 104 | * Variables which are declared by `let` in the loop is safe.
|
---|
| 105 | * It's a different instance from the next loop step's.
|
---|
| 106 | */
|
---|
| 107 | if (kind === "let" &&
|
---|
| 108 | declaration.range[0] > loopNode.range[0] &&
|
---|
| 109 | declaration.range[1] < loopNode.range[1]
|
---|
| 110 | ) {
|
---|
| 111 | return true;
|
---|
| 112 | }
|
---|
| 113 |
|
---|
| 114 | /*
|
---|
| 115 | * WriteReferences which exist after this border are unsafe because those
|
---|
| 116 | * can modify the variable.
|
---|
| 117 | */
|
---|
| 118 | const border = getTopLoopNode(
|
---|
| 119 | loopNode,
|
---|
| 120 | (kind === "let") ? declaration : null
|
---|
| 121 | ).range[0];
|
---|
| 122 |
|
---|
| 123 | /**
|
---|
| 124 | * Checks whether a given reference is safe or not.
|
---|
| 125 | * The reference is every reference of the upper scope's variable we are
|
---|
| 126 | * looking now.
|
---|
| 127 | *
|
---|
| 128 | * It's safe if the reference matches one of the following condition.
|
---|
| 129 | * - is readonly.
|
---|
| 130 | * - doesn't exist inside a local function and after the border.
|
---|
| 131 | * @param {eslint-scope.Reference} upperRef A reference to check.
|
---|
| 132 | * @returns {boolean} `true` if the reference is safe.
|
---|
| 133 | */
|
---|
| 134 | function isSafeReference(upperRef) {
|
---|
| 135 | const id = upperRef.identifier;
|
---|
| 136 |
|
---|
| 137 | return (
|
---|
| 138 | !upperRef.isWrite() ||
|
---|
| 139 | variable.scope.variableScope === upperRef.from.variableScope &&
|
---|
| 140 | id.range[0] < border
|
---|
| 141 | );
|
---|
| 142 | }
|
---|
| 143 |
|
---|
| 144 | return Boolean(variable) && variable.references.every(isSafeReference);
|
---|
| 145 | }
|
---|
| 146 |
|
---|
| 147 | //------------------------------------------------------------------------------
|
---|
| 148 | // Rule Definition
|
---|
| 149 | //------------------------------------------------------------------------------
|
---|
| 150 |
|
---|
| 151 | /** @type {import('../shared/types').Rule} */
|
---|
| 152 | module.exports = {
|
---|
| 153 | meta: {
|
---|
| 154 | type: "suggestion",
|
---|
| 155 |
|
---|
| 156 | docs: {
|
---|
| 157 | description: "Disallow function declarations that contain unsafe references inside loop statements",
|
---|
| 158 | recommended: false,
|
---|
| 159 | url: "https://eslint.org/docs/latest/rules/no-loop-func"
|
---|
| 160 | },
|
---|
| 161 |
|
---|
| 162 | schema: [],
|
---|
| 163 |
|
---|
| 164 | messages: {
|
---|
| 165 | unsafeRefs: "Function declared in a loop contains unsafe references to variable(s) {{ varNames }}."
|
---|
| 166 | }
|
---|
| 167 | },
|
---|
| 168 |
|
---|
| 169 | create(context) {
|
---|
| 170 |
|
---|
| 171 | const sourceCode = context.sourceCode;
|
---|
| 172 |
|
---|
| 173 | /**
|
---|
| 174 | * Reports functions which match the following condition:
|
---|
| 175 | *
|
---|
| 176 | * - has a loop node in ancestors.
|
---|
| 177 | * - has any references which refers to an unsafe variable.
|
---|
| 178 | * @param {ASTNode} node The AST node to check.
|
---|
| 179 | * @returns {void}
|
---|
| 180 | */
|
---|
| 181 | function checkForLoops(node) {
|
---|
| 182 | const loopNode = getContainingLoopNode(node);
|
---|
| 183 |
|
---|
| 184 | if (!loopNode) {
|
---|
| 185 | return;
|
---|
| 186 | }
|
---|
| 187 |
|
---|
| 188 | const references = sourceCode.getScope(node).through;
|
---|
| 189 | const unsafeRefs = references.filter(r => r.resolved && !isSafe(loopNode, r)).map(r => r.identifier.name);
|
---|
| 190 |
|
---|
| 191 | if (unsafeRefs.length > 0) {
|
---|
| 192 | context.report({
|
---|
| 193 | node,
|
---|
| 194 | messageId: "unsafeRefs",
|
---|
| 195 | data: { varNames: `'${unsafeRefs.join("', '")}'` }
|
---|
| 196 | });
|
---|
| 197 | }
|
---|
| 198 | }
|
---|
| 199 |
|
---|
| 200 | return {
|
---|
| 201 | ArrowFunctionExpression: checkForLoops,
|
---|
| 202 | FunctionExpression: checkForLoops,
|
---|
| 203 | FunctionDeclaration: checkForLoops
|
---|
| 204 | };
|
---|
| 205 | }
|
---|
| 206 | };
|
---|