eslint 9.0.0-alpha.2 → 9.0.0-beta.1
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 +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/linter.js +105 -110
- package/lib/linter/report-translator.js +1 -1
- package/lib/rule-tester/rule-tester.js +142 -68
- package/lib/rules/no-constant-binary-expression.js +2 -3
- package/lib/rules/no-extend-native.js +1 -2
- 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 +100 -6
- package/lib/rules/utils/ast-utils.js +9 -0
- package/package.json +10 -10
package/README.md
CHANGED
@@ -103,7 +103,7 @@ We are now at or near 100% compatibility with JSCS. If you try ESLint and believ
|
|
103
103
|
|
104
104
|
### Does Prettier replace ESLint?
|
105
105
|
|
106
|
-
No, ESLint and Prettier have
|
106
|
+
No, ESLint and Prettier have different jobs: ESLint is a linter (looking for problematic patterns) and Prettier is a code formatter. Using both tools is common, refer to [Prettier's documentation](https://prettier.io/docs/en/install#eslint-and-other-linters) to learn how to configure them to work well with each other.
|
107
107
|
|
108
108
|
### Why can't ESLint find my plugins?
|
109
109
|
|
@@ -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://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>
|
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
|
};
|
package/lib/linter/linter.js
CHANGED
@@ -43,7 +43,7 @@ const
|
|
43
43
|
const { getRuleFromConfig } = require("../config/flat-config-helpers");
|
44
44
|
const { FlatConfigArray } = require("../config/flat-config-array");
|
45
45
|
const { RuleValidator } = require("../config/rule-validator");
|
46
|
-
const {
|
46
|
+
const { assertIsRuleSeverity } = require("../config/flat-config-schema");
|
47
47
|
const { normalizeSeverityToString } = require("../shared/severity");
|
48
48
|
const debug = require("debug")("eslint:linter");
|
49
49
|
const MAX_AUTOFIX_PASSES = 10;
|
@@ -326,10 +326,11 @@ function createDisableDirectives(options) {
|
|
326
326
|
* @param {SourceCode} sourceCode The SourceCode object to get comments from.
|
327
327
|
* @param {function(string): {create: Function}} ruleMapper A map from rule IDs to defined rules
|
328
328
|
* @param {string|null} warnInlineConfig If a string then it should warn directive comments as disabled. The string value is the config name what the setting came from.
|
329
|
+
* @param {ConfigData} config Provided config.
|
329
330
|
* @returns {{configuredRules: Object, enabledGlobals: {value:string,comment:Token}[], exportedVariables: Object, problems: LintMessage[], disableDirectives: DisableDirective[]}}
|
330
331
|
* A collection of the directive comments that were found, along with any problems that occurred when parsing
|
331
332
|
*/
|
332
|
-
function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig) {
|
333
|
+
function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig, config) {
|
333
334
|
const configuredRules = {};
|
334
335
|
const enabledGlobals = Object.create(null);
|
335
336
|
const exportedVariables = {};
|
@@ -438,8 +439,50 @@ function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig) {
|
|
438
439
|
return;
|
439
440
|
}
|
440
441
|
|
442
|
+
let ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue];
|
443
|
+
|
444
|
+
/*
|
445
|
+
* If the rule was already configured, inline rule configuration that
|
446
|
+
* only has severity should retain options from the config and just override the severity.
|
447
|
+
*
|
448
|
+
* Example:
|
449
|
+
*
|
450
|
+
* {
|
451
|
+
* rules: {
|
452
|
+
* curly: ["error", "multi"]
|
453
|
+
* }
|
454
|
+
* }
|
455
|
+
*
|
456
|
+
* /* eslint curly: ["warn"] * /
|
457
|
+
*
|
458
|
+
* Results in:
|
459
|
+
*
|
460
|
+
* curly: ["warn", "multi"]
|
461
|
+
*/
|
462
|
+
if (
|
463
|
+
|
464
|
+
/*
|
465
|
+
* If inline config for the rule has only severity
|
466
|
+
*/
|
467
|
+
ruleOptions.length === 1 &&
|
468
|
+
|
469
|
+
/*
|
470
|
+
* And the rule was already configured
|
471
|
+
*/
|
472
|
+
config.rules && Object.hasOwn(config.rules, name)
|
473
|
+
) {
|
474
|
+
|
475
|
+
/*
|
476
|
+
* Then use severity from the inline config and options from the provided config
|
477
|
+
*/
|
478
|
+
ruleOptions = [
|
479
|
+
ruleOptions[0], // severity from the inline config
|
480
|
+
...Array.isArray(config.rules[name]) ? config.rules[name].slice(1) : [] // options from the provided config
|
481
|
+
];
|
482
|
+
}
|
483
|
+
|
441
484
|
try {
|
442
|
-
validator.validateRuleOptions(rule, name,
|
485
|
+
validator.validateRuleOptions(rule, name, ruleOptions);
|
443
486
|
} catch (err) {
|
444
487
|
|
445
488
|
/*
|
@@ -460,7 +503,7 @@ function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig) {
|
|
460
503
|
return;
|
461
504
|
}
|
462
505
|
|
463
|
-
configuredRules[name] =
|
506
|
+
configuredRules[name] = ruleOptions;
|
464
507
|
});
|
465
508
|
} else {
|
466
509
|
problems.push(parseResult.error);
|
@@ -1322,7 +1365,7 @@ class Linter {
|
|
1322
1365
|
|
1323
1366
|
const sourceCode = slots.lastSourceCode;
|
1324
1367
|
const commentDirectives = options.allowInlineConfig
|
1325
|
-
? getDirectiveComments(sourceCode, ruleId => getRule(slots, ruleId), options.warnInlineConfig)
|
1368
|
+
? getDirectiveComments(sourceCode, ruleId => getRule(slots, ruleId), options.warnInlineConfig, config)
|
1326
1369
|
: { configuredRules: {}, enabledGlobals: {}, exportedVariables: {}, problems: [], disableDirectives: [] };
|
1327
1370
|
|
1328
1371
|
// augment global scope with declared global variables
|
@@ -1332,56 +1375,8 @@ class Linter {
|
|
1332
1375
|
{ exportedVariables: commentDirectives.exportedVariables, enabledGlobals: commentDirectives.enabledGlobals }
|
1333
1376
|
);
|
1334
1377
|
|
1335
|
-
/*
|
1336
|
-
* Now we determine the final configurations for rules.
|
1337
|
-
* First, let all inline rule configurations override those from the config.
|
1338
|
-
* Then, check for a special case: if a rule is configured in both places,
|
1339
|
-
* inline rule configuration that only has severity should retain options from
|
1340
|
-
* the config and just override the severity.
|
1341
|
-
*
|
1342
|
-
* Example:
|
1343
|
-
*
|
1344
|
-
* {
|
1345
|
-
* rules: {
|
1346
|
-
* curly: ["error", "multi"]
|
1347
|
-
* }
|
1348
|
-
* }
|
1349
|
-
*
|
1350
|
-
* /* eslint curly: ["warn"] * /
|
1351
|
-
*
|
1352
|
-
* Results in:
|
1353
|
-
*
|
1354
|
-
* curly: ["warn", "multi"]
|
1355
|
-
*/
|
1356
1378
|
const configuredRules = Object.assign({}, config.rules, commentDirectives.configuredRules);
|
1357
1379
|
|
1358
|
-
if (config.rules) {
|
1359
|
-
for (const [ruleId, ruleInlineConfig] of Object.entries(commentDirectives.configuredRules)) {
|
1360
|
-
if (
|
1361
|
-
|
1362
|
-
/*
|
1363
|
-
* If inline config for the rule has only severity
|
1364
|
-
*/
|
1365
|
-
(!Array.isArray(ruleInlineConfig) || ruleInlineConfig.length === 1) &&
|
1366
|
-
|
1367
|
-
/*
|
1368
|
-
* And provided config for the rule has options
|
1369
|
-
*/
|
1370
|
-
Object.hasOwn(config.rules, ruleId) &&
|
1371
|
-
(Array.isArray(config.rules[ruleId]) && config.rules[ruleId].length > 1)
|
1372
|
-
) {
|
1373
|
-
|
1374
|
-
/*
|
1375
|
-
* Then use severity from the inline config and options from the provided config
|
1376
|
-
*/
|
1377
|
-
configuredRules[ruleId] = [
|
1378
|
-
Array.isArray(ruleInlineConfig) ? ruleInlineConfig[0] : ruleInlineConfig, // severity from the inline config
|
1379
|
-
...config.rules[ruleId].slice(1) // options from the provided config
|
1380
|
-
];
|
1381
|
-
}
|
1382
|
-
}
|
1383
|
-
}
|
1384
|
-
|
1385
1380
|
let lintingProblems;
|
1386
1381
|
|
1387
1382
|
try {
|
@@ -1713,17 +1708,67 @@ class Linter {
|
|
1713
1708
|
|
1714
1709
|
try {
|
1715
1710
|
|
1716
|
-
|
1711
|
+
let ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue];
|
1717
1712
|
|
1718
|
-
assertIsRuleOptions(ruleId, ruleValue);
|
1719
1713
|
assertIsRuleSeverity(ruleId, ruleOptions[0]);
|
1720
1714
|
|
1721
|
-
|
1722
|
-
|
1723
|
-
|
1724
|
-
|
1715
|
+
/*
|
1716
|
+
* If the rule was already configured, inline rule configuration that
|
1717
|
+
* only has severity should retain options from the config and just override the severity.
|
1718
|
+
*
|
1719
|
+
* Example:
|
1720
|
+
*
|
1721
|
+
* {
|
1722
|
+
* rules: {
|
1723
|
+
* curly: ["error", "multi"]
|
1724
|
+
* }
|
1725
|
+
* }
|
1726
|
+
*
|
1727
|
+
* /* eslint curly: ["warn"] * /
|
1728
|
+
*
|
1729
|
+
* Results in:
|
1730
|
+
*
|
1731
|
+
* curly: ["warn", "multi"]
|
1732
|
+
*/
|
1733
|
+
|
1734
|
+
let shouldValidateOptions = true;
|
1735
|
+
|
1736
|
+
if (
|
1737
|
+
|
1738
|
+
/*
|
1739
|
+
* If inline config for the rule has only severity
|
1740
|
+
*/
|
1741
|
+
ruleOptions.length === 1 &&
|
1742
|
+
|
1743
|
+
/*
|
1744
|
+
* And the rule was already configured
|
1745
|
+
*/
|
1746
|
+
config.rules && Object.hasOwn(config.rules, ruleId)
|
1747
|
+
) {
|
1748
|
+
|
1749
|
+
/*
|
1750
|
+
* Then use severity from the inline config and options from the provided config
|
1751
|
+
*/
|
1752
|
+
ruleOptions = [
|
1753
|
+
ruleOptions[0], // severity from the inline config
|
1754
|
+
...config.rules[ruleId].slice(1) // options from the provided config
|
1755
|
+
];
|
1756
|
+
|
1757
|
+
// if the rule was enabled, the options have already been validated
|
1758
|
+
if (config.rules[ruleId][0] > 0) {
|
1759
|
+
shouldValidateOptions = false;
|
1725
1760
|
}
|
1726
|
-
}
|
1761
|
+
}
|
1762
|
+
|
1763
|
+
if (shouldValidateOptions) {
|
1764
|
+
ruleValidator.validate({
|
1765
|
+
plugins: config.plugins,
|
1766
|
+
rules: {
|
1767
|
+
[ruleId]: ruleOptions
|
1768
|
+
}
|
1769
|
+
});
|
1770
|
+
}
|
1771
|
+
|
1727
1772
|
mergedInlineConfig.rules[ruleId] = ruleOptions;
|
1728
1773
|
} catch (err) {
|
1729
1774
|
|
@@ -1763,58 +1808,8 @@ class Linter {
|
|
1763
1808
|
)
|
1764
1809
|
: { problems: [], disableDirectives: [] };
|
1765
1810
|
|
1766
|
-
/*
|
1767
|
-
* Now we determine the final configurations for rules.
|
1768
|
-
* First, let all inline rule configurations override those from the config.
|
1769
|
-
* Then, check for a special case: if a rule is configured in both places,
|
1770
|
-
* inline rule configuration that only has severity should retain options from
|
1771
|
-
* the config and just override the severity.
|
1772
|
-
*
|
1773
|
-
* Example:
|
1774
|
-
*
|
1775
|
-
* {
|
1776
|
-
* rules: {
|
1777
|
-
* curly: ["error", "multi"]
|
1778
|
-
* }
|
1779
|
-
* }
|
1780
|
-
*
|
1781
|
-
* /* eslint curly: ["warn"] * /
|
1782
|
-
*
|
1783
|
-
* Results in:
|
1784
|
-
*
|
1785
|
-
* curly: ["warn", "multi"]
|
1786
|
-
*
|
1787
|
-
* At this point, all rule configurations are normalized to arrays.
|
1788
|
-
*/
|
1789
1811
|
const configuredRules = Object.assign({}, config.rules, mergedInlineConfig.rules);
|
1790
1812
|
|
1791
|
-
if (config.rules) {
|
1792
|
-
for (const [ruleId, ruleInlineConfig] of Object.entries(mergedInlineConfig.rules)) {
|
1793
|
-
if (
|
1794
|
-
|
1795
|
-
/*
|
1796
|
-
* If inline config for the rule has only severity
|
1797
|
-
*/
|
1798
|
-
ruleInlineConfig.length === 1 &&
|
1799
|
-
|
1800
|
-
/*
|
1801
|
-
* And provided config for the rule has options
|
1802
|
-
*/
|
1803
|
-
Object.hasOwn(config.rules, ruleId) &&
|
1804
|
-
config.rules[ruleId].length > 1
|
1805
|
-
) {
|
1806
|
-
|
1807
|
-
/*
|
1808
|
-
* Then use severity from the inline config and options from the provided config
|
1809
|
-
*/
|
1810
|
-
configuredRules[ruleId] = [
|
1811
|
-
ruleInlineConfig[0], // severity from the inline config
|
1812
|
-
...config.rules[ruleId].slice(1) // options from the provided config
|
1813
|
-
];
|
1814
|
-
}
|
1815
|
-
}
|
1816
|
-
}
|
1817
|
-
|
1818
1813
|
let lintingProblems;
|
1819
1814
|
|
1820
1815
|
sourceCode.finalize();
|
@@ -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(
|
@@ -5,8 +5,7 @@
|
|
5
5
|
|
6
6
|
"use strict";
|
7
7
|
|
8
|
-
const
|
9
|
-
const { isNullLiteral, isConstant, isReferenceToGlobalVariable, isLogicalAssignmentOperator } = require("./utils/ast-utils");
|
8
|
+
const { isNullLiteral, isConstant, isReferenceToGlobalVariable, isLogicalAssignmentOperator, ECMASCRIPT_GLOBALS } = require("./utils/ast-utils");
|
10
9
|
|
11
10
|
const NUMERIC_OR_STRING_BINARY_OPERATORS = new Set(["+", "-", "*", "/", "%", "|", "^", "&", "**", "<<", ">>", ">>>"]);
|
12
11
|
|
@@ -376,7 +375,7 @@ function isAlwaysNew(scope, node) {
|
|
376
375
|
* Catching these is especially useful for primitive constructors
|
377
376
|
* which return boxed values, a surprising gotcha' in JavaScript.
|
378
377
|
*/
|
379
|
-
return Object.hasOwn(
|
378
|
+
return Object.hasOwn(ECMASCRIPT_GLOBALS, node.callee.name) &&
|
380
379
|
isReferenceToGlobalVariable(scope, node.callee);
|
381
380
|
}
|
382
381
|
case "Literal":
|
@@ -10,7 +10,6 @@
|
|
10
10
|
//------------------------------------------------------------------------------
|
11
11
|
|
12
12
|
const astUtils = require("./utils/ast-utils");
|
13
|
-
const globals = require("globals");
|
14
13
|
|
15
14
|
//------------------------------------------------------------------------------
|
16
15
|
// Rule Definition
|
@@ -54,7 +53,7 @@ module.exports = {
|
|
54
53
|
const sourceCode = context.sourceCode;
|
55
54
|
const exceptions = new Set(config.exceptions || []);
|
56
55
|
const modifiedBuiltins = new Set(
|
57
|
-
Object.keys(
|
56
|
+
Object.keys(astUtils.ECMASCRIPT_GLOBALS)
|
58
57
|
.filter(builtin => builtin[0].toUpperCase() === builtin[0])
|
59
58
|
.filter(builtin => !exceptions.has(builtin))
|
60
59
|
);
|
@@ -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,10 @@ 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 and cast to a Number.",
|
78
|
+
replaceWithFindIndex: "Replace with Array.prototype.{{ methodName }}."
|
67
79
|
}
|
68
80
|
},
|
69
81
|
|
@@ -71,6 +83,35 @@ module.exports = {
|
|
71
83
|
|
72
84
|
const enforceForSwitchCase = !context.options[0] || context.options[0].enforceForSwitchCase;
|
73
85
|
const enforceForIndexOf = context.options[0] && context.options[0].enforceForIndexOf;
|
86
|
+
const sourceCode = context.sourceCode;
|
87
|
+
|
88
|
+
const fixableOperators = new Set(["==", "===", "!=", "!=="]);
|
89
|
+
const castableOperators = new Set(["==", "!="]);
|
90
|
+
|
91
|
+
/**
|
92
|
+
* Get a fixer for a binary expression that compares to NaN.
|
93
|
+
* @param {ASTNode} node The node to fix.
|
94
|
+
* @param {function(string): string} wrapValue A function that wraps the compared value with a fix.
|
95
|
+
* @returns {function(Fixer): Fix} The fixer function.
|
96
|
+
*/
|
97
|
+
function getBinaryExpressionFixer(node, wrapValue) {
|
98
|
+
return fixer => {
|
99
|
+
const comparedValue = isNaNIdentifier(node.left) ? node.right : node.left;
|
100
|
+
const shouldWrap = comparedValue.type === "SequenceExpression";
|
101
|
+
const shouldNegate = node.operator[0] === "!";
|
102
|
+
|
103
|
+
const negation = shouldNegate ? "!" : "";
|
104
|
+
let comparedValueText = sourceCode.getText(comparedValue);
|
105
|
+
|
106
|
+
if (shouldWrap) {
|
107
|
+
comparedValueText = `(${comparedValueText})`;
|
108
|
+
}
|
109
|
+
|
110
|
+
const fixedValue = wrapValue(comparedValueText);
|
111
|
+
|
112
|
+
return fixer.replaceText(node, `${negation}${fixedValue}`);
|
113
|
+
};
|
114
|
+
}
|
74
115
|
|
75
116
|
/**
|
76
117
|
* Checks the given `BinaryExpression` node for `foo === NaN` and other comparisons.
|
@@ -82,7 +123,32 @@ module.exports = {
|
|
82
123
|
/^(?:[<>]|[!=]=)=?$/u.test(node.operator) &&
|
83
124
|
(isNaNIdentifier(node.left) || isNaNIdentifier(node.right))
|
84
125
|
) {
|
85
|
-
|
126
|
+
const suggestedFixes = [];
|
127
|
+
const NaNNode = isNaNIdentifier(node.left) ? node.left : node.right;
|
128
|
+
|
129
|
+
const isSequenceExpression = NaNNode.type === "SequenceExpression";
|
130
|
+
const isSuggestable = fixableOperators.has(node.operator) && !isSequenceExpression;
|
131
|
+
const isCastable = castableOperators.has(node.operator);
|
132
|
+
|
133
|
+
if (isSuggestable) {
|
134
|
+
suggestedFixes.push({
|
135
|
+
messageId: "replaceWithIsNaN",
|
136
|
+
fix: getBinaryExpressionFixer(node, value => `Number.isNaN(${value})`)
|
137
|
+
});
|
138
|
+
|
139
|
+
if (isCastable) {
|
140
|
+
suggestedFixes.push({
|
141
|
+
messageId: "replaceWithCastingAndIsNaN",
|
142
|
+
fix: getBinaryExpressionFixer(node, value => `Number.isNaN(Number(${value}))`)
|
143
|
+
});
|
144
|
+
}
|
145
|
+
}
|
146
|
+
|
147
|
+
context.report({
|
148
|
+
node,
|
149
|
+
messageId: "comparisonWithNaN",
|
150
|
+
suggest: suggestedFixes
|
151
|
+
});
|
86
152
|
}
|
87
153
|
}
|
88
154
|
|
@@ -119,7 +185,35 @@ module.exports = {
|
|
119
185
|
node.arguments.length === 1 &&
|
120
186
|
isNaNIdentifier(node.arguments[0])
|
121
187
|
) {
|
122
|
-
|
188
|
+
|
189
|
+
/*
|
190
|
+
* To retain side effects, it's essential to address `NaN` beforehand, which
|
191
|
+
* is not possible with fixes like `arr.findIndex(Number.isNaN)`.
|
192
|
+
*/
|
193
|
+
const isSuggestable = node.arguments[0].type !== "SequenceExpression";
|
194
|
+
const suggestedFixes = [];
|
195
|
+
|
196
|
+
if (isSuggestable) {
|
197
|
+
const shouldWrap = callee.computed;
|
198
|
+
const findIndexMethod = methodName === "indexOf" ? "findIndex" : "findLastIndex";
|
199
|
+
const propertyName = shouldWrap ? `"${findIndexMethod}"` : findIndexMethod;
|
200
|
+
|
201
|
+
suggestedFixes.push({
|
202
|
+
messageId: "replaceWithFindIndex",
|
203
|
+
data: { methodName: findIndexMethod },
|
204
|
+
fix: fixer => [
|
205
|
+
fixer.replaceText(callee.property, propertyName),
|
206
|
+
fixer.replaceText(node.arguments[0], "Number.isNaN")
|
207
|
+
]
|
208
|
+
});
|
209
|
+
}
|
210
|
+
|
211
|
+
context.report({
|
212
|
+
node,
|
213
|
+
messageId: "indexOfNaN",
|
214
|
+
data: { methodName },
|
215
|
+
suggest: suggestedFixes
|
216
|
+
});
|
123
217
|
}
|
124
218
|
}
|
125
219
|
}
|
@@ -19,6 +19,8 @@ const {
|
|
19
19
|
lineBreakPattern,
|
20
20
|
shebangPattern
|
21
21
|
} = require("../../shared/ast-utils");
|
22
|
+
const globals = require("../../../conf/globals");
|
23
|
+
const { LATEST_ECMA_VERSION } = require("../../../conf/ecma-version");
|
22
24
|
|
23
25
|
//------------------------------------------------------------------------------
|
24
26
|
// Helpers
|
@@ -46,6 +48,12 @@ const OCTAL_OR_NON_OCTAL_DECIMAL_ESCAPE_PATTERN = /^(?:[^\\]|\\.)*\\(?:[1-9]|0[0
|
|
46
48
|
|
47
49
|
const LOGICAL_ASSIGNMENT_OPERATORS = new Set(["&&=", "||=", "??="]);
|
48
50
|
|
51
|
+
/**
|
52
|
+
* All builtin global variables defined in the latest ECMAScript specification.
|
53
|
+
* @type {Record<string,boolean>} Key is the name of the variable. Value is `true` if the variable is considered writable, `false` otherwise.
|
54
|
+
*/
|
55
|
+
const ECMASCRIPT_GLOBALS = globals[`es${LATEST_ECMA_VERSION}`];
|
56
|
+
|
49
57
|
/**
|
50
58
|
* Checks reference if is non initializer and writable.
|
51
59
|
* @param {Reference} reference A reference to check.
|
@@ -1133,6 +1141,7 @@ module.exports = {
|
|
1133
1141
|
LINEBREAK_MATCHER: lineBreakPattern,
|
1134
1142
|
SHEBANG_MATCHER: shebangPattern,
|
1135
1143
|
STATEMENT_LIST_PARENTS,
|
1144
|
+
ECMASCRIPT_GLOBALS,
|
1136
1145
|
|
1137
1146
|
/**
|
1138
1147
|
* Determines whether two adjacent tokens are on the same line.
|
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.1",
|
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.2",
|
69
|
+
"@eslint/js": "9.0.0-beta.1",
|
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,15 +76,14 @@
|
|
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
|
-
"globals": "^13.19.0",
|
88
87
|
"graphemer": "^1.4.0",
|
89
88
|
"ignore": "^5.2.0",
|
90
89
|
"imurmurhash": "^0.1.4",
|
@@ -122,12 +121,13 @@
|
|
122
121
|
"eslint-plugin-jsdoc": "^46.9.0",
|
123
122
|
"eslint-plugin-n": "^16.6.0",
|
124
123
|
"eslint-plugin-unicorn": "^49.0.0",
|
125
|
-
"eslint-release": "^3.2.
|
124
|
+
"eslint-release": "^3.2.2",
|
126
125
|
"eslump": "^3.0.0",
|
127
126
|
"esprima": "^4.0.1",
|
128
127
|
"fast-glob": "^3.2.11",
|
129
128
|
"fs-teardown": "^0.1.3",
|
130
129
|
"glob": "^10.0.0",
|
130
|
+
"globals": "^14.0.0",
|
131
131
|
"got": "^11.8.3",
|
132
132
|
"gray-matter": "^4.0.3",
|
133
133
|
"js-yaml": "^4.1.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",
|