eslint 9.0.0-alpha.1 → 9.0.0-beta.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 -7
- package/lib/api.js +25 -1
- package/lib/eslint/eslint.js +13 -0
- package/lib/eslint/legacy-eslint.js +6 -0
- package/lib/linter/code-path-analysis/code-path.js +28 -12
- package/lib/linter/index.js +1 -1
- package/lib/linter/interpolate.js +24 -2
- package/lib/linter/report-translator.js +1 -1
- package/lib/rule-tester/rule-tester.js +187 -69
- package/lib/rules/no-implicit-coercion.js +51 -25
- package/lib/rules/no-restricted-imports.js +53 -43
- package/lib/rules/no-this-before-super.js +17 -4
- package/lib/rules/no-unused-vars.js +1 -1
- package/lib/rules/no-useless-computed-key.js +2 -2
- package/lib/rules/use-isnan.js +70 -5
- package/lib/shared/serialization.js +55 -0
- package/package.json +9 -9
package/README.md
CHANGED
@@ -117,7 +117,7 @@ Yes, ESLint natively supports parsing JSX syntax (this must be enabled in [confi
|
|
117
117
|
|
118
118
|
### What ECMAScript versions does ESLint support?
|
119
119
|
|
120
|
-
ESLint has full support for ECMAScript 3, 5
|
120
|
+
ESLint has full support for ECMAScript 3, 5, and every year from 2015 up until the most recent stage 4 specification (the default). You can set your desired ECMAScript syntax and other settings (like global variables) through [configuration](https://eslint.org/docs/latest/use/configure).
|
121
121
|
|
122
122
|
### What about experimental features?
|
123
123
|
|
@@ -255,11 +255,6 @@ Josh Goldberg ✨
|
|
255
255
|
Francesco Trotta
|
256
256
|
</a>
|
257
257
|
</td><td align="center" valign="top" width="11%">
|
258
|
-
<a href="https://github.com/ota-meshi">
|
259
|
-
<img src="https://github.com/ota-meshi.png?s=75" width="75" height="75" alt="Yosuke Ota's Avatar"><br />
|
260
|
-
Yosuke Ota
|
261
|
-
</a>
|
262
|
-
</td><td align="center" valign="top" width="11%">
|
263
258
|
<a href="https://github.com/Tanujkanti4441">
|
264
259
|
<img src="https://github.com/Tanujkanti4441.png?s=75" width="75" height="75" alt="Tanuj Kanti's Avatar"><br />
|
265
260
|
Tanuj Kanti
|
@@ -299,7 +294,7 @@ The following companies, organizations, and individuals support ESLint's ongoing
|
|
299
294
|
<p><a href="#"><img src="https://images.opencollective.com/2021-frameworks-fund/logo.png" alt="Chrome Frameworks Fund" height="undefined"></a> <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>
|
300
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>
|
301
296
|
<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>
|
302
|
-
<p><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></p>
|
297
|
+
<p><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>
|
303
298
|
<!--sponsorsend-->
|
304
299
|
|
305
300
|
## Technology Sponsors
|
package/lib/api.js
CHANGED
@@ -9,17 +9,41 @@
|
|
9
9
|
// Requirements
|
10
10
|
//-----------------------------------------------------------------------------
|
11
11
|
|
12
|
-
const { ESLint } = require("./eslint/eslint");
|
12
|
+
const { ESLint, shouldUseFlatConfig } = require("./eslint/eslint");
|
13
|
+
const { LegacyESLint } = require("./eslint/legacy-eslint");
|
13
14
|
const { Linter } = require("./linter");
|
14
15
|
const { RuleTester } = require("./rule-tester");
|
15
16
|
const { SourceCode } = require("./source-code");
|
16
17
|
|
18
|
+
//-----------------------------------------------------------------------------
|
19
|
+
// Functions
|
20
|
+
//-----------------------------------------------------------------------------
|
21
|
+
|
22
|
+
/**
|
23
|
+
* Loads the correct ESLint constructor given the options.
|
24
|
+
* @param {Object} [options] The options object
|
25
|
+
* @param {boolean} [options.useFlatConfig] Whether or not to use a flat config
|
26
|
+
* @returns {Promise<ESLint|LegacyESLint>} The ESLint constructor
|
27
|
+
*/
|
28
|
+
async function loadESLint({ useFlatConfig } = {}) {
|
29
|
+
|
30
|
+
/*
|
31
|
+
* Note: The v8.x version of this function also accepted a `cwd` option, but
|
32
|
+
* it is not used in this implementation so we silently ignore it.
|
33
|
+
*/
|
34
|
+
|
35
|
+
const shouldESLintUseFlatConfig = useFlatConfig ?? (await shouldUseFlatConfig());
|
36
|
+
|
37
|
+
return shouldESLintUseFlatConfig ? ESLint : LegacyESLint;
|
38
|
+
}
|
39
|
+
|
17
40
|
//-----------------------------------------------------------------------------
|
18
41
|
// Exports
|
19
42
|
//-----------------------------------------------------------------------------
|
20
43
|
|
21
44
|
module.exports = {
|
22
45
|
Linter,
|
46
|
+
loadESLint,
|
23
47
|
ESLint,
|
24
48
|
RuleTester,
|
25
49
|
SourceCode
|
package/lib/eslint/eslint.js
CHANGED
@@ -565,6 +565,12 @@ function createExtraneousResultsError() {
|
|
565
565
|
*/
|
566
566
|
class ESLint {
|
567
567
|
|
568
|
+
/**
|
569
|
+
* The type of configuration used by this class.
|
570
|
+
* @type {string}
|
571
|
+
*/
|
572
|
+
static configType = "flat";
|
573
|
+
|
568
574
|
/**
|
569
575
|
* Creates a new instance of the main ESLint API.
|
570
576
|
* @param {ESLintOptions} options The options for this instance.
|
@@ -613,6 +619,13 @@ class ESLint {
|
|
613
619
|
});
|
614
620
|
}
|
615
621
|
|
622
|
+
// Check for the .eslintignore file, and warn if it's present.
|
623
|
+
if (existsSync(path.resolve(processedOptions.cwd, ".eslintignore"))) {
|
624
|
+
process.emitWarning(
|
625
|
+
"The \".eslintignore\" file is no longer supported. Switch to using the \"ignores\" property in \"eslint.config.js\": https://eslint.org/docs/latest/use/configure/migration-guide#ignoring-files",
|
626
|
+
"ESLintIgnoreWarning"
|
627
|
+
);
|
628
|
+
}
|
616
629
|
}
|
617
630
|
|
618
631
|
/**
|
@@ -438,6 +438,12 @@ function compareResultsByFilePath(a, b) {
|
|
438
438
|
*/
|
439
439
|
class LegacyESLint {
|
440
440
|
|
441
|
+
/**
|
442
|
+
* The type of configuration used by this class.
|
443
|
+
* @type {string}
|
444
|
+
*/
|
445
|
+
static configType = "eslintrc";
|
446
|
+
|
441
447
|
/**
|
442
448
|
* Creates a new instance of the main ESLint API.
|
443
449
|
* @param {LegacyESLintOptions} options The options for this instance.
|
@@ -177,8 +177,8 @@ class CodePath {
|
|
177
177
|
// tracks the traversal steps
|
178
178
|
const stack = [[startSegment, 0]];
|
179
179
|
|
180
|
-
//
|
181
|
-
|
180
|
+
// segments that have been skipped during traversal
|
181
|
+
const skipped = new Set();
|
182
182
|
|
183
183
|
// indicates if we exited early from the traversal
|
184
184
|
let broken = false;
|
@@ -193,11 +193,7 @@ class CodePath {
|
|
193
193
|
* @returns {void}
|
194
194
|
*/
|
195
195
|
skip() {
|
196
|
-
|
197
|
-
broken = true;
|
198
|
-
} else {
|
199
|
-
skippedSegment = stack.at(-2)[0];
|
200
|
-
}
|
196
|
+
skipped.add(segment);
|
201
197
|
},
|
202
198
|
|
203
199
|
/**
|
@@ -222,6 +218,18 @@ class CodePath {
|
|
222
218
|
);
|
223
219
|
}
|
224
220
|
|
221
|
+
/**
|
222
|
+
* Checks if a given previous segment has been skipped.
|
223
|
+
* @param {CodePathSegment} prevSegment A previous segment to check.
|
224
|
+
* @returns {boolean} `true` if the segment has been skipped.
|
225
|
+
*/
|
226
|
+
function isSkipped(prevSegment) {
|
227
|
+
return (
|
228
|
+
skipped.has(prevSegment) ||
|
229
|
+
segment.isLoopedPrevSegment(prevSegment)
|
230
|
+
);
|
231
|
+
}
|
232
|
+
|
225
233
|
// the traversal
|
226
234
|
while (stack.length > 0) {
|
227
235
|
|
@@ -258,17 +266,21 @@ class CodePath {
|
|
258
266
|
continue;
|
259
267
|
}
|
260
268
|
|
261
|
-
// Reset the skipping flag if all branches have been skipped.
|
262
|
-
if (skippedSegment && segment.prevSegments.includes(skippedSegment)) {
|
263
|
-
skippedSegment = null;
|
264
|
-
}
|
265
269
|
visited.add(segment);
|
266
270
|
|
271
|
+
|
272
|
+
// Skips the segment if all previous segments have been skipped.
|
273
|
+
const shouldSkip = (
|
274
|
+
skipped.size > 0 &&
|
275
|
+
segment.prevSegments.length > 0 &&
|
276
|
+
segment.prevSegments.every(isSkipped)
|
277
|
+
);
|
278
|
+
|
267
279
|
/*
|
268
280
|
* If the most recent segment hasn't been skipped, then we call
|
269
281
|
* the callback, passing in the segment and the controller.
|
270
282
|
*/
|
271
|
-
if (!
|
283
|
+
if (!shouldSkip) {
|
272
284
|
resolvedCallback.call(this, segment, controller);
|
273
285
|
|
274
286
|
// exit if we're at the last segment
|
@@ -284,6 +296,10 @@ class CodePath {
|
|
284
296
|
if (broken) {
|
285
297
|
break;
|
286
298
|
}
|
299
|
+
} else {
|
300
|
+
|
301
|
+
// If the most recent segment has been skipped, then mark it as skipped.
|
302
|
+
skipped.add(segment);
|
287
303
|
}
|
288
304
|
}
|
289
305
|
|
package/lib/linter/index.js
CHANGED
@@ -9,13 +9,30 @@
|
|
9
9
|
// Public Interface
|
10
10
|
//------------------------------------------------------------------------------
|
11
11
|
|
12
|
-
|
12
|
+
/**
|
13
|
+
* Returns a global expression matching placeholders in messages.
|
14
|
+
* @returns {RegExp} Global regular expression matching placeholders
|
15
|
+
*/
|
16
|
+
function getPlaceholderMatcher() {
|
17
|
+
return /\{\{([^{}]+?)\}\}/gu;
|
18
|
+
}
|
19
|
+
|
20
|
+
/**
|
21
|
+
* Replaces {{ placeholders }} in the message with the provided data.
|
22
|
+
* Does not replace placeholders not available in the data.
|
23
|
+
* @param {string} text Original message with potential placeholders
|
24
|
+
* @param {Record<string, string>} data Map of placeholder name to its value
|
25
|
+
* @returns {string} Message with replaced placeholders
|
26
|
+
*/
|
27
|
+
function interpolate(text, data) {
|
13
28
|
if (!data) {
|
14
29
|
return text;
|
15
30
|
}
|
16
31
|
|
32
|
+
const matcher = getPlaceholderMatcher();
|
33
|
+
|
17
34
|
// Substitution content for any {{ }} markers.
|
18
|
-
return text.replace(
|
35
|
+
return text.replace(matcher, (fullMatch, termWithWhitespace) => {
|
19
36
|
const term = termWithWhitespace.trim();
|
20
37
|
|
21
38
|
if (term in data) {
|
@@ -25,4 +42,9 @@ module.exports = (text, data) => {
|
|
25
42
|
// Preserve old behavior: If parameter name not provided, don't replace it.
|
26
43
|
return fullMatch;
|
27
44
|
});
|
45
|
+
}
|
46
|
+
|
47
|
+
module.exports = {
|
48
|
+
getPlaceholderMatcher,
|
49
|
+
interpolate
|
28
50
|
};
|
@@ -11,7 +11,7 @@
|
|
11
11
|
|
12
12
|
const assert = require("assert");
|
13
13
|
const ruleFixer = require("./rule-fixer");
|
14
|
-
const interpolate = require("./interpolate");
|
14
|
+
const { interpolate } = require("./interpolate");
|
15
15
|
|
16
16
|
//------------------------------------------------------------------------------
|
17
17
|
// Typedefs
|
@@ -13,10 +13,13 @@
|
|
13
13
|
const
|
14
14
|
assert = require("assert"),
|
15
15
|
util = require("util"),
|
16
|
+
path = require("path"),
|
16
17
|
equal = require("fast-deep-equal"),
|
17
18
|
Traverser = require("../shared/traverser"),
|
18
19
|
{ getRuleOptionsSchema } = require("../config/flat-config-helpers"),
|
19
|
-
{ Linter, SourceCodeFixer
|
20
|
+
{ Linter, SourceCodeFixer } = require("../linter"),
|
21
|
+
{ interpolate, getPlaceholderMatcher } = require("../linter/interpolate"),
|
22
|
+
stringify = require("json-stable-stringify-without-jsonify");
|
20
23
|
|
21
24
|
const { FlatConfigArray } = require("../config/flat-config-array");
|
22
25
|
const { defaultConfig } = require("../config/default-config");
|
@@ -26,6 +29,7 @@ const ajv = require("../shared/ajv")({ strictDefaults: true });
|
|
26
29
|
const parserSymbol = Symbol.for("eslint.RuleTester.parser");
|
27
30
|
const { SourceCode } = require("../source-code");
|
28
31
|
const { ConfigArraySymbol } = require("@humanwhocodes/config-array");
|
32
|
+
const { isSerializable } = require("../shared/serialization");
|
29
33
|
|
30
34
|
//------------------------------------------------------------------------------
|
31
35
|
// Typedefs
|
@@ -301,6 +305,39 @@ function throwForbiddenMethodError(methodName, prototype) {
|
|
301
305
|
};
|
302
306
|
}
|
303
307
|
|
308
|
+
/**
|
309
|
+
* Extracts names of {{ placeholders }} from the reported message.
|
310
|
+
* @param {string} message Reported message
|
311
|
+
* @returns {string[]} Array of placeholder names
|
312
|
+
*/
|
313
|
+
function getMessagePlaceholders(message) {
|
314
|
+
const matcher = getPlaceholderMatcher();
|
315
|
+
|
316
|
+
return Array.from(message.matchAll(matcher), ([, name]) => name.trim());
|
317
|
+
}
|
318
|
+
|
319
|
+
/**
|
320
|
+
* Returns the placeholders in the reported messages but
|
321
|
+
* only includes the placeholders available in the raw message and not in the provided data.
|
322
|
+
* @param {string} message The reported message
|
323
|
+
* @param {string} raw The raw message specified in the rule meta.messages
|
324
|
+
* @param {undefined|Record<unknown, unknown>} data The passed
|
325
|
+
* @returns {string[]} Missing placeholder names
|
326
|
+
*/
|
327
|
+
function getUnsubstitutedMessagePlaceholders(message, raw, data = {}) {
|
328
|
+
const unsubstituted = getMessagePlaceholders(message);
|
329
|
+
|
330
|
+
if (unsubstituted.length === 0) {
|
331
|
+
return [];
|
332
|
+
}
|
333
|
+
|
334
|
+
// Remove false positives by only counting placeholders in the raw message, which were not provided in the data matcher or added with a data property
|
335
|
+
const known = getMessagePlaceholders(raw);
|
336
|
+
const provided = Object.keys(data);
|
337
|
+
|
338
|
+
return unsubstituted.filter(name => known.includes(name) && !provided.includes(name));
|
339
|
+
}
|
340
|
+
|
304
341
|
const metaSchemaDescription = `
|
305
342
|
\t- If the rule has options, set \`meta.schema\` to an array or non-empty object to enable options validation.
|
306
343
|
\t- If the rule doesn't have options, omit \`meta.schema\` to enforce that no options can be passed to the rule.
|
@@ -499,6 +536,9 @@ class RuleTester {
|
|
499
536
|
linter = this.linter,
|
500
537
|
ruleId = `rule-to-test/${ruleName}`;
|
501
538
|
|
539
|
+
const seenValidTestCases = new Set();
|
540
|
+
const seenInvalidTestCases = new Set();
|
541
|
+
|
502
542
|
if (!rule || typeof rule !== "object" || typeof rule.create !== "function") {
|
503
543
|
throw new TypeError("Rule must be an object with a `create` method");
|
504
544
|
}
|
@@ -577,7 +617,15 @@ class RuleTester {
|
|
577
617
|
* @private
|
578
618
|
*/
|
579
619
|
function runRuleForItem(item) {
|
580
|
-
const
|
620
|
+
const flatConfigArrayOptions = {
|
621
|
+
baseConfig
|
622
|
+
};
|
623
|
+
|
624
|
+
if (item.filename) {
|
625
|
+
flatConfigArrayOptions.basePath = path.parse(item.filename).root;
|
626
|
+
}
|
627
|
+
|
628
|
+
const configs = new FlatConfigArray(testerConfig, flatConfigArrayOptions);
|
581
629
|
|
582
630
|
/*
|
583
631
|
* Modify the returned config so that the parser is wrapped to catch
|
@@ -631,7 +679,11 @@ class RuleTester {
|
|
631
679
|
configs.push(itemConfig);
|
632
680
|
}
|
633
681
|
|
634
|
-
if (item
|
682
|
+
if (hasOwnProperty(item, "only")) {
|
683
|
+
assert.ok(typeof item.only === "boolean", "Optional test case property 'only' must be a boolean");
|
684
|
+
}
|
685
|
+
if (hasOwnProperty(item, "filename")) {
|
686
|
+
assert.ok(typeof item.filename === "string", "Optional test case property 'filename' must be a string");
|
635
687
|
filename = item.filename;
|
636
688
|
}
|
637
689
|
|
@@ -794,6 +846,32 @@ class RuleTester {
|
|
794
846
|
}
|
795
847
|
}
|
796
848
|
|
849
|
+
/**
|
850
|
+
* Check if this test case is a duplicate of one we have seen before.
|
851
|
+
* @param {Object} item test case object
|
852
|
+
* @param {Set<string>} seenTestCases set of serialized test cases we have seen so far (managed by this function)
|
853
|
+
* @returns {void}
|
854
|
+
* @private
|
855
|
+
*/
|
856
|
+
function checkDuplicateTestCase(item, seenTestCases) {
|
857
|
+
if (!isSerializable(item)) {
|
858
|
+
|
859
|
+
/*
|
860
|
+
* If we can't serialize a test case (because it contains a function, RegExp, etc), skip the check.
|
861
|
+
* This might happen with properties like: options, plugins, settings, languageOptions.parser, languageOptions.parserOptions.
|
862
|
+
*/
|
863
|
+
return;
|
864
|
+
}
|
865
|
+
|
866
|
+
const serializedTestCase = stringify(item);
|
867
|
+
|
868
|
+
assert(
|
869
|
+
!seenTestCases.has(serializedTestCase),
|
870
|
+
"detected duplicate test case"
|
871
|
+
);
|
872
|
+
seenTestCases.add(serializedTestCase);
|
873
|
+
}
|
874
|
+
|
797
875
|
/**
|
798
876
|
* Check if the template is valid or not
|
799
877
|
* all valid cases go through this
|
@@ -809,6 +887,8 @@ class RuleTester {
|
|
809
887
|
assert.ok(typeof item.name === "string", "Optional test case property 'name' must be a string");
|
810
888
|
}
|
811
889
|
|
890
|
+
checkDuplicateTestCase(item, seenValidTestCases);
|
891
|
+
|
812
892
|
const result = runRuleForItem(item);
|
813
893
|
const messages = result.messages;
|
814
894
|
|
@@ -860,6 +940,8 @@ class RuleTester {
|
|
860
940
|
assert.fail("Invalid cases must have at least one error");
|
861
941
|
}
|
862
942
|
|
943
|
+
checkDuplicateTestCase(item, seenInvalidTestCases);
|
944
|
+
|
863
945
|
const ruleHasMetaMessages = hasOwnProperty(rule, "meta") && hasOwnProperty(rule.meta, "messages");
|
864
946
|
const friendlyIDList = ruleHasMetaMessages ? `[${Object.keys(rule.meta.messages).map(key => `'${key}'`).join(", ")}]` : null;
|
865
947
|
|
@@ -916,6 +998,7 @@ class RuleTester {
|
|
916
998
|
|
917
999
|
// Just an error message.
|
918
1000
|
assertMessageMatches(message.message, error);
|
1001
|
+
assert.ok(message.suggestions === void 0, `Error at index ${i} has suggestions. Please convert the test error into an object and specify 'suggestions' property on it to test suggestions.`);
|
919
1002
|
} else if (typeof error === "object" && error !== null) {
|
920
1003
|
|
921
1004
|
/*
|
@@ -948,6 +1031,18 @@ class RuleTester {
|
|
948
1031
|
error.messageId,
|
949
1032
|
`messageId '${message.messageId}' does not match expected messageId '${error.messageId}'.`
|
950
1033
|
);
|
1034
|
+
|
1035
|
+
const unsubstitutedPlaceholders = getUnsubstitutedMessagePlaceholders(
|
1036
|
+
message.message,
|
1037
|
+
rule.meta.messages[message.messageId],
|
1038
|
+
error.data
|
1039
|
+
);
|
1040
|
+
|
1041
|
+
assert.ok(
|
1042
|
+
unsubstitutedPlaceholders.length === 0,
|
1043
|
+
`The reported message has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(", ")}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? "values" : "value"} via the 'data' property in the context.report() call.`
|
1044
|
+
);
|
1045
|
+
|
951
1046
|
if (hasOwnProperty(error, "data")) {
|
952
1047
|
|
953
1048
|
/*
|
@@ -964,13 +1059,10 @@ class RuleTester {
|
|
964
1059
|
`Hydrated message "${rehydratedMessage}" does not match "${message.message}"`
|
965
1060
|
);
|
966
1061
|
}
|
1062
|
+
} else {
|
1063
|
+
assert.fail("Test error must specify either a 'messageId' or 'message'.");
|
967
1064
|
}
|
968
1065
|
|
969
|
-
assert.ok(
|
970
|
-
hasOwnProperty(error, "data") ? hasOwnProperty(error, "messageId") : true,
|
971
|
-
"Error must specify 'messageId' if 'data' is used."
|
972
|
-
);
|
973
|
-
|
974
1066
|
if (error.type) {
|
975
1067
|
assert.strictEqual(message.nodeType, error.type, `Error type should be ${error.type}, found ${message.nodeType}`);
|
976
1068
|
}
|
@@ -991,81 +1083,103 @@ class RuleTester {
|
|
991
1083
|
assert.strictEqual(message.endColumn, error.endColumn, `Error endColumn should be ${error.endColumn}`);
|
992
1084
|
}
|
993
1085
|
|
1086
|
+
assert.ok(!message.suggestions || hasOwnProperty(error, "suggestions"), `Error at index ${i} has suggestions. Please specify 'suggestions' property on the test error object.`);
|
994
1087
|
if (hasOwnProperty(error, "suggestions")) {
|
995
1088
|
|
996
1089
|
// Support asserting there are no suggestions
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
|
1002
|
-
|
1003
|
-
assert.
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1007
|
-
|
1008
|
-
|
1009
|
-
)
|
1010
|
-
Object.keys(expectedSuggestion).forEach(propertyName => {
|
1090
|
+
const expectsSuggestions = Array.isArray(error.suggestions) ? error.suggestions.length > 0 : Boolean(error.suggestions);
|
1091
|
+
const hasSuggestions = message.suggestions !== void 0;
|
1092
|
+
|
1093
|
+
if (!hasSuggestions && expectsSuggestions) {
|
1094
|
+
assert.ok(!error.suggestions, `Error should have suggestions on error with message: "${message.message}"`);
|
1095
|
+
} else if (hasSuggestions) {
|
1096
|
+
assert.ok(expectsSuggestions, `Error should have no suggestions on error with message: "${message.message}"`);
|
1097
|
+
if (typeof error.suggestions === "number") {
|
1098
|
+
assert.strictEqual(message.suggestions.length, error.suggestions, `Error should have ${error.suggestions} suggestions. Instead found ${message.suggestions.length} suggestions`);
|
1099
|
+
} else if (Array.isArray(error.suggestions)) {
|
1100
|
+
assert.strictEqual(message.suggestions.length, error.suggestions.length, `Error should have ${error.suggestions.length} suggestions. Instead found ${message.suggestions.length} suggestions`);
|
1101
|
+
|
1102
|
+
error.suggestions.forEach((expectedSuggestion, index) => {
|
1011
1103
|
assert.ok(
|
1012
|
-
|
1013
|
-
|
1104
|
+
typeof expectedSuggestion === "object" && expectedSuggestion !== null,
|
1105
|
+
"Test suggestion in 'suggestions' array must be an object."
|
1014
1106
|
);
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
assert.ok(
|
1022
|
-
!hasOwnProperty(expectedSuggestion, "data"),
|
1023
|
-
`${suggestionPrefix} Test should not specify both 'desc' and 'data'.`
|
1024
|
-
);
|
1025
|
-
assert.strictEqual(
|
1026
|
-
actualSuggestion.desc,
|
1027
|
-
expectedSuggestion.desc,
|
1028
|
-
`${suggestionPrefix} desc should be "${expectedSuggestion.desc}" but got "${actualSuggestion.desc}" instead.`
|
1029
|
-
);
|
1030
|
-
}
|
1107
|
+
Object.keys(expectedSuggestion).forEach(propertyName => {
|
1108
|
+
assert.ok(
|
1109
|
+
suggestionObjectParameters.has(propertyName),
|
1110
|
+
`Invalid suggestion property name '${propertyName}'. Expected one of ${friendlySuggestionObjectParameterList}.`
|
1111
|
+
);
|
1112
|
+
});
|
1031
1113
|
|
1032
|
-
|
1033
|
-
|
1034
|
-
ruleHasMetaMessages,
|
1035
|
-
`${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.`
|
1036
|
-
);
|
1037
|
-
assert.ok(
|
1038
|
-
hasOwnProperty(rule.meta.messages, expectedSuggestion.messageId),
|
1039
|
-
`${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.`
|
1040
|
-
);
|
1041
|
-
assert.strictEqual(
|
1042
|
-
actualSuggestion.messageId,
|
1043
|
-
expectedSuggestion.messageId,
|
1044
|
-
`${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.`
|
1045
|
-
);
|
1046
|
-
if (hasOwnProperty(expectedSuggestion, "data")) {
|
1047
|
-
const unformattedMetaMessage = rule.meta.messages[expectedSuggestion.messageId];
|
1048
|
-
const rehydratedDesc = interpolate(unformattedMetaMessage, expectedSuggestion.data);
|
1114
|
+
const actualSuggestion = message.suggestions[index];
|
1115
|
+
const suggestionPrefix = `Error Suggestion at index ${index}:`;
|
1049
1116
|
|
1117
|
+
if (hasOwnProperty(expectedSuggestion, "desc")) {
|
1118
|
+
assert.ok(
|
1119
|
+
!hasOwnProperty(expectedSuggestion, "data"),
|
1120
|
+
`${suggestionPrefix} Test should not specify both 'desc' and 'data'.`
|
1121
|
+
);
|
1122
|
+
assert.ok(
|
1123
|
+
!hasOwnProperty(expectedSuggestion, "messageId"),
|
1124
|
+
`${suggestionPrefix} Test should not specify both 'desc' and 'messageId'.`
|
1125
|
+
);
|
1050
1126
|
assert.strictEqual(
|
1051
1127
|
actualSuggestion.desc,
|
1052
|
-
|
1053
|
-
`${suggestionPrefix}
|
1128
|
+
expectedSuggestion.desc,
|
1129
|
+
`${suggestionPrefix} desc should be "${expectedSuggestion.desc}" but got "${actualSuggestion.desc}" instead.`
|
1130
|
+
);
|
1131
|
+
} else if (hasOwnProperty(expectedSuggestion, "messageId")) {
|
1132
|
+
assert.ok(
|
1133
|
+
ruleHasMetaMessages,
|
1134
|
+
`${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.`
|
1135
|
+
);
|
1136
|
+
assert.ok(
|
1137
|
+
hasOwnProperty(rule.meta.messages, expectedSuggestion.messageId),
|
1138
|
+
`${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.`
|
1139
|
+
);
|
1140
|
+
assert.strictEqual(
|
1141
|
+
actualSuggestion.messageId,
|
1142
|
+
expectedSuggestion.messageId,
|
1143
|
+
`${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.`
|
1144
|
+
);
|
1145
|
+
|
1146
|
+
const unsubstitutedPlaceholders = getUnsubstitutedMessagePlaceholders(
|
1147
|
+
actualSuggestion.desc,
|
1148
|
+
rule.meta.messages[expectedSuggestion.messageId],
|
1149
|
+
expectedSuggestion.data
|
1150
|
+
);
|
1151
|
+
|
1152
|
+
assert.ok(
|
1153
|
+
unsubstitutedPlaceholders.length === 0,
|
1154
|
+
`The message of the suggestion has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(", ")}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? "values" : "value"} via the 'data' property for the suggestion in the context.report() call.`
|
1155
|
+
);
|
1156
|
+
|
1157
|
+
if (hasOwnProperty(expectedSuggestion, "data")) {
|
1158
|
+
const unformattedMetaMessage = rule.meta.messages[expectedSuggestion.messageId];
|
1159
|
+
const rehydratedDesc = interpolate(unformattedMetaMessage, expectedSuggestion.data);
|
1160
|
+
|
1161
|
+
assert.strictEqual(
|
1162
|
+
actualSuggestion.desc,
|
1163
|
+
rehydratedDesc,
|
1164
|
+
`${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".`
|
1165
|
+
);
|
1166
|
+
}
|
1167
|
+
} else if (hasOwnProperty(expectedSuggestion, "data")) {
|
1168
|
+
assert.fail(
|
1169
|
+
`${suggestionPrefix} Test must specify 'messageId' if 'data' is used.`
|
1170
|
+
);
|
1171
|
+
} else {
|
1172
|
+
assert.fail(
|
1173
|
+
`${suggestionPrefix} Test must specify either 'messageId' or 'desc'.`
|
1054
1174
|
);
|
1055
1175
|
}
|
1056
|
-
} else {
|
1057
|
-
assert.ok(
|
1058
|
-
!hasOwnProperty(expectedSuggestion, "data"),
|
1059
|
-
`${suggestionPrefix} Test must specify 'messageId' if 'data' is used.`
|
1060
|
-
);
|
1061
|
-
}
|
1062
1176
|
|
1063
|
-
|
1177
|
+
assert.ok(hasOwnProperty(expectedSuggestion, "output"), `${suggestionPrefix} The "output" property is required.`);
|
1064
1178
|
const codeWithAppliedSuggestion = SourceCodeFixer.applyFixes(item.code, [actualSuggestion]).output;
|
1065
1179
|
|
1066
1180
|
// Verify if suggestion fix makes a syntax error or not.
|
1067
1181
|
const errorMessageInSuggestion =
|
1068
|
-
|
1182
|
+
linter.verify(codeWithAppliedSuggestion, result.configs, result.filename).find(m => m.fatal);
|
1069
1183
|
|
1070
1184
|
assert(!errorMessageInSuggestion, [
|
1071
1185
|
"A fatal parsing error occurred in suggestion fix.",
|
@@ -1075,8 +1189,11 @@ class RuleTester {
|
|
1075
1189
|
].join("\n"));
|
1076
1190
|
|
1077
1191
|
assert.strictEqual(codeWithAppliedSuggestion, expectedSuggestion.output, `Expected the applied suggestion fix to match the test suggestion output for suggestion at index: ${index} on error with message: "${message.message}"`);
|
1078
|
-
|
1079
|
-
|
1192
|
+
assert.notStrictEqual(expectedSuggestion.output, item.code, `The output of a suggestion should differ from the original source code for suggestion at index: ${index} on error with message: "${message.message}"`);
|
1193
|
+
});
|
1194
|
+
} else {
|
1195
|
+
assert.fail("Test error object property 'suggestions' should be an array or a number");
|
1196
|
+
}
|
1080
1197
|
}
|
1081
1198
|
}
|
1082
1199
|
} else {
|
@@ -1096,6 +1213,7 @@ class RuleTester {
|
|
1096
1213
|
);
|
1097
1214
|
} else {
|
1098
1215
|
assert.strictEqual(result.output, item.output, "Output is incorrect.");
|
1216
|
+
assert.notStrictEqual(item.code, item.output, "Test property 'output' matches 'code'. If no autofix is expected, then omit the 'output' property or set it to null.");
|
1099
1217
|
}
|
1100
1218
|
} else {
|
1101
1219
|
assert.strictEqual(
|
@@ -188,6 +188,7 @@ function getNonEmptyOperand(node) {
|
|
188
188
|
/** @type {import('../shared/types').Rule} */
|
189
189
|
module.exports = {
|
190
190
|
meta: {
|
191
|
+
hasSuggestions: true,
|
191
192
|
type: "suggestion",
|
192
193
|
|
193
194
|
docs: {
|
@@ -229,7 +230,8 @@ module.exports = {
|
|
229
230
|
}],
|
230
231
|
|
231
232
|
messages: {
|
232
|
-
|
233
|
+
implicitCoercion: "Unexpected implicit coercion encountered. Use `{{recommendation}}` instead.",
|
234
|
+
useRecommendation: "Use `{{recommendation}}` instead."
|
233
235
|
}
|
234
236
|
},
|
235
237
|
|
@@ -241,32 +243,54 @@ module.exports = {
|
|
241
243
|
* Reports an error and autofixes the node
|
242
244
|
* @param {ASTNode} node An ast node to report the error on.
|
243
245
|
* @param {string} recommendation The recommended code for the issue
|
246
|
+
* @param {bool} shouldSuggest Whether this report should offer a suggestion
|
244
247
|
* @param {bool} shouldFix Whether this report should fix the node
|
245
248
|
* @returns {void}
|
246
249
|
*/
|
247
|
-
function report(node, recommendation, shouldFix) {
|
250
|
+
function report(node, recommendation, shouldSuggest, shouldFix) {
|
251
|
+
|
252
|
+
/**
|
253
|
+
* Fix function
|
254
|
+
* @param {RuleFixer} fixer The fixer to fix.
|
255
|
+
* @returns {Fix} The fix object.
|
256
|
+
*/
|
257
|
+
function fix(fixer) {
|
258
|
+
const tokenBefore = sourceCode.getTokenBefore(node);
|
259
|
+
|
260
|
+
if (
|
261
|
+
tokenBefore?.range[1] === node.range[0] &&
|
262
|
+
!astUtils.canTokensBeAdjacent(tokenBefore, recommendation)
|
263
|
+
) {
|
264
|
+
return fixer.replaceText(node, ` ${recommendation}`);
|
265
|
+
}
|
266
|
+
|
267
|
+
return fixer.replaceText(node, recommendation);
|
268
|
+
}
|
269
|
+
|
248
270
|
context.report({
|
249
271
|
node,
|
250
|
-
messageId: "
|
251
|
-
data: {
|
252
|
-
recommendation
|
253
|
-
},
|
272
|
+
messageId: "implicitCoercion",
|
273
|
+
data: { recommendation },
|
254
274
|
fix(fixer) {
|
255
275
|
if (!shouldFix) {
|
256
276
|
return null;
|
257
277
|
}
|
258
278
|
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
279
|
+
return fix(fixer);
|
280
|
+
},
|
281
|
+
suggest: [
|
282
|
+
{
|
283
|
+
messageId: "useRecommendation",
|
284
|
+
data: { recommendation },
|
285
|
+
fix(fixer) {
|
286
|
+
if (shouldFix || !shouldSuggest) {
|
287
|
+
return null;
|
288
|
+
}
|
289
|
+
|
290
|
+
return fix(fixer);
|
291
|
+
}
|
267
292
|
}
|
268
|
-
|
269
|
-
}
|
293
|
+
]
|
270
294
|
});
|
271
295
|
}
|
272
296
|
|
@@ -278,8 +302,10 @@ module.exports = {
|
|
278
302
|
operatorAllowed = options.allow.includes("!!");
|
279
303
|
if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) {
|
280
304
|
const recommendation = `Boolean(${sourceCode.getText(node.argument.argument)})`;
|
305
|
+
const variable = astUtils.getVariableByName(sourceCode.getScope(node), "Boolean");
|
306
|
+
const booleanExists = variable?.identifiers.length === 0;
|
281
307
|
|
282
|
-
report(node, recommendation, true);
|
308
|
+
report(node, recommendation, true, booleanExists);
|
283
309
|
}
|
284
310
|
|
285
311
|
// ~foo.indexOf(bar)
|
@@ -290,7 +316,7 @@ module.exports = {
|
|
290
316
|
const comparison = node.argument.type === "ChainExpression" ? ">= 0" : "!== -1";
|
291
317
|
const recommendation = `${sourceCode.getText(node.argument)} ${comparison}`;
|
292
318
|
|
293
|
-
report(node, recommendation, false);
|
319
|
+
report(node, recommendation, false, false);
|
294
320
|
}
|
295
321
|
|
296
322
|
// +foo
|
@@ -298,7 +324,7 @@ module.exports = {
|
|
298
324
|
if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) {
|
299
325
|
const recommendation = `Number(${sourceCode.getText(node.argument)})`;
|
300
326
|
|
301
|
-
report(node, recommendation, true);
|
327
|
+
report(node, recommendation, true, false);
|
302
328
|
}
|
303
329
|
|
304
330
|
// -(-foo)
|
@@ -306,7 +332,7 @@ module.exports = {
|
|
306
332
|
if (!operatorAllowed && options.number && node.operator === "-" && node.argument.type === "UnaryExpression" && node.argument.operator === "-" && !isNumeric(node.argument.argument)) {
|
307
333
|
const recommendation = `Number(${sourceCode.getText(node.argument.argument)})`;
|
308
334
|
|
309
|
-
report(node, recommendation, false);
|
335
|
+
report(node, recommendation, true, false);
|
310
336
|
}
|
311
337
|
},
|
312
338
|
|
@@ -322,7 +348,7 @@ module.exports = {
|
|
322
348
|
if (nonNumericOperand) {
|
323
349
|
const recommendation = `Number(${sourceCode.getText(nonNumericOperand)})`;
|
324
350
|
|
325
|
-
report(node, recommendation, true);
|
351
|
+
report(node, recommendation, true, false);
|
326
352
|
}
|
327
353
|
|
328
354
|
// foo - 0
|
@@ -330,7 +356,7 @@ module.exports = {
|
|
330
356
|
if (!operatorAllowed && options.number && node.operator === "-" && node.right.type === "Literal" && node.right.value === 0 && !isNumeric(node.left)) {
|
331
357
|
const recommendation = `Number(${sourceCode.getText(node.left)})`;
|
332
358
|
|
333
|
-
report(node, recommendation, true);
|
359
|
+
report(node, recommendation, true, false);
|
334
360
|
}
|
335
361
|
|
336
362
|
// "" + foo
|
@@ -338,7 +364,7 @@ module.exports = {
|
|
338
364
|
if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) {
|
339
365
|
const recommendation = `String(${sourceCode.getText(getNonEmptyOperand(node))})`;
|
340
366
|
|
341
|
-
report(node, recommendation, true);
|
367
|
+
report(node, recommendation, true, false);
|
342
368
|
}
|
343
369
|
},
|
344
370
|
|
@@ -351,7 +377,7 @@ module.exports = {
|
|
351
377
|
const code = sourceCode.getText(getNonEmptyOperand(node));
|
352
378
|
const recommendation = `${code} = String(${code})`;
|
353
379
|
|
354
|
-
report(node, recommendation, true);
|
380
|
+
report(node, recommendation, true, false);
|
355
381
|
}
|
356
382
|
},
|
357
383
|
|
@@ -389,7 +415,7 @@ module.exports = {
|
|
389
415
|
const code = sourceCode.getText(node.expressions[0]);
|
390
416
|
const recommendation = `String(${code})`;
|
391
417
|
|
392
|
-
report(node, recommendation, true);
|
418
|
+
report(node, recommendation, true, false);
|
393
419
|
}
|
394
420
|
};
|
395
421
|
}
|
@@ -161,17 +161,25 @@ module.exports = {
|
|
161
161
|
(Object.hasOwn(options[0], "paths") || Object.hasOwn(options[0], "patterns"));
|
162
162
|
|
163
163
|
const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || [];
|
164
|
-
const
|
164
|
+
const groupedRestrictedPaths = restrictedPaths.reduce((memo, importSource) => {
|
165
|
+
const path = typeof importSource === "string"
|
166
|
+
? importSource
|
167
|
+
: importSource.name;
|
168
|
+
|
169
|
+
if (!memo[path]) {
|
170
|
+
memo[path] = [];
|
171
|
+
}
|
172
|
+
|
165
173
|
if (typeof importSource === "string") {
|
166
|
-
memo[
|
174
|
+
memo[path].push({});
|
167
175
|
} else {
|
168
|
-
memo[
|
176
|
+
memo[path].push({
|
169
177
|
message: importSource.message,
|
170
178
|
importNames: importSource.importNames
|
171
|
-
};
|
179
|
+
});
|
172
180
|
}
|
173
181
|
return memo;
|
174
|
-
},
|
182
|
+
}, Object.create(null));
|
175
183
|
|
176
184
|
// Handle patterns too, either as strings or groups
|
177
185
|
let restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || [];
|
@@ -203,57 +211,59 @@ module.exports = {
|
|
203
211
|
* @private
|
204
212
|
*/
|
205
213
|
function checkRestrictedPathAndReport(importSource, importNames, node) {
|
206
|
-
if (!Object.hasOwn(
|
214
|
+
if (!Object.hasOwn(groupedRestrictedPaths, importSource)) {
|
207
215
|
return;
|
208
216
|
}
|
209
217
|
|
210
|
-
|
211
|
-
|
218
|
+
groupedRestrictedPaths[importSource].forEach(restrictedPathEntry => {
|
219
|
+
const customMessage = restrictedPathEntry.message;
|
220
|
+
const restrictedImportNames = restrictedPathEntry.importNames;
|
212
221
|
|
213
|
-
|
214
|
-
|
215
|
-
|
222
|
+
if (restrictedImportNames) {
|
223
|
+
if (importNames.has("*")) {
|
224
|
+
const specifierData = importNames.get("*")[0];
|
225
|
+
|
226
|
+
context.report({
|
227
|
+
node,
|
228
|
+
messageId: customMessage ? "everythingWithCustomMessage" : "everything",
|
229
|
+
loc: specifierData.loc,
|
230
|
+
data: {
|
231
|
+
importSource,
|
232
|
+
importNames: restrictedImportNames,
|
233
|
+
customMessage
|
234
|
+
}
|
235
|
+
});
|
236
|
+
}
|
216
237
|
|
238
|
+
restrictedImportNames.forEach(importName => {
|
239
|
+
if (importNames.has(importName)) {
|
240
|
+
const specifiers = importNames.get(importName);
|
241
|
+
|
242
|
+
specifiers.forEach(specifier => {
|
243
|
+
context.report({
|
244
|
+
node,
|
245
|
+
messageId: customMessage ? "importNameWithCustomMessage" : "importName",
|
246
|
+
loc: specifier.loc,
|
247
|
+
data: {
|
248
|
+
importSource,
|
249
|
+
customMessage,
|
250
|
+
importName
|
251
|
+
}
|
252
|
+
});
|
253
|
+
});
|
254
|
+
}
|
255
|
+
});
|
256
|
+
} else {
|
217
257
|
context.report({
|
218
258
|
node,
|
219
|
-
messageId: customMessage ? "
|
220
|
-
loc: specifierData.loc,
|
259
|
+
messageId: customMessage ? "pathWithCustomMessage" : "path",
|
221
260
|
data: {
|
222
261
|
importSource,
|
223
|
-
importNames: restrictedImportNames,
|
224
262
|
customMessage
|
225
263
|
}
|
226
264
|
});
|
227
265
|
}
|
228
|
-
|
229
|
-
restrictedImportNames.forEach(importName => {
|
230
|
-
if (importNames.has(importName)) {
|
231
|
-
const specifiers = importNames.get(importName);
|
232
|
-
|
233
|
-
specifiers.forEach(specifier => {
|
234
|
-
context.report({
|
235
|
-
node,
|
236
|
-
messageId: customMessage ? "importNameWithCustomMessage" : "importName",
|
237
|
-
loc: specifier.loc,
|
238
|
-
data: {
|
239
|
-
importSource,
|
240
|
-
customMessage,
|
241
|
-
importName
|
242
|
-
}
|
243
|
-
});
|
244
|
-
});
|
245
|
-
}
|
246
|
-
});
|
247
|
-
} else {
|
248
|
-
context.report({
|
249
|
-
node,
|
250
|
-
messageId: customMessage ? "pathWithCustomMessage" : "path",
|
251
|
-
data: {
|
252
|
-
importSource,
|
253
|
-
customMessage
|
254
|
-
}
|
255
|
-
});
|
256
|
-
}
|
266
|
+
});
|
257
267
|
}
|
258
268
|
|
259
269
|
/**
|
@@ -197,11 +197,26 @@ module.exports = {
|
|
197
197
|
return;
|
198
198
|
}
|
199
199
|
|
200
|
+
/**
|
201
|
+
* A collection of nodes to avoid duplicate reports.
|
202
|
+
* @type {Set<ASTNode>}
|
203
|
+
*/
|
204
|
+
const reported = new Set();
|
205
|
+
|
200
206
|
codePath.traverseSegments((segment, controller) => {
|
201
207
|
const info = segInfoMap[segment.id];
|
208
|
+
const invalidNodes = info.invalidNodes
|
209
|
+
.filter(
|
210
|
+
|
211
|
+
/*
|
212
|
+
* Avoid duplicate reports.
|
213
|
+
* When there is a `finally`, invalidNodes may contain already reported node.
|
214
|
+
*/
|
215
|
+
node => !reported.has(node)
|
216
|
+
);
|
202
217
|
|
203
|
-
for (
|
204
|
-
|
218
|
+
for (const invalidNode of invalidNodes) {
|
219
|
+
reported.add(invalidNode);
|
205
220
|
|
206
221
|
context.report({
|
207
222
|
messageId: "noBeforeSuper",
|
@@ -273,14 +288,12 @@ module.exports = {
|
|
273
288
|
const info = segInfoMap[segment.id];
|
274
289
|
|
275
290
|
if (info.superCalled) {
|
276
|
-
info.invalidNodes = [];
|
277
291
|
controller.skip();
|
278
292
|
} else if (
|
279
293
|
segment.prevSegments.length > 0 &&
|
280
294
|
segment.prevSegments.every(isCalled)
|
281
295
|
) {
|
282
296
|
info.superCalled = true;
|
283
|
-
info.invalidNodes = [];
|
284
297
|
}
|
285
298
|
}
|
286
299
|
);
|
@@ -101,7 +101,7 @@ module.exports = {
|
|
101
101
|
properties: {
|
102
102
|
enforceForClassMembers: {
|
103
103
|
type: "boolean",
|
104
|
-
default:
|
104
|
+
default: true
|
105
105
|
}
|
106
106
|
},
|
107
107
|
additionalProperties: false
|
@@ -114,7 +114,7 @@ module.exports = {
|
|
114
114
|
},
|
115
115
|
create(context) {
|
116
116
|
const sourceCode = context.sourceCode;
|
117
|
-
const enforceForClassMembers = context.options[0]
|
117
|
+
const enforceForClassMembers = context.options[0]?.enforceForClassMembers ?? true;
|
118
118
|
|
119
119
|
/**
|
120
120
|
* Reports a given node if it violated this rule.
|
package/lib/rules/use-isnan.js
CHANGED
@@ -21,9 +21,17 @@ const astUtils = require("./utils/ast-utils");
|
|
21
21
|
* @returns {boolean} `true` if the node is 'NaN' identifier.
|
22
22
|
*/
|
23
23
|
function isNaNIdentifier(node) {
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
if (!node) {
|
25
|
+
return false;
|
26
|
+
}
|
27
|
+
|
28
|
+
const nodeToCheck = node.type === "SequenceExpression"
|
29
|
+
? node.expressions.at(-1)
|
30
|
+
: node;
|
31
|
+
|
32
|
+
return (
|
33
|
+
astUtils.isSpecificId(nodeToCheck, "NaN") ||
|
34
|
+
astUtils.isSpecificMemberAccess(nodeToCheck, "Number", "NaN")
|
27
35
|
);
|
28
36
|
}
|
29
37
|
|
@@ -34,6 +42,7 @@ function isNaNIdentifier(node) {
|
|
34
42
|
/** @type {import('../shared/types').Rule} */
|
35
43
|
module.exports = {
|
36
44
|
meta: {
|
45
|
+
hasSuggestions: true,
|
37
46
|
type: "problem",
|
38
47
|
|
39
48
|
docs: {
|
@@ -63,7 +72,9 @@ module.exports = {
|
|
63
72
|
comparisonWithNaN: "Use the isNaN function to compare with NaN.",
|
64
73
|
switchNaN: "'switch(NaN)' can never match a case clause. Use Number.isNaN instead of the switch.",
|
65
74
|
caseNaN: "'case NaN' can never match. Use Number.isNaN before the switch.",
|
66
|
-
indexOfNaN: "Array prototype method '{{ methodName }}' cannot find NaN."
|
75
|
+
indexOfNaN: "Array prototype method '{{ methodName }}' cannot find NaN.",
|
76
|
+
replaceWithIsNaN: "Replace with Number.isNaN.",
|
77
|
+
replaceWithCastingAndIsNaN: "Replace with Number.isNaN cast to a Number."
|
67
78
|
}
|
68
79
|
},
|
69
80
|
|
@@ -71,6 +82,35 @@ module.exports = {
|
|
71
82
|
|
72
83
|
const enforceForSwitchCase = !context.options[0] || context.options[0].enforceForSwitchCase;
|
73
84
|
const enforceForIndexOf = context.options[0] && context.options[0].enforceForIndexOf;
|
85
|
+
const sourceCode = context.sourceCode;
|
86
|
+
|
87
|
+
const fixableOperators = new Set(["==", "===", "!=", "!=="]);
|
88
|
+
const castableOperators = new Set(["==", "!="]);
|
89
|
+
|
90
|
+
/**
|
91
|
+
* Get a fixer for a binary expression that compares to NaN.
|
92
|
+
* @param {ASTNode} node The node to fix.
|
93
|
+
* @param {function(string): string} wrapValue A function that wraps the compared value with a fix.
|
94
|
+
* @returns {function(Fixer): Fix} The fixer function.
|
95
|
+
*/
|
96
|
+
function getBinaryExpressionFixer(node, wrapValue) {
|
97
|
+
return fixer => {
|
98
|
+
const comparedValue = isNaNIdentifier(node.left) ? node.right : node.left;
|
99
|
+
const shouldWrap = comparedValue.type === "SequenceExpression";
|
100
|
+
const shouldNegate = node.operator[0] === "!";
|
101
|
+
|
102
|
+
const negation = shouldNegate ? "!" : "";
|
103
|
+
let comparedValueText = sourceCode.getText(comparedValue);
|
104
|
+
|
105
|
+
if (shouldWrap) {
|
106
|
+
comparedValueText = `(${comparedValueText})`;
|
107
|
+
}
|
108
|
+
|
109
|
+
const fixedValue = wrapValue(comparedValueText);
|
110
|
+
|
111
|
+
return fixer.replaceText(node, `${negation}${fixedValue}`);
|
112
|
+
};
|
113
|
+
}
|
74
114
|
|
75
115
|
/**
|
76
116
|
* Checks the given `BinaryExpression` node for `foo === NaN` and other comparisons.
|
@@ -82,7 +122,32 @@ module.exports = {
|
|
82
122
|
/^(?:[<>]|[!=]=)=?$/u.test(node.operator) &&
|
83
123
|
(isNaNIdentifier(node.left) || isNaNIdentifier(node.right))
|
84
124
|
) {
|
85
|
-
|
125
|
+
const suggestedFixes = [];
|
126
|
+
const NaNNode = isNaNIdentifier(node.left) ? node.left : node.right;
|
127
|
+
|
128
|
+
const isSequenceExpression = NaNNode.type === "SequenceExpression";
|
129
|
+
const isFixable = fixableOperators.has(node.operator) && !isSequenceExpression;
|
130
|
+
const isCastable = castableOperators.has(node.operator);
|
131
|
+
|
132
|
+
if (isFixable) {
|
133
|
+
suggestedFixes.push({
|
134
|
+
messageId: "replaceWithIsNaN",
|
135
|
+
fix: getBinaryExpressionFixer(node, value => `Number.isNaN(${value})`)
|
136
|
+
});
|
137
|
+
|
138
|
+
if (isCastable) {
|
139
|
+
suggestedFixes.push({
|
140
|
+
messageId: "replaceWithCastingAndIsNaN",
|
141
|
+
fix: getBinaryExpressionFixer(node, value => `Number.isNaN(Number(${value}))`)
|
142
|
+
});
|
143
|
+
}
|
144
|
+
}
|
145
|
+
|
146
|
+
context.report({
|
147
|
+
node,
|
148
|
+
messageId: "comparisonWithNaN",
|
149
|
+
suggest: suggestedFixes
|
150
|
+
});
|
86
151
|
}
|
87
152
|
}
|
88
153
|
|
@@ -0,0 +1,55 @@
|
|
1
|
+
/**
|
2
|
+
* @fileoverview Serialization utils.
|
3
|
+
* @author Bryan Mishkin
|
4
|
+
*/
|
5
|
+
|
6
|
+
"use strict";
|
7
|
+
|
8
|
+
/**
|
9
|
+
* Check if a value is a primitive or plain object created by the Object constructor.
|
10
|
+
* @param {any} val the value to check
|
11
|
+
* @returns {boolean} true if so
|
12
|
+
* @private
|
13
|
+
*/
|
14
|
+
function isSerializablePrimitiveOrPlainObject(val) {
|
15
|
+
return (
|
16
|
+
val === null ||
|
17
|
+
typeof val === "string" ||
|
18
|
+
typeof val === "boolean" ||
|
19
|
+
typeof val === "number" ||
|
20
|
+
(typeof val === "object" && val.constructor === Object) ||
|
21
|
+
Array.isArray(val)
|
22
|
+
);
|
23
|
+
}
|
24
|
+
|
25
|
+
/**
|
26
|
+
* Check if a value is serializable.
|
27
|
+
* Functions or objects like RegExp cannot be serialized by JSON.stringify().
|
28
|
+
* Inspired by: https://stackoverflow.com/questions/30579940/reliable-way-to-check-if-objects-is-serializable-in-javascript
|
29
|
+
* @param {any} val the value
|
30
|
+
* @returns {boolean} true if the value is serializable
|
31
|
+
*/
|
32
|
+
function isSerializable(val) {
|
33
|
+
if (!isSerializablePrimitiveOrPlainObject(val)) {
|
34
|
+
return false;
|
35
|
+
}
|
36
|
+
if (typeof val === "object") {
|
37
|
+
for (const property in val) {
|
38
|
+
if (Object.hasOwn(val, property)) {
|
39
|
+
if (!isSerializablePrimitiveOrPlainObject(val[property])) {
|
40
|
+
return false;
|
41
|
+
}
|
42
|
+
if (typeof val[property] === "object") {
|
43
|
+
if (!isSerializable(val[property])) {
|
44
|
+
return false;
|
45
|
+
}
|
46
|
+
}
|
47
|
+
}
|
48
|
+
}
|
49
|
+
}
|
50
|
+
return true;
|
51
|
+
}
|
52
|
+
|
53
|
+
module.exports = {
|
54
|
+
isSerializable
|
55
|
+
};
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "eslint",
|
3
|
-
"version": "9.0.0-
|
3
|
+
"version": "9.0.0-beta.0",
|
4
4
|
"author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>",
|
5
5
|
"description": "An AST-based pattern checker for JavaScript.",
|
6
6
|
"bin": {
|
@@ -65,8 +65,8 @@
|
|
65
65
|
"dependencies": {
|
66
66
|
"@eslint-community/eslint-utils": "^4.2.0",
|
67
67
|
"@eslint-community/regexpp": "^4.6.1",
|
68
|
-
"@eslint/eslintrc": "^3.0.
|
69
|
-
"@eslint/js": "9.0.0-
|
68
|
+
"@eslint/eslintrc": "^3.0.1",
|
69
|
+
"@eslint/js": "9.0.0-beta.0",
|
70
70
|
"@humanwhocodes/config-array": "^0.11.14",
|
71
71
|
"@humanwhocodes/module-importer": "^1.0.1",
|
72
72
|
"@nodelib/fs.walk": "^1.2.8",
|
@@ -76,12 +76,12 @@
|
|
76
76
|
"debug": "^4.3.2",
|
77
77
|
"escape-string-regexp": "^4.0.0",
|
78
78
|
"eslint-scope": "^8.0.0",
|
79
|
-
"eslint-visitor-keys": "^
|
80
|
-
"espree": "^
|
79
|
+
"eslint-visitor-keys": "^4.0.0",
|
80
|
+
"espree": "^10.0.1",
|
81
81
|
"esquery": "^1.4.2",
|
82
82
|
"esutils": "^2.0.2",
|
83
83
|
"fast-deep-equal": "^3.1.3",
|
84
|
-
"file-entry-cache": "^
|
84
|
+
"file-entry-cache": "^8.0.0",
|
85
85
|
"find-up": "^5.0.0",
|
86
86
|
"glob-parent": "^6.0.2",
|
87
87
|
"globals": "^13.19.0",
|
@@ -135,8 +135,8 @@
|
|
135
135
|
"load-perf": "^0.2.0",
|
136
136
|
"markdown-it": "^12.2.0",
|
137
137
|
"markdown-it-container": "^3.0.0",
|
138
|
-
"markdownlint": "^0.
|
139
|
-
"markdownlint-cli": "^0.
|
138
|
+
"markdownlint": "^0.33.0",
|
139
|
+
"markdownlint-cli": "^0.39.0",
|
140
140
|
"marked": "^4.0.8",
|
141
141
|
"memfs": "^3.0.1",
|
142
142
|
"metascraper": "^5.25.7",
|
@@ -155,7 +155,7 @@
|
|
155
155
|
"regenerator-runtime": "^0.14.0",
|
156
156
|
"rollup-plugin-node-polyfills": "^0.2.1",
|
157
157
|
"semver": "^7.5.3",
|
158
|
-
"shelljs": "^0.8.
|
158
|
+
"shelljs": "^0.8.5",
|
159
159
|
"sinon": "^11.0.0",
|
160
160
|
"vite-plugin-commonjs": "^0.10.0",
|
161
161
|
"webdriverio": "^8.14.6",
|