eslint 9.0.0-beta.1 → 9.0.0-rc.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 +19 -7
- package/bin/eslint.js +2 -1
- package/lib/cli.js +28 -11
- package/lib/config/flat-config-schema.js +1 -2
- package/lib/config/rule-validator.js +15 -2
- package/lib/eslint/eslint-helpers.js +0 -1
- package/lib/eslint/eslint.js +8 -1
- package/lib/linter/code-path-analysis/code-path-analyzer.js +0 -1
- package/lib/linter/index.js +1 -3
- package/lib/linter/linter.js +46 -31
- package/lib/rule-tester/index.js +3 -1
- package/lib/rules/complexity.js +13 -0
- package/lib/rules/constructor-super.js +53 -23
- package/lib/rules/no-fallthrough.js +41 -16
- package/lib/rules/no-misleading-character-class.js +110 -63
- package/lib/rules/no-restricted-imports.js +183 -47
- package/lib/rules/no-this-before-super.js +28 -9
- package/lib/rules/no-unused-vars.js +14 -1
- package/lib/rules/no-useless-return.js +7 -2
- package/lib/rules/utils/char-source.js +240 -0
- package/lib/rules/utils/lazy-loading-rule-map.js +1 -1
- package/lib/rules/utils/unicode/index.js +9 -4
- package/lib/shared/runtime-info.js +1 -0
- package/lib/source-code/index.js +3 -1
- package/lib/source-code/source-code.js +165 -1
- package/package.json +9 -6
- package/lib/cli-engine/xml-escape.js +0 -34
- package/lib/shared/deprecation-warnings.js +0 -58
package/README.md
CHANGED
@@ -127,6 +127,18 @@ In other cases (including if rules need to warn on more or fewer cases due to ne
|
|
127
127
|
|
128
128
|
Once a language feature has been adopted into the ECMAScript standard (stage 4 according to the [TC39 process](https://tc39.github.io/process-document/)), we will accept issues and pull requests related to the new feature, subject to our [contributing guidelines](https://eslint.org/docs/latest/contribute). Until then, please use the appropriate parser and plugin(s) for your experimental feature.
|
129
129
|
|
130
|
+
### Which Node.js versions does ESLint support?
|
131
|
+
|
132
|
+
ESLint updates the supported Node.js versions with each major release of ESLint. At that time, ESLint's supported Node.js versions are updated to be:
|
133
|
+
|
134
|
+
1. The most recent maintenance release of Node.js
|
135
|
+
1. The lowest minor version of the Node.js LTS release that includes the features the ESLint team wants to use.
|
136
|
+
1. The Node.js Current release
|
137
|
+
|
138
|
+
ESLint is also expected to work with Node.js versions released after the Node.js Current release.
|
139
|
+
|
140
|
+
Refer to the [Quick Start Guide](https://eslint.org/docs/latest/use/getting-started#prerequisites) for the officially supported Node.js versions for a given ESLint release.
|
141
|
+
|
130
142
|
### Where to ask for help?
|
131
143
|
|
132
144
|
Open a [discussion](https://github.com/eslint/eslint/discussions) or stop by our [Discord server](https://eslint.org/chat).
|
@@ -213,6 +225,11 @@ The people who manage releases, review feature requests, and meet regularly to e
|
|
213
225
|
Nicholas C. Zakas
|
214
226
|
</a>
|
215
227
|
</td><td align="center" valign="top" width="11%">
|
228
|
+
<a href="https://github.com/fasttime">
|
229
|
+
<img src="https://github.com/fasttime.png?s=75" width="75" height="75" alt="Francesco Trotta's Avatar"><br />
|
230
|
+
Francesco Trotta
|
231
|
+
</a>
|
232
|
+
</td><td align="center" valign="top" width="11%">
|
216
233
|
<a href="https://github.com/mdjermanovic">
|
217
234
|
<img src="https://github.com/mdjermanovic.png?s=75" width="75" height="75" alt="Milos Djermanovic's Avatar"><br />
|
218
235
|
Milos Djermanovic
|
@@ -250,11 +267,6 @@ Bryan Mishkin
|
|
250
267
|
Josh Goldberg ✨
|
251
268
|
</a>
|
252
269
|
</td><td align="center" valign="top" width="11%">
|
253
|
-
<a href="https://github.com/fasttime">
|
254
|
-
<img src="https://github.com/fasttime.png?s=75" width="75" height="75" alt="Francesco Trotta's Avatar"><br />
|
255
|
-
Francesco Trotta
|
256
|
-
</a>
|
257
|
-
</td><td align="center" valign="top" width="11%">
|
258
270
|
<a href="https://github.com/Tanujkanti4441">
|
259
271
|
<img src="https://github.com/Tanujkanti4441.png?s=75" width="75" height="75" alt="Tanuj Kanti's Avatar"><br />
|
260
272
|
Tanuj Kanti
|
@@ -291,8 +303,8 @@ The following companies, organizations, and individuals support ESLint's ongoing
|
|
291
303
|
<!-- NOTE: This section is autogenerated. Do not manually edit.-->
|
292
304
|
<!--sponsorsstart-->
|
293
305
|
<h3>Platinum Sponsors</h3>
|
294
|
-
<p><a href="
|
295
|
-
<p><a href="https://engineering.salesforce.com"><img src="https://images.opencollective.com/salesforce/ca8f997/logo.png" alt="Salesforce" height="96"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="96"></a></p><h3>Silver Sponsors</h3>
|
306
|
+
<p><a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="undefined"></a></p><h3>Gold Sponsors</h3>
|
307
|
+
<p><a href="https://bitwarden.com"><img src="https://avatars.githubusercontent.com/u/15990069?v=4" alt="Bitwarden" height="96"></a> <a href="https://engineering.salesforce.com"><img src="https://images.opencollective.com/salesforce/ca8f997/logo.png" alt="Salesforce" height="96"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="96"></a></p><h3>Silver Sponsors</h3>
|
296
308
|
<p><a href="https://www.jetbrains.com/"><img src="https://images.opencollective.com/jetbrains/eb04ddc/logo.png" alt="JetBrains" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301?v=4" alt="American Express" height="64"></a> <a href="https://www.workleap.com"><img src="https://avatars.githubusercontent.com/u/53535748?u=d1e55d7661d724bf2281c1bfd33cb8f99fe2465f&v=4" alt="Workleap" height="64"></a></p><h3>Bronze Sponsors</h3>
|
297
309
|
<p><a href="https://www.notion.so"><img src="https://images.opencollective.com/notion/bf3b117/logo.png" alt="notion" height="32"></a> <a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/d5592fe/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://transloadit.com/"><img src="https://avatars.githubusercontent.com/u/125754?v=4" alt="Transloadit" height="32"></a> <a href="https://www.ignitionapp.com"><img src="https://avatars.githubusercontent.com/u/5753491?v=4" alt="Ignition" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104?v=4" alt="Nx" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774?v=4" alt="HeroCoders" height="32"></a> <a href="https://usenextbase.com"><img src="https://avatars.githubusercontent.com/u/145838380?v=4" alt="Nextbase Starter Kit" height="32"></a></p>
|
298
310
|
<!--sponsorsend-->
|
package/bin/eslint.js
CHANGED
@@ -149,7 +149,8 @@ ${getErrorMessage(error)}`;
|
|
149
149
|
}
|
150
150
|
|
151
151
|
// Otherwise, call the CLI.
|
152
|
-
const
|
152
|
+
const cli = require("../lib/cli");
|
153
|
+
const exitCode = await cli.execute(
|
153
154
|
process.argv,
|
154
155
|
process.argv.includes("--stdin") ? await readStdin() : null,
|
155
156
|
true
|
package/lib/cli.js
CHANGED
@@ -37,6 +37,7 @@ const debug = require("debug")("eslint:cli");
|
|
37
37
|
/** @typedef {import("./eslint/eslint").LintMessage} LintMessage */
|
38
38
|
/** @typedef {import("./eslint/eslint").LintResult} LintResult */
|
39
39
|
/** @typedef {import("./options").ParsedCLIOptions} ParsedCLIOptions */
|
40
|
+
/** @typedef {import("./shared/types").Plugin} Plugin */
|
40
41
|
/** @typedef {import("./shared/types").ResultsMeta} ResultsMeta */
|
41
42
|
|
42
43
|
//------------------------------------------------------------------------------
|
@@ -47,6 +48,32 @@ const mkdir = promisify(fs.mkdir);
|
|
47
48
|
const stat = promisify(fs.stat);
|
48
49
|
const writeFile = promisify(fs.writeFile);
|
49
50
|
|
51
|
+
/**
|
52
|
+
* Loads plugins with the specified names.
|
53
|
+
* @param {{ "import": (name: string) => Promise<any> }} importer An object with an `import` method called once for each plugin.
|
54
|
+
* @param {string[]} pluginNames The names of the plugins to be loaded, with or without the "eslint-plugin-" prefix.
|
55
|
+
* @returns {Promise<Record<string, Plugin>>} A mapping of plugin short names to implementations.
|
56
|
+
*/
|
57
|
+
async function loadPlugins(importer, pluginNames) {
|
58
|
+
const plugins = {};
|
59
|
+
|
60
|
+
await Promise.all(pluginNames.map(async pluginName => {
|
61
|
+
|
62
|
+
const longName = naming.normalizePackageName(pluginName, "eslint-plugin");
|
63
|
+
const module = await importer.import(longName);
|
64
|
+
|
65
|
+
if (!("default" in module)) {
|
66
|
+
throw new Error(`"${longName}" cannot be used with the \`--plugin\` option because its default module does not provide a \`default\` export`);
|
67
|
+
}
|
68
|
+
|
69
|
+
const shortName = naming.getShorthandName(pluginName, "eslint-plugin");
|
70
|
+
|
71
|
+
plugins[shortName] = module.default;
|
72
|
+
}));
|
73
|
+
|
74
|
+
return plugins;
|
75
|
+
}
|
76
|
+
|
50
77
|
/**
|
51
78
|
* Predicate function for whether or not to apply fixes in quiet mode.
|
52
79
|
* If a message is a warning, do not apply a fix.
|
@@ -152,17 +179,7 @@ async function translateOptions({
|
|
152
179
|
}
|
153
180
|
|
154
181
|
if (plugin) {
|
155
|
-
|
156
|
-
|
157
|
-
for (const pluginName of plugin) {
|
158
|
-
|
159
|
-
const shortName = naming.getShorthandName(pluginName, "eslint-plugin");
|
160
|
-
const longName = naming.normalizePackageName(pluginName, "eslint-plugin");
|
161
|
-
|
162
|
-
plugins[shortName] = await importer.import(longName);
|
163
|
-
}
|
164
|
-
|
165
|
-
overrideConfig[0].plugins = plugins;
|
182
|
+
overrideConfig[0].plugins = await loadPlugins(importer, plugin);
|
166
183
|
}
|
167
184
|
|
168
185
|
} else {
|
@@ -167,9 +167,22 @@ class RuleValidator {
|
|
167
167
|
validateRule(ruleOptions.slice(1));
|
168
168
|
|
169
169
|
if (validateRule.errors) {
|
170
|
-
throw new Error(`Key "rules": Key "${ruleId}"
|
170
|
+
throw new Error(`Key "rules": Key "${ruleId}":\n${
|
171
171
|
validateRule.errors.map(
|
172
|
-
error =>
|
172
|
+
error => {
|
173
|
+
if (
|
174
|
+
error.keyword === "additionalProperties" &&
|
175
|
+
error.schema === false &&
|
176
|
+
typeof error.parentSchema?.properties === "object" &&
|
177
|
+
typeof error.params?.additionalProperty === "string"
|
178
|
+
) {
|
179
|
+
const expectedProperties = Object.keys(error.parentSchema.properties).map(property => `"${property}"`);
|
180
|
+
|
181
|
+
return `\tValue ${JSON.stringify(error.data)} ${error.message}.\n\t\tUnexpected property "${error.params.additionalProperty}". Expected properties: ${expectedProperties.join(", ")}.\n`;
|
182
|
+
}
|
183
|
+
|
184
|
+
return `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`;
|
185
|
+
}
|
173
186
|
).join("")
|
174
187
|
}`);
|
175
188
|
}
|
package/lib/eslint/eslint.js
CHANGED
@@ -838,6 +838,7 @@ class ESLint {
|
|
838
838
|
configs,
|
839
839
|
errorOnUnmatchedPattern
|
840
840
|
});
|
841
|
+
const controller = new AbortController();
|
841
842
|
|
842
843
|
debug(`${filePaths.length} files found in: ${Date.now() - startTime}ms`);
|
843
844
|
|
@@ -906,9 +907,12 @@ class ESLint {
|
|
906
907
|
fixer = message => shouldMessageBeFixed(message, config, fixTypesSet) && originalFix(message);
|
907
908
|
}
|
908
909
|
|
909
|
-
return fs.readFile(filePath, "utf8")
|
910
|
+
return fs.readFile(filePath, { encoding: "utf8", signal: controller.signal })
|
910
911
|
.then(text => {
|
911
912
|
|
913
|
+
// fail immediately if an error occurred in another file
|
914
|
+
controller.signal.throwIfAborted();
|
915
|
+
|
912
916
|
// do the linting
|
913
917
|
const result = verifyText({
|
914
918
|
text,
|
@@ -932,6 +936,9 @@ class ESLint {
|
|
932
936
|
}
|
933
937
|
|
934
938
|
return result;
|
939
|
+
}).catch(error => {
|
940
|
+
controller.abort(error);
|
941
|
+
throw error;
|
935
942
|
});
|
936
943
|
|
937
944
|
})
|
package/lib/linter/index.js
CHANGED
@@ -1,13 +1,11 @@
|
|
1
1
|
"use strict";
|
2
2
|
|
3
3
|
const { Linter } = require("./linter");
|
4
|
-
const { interpolate } = require("./interpolate");
|
5
4
|
const SourceCodeFixer = require("./source-code-fixer");
|
6
5
|
|
7
6
|
module.exports = {
|
8
7
|
Linter,
|
9
8
|
|
10
9
|
// For testers.
|
11
|
-
SourceCodeFixer
|
12
|
-
interpolate
|
10
|
+
SourceCodeFixer
|
13
11
|
};
|
package/lib/linter/linter.js
CHANGED
@@ -30,7 +30,6 @@ const
|
|
30
30
|
} = require("@eslint/eslintrc/universal"),
|
31
31
|
Traverser = require("../shared/traverser"),
|
32
32
|
{ SourceCode } = require("../source-code"),
|
33
|
-
CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"),
|
34
33
|
applyDisableDirectives = require("./apply-disable-directives"),
|
35
34
|
ConfigCommentParser = require("./config-comment-parser"),
|
36
35
|
NodeEventGenerator = require("./node-event-generator"),
|
@@ -53,13 +52,13 @@ const commentParser = new ConfigCommentParser();
|
|
53
52
|
const DEFAULT_ERROR_LOC = { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } };
|
54
53
|
const parserSymbol = Symbol.for("eslint.RuleTester.parser");
|
55
54
|
const { LATEST_ECMA_VERSION } = require("../../conf/ecma-version");
|
55
|
+
const STEP_KIND_VISIT = 1;
|
56
|
+
const STEP_KIND_CALL = 2;
|
56
57
|
|
57
58
|
//------------------------------------------------------------------------------
|
58
59
|
// Typedefs
|
59
60
|
//------------------------------------------------------------------------------
|
60
61
|
|
61
|
-
/** @typedef {InstanceType<import("../cli-engine/config-array").ConfigArray>} ConfigArray */
|
62
|
-
/** @typedef {InstanceType<import("../cli-engine/config-array").ExtractedConfig>} ExtractedConfig */
|
63
62
|
/** @typedef {import("../shared/types").ConfigData} ConfigData */
|
64
63
|
/** @typedef {import("../shared/types").Environment} Environment */
|
65
64
|
/** @typedef {import("../shared/types").GlobalConf} GlobalConf */
|
@@ -439,6 +438,14 @@ function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig, config)
|
|
439
438
|
return;
|
440
439
|
}
|
441
440
|
|
441
|
+
if (Object.hasOwn(configuredRules, name)) {
|
442
|
+
problems.push(createLintingProblem({
|
443
|
+
message: `Rule "${name}" is already configured by another configuration comment in the preceding code. This configuration is ignored.`,
|
444
|
+
loc: comment.loc
|
445
|
+
}));
|
446
|
+
return;
|
447
|
+
}
|
448
|
+
|
442
449
|
let ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue];
|
443
450
|
|
444
451
|
/*
|
@@ -980,22 +987,13 @@ function createRuleListeners(rule, ruleContext) {
|
|
980
987
|
* @param {string} physicalFilename The full path of the file on disk without any code block information
|
981
988
|
* @param {Function} ruleFilter A predicate function to filter which rules should be executed.
|
982
989
|
* @returns {LintMessage[]} An array of reported problems
|
990
|
+
* @throws {Error} If traversal into a node fails.
|
983
991
|
*/
|
984
992
|
function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageOptions, settings, filename, disableFixes, cwd, physicalFilename, ruleFilter) {
|
985
993
|
const emitter = createEmitter();
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
Traverser.traverse(sourceCode.ast, {
|
990
|
-
enter(node, parent) {
|
991
|
-
node.parent = parent;
|
992
|
-
nodeQueue.push({ isEntering: true, node });
|
993
|
-
},
|
994
|
-
leave(node) {
|
995
|
-
nodeQueue.push({ isEntering: false, node });
|
996
|
-
},
|
997
|
-
visitorKeys: sourceCode.visitorKeys
|
998
|
-
});
|
994
|
+
|
995
|
+
// must happen first to assign all node.parent properties
|
996
|
+
const eventQueue = sourceCode.traverse();
|
999
997
|
|
1000
998
|
/*
|
1001
999
|
* Create a frozen object with the ruleContext properties and methods that are shared by all rules.
|
@@ -1125,25 +1123,34 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO
|
|
1125
1123
|
});
|
1126
1124
|
});
|
1127
1125
|
|
1128
|
-
|
1129
|
-
const eventGenerator = nodeQueue[0].node.type === "Program"
|
1130
|
-
? new CodePathAnalyzer(new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys }))
|
1131
|
-
: new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys });
|
1126
|
+
const eventGenerator = new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys });
|
1132
1127
|
|
1133
|
-
|
1134
|
-
|
1128
|
+
for (const step of eventQueue) {
|
1129
|
+
switch (step.kind) {
|
1130
|
+
case STEP_KIND_VISIT: {
|
1131
|
+
try {
|
1132
|
+
if (step.phase === 1) {
|
1133
|
+
eventGenerator.enterNode(step.target);
|
1134
|
+
} else {
|
1135
|
+
eventGenerator.leaveNode(step.target);
|
1136
|
+
}
|
1137
|
+
} catch (err) {
|
1138
|
+
err.currentNode = step.target;
|
1139
|
+
throw err;
|
1140
|
+
}
|
1141
|
+
break;
|
1142
|
+
}
|
1135
1143
|
|
1136
|
-
|
1137
|
-
|
1138
|
-
|
1139
|
-
} else {
|
1140
|
-
eventGenerator.leaveNode(currentNode);
|
1144
|
+
case STEP_KIND_CALL: {
|
1145
|
+
emitter.emit(step.target, ...step.args);
|
1146
|
+
break;
|
1141
1147
|
}
|
1142
|
-
|
1143
|
-
|
1144
|
-
|
1148
|
+
|
1149
|
+
default:
|
1150
|
+
throw new Error(`Invalid traversal step found: "${step.type}".`);
|
1145
1151
|
}
|
1146
|
-
|
1152
|
+
|
1153
|
+
}
|
1147
1154
|
|
1148
1155
|
return lintingProblems;
|
1149
1156
|
}
|
@@ -1706,6 +1713,14 @@ class Linter {
|
|
1706
1713
|
return;
|
1707
1714
|
}
|
1708
1715
|
|
1716
|
+
if (Object.hasOwn(mergedInlineConfig.rules, ruleId)) {
|
1717
|
+
inlineConfigProblems.push(createLintingProblem({
|
1718
|
+
message: `Rule "${ruleId}" is already configured by another configuration comment in the preceding code. This configuration is ignored.`,
|
1719
|
+
loc: node.loc
|
1720
|
+
}));
|
1721
|
+
return;
|
1722
|
+
}
|
1723
|
+
|
1709
1724
|
try {
|
1710
1725
|
|
1711
1726
|
let ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue];
|
package/lib/rule-tester/index.js
CHANGED
package/lib/rules/complexity.js
CHANGED
@@ -109,6 +109,7 @@ module.exports = {
|
|
109
109
|
IfStatement: increaseComplexity,
|
110
110
|
WhileStatement: increaseComplexity,
|
111
111
|
DoWhileStatement: increaseComplexity,
|
112
|
+
AssignmentPattern: increaseComplexity,
|
112
113
|
|
113
114
|
// Avoid `default`
|
114
115
|
"SwitchCase[test]": increaseComplexity,
|
@@ -120,6 +121,18 @@ module.exports = {
|
|
120
121
|
}
|
121
122
|
},
|
122
123
|
|
124
|
+
MemberExpression(node) {
|
125
|
+
if (node.optional === true) {
|
126
|
+
increaseComplexity();
|
127
|
+
}
|
128
|
+
},
|
129
|
+
|
130
|
+
CallExpression(node) {
|
131
|
+
if (node.optional === true) {
|
132
|
+
increaseComplexity();
|
133
|
+
}
|
134
|
+
},
|
135
|
+
|
123
136
|
onCodePathEnd(codePath, node) {
|
124
137
|
const complexity = complexities.pop();
|
125
138
|
|
@@ -119,6 +119,30 @@ function isPossibleConstructor(node) {
|
|
119
119
|
}
|
120
120
|
}
|
121
121
|
|
122
|
+
/**
|
123
|
+
* A class to store information about a code path segment.
|
124
|
+
*/
|
125
|
+
class SegmentInfo {
|
126
|
+
|
127
|
+
/**
|
128
|
+
* Indicates if super() is called in all code paths.
|
129
|
+
* @type {boolean}
|
130
|
+
*/
|
131
|
+
calledInEveryPaths = false;
|
132
|
+
|
133
|
+
/**
|
134
|
+
* Indicates if super() is called in any code paths.
|
135
|
+
* @type {boolean}
|
136
|
+
*/
|
137
|
+
calledInSomePaths = false;
|
138
|
+
|
139
|
+
/**
|
140
|
+
* The nodes which have been validated and don't need to be reconsidered.
|
141
|
+
* @type {ASTNode[]}
|
142
|
+
*/
|
143
|
+
validNodes = [];
|
144
|
+
}
|
145
|
+
|
122
146
|
//------------------------------------------------------------------------------
|
123
147
|
// Rule Definition
|
124
148
|
//------------------------------------------------------------------------------
|
@@ -159,12 +183,8 @@ module.exports = {
|
|
159
183
|
*/
|
160
184
|
let funcInfo = null;
|
161
185
|
|
162
|
-
|
163
|
-
* {
|
164
|
-
* Information for each code path segment.
|
165
|
-
* - calledInSomePaths: A flag of be called `super()` in some code paths.
|
166
|
-
* - calledInEveryPaths: A flag of be called `super()` in all code paths.
|
167
|
-
* - validNodes:
|
186
|
+
/**
|
187
|
+
* @type {Record<string, SegmentInfo>}
|
168
188
|
*/
|
169
189
|
let segInfoMap = Object.create(null);
|
170
190
|
|
@@ -174,7 +194,16 @@ module.exports = {
|
|
174
194
|
* @returns {boolean} The flag which shows `super()` is called in some paths
|
175
195
|
*/
|
176
196
|
function isCalledInSomePath(segment) {
|
177
|
-
return segment.reachable && segInfoMap[segment.id]
|
197
|
+
return segment.reachable && segInfoMap[segment.id]?.calledInSomePaths;
|
198
|
+
}
|
199
|
+
|
200
|
+
/**
|
201
|
+
* Determines if a segment has been seen in the traversal.
|
202
|
+
* @param {CodePathSegment} segment A code path segment to check.
|
203
|
+
* @returns {boolean} `true` if the segment has been seen.
|
204
|
+
*/
|
205
|
+
function hasSegmentBeenSeen(segment) {
|
206
|
+
return !!segInfoMap[segment.id];
|
178
207
|
}
|
179
208
|
|
180
209
|
/**
|
@@ -190,10 +219,10 @@ module.exports = {
|
|
190
219
|
* If not skipped, this never becomes true after a loop.
|
191
220
|
*/
|
192
221
|
if (segment.nextSegments.length === 1 &&
|
193
|
-
segment.nextSegments[0]
|
194
|
-
) {
|
222
|
+
segment.nextSegments[0]?.isLoopedPrevSegment(segment)) {
|
195
223
|
return true;
|
196
224
|
}
|
225
|
+
|
197
226
|
return segment.reachable && segInfoMap[segment.id].calledInEveryPaths;
|
198
227
|
}
|
199
228
|
|
@@ -250,9 +279,9 @@ module.exports = {
|
|
250
279
|
}
|
251
280
|
|
252
281
|
// Reports if `super()` lacked.
|
253
|
-
const
|
254
|
-
const calledInEveryPaths =
|
255
|
-
const calledInSomePaths =
|
282
|
+
const seenSegments = codePath.returnedSegments.filter(hasSegmentBeenSeen);
|
283
|
+
const calledInEveryPaths = seenSegments.every(isCalledInEveryPath);
|
284
|
+
const calledInSomePaths = seenSegments.some(isCalledInSomePath);
|
256
285
|
|
257
286
|
if (!calledInEveryPaths) {
|
258
287
|
context.report({
|
@@ -278,18 +307,16 @@ module.exports = {
|
|
278
307
|
}
|
279
308
|
|
280
309
|
// Initialize info.
|
281
|
-
const info = segInfoMap[segment.id] =
|
282
|
-
calledInSomePaths: false,
|
283
|
-
calledInEveryPaths: false,
|
284
|
-
validNodes: []
|
285
|
-
};
|
310
|
+
const info = segInfoMap[segment.id] = new SegmentInfo();
|
286
311
|
|
287
312
|
// When there are previous segments, aggregates these.
|
288
313
|
const prevSegments = segment.prevSegments;
|
289
314
|
|
290
315
|
if (prevSegments.length > 0) {
|
291
|
-
|
292
|
-
|
316
|
+
const seenPrevSegments = prevSegments.filter(hasSegmentBeenSeen);
|
317
|
+
|
318
|
+
info.calledInSomePaths = seenPrevSegments.some(isCalledInSomePath);
|
319
|
+
info.calledInEveryPaths = seenPrevSegments.every(isCalledInEveryPath);
|
293
320
|
}
|
294
321
|
},
|
295
322
|
|
@@ -326,12 +353,12 @@ module.exports = {
|
|
326
353
|
funcInfo.codePath.traverseSegments(
|
327
354
|
{ first: toSegment, last: fromSegment },
|
328
355
|
segment => {
|
329
|
-
const info = segInfoMap[segment.id];
|
330
|
-
const
|
356
|
+
const info = segInfoMap[segment.id] ?? new SegmentInfo();
|
357
|
+
const seenPrevSegments = segment.prevSegments.filter(hasSegmentBeenSeen);
|
331
358
|
|
332
359
|
// Updates flags.
|
333
|
-
info.calledInSomePaths =
|
334
|
-
info.calledInEveryPaths =
|
360
|
+
info.calledInSomePaths = seenPrevSegments.some(isCalledInSomePath);
|
361
|
+
info.calledInEveryPaths = seenPrevSegments.every(isCalledInEveryPath);
|
335
362
|
|
336
363
|
// If flags become true anew, reports the valid nodes.
|
337
364
|
if (info.calledInSomePaths || isRealLoop) {
|
@@ -348,6 +375,9 @@ module.exports = {
|
|
348
375
|
});
|
349
376
|
}
|
350
377
|
}
|
378
|
+
|
379
|
+
// save just in case we created a new SegmentInfo object
|
380
|
+
segInfoMap[segment.id] = info;
|
351
381
|
}
|
352
382
|
);
|
353
383
|
},
|
@@ -48,9 +48,9 @@ function isFallThroughComment(comment, fallthroughCommentPattern) {
|
|
48
48
|
* @param {ASTNode} subsequentCase The case after caseWhichFallsThrough.
|
49
49
|
* @param {RuleContext} context A rule context which stores comments.
|
50
50
|
* @param {RegExp} fallthroughCommentPattern A pattern to match comment to.
|
51
|
-
* @returns {
|
51
|
+
* @returns {null | object} the comment if the case has a valid fallthrough comment, otherwise null
|
52
52
|
*/
|
53
|
-
function
|
53
|
+
function getFallthroughComment(caseWhichFallsThrough, subsequentCase, context, fallthroughCommentPattern) {
|
54
54
|
const sourceCode = context.sourceCode;
|
55
55
|
|
56
56
|
if (caseWhichFallsThrough.consequent.length === 1 && caseWhichFallsThrough.consequent[0].type === "BlockStatement") {
|
@@ -58,13 +58,17 @@ function hasFallthroughComment(caseWhichFallsThrough, subsequentCase, context, f
|
|
58
58
|
const commentInBlock = sourceCode.getCommentsBefore(trailingCloseBrace).pop();
|
59
59
|
|
60
60
|
if (commentInBlock && isFallThroughComment(commentInBlock.value, fallthroughCommentPattern)) {
|
61
|
-
return
|
61
|
+
return commentInBlock;
|
62
62
|
}
|
63
63
|
}
|
64
64
|
|
65
65
|
const comment = sourceCode.getCommentsBefore(subsequentCase).pop();
|
66
66
|
|
67
|
-
|
67
|
+
if (comment && isFallThroughComment(comment.value, fallthroughCommentPattern)) {
|
68
|
+
return comment;
|
69
|
+
}
|
70
|
+
|
71
|
+
return null;
|
68
72
|
}
|
69
73
|
|
70
74
|
/**
|
@@ -103,12 +107,17 @@ module.exports = {
|
|
103
107
|
allowEmptyCase: {
|
104
108
|
type: "boolean",
|
105
109
|
default: false
|
110
|
+
},
|
111
|
+
reportUnusedFallthroughComment: {
|
112
|
+
type: "boolean",
|
113
|
+
default: false
|
106
114
|
}
|
107
115
|
},
|
108
116
|
additionalProperties: false
|
109
117
|
}
|
110
118
|
],
|
111
119
|
messages: {
|
120
|
+
unusedFallthroughComment: "Found a comment that would permit fallthrough, but case cannot fall through.",
|
112
121
|
case: "Expected a 'break' statement before 'case'.",
|
113
122
|
default: "Expected a 'break' statement before 'default'."
|
114
123
|
}
|
@@ -120,12 +129,13 @@ module.exports = {
|
|
120
129
|
let currentCodePathSegments = new Set();
|
121
130
|
const sourceCode = context.sourceCode;
|
122
131
|
const allowEmptyCase = options.allowEmptyCase || false;
|
132
|
+
const reportUnusedFallthroughComment = options.reportUnusedFallthroughComment || false;
|
123
133
|
|
124
134
|
/*
|
125
135
|
* We need to use leading comments of the next SwitchCase node because
|
126
136
|
* trailing comments is wrong if semicolons are omitted.
|
127
137
|
*/
|
128
|
-
let
|
138
|
+
let previousCase = null;
|
129
139
|
let fallthroughCommentPattern = null;
|
130
140
|
|
131
141
|
if (options.commentPattern) {
|
@@ -168,13 +178,23 @@ module.exports = {
|
|
168
178
|
* And reports the previous fallthrough node if that does not exist.
|
169
179
|
*/
|
170
180
|
|
171
|
-
if (
|
172
|
-
context
|
173
|
-
|
174
|
-
|
175
|
-
|
181
|
+
if (previousCase && previousCase.node.parent === node.parent) {
|
182
|
+
const previousCaseFallthroughComment = getFallthroughComment(previousCase.node, node, context, fallthroughCommentPattern);
|
183
|
+
|
184
|
+
if (previousCase.isFallthrough && !(previousCaseFallthroughComment)) {
|
185
|
+
context.report({
|
186
|
+
messageId: node.test ? "case" : "default",
|
187
|
+
node
|
188
|
+
});
|
189
|
+
} else if (reportUnusedFallthroughComment && !previousCase.isSwitchExitReachable && previousCaseFallthroughComment) {
|
190
|
+
context.report({
|
191
|
+
messageId: "unusedFallthroughComment",
|
192
|
+
node: previousCaseFallthroughComment
|
193
|
+
});
|
194
|
+
}
|
195
|
+
|
176
196
|
}
|
177
|
-
|
197
|
+
previousCase = null;
|
178
198
|
},
|
179
199
|
|
180
200
|
"SwitchCase:exit"(node) {
|
@@ -185,11 +205,16 @@ module.exports = {
|
|
185
205
|
* `break`, `return`, or `throw` are unreachable.
|
186
206
|
* And allows empty cases and the last case.
|
187
207
|
*/
|
188
|
-
|
189
|
-
|
190
|
-
node.parent.cases.at(-1) !== node
|
191
|
-
|
192
|
-
|
208
|
+
const isSwitchExitReachable = isAnySegmentReachable(currentCodePathSegments);
|
209
|
+
const isFallthrough = isSwitchExitReachable && (node.consequent.length > 0 || (!allowEmptyCase && hasBlankLinesBetween(node, nextToken))) &&
|
210
|
+
node.parent.cases.at(-1) !== node;
|
211
|
+
|
212
|
+
previousCase = {
|
213
|
+
node,
|
214
|
+
isSwitchExitReachable,
|
215
|
+
isFallthrough
|
216
|
+
};
|
217
|
+
|
193
218
|
}
|
194
219
|
};
|
195
220
|
}
|