eslint 9.0.0-alpha.2 → 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 +1 -6
- package/lib/api.js +25 -1
- package/lib/eslint/eslint.js +6 -0
- package/lib/eslint/legacy-eslint.js +6 -0
- 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 +142 -68
- 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/package.json +8 -8
package/README.md
CHANGED
@@ -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.
|
@@ -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.
|
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
|
@@ -17,7 +17,8 @@ const
|
|
17
17
|
equal = require("fast-deep-equal"),
|
18
18
|
Traverser = require("../shared/traverser"),
|
19
19
|
{ getRuleOptionsSchema } = require("../config/flat-config-helpers"),
|
20
|
-
{ Linter, SourceCodeFixer
|
20
|
+
{ Linter, SourceCodeFixer } = require("../linter"),
|
21
|
+
{ interpolate, getPlaceholderMatcher } = require("../linter/interpolate"),
|
21
22
|
stringify = require("json-stable-stringify-without-jsonify");
|
22
23
|
|
23
24
|
const { FlatConfigArray } = require("../config/flat-config-array");
|
@@ -304,6 +305,39 @@ function throwForbiddenMethodError(methodName, prototype) {
|
|
304
305
|
};
|
305
306
|
}
|
306
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
|
+
|
307
341
|
const metaSchemaDescription = `
|
308
342
|
\t- If the rule has options, set \`meta.schema\` to an array or non-empty object to enable options validation.
|
309
343
|
\t- If the rule doesn't have options, omit \`meta.schema\` to enforce that no options can be passed to the rule.
|
@@ -645,7 +679,11 @@ class RuleTester {
|
|
645
679
|
configs.push(itemConfig);
|
646
680
|
}
|
647
681
|
|
648
|
-
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");
|
649
687
|
filename = item.filename;
|
650
688
|
}
|
651
689
|
|
@@ -960,6 +998,7 @@ class RuleTester {
|
|
960
998
|
|
961
999
|
// Just an error message.
|
962
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.`);
|
963
1002
|
} else if (typeof error === "object" && error !== null) {
|
964
1003
|
|
965
1004
|
/*
|
@@ -992,6 +1031,18 @@ class RuleTester {
|
|
992
1031
|
error.messageId,
|
993
1032
|
`messageId '${message.messageId}' does not match expected messageId '${error.messageId}'.`
|
994
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
|
+
|
995
1046
|
if (hasOwnProperty(error, "data")) {
|
996
1047
|
|
997
1048
|
/*
|
@@ -1008,13 +1059,10 @@ class RuleTester {
|
|
1008
1059
|
`Hydrated message "${rehydratedMessage}" does not match "${message.message}"`
|
1009
1060
|
);
|
1010
1061
|
}
|
1062
|
+
} else {
|
1063
|
+
assert.fail("Test error must specify either a 'messageId' or 'message'.");
|
1011
1064
|
}
|
1012
1065
|
|
1013
|
-
assert.ok(
|
1014
|
-
hasOwnProperty(error, "data") ? hasOwnProperty(error, "messageId") : true,
|
1015
|
-
"Error must specify 'messageId' if 'data' is used."
|
1016
|
-
);
|
1017
|
-
|
1018
1066
|
if (error.type) {
|
1019
1067
|
assert.strictEqual(message.nodeType, error.type, `Error type should be ${error.type}, found ${message.nodeType}`);
|
1020
1068
|
}
|
@@ -1035,81 +1083,103 @@ class RuleTester {
|
|
1035
1083
|
assert.strictEqual(message.endColumn, error.endColumn, `Error endColumn should be ${error.endColumn}`);
|
1036
1084
|
}
|
1037
1085
|
|
1086
|
+
assert.ok(!message.suggestions || hasOwnProperty(error, "suggestions"), `Error at index ${i} has suggestions. Please specify 'suggestions' property on the test error object.`);
|
1038
1087
|
if (hasOwnProperty(error, "suggestions")) {
|
1039
1088
|
|
1040
1089
|
// Support asserting there are no suggestions
|
1041
|
-
|
1042
|
-
|
1043
|
-
|
1044
|
-
|
1045
|
-
|
1046
|
-
|
1047
|
-
assert.
|
1048
|
-
|
1049
|
-
|
1050
|
-
|
1051
|
-
|
1052
|
-
|
1053
|
-
)
|
1054
|
-
Object.keys(expectedSuggestion).forEach(propertyName => {
|
1055
|
-
assert.ok(
|
1056
|
-
suggestionObjectParameters.has(propertyName),
|
1057
|
-
`Invalid suggestion property name '${propertyName}'. Expected one of ${friendlySuggestionObjectParameterList}.`
|
1058
|
-
);
|
1059
|
-
});
|
1060
|
-
|
1061
|
-
const actualSuggestion = message.suggestions[index];
|
1062
|
-
const suggestionPrefix = `Error Suggestion at index ${index} :`;
|
1063
|
-
|
1064
|
-
if (hasOwnProperty(expectedSuggestion, "desc")) {
|
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) => {
|
1065
1103
|
assert.ok(
|
1066
|
-
|
1067
|
-
|
1104
|
+
typeof expectedSuggestion === "object" && expectedSuggestion !== null,
|
1105
|
+
"Test suggestion in 'suggestions' array must be an object."
|
1068
1106
|
);
|
1069
|
-
|
1070
|
-
|
1071
|
-
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
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
|
+
});
|
1075
1113
|
|
1076
|
-
|
1077
|
-
|
1078
|
-
ruleHasMetaMessages,
|
1079
|
-
`${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.`
|
1080
|
-
);
|
1081
|
-
assert.ok(
|
1082
|
-
hasOwnProperty(rule.meta.messages, expectedSuggestion.messageId),
|
1083
|
-
`${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.`
|
1084
|
-
);
|
1085
|
-
assert.strictEqual(
|
1086
|
-
actualSuggestion.messageId,
|
1087
|
-
expectedSuggestion.messageId,
|
1088
|
-
`${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.`
|
1089
|
-
);
|
1090
|
-
if (hasOwnProperty(expectedSuggestion, "data")) {
|
1091
|
-
const unformattedMetaMessage = rule.meta.messages[expectedSuggestion.messageId];
|
1092
|
-
const rehydratedDesc = interpolate(unformattedMetaMessage, expectedSuggestion.data);
|
1114
|
+
const actualSuggestion = message.suggestions[index];
|
1115
|
+
const suggestionPrefix = `Error Suggestion at index ${index}:`;
|
1093
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
|
+
);
|
1094
1126
|
assert.strictEqual(
|
1095
1127
|
actualSuggestion.desc,
|
1096
|
-
|
1097
|
-
`${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'.`
|
1098
1174
|
);
|
1099
1175
|
}
|
1100
|
-
} else {
|
1101
|
-
assert.ok(
|
1102
|
-
!hasOwnProperty(expectedSuggestion, "data"),
|
1103
|
-
`${suggestionPrefix} Test must specify 'messageId' if 'data' is used.`
|
1104
|
-
);
|
1105
|
-
}
|
1106
1176
|
|
1107
|
-
|
1177
|
+
assert.ok(hasOwnProperty(expectedSuggestion, "output"), `${suggestionPrefix} The "output" property is required.`);
|
1108
1178
|
const codeWithAppliedSuggestion = SourceCodeFixer.applyFixes(item.code, [actualSuggestion]).output;
|
1109
1179
|
|
1110
1180
|
// Verify if suggestion fix makes a syntax error or not.
|
1111
1181
|
const errorMessageInSuggestion =
|
1112
|
-
|
1182
|
+
linter.verify(codeWithAppliedSuggestion, result.configs, result.filename).find(m => m.fatal);
|
1113
1183
|
|
1114
1184
|
assert(!errorMessageInSuggestion, [
|
1115
1185
|
"A fatal parsing error occurred in suggestion fix.",
|
@@ -1119,8 +1189,11 @@ class RuleTester {
|
|
1119
1189
|
].join("\n"));
|
1120
1190
|
|
1121
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}"`);
|
1122
|
-
|
1123
|
-
|
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
|
+
}
|
1124
1197
|
}
|
1125
1198
|
}
|
1126
1199
|
} else {
|
@@ -1140,6 +1213,7 @@ class RuleTester {
|
|
1140
1213
|
);
|
1141
1214
|
} else {
|
1142
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.");
|
1143
1217
|
}
|
1144
1218
|
} else {
|
1145
1219
|
assert.strictEqual(
|
@@ -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
|
|
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": "^10.0.
|
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",
|
@@ -136,7 +136,7 @@
|
|
136
136
|
"markdown-it": "^12.2.0",
|
137
137
|
"markdown-it-container": "^3.0.0",
|
138
138
|
"markdownlint": "^0.33.0",
|
139
|
-
"markdownlint-cli": "^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",
|