blob: a866a6d20cc1938da12c63aba40d4e5167c29d0d (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
|
/**
* @fileoverview A rule to suggest using of const declaration for variables that are never modified after declared.
* @author Toru Nagashima
* @copyright 2015 Toru Nagashima. All rights reserved.
*/
"use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
var LOOP_TYPES = /^(?:While|DoWhile|For|ForIn|ForOf)Statement$/;
var FOR_IN_OF_TYPES = /^For(?:In|Of)Statement$/;
var SENTINEL_TYPES = /(?:Declaration|Statement)$/;
var END_POSITION_TYPES = /^(?:Assignment|Update)/;
/**
* Gets a write reference from a given variable if the variable is modified only
* once.
*
* @param {escope.Variable} variable - A variable to get.
* @returns {escope.Reference|null} A write reference or null.
*/
function getWriteReferenceIfOnce(variable) {
var retv = null;
var references = variable.references;
for (var i = 0; i < references.length; ++i) {
var reference = references[i];
if (reference.isWrite()) {
if (retv && !(retv.init && reference.init)) {
// This variable is modified two or more times.
return null;
}
retv = reference;
}
}
return retv;
}
/**
* Checks whether or not a given reference is in a loop condition or a
* for-loop's updater.
*
* @param {escope.Reference} reference - A reference to check.
* @returns {boolean} `true` if the reference is in a loop condition or a
* for-loop's updater.
*/
function isInLoopHead(reference) {
var node = reference.identifier;
var parent = node.parent;
var assignment = false;
while (parent) {
if (LOOP_TYPES.test(parent.type)) {
return true;
}
// VariableDeclaration can be at ForInStatement.left
// This is catching the code like `for (const {b = ++a} of foo()) { ... }`
if (assignment &&
parent.type === "VariableDeclaration" &&
FOR_IN_OF_TYPES.test(parent.parent.type) &&
parent.parent.left === parent
) {
return true;
}
if (parent.type === "AssignmentPattern") {
assignment = true;
}
// If a declaration or a statement was found before a loop,
// it's not in the head of a loop.
if (SENTINEL_TYPES.test(parent.type)) {
break;
}
node = parent;
parent = parent.parent;
}
return false;
}
/**
* Gets the end position of a given reference.
* This position is used to check every ReadReferences exist after the given
* reference.
*
* If the reference is belonging to AssignmentExpression or UpdateExpression,
* this function returns the most rear position of those nodes.
* The range of those nodes are executed before the assignment.
*
* @param {escope.Reference} writer - A reference to get.
* @returns {number} The end position of the reference.
*/
function getEndPosition(writer) {
var node = writer.identifier;
var end = node.range[1];
// Detects the end position of the assignment expression the reference is
// belonging to.
while ((node = node.parent)) {
if (END_POSITION_TYPES.test(node.type)) {
end = node.range[1];
}
if (SENTINEL_TYPES.test(node.type)) {
break;
}
}
return end;
}
/**
* Gets a function which checks a given reference with the following condition:
*
* - The reference is a ReadReference.
* - The reference exists after a specific WriteReference.
* - The reference exists inside of the scope a specific WriteReference is
* belonging to.
*
* @param {escope.Reference} writer - A reference to check.
* @returns {function} A function which checks a given reference.
*/
function isInScope(writer) {
var start = getEndPosition(writer);
var end = writer.from.block.range[1];
return function(reference) {
if (!reference.isRead()) {
return true;
}
var range = reference.identifier.range;
return start <= range[0] && range[1] <= end;
};
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = function(context) {
/**
* Searches and reports variables that are never modified after declared.
* @param {Scope} scope - A scope of the search domain.
* @returns {void}
*/
function checkForVariables(scope) {
// Skip the TDZ type.
if (scope.type === "TDZ") {
return;
}
var variables = scope.variables;
for (var i = 0; i < variables.length; ++i) {
var variable = variables[i];
var def = variable.defs[0];
var declaration = def && def.parent;
var statement = declaration && declaration.parent;
var references = variable.references;
var identifier = variable.identifiers[0];
// Skips excludes `let`.
// And skips if it's at `ForStatement.init`.
if (!declaration ||
declaration.type !== "VariableDeclaration" ||
declaration.kind !== "let" ||
(statement.type === "ForStatement" && statement.init === declaration)
) {
continue;
}
// Checks references.
// - One WriteReference exists.
// - Two or more WriteReference don't exist.
// - Every ReadReference exists after the WriteReference.
// - The WriteReference doesn't exist in a loop condition.
// - If `eslintUsed` is true, we cannot know where it was used from.
// In this case, if the scope of the variable would change, it
// skips the variable.
var writer = getWriteReferenceIfOnce(variable);
if (writer &&
!(variable.eslintUsed && variable.scope !== writer.from) &&
!isInLoopHead(writer) &&
references.every(isInScope(writer))
) {
context.report({
node: identifier,
message: "'{{name}}' is never modified, use 'const' instead.",
data: identifier
});
}
}
}
/**
* Adds multiple items to the tail of an array.
* @param {any[]} array - A destination to add.
* @param {any[]} values - Items to be added.
* @returns {void}
*/
var pushAll = Function.apply.bind(Array.prototype.push);
return {
"Program:exit": function() {
var stack = [context.getScope()];
while (stack.length) {
var scope = stack.pop();
pushAll(stack, scope.childScopes);
checkForVariables(scope);
}
}
};
};
module.exports.schema = [];
|