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 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
@@ -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.
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const { Linter } = require("./linter");
4
- const interpolate = require("./interpolate");
4
+ const { interpolate } = require("./interpolate");
5
5
  const SourceCodeFixer = require("./source-code-fixer");
6
6
 
7
7
  module.exports = {
@@ -9,13 +9,30 @@
9
9
  // Public Interface
10
10
  //------------------------------------------------------------------------------
11
11
 
12
- module.exports = (text, data) => {
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(/\{\{([^{}]+?)\}\}/gu, (fullMatch, termWithWhitespace) => {
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, interpolate } = require("../linter"),
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.filename) {
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
- if (!error.suggestions || (Array.isArray(error.suggestions) && error.suggestions.length === 0)) {
1042
- if (Array.isArray(message.suggestions) && message.suggestions.length > 0) {
1043
- assert.fail(`Error should have no suggestions on error with message: "${message.message}"`);
1044
- }
1045
- } else {
1046
- assert.strictEqual(Array.isArray(message.suggestions), true, `Error should have an array of suggestions. Instead received "${message.suggestions}" on error with message: "${message.message}"`);
1047
- assert.strictEqual(message.suggestions.length, error.suggestions.length, `Error should have ${error.suggestions.length} suggestions. Instead found ${message.suggestions.length} suggestions`);
1048
-
1049
- error.suggestions.forEach((expectedSuggestion, index) => {
1050
- assert.ok(
1051
- typeof expectedSuggestion === "object" && expectedSuggestion !== null,
1052
- "Test suggestion in 'suggestions' array must be an object."
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
- !hasOwnProperty(expectedSuggestion, "data"),
1067
- `${suggestionPrefix} Test should not specify both 'desc' and 'data'.`
1104
+ typeof expectedSuggestion === "object" && expectedSuggestion !== null,
1105
+ "Test suggestion in 'suggestions' array must be an object."
1068
1106
  );
1069
- assert.strictEqual(
1070
- actualSuggestion.desc,
1071
- expectedSuggestion.desc,
1072
- `${suggestionPrefix} desc should be "${expectedSuggestion.desc}" but got "${actualSuggestion.desc}" instead.`
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
- if (hasOwnProperty(expectedSuggestion, "messageId")) {
1077
- assert.ok(
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
- rehydratedDesc,
1097
- `${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".`
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
- if (hasOwnProperty(expectedSuggestion, "output")) {
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
- linter.verify(codeWithAppliedSuggestion, result.configs, result.filename).find(m => m.fatal);
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(
@@ -92,7 +92,7 @@ module.exports = {
92
92
  vars: "all",
93
93
  args: "after-used",
94
94
  ignoreRestSiblings: false,
95
- caughtErrors: "none"
95
+ caughtErrors: "all"
96
96
  };
97
97
 
98
98
  const firstOption = context.options[0];
@@ -101,7 +101,7 @@ module.exports = {
101
101
  properties: {
102
102
  enforceForClassMembers: {
103
103
  type: "boolean",
104
- default: false
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] && context.options[0].enforceForClassMembers;
117
+ const enforceForClassMembers = context.options[0]?.enforceForClassMembers ?? true;
118
118
 
119
119
  /**
120
120
  * Reports a given node if it violated this rule.
@@ -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
- return Boolean(node) && (
25
- astUtils.isSpecificId(node, "NaN") ||
26
- astUtils.isSpecificMemberAccess(node, "Number", "NaN")
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
- context.report({ node, messageId: "comparisonWithNaN" });
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-alpha.2",
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.0",
69
- "@eslint/js": "9.0.0-alpha.2",
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": "^3.4.3",
80
- "espree": "^10.0.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": "^6.0.1",
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.38.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.2",
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",