1 | /**
|
2 | * @fileoverview Rule to flag creation of function inside a loop
|
3 | * @author Ilya Volodin
|
4 | * @copyright 2013 Ilya Volodin. All rights reserved.
|
5 | */
|
6 |
|
7 | ;
|
8 |
|
9 | //------------------------------------------------------------------------------
|
10 | // Helpers
|
11 | //------------------------------------------------------------------------------
|
12 |
|
13 | /**
|
14 | * Gets the containing loop node of a specified node.
|
15 | *
|
16 | * We don't need to check nested functions, so this ignores those.
|
17 | * `Scope.through` contains references of nested functions.
|
18 | *
|
19 | * @param {ASTNode} node - An AST node to get.
|
20 | * @returns {ASTNode|null} The containing loop node of the specified node, or `null`.
|
21 | */
|
22 | function getContainingLoopNode(node) {
|
23 | var parent = node.parent;
|
24 | while (parent) {
|
25 | switch (parent.type) {
|
26 | case "WhileStatement":
|
27 | case "DoWhileStatement":
|
28 | return parent;
|
29 |
|
30 | case "ForStatement":
|
31 | // `init` is outside of the loop.
|
32 | if (parent.init !== node) {
|
33 | return parent;
|
34 | }
|
35 | break;
|
36 |
|
37 | case "ForInStatement":
|
38 | case "ForOfStatement":
|
39 | // `right` is outside of the loop.
|
40 | if (parent.right !== node) {
|
41 | return parent;
|
42 | }
|
43 | break;
|
44 |
|
45 | case "ArrowFunctionExpression":
|
46 | case "FunctionExpression":
|
47 | case "FunctionDeclaration":
|
48 | // We don't need to check nested functions.
|
49 | return null;
|
50 |
|
51 | default:
|
52 | break;
|
53 | }
|
54 |
|
55 | node = parent;
|
56 | parent = node.parent;
|
57 | }
|
58 |
|
59 | return null;
|
60 | }
|
61 |
|
62 | /**
|
63 | * Checks whether or not a reference refers to a variable that is block-binding in the loop.
|
64 | * @param {ASTNode} loopNode - A containing loop node.
|
65 | * @param {escope.Reference} reference - A reference to check.
|
66 | * @returns {boolean} Whether or not a reference refers to a variable that is block-binding in the loop.
|
67 | */
|
68 | function isBlockBindingsInLoop(loopNode, reference) {
|
69 | var variable = reference.resolved;
|
70 | var definition = variable && variable.defs[0];
|
71 | var declaration = definition && definition.parent;
|
72 |
|
73 | return (
|
74 | // Checks whether this is `let`/`const`.
|
75 | declaration &&
|
76 | declaration.type === "VariableDeclaration" &&
|
77 | (declaration.kind === "let" || declaration.kind === "const") &&
|
78 | // Checks whether this is in the loop.
|
79 | declaration.range[0] > loopNode.range[0] &&
|
80 | declaration.range[1] < loopNode.range[1]
|
81 | );
|
82 | }
|
83 |
|
84 | //------------------------------------------------------------------------------
|
85 | // Rule Definition
|
86 | //------------------------------------------------------------------------------
|
87 |
|
88 | module.exports = function(context) {
|
89 | /**
|
90 | * Reports such functions:
|
91 | *
|
92 | * - has an ancestor node which is a loop.
|
93 | * - has a reference that refers to a variable that is block-binding in the loop.
|
94 | *
|
95 | * @param {ASTNode} node The AST node to check.
|
96 | * @returns {boolean} Whether or not the node is within a loop.
|
97 | */
|
98 | function checkForLoops(node) {
|
99 | var loopNode = getContainingLoopNode(node);
|
100 | if (!loopNode) {
|
101 | return;
|
102 | }
|
103 |
|
104 | var references = context.getScope().through;
|
105 | if (references.length > 0 && !references.every(isBlockBindingsInLoop.bind(null, loopNode))) {
|
106 | context.report(node, "Don't make functions within a loop");
|
107 | }
|
108 | }
|
109 |
|
110 | return {
|
111 | "ArrowFunctionExpression": checkForLoops,
|
112 | "FunctionExpression": checkForLoops,
|
113 | "FunctionDeclaration": checkForLoops
|
114 | };
|
115 | };
|
116 |
|
117 | module.exports.schema = [];
|