eslint 8.47.0 → 8.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/config/rule-validator.js +2 -1
- package/lib/linter/code-path-analysis/code-path-analyzer.js +32 -24
- package/lib/linter/code-path-analysis/code-path-segment.js +52 -24
- package/lib/linter/code-path-analysis/code-path.js +1 -0
- package/lib/linter/linter.js +1 -0
- package/lib/rule-tester/flat-rule-tester.js +54 -20
- package/lib/rule-tester/rule-tester.js +117 -22
- package/lib/rules/array-callback-return.js +36 -11
- package/lib/rules/consistent-return.js +32 -7
- package/lib/rules/constructor-super.js +37 -14
- package/lib/rules/for-direction.js +15 -8
- package/lib/rules/getter-return.js +33 -8
- package/lib/rules/lines-between-class-members.js +92 -7
- package/lib/rules/no-fallthrough.js +42 -14
- package/lib/rules/no-promise-executor-return.js +154 -16
- package/lib/rules/no-this-before-super.js +38 -11
- package/lib/rules/no-unreachable-loop.js +47 -12
- package/lib/rules/no-unreachable.js +39 -10
- package/lib/rules/no-useless-return.js +35 -4
- package/lib/rules/require-atomic-updates.js +21 -7
- package/messages/eslintrc-incompat.js +1 -1
- package/package.json +11 -9
@@ -10,6 +10,21 @@
|
|
10
10
|
|
11
11
|
const astUtils = require("./utils/ast-utils");
|
12
12
|
|
13
|
+
//------------------------------------------------------------------------------
|
14
|
+
// Helpers
|
15
|
+
//------------------------------------------------------------------------------
|
16
|
+
|
17
|
+
/**
|
18
|
+
* Types of class members.
|
19
|
+
* Those have `test` method to check it matches to the given class member.
|
20
|
+
* @private
|
21
|
+
*/
|
22
|
+
const ClassMemberTypes = {
|
23
|
+
"*": { test: () => true },
|
24
|
+
field: { test: node => node.type === "PropertyDefinition" },
|
25
|
+
method: { test: node => node.type === "MethodDefinition" }
|
26
|
+
};
|
27
|
+
|
13
28
|
//------------------------------------------------------------------------------
|
14
29
|
// Rule Definition
|
15
30
|
//------------------------------------------------------------------------------
|
@@ -29,7 +44,32 @@ module.exports = {
|
|
29
44
|
|
30
45
|
schema: [
|
31
46
|
{
|
32
|
-
|
47
|
+
anyOf: [
|
48
|
+
{
|
49
|
+
type: "object",
|
50
|
+
properties: {
|
51
|
+
enforce: {
|
52
|
+
type: "array",
|
53
|
+
items: {
|
54
|
+
type: "object",
|
55
|
+
properties: {
|
56
|
+
blankLine: { enum: ["always", "never"] },
|
57
|
+
prev: { enum: ["method", "field", "*"] },
|
58
|
+
next: { enum: ["method", "field", "*"] }
|
59
|
+
},
|
60
|
+
additionalProperties: false,
|
61
|
+
required: ["blankLine", "prev", "next"]
|
62
|
+
},
|
63
|
+
minItems: 1
|
64
|
+
}
|
65
|
+
},
|
66
|
+
additionalProperties: false,
|
67
|
+
required: ["enforce"]
|
68
|
+
},
|
69
|
+
{
|
70
|
+
enum: ["always", "never"]
|
71
|
+
}
|
72
|
+
]
|
33
73
|
},
|
34
74
|
{
|
35
75
|
type: "object",
|
@@ -55,6 +95,7 @@ module.exports = {
|
|
55
95
|
options[0] = context.options[0] || "always";
|
56
96
|
options[1] = context.options[1] || { exceptAfterSingleLine: false };
|
57
97
|
|
98
|
+
const configureList = typeof options[0] === "object" ? options[0].enforce : [{ blankLine: options[0], prev: "*", next: "*" }];
|
58
99
|
const sourceCode = context.sourceCode;
|
59
100
|
|
60
101
|
/**
|
@@ -144,6 +185,38 @@ module.exports = {
|
|
144
185
|
return sourceCode.getTokensBetween(before, after, { includeComments: true }).length !== 0;
|
145
186
|
}
|
146
187
|
|
188
|
+
/**
|
189
|
+
* Checks whether the given node matches the given type.
|
190
|
+
* @param {ASTNode} node The class member node to check.
|
191
|
+
* @param {string} type The class member type to check.
|
192
|
+
* @returns {boolean} `true` if the class member node matched the type.
|
193
|
+
* @private
|
194
|
+
*/
|
195
|
+
function match(node, type) {
|
196
|
+
return ClassMemberTypes[type].test(node);
|
197
|
+
}
|
198
|
+
|
199
|
+
/**
|
200
|
+
* Finds the last matched configuration from the configureList.
|
201
|
+
* @param {ASTNode} prevNode The previous node to match.
|
202
|
+
* @param {ASTNode} nextNode The current node to match.
|
203
|
+
* @returns {string|null} Padding type or `null` if no matches were found.
|
204
|
+
* @private
|
205
|
+
*/
|
206
|
+
function getPaddingType(prevNode, nextNode) {
|
207
|
+
for (let i = configureList.length - 1; i >= 0; --i) {
|
208
|
+
const configure = configureList[i];
|
209
|
+
const matched =
|
210
|
+
match(prevNode, configure.prev) &&
|
211
|
+
match(nextNode, configure.next);
|
212
|
+
|
213
|
+
if (matched) {
|
214
|
+
return configure.blankLine;
|
215
|
+
}
|
216
|
+
}
|
217
|
+
return null;
|
218
|
+
}
|
219
|
+
|
147
220
|
return {
|
148
221
|
ClassBody(node) {
|
149
222
|
const body = node.body;
|
@@ -158,22 +231,34 @@ module.exports = {
|
|
158
231
|
const isPadded = afterPadding.loc.start.line - beforePadding.loc.end.line > 1;
|
159
232
|
const hasTokenInPadding = hasTokenOrCommentBetween(beforePadding, afterPadding);
|
160
233
|
const curLineLastToken = findLastConsecutiveTokenAfter(curLast, nextFirst, 0);
|
234
|
+
const paddingType = getPaddingType(body[i], body[i + 1]);
|
235
|
+
|
236
|
+
if (paddingType === "never" && isPadded) {
|
237
|
+
context.report({
|
238
|
+
node: body[i + 1],
|
239
|
+
messageId: "never",
|
161
240
|
|
162
|
-
|
163
|
-
|
241
|
+
fix(fixer) {
|
242
|
+
if (hasTokenInPadding) {
|
243
|
+
return null;
|
244
|
+
}
|
245
|
+
return fixer.replaceTextRange([beforePadding.range[1], afterPadding.range[0]], "\n");
|
246
|
+
}
|
247
|
+
});
|
248
|
+
} else if (paddingType === "always" && !skip && !isPadded) {
|
164
249
|
context.report({
|
165
250
|
node: body[i + 1],
|
166
|
-
messageId:
|
251
|
+
messageId: "always",
|
252
|
+
|
167
253
|
fix(fixer) {
|
168
254
|
if (hasTokenInPadding) {
|
169
255
|
return null;
|
170
256
|
}
|
171
|
-
return
|
172
|
-
? fixer.replaceTextRange([beforePadding.range[1], afterPadding.range[0]], "\n")
|
173
|
-
: fixer.insertTextAfter(curLineLastToken, "\n");
|
257
|
+
return fixer.insertTextAfter(curLineLastToken, "\n");
|
174
258
|
}
|
175
259
|
});
|
176
260
|
}
|
261
|
+
|
177
262
|
}
|
178
263
|
}
|
179
264
|
};
|
@@ -16,6 +16,22 @@ const { directivesPattern } = require("../shared/directives");
|
|
16
16
|
|
17
17
|
const DEFAULT_FALLTHROUGH_COMMENT = /falls?\s?through/iu;
|
18
18
|
|
19
|
+
/**
|
20
|
+
* Checks all segments in a set and returns true if any are reachable.
|
21
|
+
* @param {Set<CodePathSegment>} segments The segments to check.
|
22
|
+
* @returns {boolean} True if any segment is reachable; false otherwise.
|
23
|
+
*/
|
24
|
+
function isAnySegmentReachable(segments) {
|
25
|
+
|
26
|
+
for (const segment of segments) {
|
27
|
+
if (segment.reachable) {
|
28
|
+
return true;
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
return false;
|
33
|
+
}
|
34
|
+
|
19
35
|
/**
|
20
36
|
* Checks whether or not a given comment string is really a fallthrough comment and not an ESLint directive.
|
21
37
|
* @param {string} comment The comment string to check.
|
@@ -51,15 +67,6 @@ function hasFallthroughComment(caseWhichFallsThrough, subsequentCase, context, f
|
|
51
67
|
return Boolean(comment && isFallThroughComment(comment.value, fallthroughCommentPattern));
|
52
68
|
}
|
53
69
|
|
54
|
-
/**
|
55
|
-
* Checks whether or not a given code path segment is reachable.
|
56
|
-
* @param {CodePathSegment} segment A CodePathSegment to check.
|
57
|
-
* @returns {boolean} `true` if the segment is reachable.
|
58
|
-
*/
|
59
|
-
function isReachable(segment) {
|
60
|
-
return segment.reachable;
|
61
|
-
}
|
62
|
-
|
63
70
|
/**
|
64
71
|
* Checks whether a node and a token are separated by blank lines
|
65
72
|
* @param {ASTNode} node The node to check
|
@@ -109,7 +116,8 @@ module.exports = {
|
|
109
116
|
|
110
117
|
create(context) {
|
111
118
|
const options = context.options[0] || {};
|
112
|
-
|
119
|
+
const codePathSegments = [];
|
120
|
+
let currentCodePathSegments = new Set();
|
113
121
|
const sourceCode = context.sourceCode;
|
114
122
|
const allowEmptyCase = options.allowEmptyCase || false;
|
115
123
|
|
@@ -126,13 +134,33 @@ module.exports = {
|
|
126
134
|
fallthroughCommentPattern = DEFAULT_FALLTHROUGH_COMMENT;
|
127
135
|
}
|
128
136
|
return {
|
129
|
-
|
130
|
-
|
137
|
+
|
138
|
+
onCodePathStart() {
|
139
|
+
codePathSegments.push(currentCodePathSegments);
|
140
|
+
currentCodePathSegments = new Set();
|
131
141
|
},
|
142
|
+
|
132
143
|
onCodePathEnd() {
|
133
|
-
|
144
|
+
currentCodePathSegments = codePathSegments.pop();
|
145
|
+
},
|
146
|
+
|
147
|
+
onUnreachableCodePathSegmentStart(segment) {
|
148
|
+
currentCodePathSegments.add(segment);
|
149
|
+
},
|
150
|
+
|
151
|
+
onUnreachableCodePathSegmentEnd(segment) {
|
152
|
+
currentCodePathSegments.delete(segment);
|
153
|
+
},
|
154
|
+
|
155
|
+
onCodePathSegmentStart(segment) {
|
156
|
+
currentCodePathSegments.add(segment);
|
134
157
|
},
|
135
158
|
|
159
|
+
onCodePathSegmentEnd(segment) {
|
160
|
+
currentCodePathSegments.delete(segment);
|
161
|
+
},
|
162
|
+
|
163
|
+
|
136
164
|
SwitchCase(node) {
|
137
165
|
|
138
166
|
/*
|
@@ -157,7 +185,7 @@ module.exports = {
|
|
157
185
|
* `break`, `return`, or `throw` are unreachable.
|
158
186
|
* And allows empty cases and the last case.
|
159
187
|
*/
|
160
|
-
if (
|
188
|
+
if (isAnySegmentReachable(currentCodePathSegments) &&
|
161
189
|
(node.consequent.length > 0 || (!allowEmptyCase && hasBlankLinesBetween(node, nextToken))) &&
|
162
190
|
node.parent.cases[node.parent.cases.length - 1] !== node) {
|
163
191
|
fallthroughCase = node;
|
@@ -10,6 +10,7 @@
|
|
10
10
|
//------------------------------------------------------------------------------
|
11
11
|
|
12
12
|
const { findVariable } = require("@eslint-community/eslint-utils");
|
13
|
+
const astUtils = require("./utils/ast-utils");
|
13
14
|
|
14
15
|
//------------------------------------------------------------------------------
|
15
16
|
// Helpers
|
@@ -59,6 +60,78 @@ function isPromiseExecutor(node, scope) {
|
|
59
60
|
isGlobalReference(parent.callee, getOuterScope(scope));
|
60
61
|
}
|
61
62
|
|
63
|
+
/**
|
64
|
+
* Checks if the given node is a void expression.
|
65
|
+
* @param {ASTNode} node The node to check.
|
66
|
+
* @returns {boolean} - `true` if the node is a void expression
|
67
|
+
*/
|
68
|
+
function expressionIsVoid(node) {
|
69
|
+
return node.type === "UnaryExpression" && node.operator === "void";
|
70
|
+
}
|
71
|
+
|
72
|
+
/**
|
73
|
+
* Fixes the linting error by prepending "void " to the given node
|
74
|
+
* @param {Object} sourceCode context given by context.sourceCode
|
75
|
+
* @param {ASTNode} node The node to fix.
|
76
|
+
* @param {Object} fixer The fixer object provided by ESLint.
|
77
|
+
* @returns {Array<Object>} - An array of fix objects to apply to the node.
|
78
|
+
*/
|
79
|
+
function voidPrependFixer(sourceCode, node, fixer) {
|
80
|
+
|
81
|
+
const requiresParens =
|
82
|
+
|
83
|
+
// prepending `void ` will fail if the node has a lower precedence than void
|
84
|
+
astUtils.getPrecedence(node) < astUtils.getPrecedence({ type: "UnaryExpression", operator: "void" }) &&
|
85
|
+
|
86
|
+
// check if there are parentheses around the node to avoid redundant parentheses
|
87
|
+
!astUtils.isParenthesised(sourceCode, node);
|
88
|
+
|
89
|
+
// avoid parentheses issues
|
90
|
+
const returnOrArrowToken = sourceCode.getTokenBefore(
|
91
|
+
node,
|
92
|
+
node.parent.type === "ArrowFunctionExpression"
|
93
|
+
? astUtils.isArrowToken
|
94
|
+
|
95
|
+
// isReturnToken
|
96
|
+
: token => token.type === "Keyword" && token.value === "return"
|
97
|
+
);
|
98
|
+
|
99
|
+
const firstToken = sourceCode.getTokenAfter(returnOrArrowToken);
|
100
|
+
|
101
|
+
const prependSpace =
|
102
|
+
|
103
|
+
// is return token, as => allows void to be adjacent
|
104
|
+
returnOrArrowToken.value === "return" &&
|
105
|
+
|
106
|
+
// If two tokens (return and "(") are adjacent
|
107
|
+
returnOrArrowToken.range[1] === firstToken.range[0];
|
108
|
+
|
109
|
+
return [
|
110
|
+
fixer.insertTextBefore(firstToken, `${prependSpace ? " " : ""}void ${requiresParens ? "(" : ""}`),
|
111
|
+
fixer.insertTextAfter(node, requiresParens ? ")" : "")
|
112
|
+
];
|
113
|
+
}
|
114
|
+
|
115
|
+
/**
|
116
|
+
* Fixes the linting error by `wrapping {}` around the given node's body.
|
117
|
+
* @param {Object} sourceCode context given by context.sourceCode
|
118
|
+
* @param {ASTNode} node The node to fix.
|
119
|
+
* @param {Object} fixer The fixer object provided by ESLint.
|
120
|
+
* @returns {Array<Object>} - An array of fix objects to apply to the node.
|
121
|
+
*/
|
122
|
+
function curlyWrapFixer(sourceCode, node, fixer) {
|
123
|
+
|
124
|
+
// https://github.com/eslint/eslint/pull/17282#issuecomment-1592795923
|
125
|
+
const arrowToken = sourceCode.getTokenBefore(node.body, astUtils.isArrowToken);
|
126
|
+
const firstToken = sourceCode.getTokenAfter(arrowToken);
|
127
|
+
const lastToken = sourceCode.getLastToken(node);
|
128
|
+
|
129
|
+
return [
|
130
|
+
fixer.insertTextBefore(firstToken, "{"),
|
131
|
+
fixer.insertTextAfter(lastToken, "}")
|
132
|
+
];
|
133
|
+
}
|
134
|
+
|
62
135
|
//------------------------------------------------------------------------------
|
63
136
|
// Rule Definition
|
64
137
|
//------------------------------------------------------------------------------
|
@@ -74,10 +147,27 @@ module.exports = {
|
|
74
147
|
url: "https://eslint.org/docs/latest/rules/no-promise-executor-return"
|
75
148
|
},
|
76
149
|
|
77
|
-
|
150
|
+
hasSuggestions: true,
|
151
|
+
|
152
|
+
schema: [{
|
153
|
+
type: "object",
|
154
|
+
properties: {
|
155
|
+
allowVoid: {
|
156
|
+
type: "boolean",
|
157
|
+
default: false
|
158
|
+
}
|
159
|
+
},
|
160
|
+
additionalProperties: false
|
161
|
+
}],
|
78
162
|
|
79
163
|
messages: {
|
80
|
-
returnsValue: "Return values from promise executor functions cannot be read."
|
164
|
+
returnsValue: "Return values from promise executor functions cannot be read.",
|
165
|
+
|
166
|
+
// arrow and function suggestions
|
167
|
+
prependVoid: "Prepend `void` to the expression.",
|
168
|
+
|
169
|
+
// only arrow suggestions
|
170
|
+
wrapBraces: "Wrap the expression in `{}`."
|
81
171
|
}
|
82
172
|
},
|
83
173
|
|
@@ -85,26 +175,52 @@ module.exports = {
|
|
85
175
|
|
86
176
|
let funcInfo = null;
|
87
177
|
const sourceCode = context.sourceCode;
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
* @param {ASTNode} node Node to report.
|
92
|
-
* @returns {void}
|
93
|
-
*/
|
94
|
-
function report(node) {
|
95
|
-
context.report({ node, messageId: "returnsValue" });
|
96
|
-
}
|
178
|
+
const {
|
179
|
+
allowVoid = false
|
180
|
+
} = context.options[0] || {};
|
97
181
|
|
98
182
|
return {
|
99
183
|
|
100
184
|
onCodePathStart(_, node) {
|
101
185
|
funcInfo = {
|
102
186
|
upper: funcInfo,
|
103
|
-
shouldCheck:
|
187
|
+
shouldCheck:
|
188
|
+
functionTypesToCheck.has(node.type) &&
|
189
|
+
isPromiseExecutor(node, sourceCode.getScope(node))
|
104
190
|
};
|
105
191
|
|
106
|
-
if (
|
107
|
-
|
192
|
+
if (// Is a Promise executor
|
193
|
+
funcInfo.shouldCheck &&
|
194
|
+
node.type === "ArrowFunctionExpression" &&
|
195
|
+
node.expression &&
|
196
|
+
|
197
|
+
// Except void
|
198
|
+
!(allowVoid && expressionIsVoid(node.body))
|
199
|
+
) {
|
200
|
+
const suggest = [];
|
201
|
+
|
202
|
+
// prevent useless refactors
|
203
|
+
if (allowVoid) {
|
204
|
+
suggest.push({
|
205
|
+
messageId: "prependVoid",
|
206
|
+
fix(fixer) {
|
207
|
+
return voidPrependFixer(sourceCode, node.body, fixer);
|
208
|
+
}
|
209
|
+
});
|
210
|
+
}
|
211
|
+
|
212
|
+
suggest.push({
|
213
|
+
messageId: "wrapBraces",
|
214
|
+
fix(fixer) {
|
215
|
+
return curlyWrapFixer(sourceCode, node, fixer);
|
216
|
+
}
|
217
|
+
});
|
218
|
+
|
219
|
+
context.report({
|
220
|
+
node: node.body,
|
221
|
+
messageId: "returnsValue",
|
222
|
+
suggest
|
223
|
+
});
|
108
224
|
}
|
109
225
|
},
|
110
226
|
|
@@ -113,9 +229,31 @@ module.exports = {
|
|
113
229
|
},
|
114
230
|
|
115
231
|
ReturnStatement(node) {
|
116
|
-
if (funcInfo.shouldCheck && node.argument) {
|
117
|
-
|
232
|
+
if (!(funcInfo.shouldCheck && node.argument)) {
|
233
|
+
return;
|
118
234
|
}
|
235
|
+
|
236
|
+
// node is `return <expression>`
|
237
|
+
if (!allowVoid) {
|
238
|
+
context.report({ node, messageId: "returnsValue" });
|
239
|
+
return;
|
240
|
+
}
|
241
|
+
|
242
|
+
if (expressionIsVoid(node.argument)) {
|
243
|
+
return;
|
244
|
+
}
|
245
|
+
|
246
|
+
// allowVoid && !expressionIsVoid
|
247
|
+
context.report({
|
248
|
+
node,
|
249
|
+
messageId: "returnsValue",
|
250
|
+
suggest: [{
|
251
|
+
messageId: "prependVoid",
|
252
|
+
fix(fixer) {
|
253
|
+
return voidPrependFixer(sourceCode, node.argument, fixer);
|
254
|
+
}
|
255
|
+
}]
|
256
|
+
});
|
119
257
|
}
|
120
258
|
};
|
121
259
|
}
|
@@ -90,6 +90,21 @@ module.exports = {
|
|
90
90
|
return Boolean(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends);
|
91
91
|
}
|
92
92
|
|
93
|
+
/**
|
94
|
+
* Determines if every segment in a set has been called.
|
95
|
+
* @param {Set<CodePathSegment>} segments The segments to search.
|
96
|
+
* @returns {boolean} True if every segment has been called; false otherwise.
|
97
|
+
*/
|
98
|
+
function isEverySegmentCalled(segments) {
|
99
|
+
for (const segment of segments) {
|
100
|
+
if (!isCalled(segment)) {
|
101
|
+
return false;
|
102
|
+
}
|
103
|
+
}
|
104
|
+
|
105
|
+
return true;
|
106
|
+
}
|
107
|
+
|
93
108
|
/**
|
94
109
|
* Checks whether or not this is before `super()` is called.
|
95
110
|
* @returns {boolean} `true` if this is before `super()` is called.
|
@@ -97,7 +112,7 @@ module.exports = {
|
|
97
112
|
function isBeforeCallOfSuper() {
|
98
113
|
return (
|
99
114
|
isInConstructorOfDerivedClass() &&
|
100
|
-
!funcInfo.
|
115
|
+
!isEverySegmentCalled(funcInfo.currentSegments)
|
101
116
|
);
|
102
117
|
}
|
103
118
|
|
@@ -108,11 +123,9 @@ module.exports = {
|
|
108
123
|
* @returns {void}
|
109
124
|
*/
|
110
125
|
function setInvalid(node) {
|
111
|
-
const segments = funcInfo.
|
112
|
-
|
113
|
-
for (let i = 0; i < segments.length; ++i) {
|
114
|
-
const segment = segments[i];
|
126
|
+
const segments = funcInfo.currentSegments;
|
115
127
|
|
128
|
+
for (const segment of segments) {
|
116
129
|
if (segment.reachable) {
|
117
130
|
segInfoMap[segment.id].invalidNodes.push(node);
|
118
131
|
}
|
@@ -124,11 +137,9 @@ module.exports = {
|
|
124
137
|
* @returns {void}
|
125
138
|
*/
|
126
139
|
function setSuperCalled() {
|
127
|
-
const segments = funcInfo.
|
128
|
-
|
129
|
-
for (let i = 0; i < segments.length; ++i) {
|
130
|
-
const segment = segments[i];
|
140
|
+
const segments = funcInfo.currentSegments;
|
131
141
|
|
142
|
+
for (const segment of segments) {
|
132
143
|
if (segment.reachable) {
|
133
144
|
segInfoMap[segment.id].superCalled = true;
|
134
145
|
}
|
@@ -156,14 +167,16 @@ module.exports = {
|
|
156
167
|
classNode.superClass &&
|
157
168
|
!astUtils.isNullOrUndefined(classNode.superClass)
|
158
169
|
),
|
159
|
-
codePath
|
170
|
+
codePath,
|
171
|
+
currentSegments: new Set()
|
160
172
|
};
|
161
173
|
} else {
|
162
174
|
funcInfo = {
|
163
175
|
upper: funcInfo,
|
164
176
|
isConstructor: false,
|
165
177
|
hasExtends: false,
|
166
|
-
codePath
|
178
|
+
codePath,
|
179
|
+
currentSegments: new Set()
|
167
180
|
};
|
168
181
|
}
|
169
182
|
},
|
@@ -211,6 +224,8 @@ module.exports = {
|
|
211
224
|
* @returns {void}
|
212
225
|
*/
|
213
226
|
onCodePathSegmentStart(segment) {
|
227
|
+
funcInfo.currentSegments.add(segment);
|
228
|
+
|
214
229
|
if (!isInConstructorOfDerivedClass()) {
|
215
230
|
return;
|
216
231
|
}
|
@@ -225,6 +240,18 @@ module.exports = {
|
|
225
240
|
};
|
226
241
|
},
|
227
242
|
|
243
|
+
onUnreachableCodePathSegmentStart(segment) {
|
244
|
+
funcInfo.currentSegments.add(segment);
|
245
|
+
},
|
246
|
+
|
247
|
+
onUnreachableCodePathSegmentEnd(segment) {
|
248
|
+
funcInfo.currentSegments.delete(segment);
|
249
|
+
},
|
250
|
+
|
251
|
+
onCodePathSegmentEnd(segment) {
|
252
|
+
funcInfo.currentSegments.delete(segment);
|
253
|
+
},
|
254
|
+
|
228
255
|
/**
|
229
256
|
* Update information of the code path segment when a code path was
|
230
257
|
* looped.
|
@@ -11,6 +11,22 @@
|
|
11
11
|
|
12
12
|
const allLoopTypes = ["WhileStatement", "DoWhileStatement", "ForStatement", "ForInStatement", "ForOfStatement"];
|
13
13
|
|
14
|
+
/**
|
15
|
+
* Checks all segments in a set and returns true if any are reachable.
|
16
|
+
* @param {Set<CodePathSegment>} segments The segments to check.
|
17
|
+
* @returns {boolean} True if any segment is reachable; false otherwise.
|
18
|
+
*/
|
19
|
+
function isAnySegmentReachable(segments) {
|
20
|
+
|
21
|
+
for (const segment of segments) {
|
22
|
+
if (segment.reachable) {
|
23
|
+
return true;
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
return false;
|
28
|
+
}
|
29
|
+
|
14
30
|
/**
|
15
31
|
* Determines whether the given node is the first node in the code path to which a loop statement
|
16
32
|
* 'loops' for the next iteration.
|
@@ -90,29 +106,36 @@ module.exports = {
|
|
90
106
|
loopsByTargetSegments = new Map(),
|
91
107
|
loopsToReport = new Set();
|
92
108
|
|
93
|
-
|
109
|
+
const codePathSegments = [];
|
110
|
+
let currentCodePathSegments = new Set();
|
94
111
|
|
95
112
|
return {
|
96
|
-
|
97
|
-
|
113
|
+
|
114
|
+
onCodePathStart() {
|
115
|
+
codePathSegments.push(currentCodePathSegments);
|
116
|
+
currentCodePathSegments = new Set();
|
98
117
|
},
|
99
118
|
|
100
119
|
onCodePathEnd() {
|
101
|
-
|
120
|
+
currentCodePathSegments = codePathSegments.pop();
|
102
121
|
},
|
103
122
|
|
104
|
-
|
123
|
+
onUnreachableCodePathSegmentStart(segment) {
|
124
|
+
currentCodePathSegments.add(segment);
|
125
|
+
},
|
105
126
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
}
|
127
|
+
onUnreachableCodePathSegmentEnd(segment) {
|
128
|
+
currentCodePathSegments.delete(segment);
|
129
|
+
},
|
130
|
+
|
131
|
+
onCodePathSegmentEnd(segment) {
|
132
|
+
currentCodePathSegments.delete(segment);
|
113
133
|
},
|
114
134
|
|
115
135
|
onCodePathSegmentStart(segment, node) {
|
136
|
+
|
137
|
+
currentCodePathSegments.add(segment);
|
138
|
+
|
116
139
|
if (isLoopingTarget(node)) {
|
117
140
|
const loop = node.parent;
|
118
141
|
|
@@ -140,6 +163,18 @@ module.exports = {
|
|
140
163
|
}
|
141
164
|
},
|
142
165
|
|
166
|
+
[loopSelector](node) {
|
167
|
+
|
168
|
+
/**
|
169
|
+
* Ignore unreachable loop statements to avoid unnecessary complexity in the implementation, or false positives otherwise.
|
170
|
+
* For unreachable segments, the code path analysis does not raise events required for this implementation.
|
171
|
+
*/
|
172
|
+
if (isAnySegmentReachable(currentCodePathSegments)) {
|
173
|
+
loopsToReport.add(node);
|
174
|
+
}
|
175
|
+
},
|
176
|
+
|
177
|
+
|
143
178
|
"Program:exit"() {
|
144
179
|
loopsToReport.forEach(
|
145
180
|
node => context.report({ node, messageId: "invalid" })
|