eslint 10.0.0-alpha.1 → 10.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
@@ -343,7 +343,7 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).
343
343
  <p><a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="128"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="128"></a></p><h3>Gold Sponsors</h3>
344
344
  <p><a href="https://qlty.sh/"><img src="https://images.opencollective.com/qltysh/33d157d/logo.png" alt="Qlty Software" height="96"></a> <a href="https://shopify.engineering/"><img src="https://avatars.githubusercontent.com/u/8085" alt="Shopify" height="96"></a></p><h3>Silver Sponsors</h3>
345
345
  <p><a href="https://vite.dev/"><img src="https://images.opencollective.com/vite/e6d15e1/logo.png" alt="Vite" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/2d6c3b6/logo.png" alt="Liftoff" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301" alt="American Express" height="64"></a> <a href="https://stackblitz.com"><img src="https://avatars.githubusercontent.com/u/28635252" alt="StackBlitz" height="64"></a></p><h3>Bronze Sponsors</h3>
346
- <p><a href="https://cybozu.co.jp/"><img src="https://images.opencollective.com/cybozu/933e46d/logo.png" alt="Cybozu" height="32"></a> <a href="https://syntax.fm"><img src="https://github.com/syntaxfm.png" alt="Syntax" height="32"></a> <a href="https://www.n-ix.com/"><img src="https://images.opencollective.com/n-ix-ltd/575a7a5/logo.png" alt="N-iX Ltd" 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://www.gitbook.com"><img src="https://avatars.githubusercontent.com/u/7111340" alt="GitBook" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104" alt="Nx" height="32"></a> <a href="https://opensource.mercedes-benz.com/"><img src="https://avatars.githubusercontent.com/u/34240465" alt="Mercedes-Benz Group" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774" alt="HeroCoders" height="32"></a> <a href="https://www.lambdatest.com"><img src="https://avatars.githubusercontent.com/u/171592363" alt="LambdaTest" height="32"></a></p>
346
+ <p><a href="https://cybozu.co.jp/"><img src="https://images.opencollective.com/cybozu/933e46d/logo.png" alt="Cybozu" height="32"></a> <a href="https://www.crawljobs.com/"><img src="https://images.opencollective.com/crawljobs-poland/fa43a17/logo.png" alt="CrawlJobs" height="32"></a> <a href="https://syntax.fm"><img src="https://github.com/syntaxfm.png" alt="Syntax" height="32"></a> <a href="https://www.n-ix.com/"><img src="https://images.opencollective.com/n-ix-ltd/575a7a5/logo.png" alt="N-iX Ltd" 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://www.gitbook.com"><img src="https://avatars.githubusercontent.com/u/7111340" alt="GitBook" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104" alt="Nx" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774" alt="HeroCoders" height="32"></a> <a href="https://www.lambdatest.com"><img src="https://avatars.githubusercontent.com/u/171592363" alt="LambdaTest" height="32"></a></p>
347
347
  <h3>Technology Sponsors</h3>
348
348
  Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.
349
349
  <p><a href="https://netlify.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/netlify-icon.svg" alt="Netlify" height="32"></a> <a href="https://algolia.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/algolia-icon.svg" alt="Algolia" height="32"></a> <a href="https://1password.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/1password-icon.svg" alt="1Password" height="32"></a></p>
package/lib/api.js CHANGED
@@ -19,8 +19,8 @@ const { SourceCode } = require("./languages/js/source-code");
19
19
  //-----------------------------------------------------------------------------
20
20
 
21
21
  /**
22
- * Loads the correct ESLint constructor given the options.
23
- * @returns {Promise<ESLint>} The ESLint constructor
22
+ * Loads the correct `ESLint` constructor.
23
+ * @returns {Promise<ESLint>} The ESLint constructor.
24
24
  */
25
25
  async function loadESLint() {
26
26
  return ESLint;
@@ -4,14 +4,30 @@
4
4
  */
5
5
  "use strict";
6
6
 
7
- const chalk = require("chalk"),
8
- util = require("node:util"),
7
+ const util = require("node:util"),
9
8
  table = require("../../shared/text-table");
10
9
 
11
10
  //------------------------------------------------------------------------------
12
11
  // Helpers
13
12
  //------------------------------------------------------------------------------
14
13
 
14
+ /**
15
+ * Returns a styling function based on the color option.
16
+ * @param {boolean|undefined} color Indicates whether to use colors.
17
+ * @returns {Function} A function that styles text.
18
+ */
19
+ function getStyleText(color) {
20
+ if (typeof color === "undefined") {
21
+ return (format, text) =>
22
+ util.styleText(format, text, { validateStream: true });
23
+ }
24
+ if (color) {
25
+ return (format, text) =>
26
+ util.styleText(format, text, { validateStream: false });
27
+ }
28
+ return (_, text) => text;
29
+ }
30
+
15
31
  /**
16
32
  * Given a word and a count, append an s if count is not one.
17
33
  * @param {string} word A word in its singular form.
@@ -26,7 +42,9 @@ function pluralize(word, count) {
26
42
  // Public Interface
27
43
  //------------------------------------------------------------------------------
28
44
 
29
- module.exports = function (results) {
45
+ module.exports = function (results, data) {
46
+ const styleText = getStyleText(data?.color);
47
+
30
48
  let output = "\n",
31
49
  errorCount = 0,
32
50
  warningCount = 0,
@@ -46,17 +64,17 @@ module.exports = function (results) {
46
64
  fixableErrorCount += result.fixableErrorCount;
47
65
  fixableWarningCount += result.fixableWarningCount;
48
66
 
49
- output += `${chalk.underline(result.filePath)}\n`;
67
+ output += `${styleText("underline", result.filePath)}\n`;
50
68
 
51
69
  output += `${table(
52
70
  messages.map(message => {
53
71
  let messageType;
54
72
 
55
73
  if (message.fatal || message.severity === 2) {
56
- messageType = chalk.red("error");
74
+ messageType = styleText("red", "error");
57
75
  summaryColor = "red";
58
76
  } else {
59
- messageType = chalk.yellow("warning");
77
+ messageType = styleText("yellow", "warning");
60
78
  }
61
79
 
62
80
  return [
@@ -65,7 +83,7 @@ module.exports = function (results) {
65
83
  String(message.column || 0),
66
84
  messageType,
67
85
  message.message.replace(/([^ ])\.$/u, "$1"),
68
- chalk.dim(message.ruleId || ""),
86
+ message.ruleId ? styleText("dim", message.ruleId) : "",
69
87
  ];
70
88
  }),
71
89
  {
@@ -78,7 +96,7 @@ module.exports = function (results) {
78
96
  .split("\n")
79
97
  .map(el =>
80
98
  el.replace(/(\d+)\s+(\d+)/u, (m, p1, p2) =>
81
- chalk.dim(`${p1}:${p2}`),
99
+ styleText("dim", `${p1}:${p2}`),
82
100
  ),
83
101
  )
84
102
  .join("\n")}\n\n`;
@@ -86,37 +104,50 @@ module.exports = function (results) {
86
104
 
87
105
  const total = errorCount + warningCount;
88
106
 
107
+ /*
108
+ * We can't use a single `styleText` call like `styleText([summaryColor, "bold"], text)` here.
109
+ * This is a bug in `util.styleText` in Node.js versions earlier than v22.15.0 (https://github.com/nodejs/node/issues/56717).
110
+ * As a workaround, we use nested `styleText` calls.
111
+ */
89
112
  if (total > 0) {
90
- output += chalk[summaryColor].bold(
91
- [
92
- "\u2716 ",
93
- total,
94
- pluralize(" problem", total),
95
- " (",
96
- errorCount,
97
- pluralize(" error", errorCount),
98
- ", ",
99
- warningCount,
100
- pluralize(" warning", warningCount),
101
- ")\n",
102
- ].join(""),
103
- );
104
-
105
- if (fixableErrorCount > 0 || fixableWarningCount > 0) {
106
- output += chalk[summaryColor].bold(
113
+ output += `${styleText(
114
+ summaryColor,
115
+ styleText(
116
+ "bold",
107
117
  [
108
- " ",
109
- fixableErrorCount,
110
- pluralize(" error", fixableErrorCount),
111
- " and ",
112
- fixableWarningCount,
113
- pluralize(" warning", fixableWarningCount),
114
- " potentially fixable with the `--fix` option.\n",
118
+ "\u2716 ",
119
+ total,
120
+ pluralize(" problem", total),
121
+ " (",
122
+ errorCount,
123
+ pluralize(" error", errorCount),
124
+ ", ",
125
+ warningCount,
126
+ pluralize(" warning", warningCount),
127
+ ")",
115
128
  ].join(""),
116
- );
129
+ ),
130
+ )}\n`;
131
+
132
+ if (fixableErrorCount > 0 || fixableWarningCount > 0) {
133
+ output += `${styleText(
134
+ summaryColor,
135
+ styleText(
136
+ "bold",
137
+ [
138
+ " ",
139
+ fixableErrorCount,
140
+ pluralize(" error", fixableErrorCount),
141
+ " and ",
142
+ fixableWarningCount,
143
+ pluralize(" warning", fixableWarningCount),
144
+ " potentially fixable with the `--fix` option.",
145
+ ].join(""),
146
+ ),
147
+ )}\n`;
117
148
  }
118
149
  }
119
150
 
120
151
  // Resets output color, for prevent change on top level
121
- return total > 0 ? chalk.reset(output) : "";
152
+ return total > 0 ? styleText("reset", output) : "";
122
153
  };
package/lib/cli.js CHANGED
@@ -454,14 +454,24 @@ const cli = {
454
454
  const tooManyWarnings =
455
455
  options.maxWarnings >= 0 &&
456
456
  resultCounts.warningCount > options.maxWarnings;
457
- const resultsMeta = tooManyWarnings
458
- ? {
459
- maxWarningsExceeded: {
460
- maxWarnings: options.maxWarnings,
461
- foundWarnings: resultCounts.warningCount,
462
- },
463
- }
464
- : {};
457
+ const resultsMeta = /** @type {ResultsMeta} */ ({});
458
+
459
+ /*
460
+ * `--color` was set, `options.color` is `true`.
461
+ * `--no-color` was set, `options.color` is `false`.
462
+ * Neither option was provided, `options.color` is omitted, so `undefined`.
463
+ */
464
+ if (options.color !== void 0) {
465
+ debug(`Color setting for output: ${options.color}`);
466
+ resultsMeta.color = options.color;
467
+ }
468
+
469
+ if (tooManyWarnings) {
470
+ resultsMeta.maxWarningsExceeded = {
471
+ maxWarnings: options.maxWarnings,
472
+ foundWarnings: resultCounts.warningCount,
473
+ };
474
+ }
465
475
 
466
476
  if (
467
477
  await printResults(
@@ -1207,6 +1207,7 @@ class ESLint {
1207
1207
  const { cwd } = privateMembers.get(this).options;
1208
1208
 
1209
1209
  let formatterPath;
1210
+ let isBuiltInFormatter = false;
1210
1211
 
1211
1212
  // if there's a slash, then it's a file (TODO: this check seems dubious for scoped npm packages)
1212
1213
  if (!namespace && normalizedFormatName.includes("/")) {
@@ -1228,6 +1229,7 @@ class ESLint {
1228
1229
  "formatters",
1229
1230
  `${normalizedFormatName}.js`,
1230
1231
  );
1232
+ isBuiltInFormatter = true;
1231
1233
  }
1232
1234
  }
1233
1235
 
@@ -1237,7 +1239,7 @@ class ESLint {
1237
1239
  formatter = (await import(pathToFileURL(formatterPath))).default;
1238
1240
  } catch (ex) {
1239
1241
  // check for formatters that have been removed
1240
- if (removedFormatters.has(name)) {
1242
+ if (isBuiltInFormatter && removedFormatters.has(name)) {
1241
1243
  ex.message = `The ${name} formatter is no longer part of core ESLint. Install it manually with \`npm install -D eslint-formatter-${name}\``;
1242
1244
  } else {
1243
1245
  ex.message = `There was a problem loading formatter: ${formatterPath}\nError: ${ex.message}`;
@@ -1344,7 +1346,7 @@ class ESLint {
1344
1346
 
1345
1347
  /**
1346
1348
  * Returns whether flat config should be used.
1347
- * @returns {Promise<boolean>} Whether flat config should be used.
1349
+ * @returns {Promise<true>} Whether flat config should be used.
1348
1350
  */
1349
1351
  async function shouldUseFlatConfig() {
1350
1352
  return true;
@@ -55,6 +55,7 @@ function analyzeScope(ast, languageOptions, visitorKeys) {
55
55
  sourceType: languageOptions.sourceType || "script",
56
56
  childVisitorKeys: visitorKeys || evk.KEYS,
57
57
  fallback: evk.getKeys,
58
+ jsx: ecmaFeatures.jsx,
58
59
  });
59
60
  }
60
61
 
@@ -473,6 +473,7 @@ function analyzeScope(ast, languageOptions, visitorKeys) {
473
473
  sourceType: languageOptions.sourceType || "script",
474
474
  childVisitorKeys: visitorKeys || evk.KEYS,
475
475
  fallback: Traverser.getKeys,
476
+ jsx: ecmaFeatures.jsx,
476
477
  });
477
478
  }
478
479
 
@@ -387,9 +387,10 @@ function normalizeTestCase(item) {
387
387
  * Asserts that the `errors` property of an invalid test case is valid.
388
388
  * @param {number | string[]} errors The `errors` property of the invalid test case.
389
389
  * @param {string} ruleName The name of the rule being tested.
390
+ * @param {Object} [assertionOptions] The assertion options for the test case.
390
391
  * @returns {void}
391
392
  */
392
- function assertErrorsProperty(errors, ruleName) {
393
+ function assertErrorsProperty(errors, ruleName, assertionOptions = {}) {
393
394
  const isNumber = typeof errors === "number";
394
395
  const isArray = Array.isArray(errors);
395
396
 
@@ -407,12 +408,75 @@ function assertErrorsProperty(errors, ruleName) {
407
408
  }
408
409
  }
409
410
 
411
+ const { requireMessage = false, requireLocation = false } =
412
+ assertionOptions;
413
+
410
414
  if (isArray) {
411
415
  assert.ok(
412
416
  errors.length !== 0,
413
417
  "Invalid cases must have at least one error",
414
418
  );
419
+
420
+ for (const [number, error] of errors.entries()) {
421
+ if (typeof error === "string" || error instanceof RegExp) {
422
+ // Just an error message.
423
+ assert.ok(
424
+ requireMessage !== "messageId" && !requireLocation,
425
+ `errors[${number}] should be an object when 'assertionOptions.requireMessage' is 'messageId' or 'assertionOptions.requireLocation' is true.`,
426
+ );
427
+ } else if (typeof error === "object" && error !== null) {
428
+ /*
429
+ * Error object.
430
+ * This may have a message, messageId, data, line, and/or column.
431
+ */
432
+
433
+ for (const propertyName of Object.keys(error)) {
434
+ assert.ok(
435
+ errorObjectParameters.has(propertyName),
436
+ `Invalid error property name '${propertyName}'. Expected one of ${friendlyErrorObjectParameterList}.`,
437
+ );
438
+ }
439
+
440
+ if (requireMessage === "message") {
441
+ assert.ok(
442
+ !hasOwnProperty(error, "messageId") &&
443
+ hasOwnProperty(error, "message"),
444
+ `errors[${number}] should specify 'message' (and not 'messageId') when 'assertionOptions.requireMessage' is 'message'.`,
445
+ );
446
+ } else if (requireMessage === "messageId") {
447
+ assert.ok(
448
+ !hasOwnProperty(error, "message") &&
449
+ hasOwnProperty(error, "messageId"),
450
+ `errors[${number}] should specify 'messageId' (and not 'message') when 'assertionOptions.requireMessage' is 'messageId'.`,
451
+ );
452
+ }
453
+
454
+ if (hasOwnProperty(error, "message")) {
455
+ assert.ok(
456
+ !hasOwnProperty(error, "messageId"),
457
+ `errors[${number}] should not specify both 'message' and 'messageId'.`,
458
+ );
459
+ assert.ok(
460
+ !hasOwnProperty(error, "data"),
461
+ `errors[${number}] should not specify both 'data' and 'message'.`,
462
+ );
463
+ } else {
464
+ assert.ok(
465
+ hasOwnProperty(error, "messageId"),
466
+ `errors[${number}] must specify either 'messageId' or 'message'.`,
467
+ );
468
+ }
469
+ } else {
470
+ assert.fail(
471
+ `errors[${number}] must be a string, RegExp, or an object.`,
472
+ );
473
+ }
474
+ }
415
475
  } else {
476
+ assert.ok(
477
+ !requireMessage && !requireLocation,
478
+ "Invalid cases must have 'errors' value as an array",
479
+ );
416
480
  assert.ok(
417
481
  errors > 0,
418
482
  "Invalid cases must have 'error' value greater than 0",
@@ -564,13 +628,19 @@ function assertValidTestCase(item, seenTestCases) {
564
628
  * @param {Object} item The invalid test case object to check.
565
629
  * @param {Set<string>} seenTestCases Set of serialized test cases to check for duplicates.
566
630
  * @param {string} ruleName The name of the rule being tested.
631
+ * @param {Object} [assertionOptions] The assertion options for the test case.
567
632
  * @returns {void}
568
633
  * @throws {AssertionError} If the test case is not valid.
569
634
  */
570
- function assertInvalidTestCase(item, seenTestCases, ruleName) {
635
+ function assertInvalidTestCase(
636
+ item,
637
+ seenTestCases,
638
+ ruleName,
639
+ assertionOptions = {},
640
+ ) {
571
641
  assertTestCommonProperties(item);
572
642
 
573
- assertErrorsProperty(item.errors, ruleName);
643
+ assertErrorsProperty(item.errors, ruleName, assertionOptions);
574
644
 
575
645
  // 'output' is optional, but if it exists it must be a string or null
576
646
  if (hasOwnProperty(item, "output")) {
@@ -763,6 +833,10 @@ class RuleTester {
763
833
  * @param {string} ruleName The name of the rule to run.
764
834
  * @param {RuleDefinition} rule The rule to test.
765
835
  * @param {{
836
+ * assertionOptions?: {
837
+ * requireMessage?: boolean | "message" | "messageId",
838
+ * requireLocation?: boolean
839
+ * },
766
840
  * valid: (ValidTestCase | string)[],
767
841
  * invalid: InvalidTestCase[]
768
842
  * }} test The collection of tests to run.
@@ -1143,6 +1217,9 @@ class RuleTester {
1143
1217
  * @private
1144
1218
  */
1145
1219
  function testInvalidTemplate(item) {
1220
+ const { requireMessage = false, requireLocation = false } =
1221
+ test.assertionOptions ?? {};
1222
+
1146
1223
  const ruleHasMetaMessages =
1147
1224
  hasOwnProperty(rule, "meta") &&
1148
1225
  hasOwnProperty(rule.meta, "messages");
@@ -1152,6 +1229,11 @@ class RuleTester {
1152
1229
  .join(", ")}]`
1153
1230
  : null;
1154
1231
 
1232
+ assert.ok(
1233
+ ruleHasMetaMessages || requireMessage !== "messageId",
1234
+ `Assertion options can not use 'requireMessage: "messageId"' if rule under test doesn't define 'meta.messages'.`,
1235
+ );
1236
+
1155
1237
  const result = runRuleForItem(item);
1156
1238
  const messages = result.messages;
1157
1239
 
@@ -1225,22 +1307,7 @@ class RuleTester {
1225
1307
  * This may have a message, messageId, data, line, and/or column.
1226
1308
  */
1227
1309
 
1228
- Object.keys(error).forEach(propertyName => {
1229
- assert.ok(
1230
- errorObjectParameters.has(propertyName),
1231
- `Invalid error property name '${propertyName}'. Expected one of ${friendlyErrorObjectParameterList}.`,
1232
- );
1233
- });
1234
-
1235
1310
  if (hasOwnProperty(error, "message")) {
1236
- assert.ok(
1237
- !hasOwnProperty(error, "messageId"),
1238
- "Error should not specify both 'message' and a 'messageId'.",
1239
- );
1240
- assert.ok(
1241
- !hasOwnProperty(error, "data"),
1242
- "Error should not specify both 'data' and 'message'.",
1243
- );
1244
1311
  assertMessageMatches(
1245
1312
  message.message,
1246
1313
  error.message,
@@ -1298,33 +1365,34 @@ class RuleTester {
1298
1365
  `Hydrated message "${rehydratedMessage}" does not match "${message.message}"`,
1299
1366
  );
1300
1367
  }
1301
- } else {
1302
- assert.fail(
1303
- "Test error must specify either a 'messageId' or 'message'.",
1304
- );
1305
1368
  }
1306
1369
 
1370
+ const locationProperties = [
1371
+ "line",
1372
+ "column",
1373
+ "endLine",
1374
+ "endColumn",
1375
+ ];
1307
1376
  const actualLocation = {};
1308
1377
  const expectedLocation = {};
1309
1378
 
1310
- if (hasOwnProperty(error, "line")) {
1311
- actualLocation.line = message.line;
1312
- expectedLocation.line = error.line;
1313
- }
1314
-
1315
- if (hasOwnProperty(error, "column")) {
1316
- actualLocation.column = message.column;
1317
- expectedLocation.column = error.column;
1318
- }
1319
-
1320
- if (hasOwnProperty(error, "endLine")) {
1321
- actualLocation.endLine = message.endLine;
1322
- expectedLocation.endLine = error.endLine;
1379
+ for (const key of locationProperties) {
1380
+ if (hasOwnProperty(error, key)) {
1381
+ actualLocation[key] = message[key];
1382
+ expectedLocation[key] = error[key];
1383
+ }
1323
1384
  }
1324
1385
 
1325
- if (hasOwnProperty(error, "endColumn")) {
1326
- actualLocation.endColumn = message.endColumn;
1327
- expectedLocation.endColumn = error.endColumn;
1386
+ if (requireLocation) {
1387
+ const missingKeys = locationProperties.filter(
1388
+ key =>
1389
+ !hasOwnProperty(error, key) &&
1390
+ hasOwnProperty(message, key),
1391
+ );
1392
+ assert.ok(
1393
+ missingKeys.length === 0,
1394
+ `Error is missing expected location properties: ${missingKeys.join(", ")}`,
1395
+ );
1328
1396
  }
1329
1397
 
1330
1398
  if (Object.keys(expectedLocation).length > 0) {
@@ -1550,11 +1618,6 @@ class RuleTester {
1550
1618
  }
1551
1619
  }
1552
1620
  }
1553
- } else {
1554
- // Message was an unexpected type
1555
- assert.fail(
1556
- `Error should be a string, object, or RegExp, but found (${util.inspect(message)})`,
1557
- );
1558
1621
  }
1559
1622
  }
1560
1623
  }
@@ -1631,6 +1694,7 @@ class RuleTester {
1631
1694
  item,
1632
1695
  seenTestCases,
1633
1696
  ruleName,
1697
+ test.assertionOptions,
1634
1698
  );
1635
1699
  testInvalidTemplate(item);
1636
1700
  } finally {
@@ -10,6 +10,30 @@
10
10
 
11
11
  const astUtils = require("./utils/ast-utils");
12
12
 
13
+ //------------------------------------------------------------------------------
14
+ // Helpers
15
+ //------------------------------------------------------------------------------
16
+
17
+ /**
18
+ * Format import names for error messages.
19
+ * @param {string[]} importNames The import names to format.
20
+ * @returns {string} The formatted import names.
21
+ */
22
+ function formatImportNames(importNames) {
23
+ return new Intl.ListFormat("en-US").format(
24
+ importNames.map(name => `'${name}'`),
25
+ );
26
+ }
27
+
28
+ /**
29
+ * Returns "is" or "are" based on the number of import names.
30
+ * @param {string[]} importNames The import names to check.
31
+ * @returns {string} "is" if one import name, otherwise "are".
32
+ */
33
+ function isOrAre(importNames) {
34
+ return importNames.length === 1 ? "is" : "are";
35
+ }
36
+
13
37
  //------------------------------------------------------------------------------
14
38
  // Rule Definition
15
39
  //------------------------------------------------------------------------------
@@ -175,22 +199,22 @@ module.exports = {
175
199
  "'{{importName}}' import from '{{importSource}}' is restricted from being used by a pattern. {{customMessage}}",
176
200
 
177
201
  patternAndEverything:
178
- "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted from being used by a pattern.",
202
+ "* import is invalid because {{importNames}} from '{{importSource}}' {{isOrAre}} restricted from being used by a pattern.",
179
203
 
180
204
  patternAndEverythingWithRegexImportName:
181
205
  "* import is invalid because import name matching '{{importNames}}' pattern from '{{importSource}}' is restricted from being used.",
182
206
  patternAndEverythingWithCustomMessage:
183
207
  // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
184
- "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted from being used by a pattern. {{customMessage}}",
208
+ "* import is invalid because {{importNames}} from '{{importSource}}' {{isOrAre}} restricted from being used by a pattern. {{customMessage}}",
185
209
  patternAndEverythingWithRegexImportNameAndCustomMessage:
186
210
  // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
187
211
  "* import is invalid because import name matching '{{importNames}}' pattern from '{{importSource}}' is restricted from being used. {{customMessage}}",
188
212
 
189
213
  everything:
190
- "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted.",
214
+ "* import is invalid because {{importNames}} from '{{importSource}}' {{isOrAre}} restricted.",
191
215
  everythingWithCustomMessage:
192
216
  // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
193
- "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted. {{customMessage}}",
217
+ "* import is invalid because {{importNames}} from '{{importSource}}' {{isOrAre}} restricted. {{customMessage}}",
194
218
 
195
219
  importName:
196
220
  "'{{importName}}' import from '{{importSource}}' is restricted.",
@@ -199,16 +223,16 @@ module.exports = {
199
223
  "'{{importName}}' import from '{{importSource}}' is restricted. {{customMessage}}",
200
224
 
201
225
  allowedImportName:
202
- "'{{importName}}' import from '{{importSource}}' is restricted because only '{{allowedImportNames}}' import(s) is/are allowed.",
226
+ "'{{importName}}' import from '{{importSource}}' is restricted because only {{allowedImportNames}} {{isOrAre}} allowed.",
203
227
  allowedImportNameWithCustomMessage:
204
228
  // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
205
- "'{{importName}}' import from '{{importSource}}' is restricted because only '{{allowedImportNames}}' import(s) is/are allowed. {{customMessage}}",
229
+ "'{{importName}}' import from '{{importSource}}' is restricted because only {{allowedImportNames}} {{isOrAre}} allowed. {{customMessage}}",
206
230
 
207
231
  everythingWithAllowImportNames:
208
- "* import is invalid because only '{{allowedImportNames}}' from '{{importSource}}' is/are allowed.",
232
+ "* import is invalid because only {{allowedImportNames}} from '{{importSource}}' {{isOrAre}} allowed.",
209
233
  everythingWithAllowImportNamesAndCustomMessage:
210
234
  // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
211
- "* import is invalid because only '{{allowedImportNames}}' from '{{importSource}}' is/are allowed. {{customMessage}}",
235
+ "* import is invalid because only {{allowedImportNames}} from '{{importSource}}' {{isOrAre}} allowed. {{customMessage}}",
212
236
 
213
237
  allowedImportNamePattern:
214
238
  "'{{importName}}' import from '{{importSource}}' is restricted because only imports that match the pattern '{{allowedImportNamePattern}}' are allowed from '{{importSource}}'.",
@@ -452,7 +476,10 @@ module.exports = {
452
476
  loc: specifier.loc,
453
477
  data: {
454
478
  importSource,
455
- importNames: restrictedImportNames,
479
+ importNames: formatImportNames(
480
+ restrictedImportNames,
481
+ ),
482
+ isOrAre: isOrAre(restrictedImportNames),
456
483
  customMessage,
457
484
  },
458
485
  });
@@ -465,7 +492,11 @@ module.exports = {
465
492
  loc: specifier.loc,
466
493
  data: {
467
494
  importSource,
468
- allowedImportNames,
495
+ allowedImportNames:
496
+ formatImportNames(
497
+ allowedImportNames,
498
+ ),
499
+ isOrAre: isOrAre(allowedImportNames),
469
500
  customMessage,
470
501
  },
471
502
  });
@@ -525,7 +556,11 @@ module.exports = {
525
556
  importSource,
526
557
  customMessage,
527
558
  importName,
528
- allowedImportNames,
559
+ allowedImportNames:
560
+ formatImportNames(
561
+ allowedImportNames,
562
+ ),
563
+ isOrAre: isOrAre(allowedImportNames),
529
564
  },
530
565
  });
531
566
  });
@@ -612,7 +647,10 @@ module.exports = {
612
647
  loc: specifier.loc,
613
648
  data: {
614
649
  importSource,
615
- importNames: restrictedImportNames,
650
+ importNames: formatImportNames(
651
+ restrictedImportNames,
652
+ ),
653
+ isOrAre: isOrAre(restrictedImportNames),
616
654
  customMessage,
617
655
  },
618
656
  });
@@ -625,7 +663,9 @@ module.exports = {
625
663
  loc: specifier.loc,
626
664
  data: {
627
665
  importSource,
628
- allowedImportNames,
666
+ allowedImportNames:
667
+ formatImportNames(allowedImportNames),
668
+ isOrAre: isOrAre(allowedImportNames),
629
669
  customMessage,
630
670
  },
631
671
  });
@@ -713,7 +753,9 @@ module.exports = {
713
753
  importSource,
714
754
  customMessage,
715
755
  importName,
716
- allowedImportNames,
756
+ allowedImportNames:
757
+ formatImportNames(allowedImportNames),
758
+ isOrAre: isOrAre(allowedImportNames),
717
759
  },
718
760
  });
719
761
  });
@@ -336,9 +336,12 @@ module.exports = {
336
336
  }
337
337
 
338
338
  if (
339
- targetAssignment.variable.references.some(
340
- ref => ref.identifier.type !== "Identifier",
341
- )
339
+ targetAssignment.variable.references.some(ref => {
340
+ const type = ref.identifier.type;
341
+ return (
342
+ type !== "Identifier" && type !== "JSXIdentifier"
343
+ );
344
+ })
342
345
  ) {
343
346
  /**
344
347
  * Skip checking for a variable that has at least one non-identifier reference.
@@ -529,7 +532,7 @@ module.exports = {
529
532
  TryStatement(node) {
530
533
  scopeStack.tryStatementBlocks.push(node.block);
531
534
  },
532
- Identifier(node) {
535
+ "Identifier, JSXIdentifier"(node) {
533
536
  for (const segment of scopeStack.currentSegments) {
534
537
  const segmentInfo = scopeStack.segments[segment.id];
535
538
 
@@ -539,7 +542,7 @@ module.exports = {
539
542
  segmentInfo.last = node;
540
543
  }
541
544
  },
542
- ":matches(VariableDeclarator[init!=null], AssignmentExpression, UpdateExpression):exit"(
545
+ "VariableDeclarator[init!=null], AssignmentExpression, UpdateExpression:exit"(
543
546
  node,
544
547
  ) {
545
548
  if (scopeStack.currentSegments.size === 0) {
@@ -704,60 +704,63 @@ function isKeywordToken(token) {
704
704
  }
705
705
 
706
706
  /**
707
- * Check to see if its a ES6 export declaration.
708
- * @param {ASTNode} astNode An AST node.
709
- * @returns {boolean} whether the given node represents an export declaration.
707
+ * Checks whether the given node represents an ES6 export declaration.
708
+ * @param {ASTNode} node A node to check.
709
+ * @returns {boolean} `true` if the node is an export declaration.
710
710
  * @private
711
711
  */
712
- function looksLikeExport(astNode) {
712
+ function isExportDeclaration(node) {
713
713
  return (
714
- astNode.type === "ExportDefaultDeclaration" ||
715
- astNode.type === "ExportNamedDeclaration" ||
716
- astNode.type === "ExportAllDeclaration" ||
717
- astNode.type === "ExportSpecifier"
714
+ node.type === "ExportDefaultDeclaration" ||
715
+ node.type === "ExportNamedDeclaration" ||
716
+ node.type === "ExportAllDeclaration"
718
717
  );
719
718
  }
720
719
 
721
720
  /**
722
- * Retrieves the JSDoc comment for a given node.
723
- * @param {ASTNode} node The AST node to get the comment for.
721
+ * Checks for the presence of a JSDoc comment for the given node and returns it.
722
+ * @param {ASTNode} node The node to get the comment for.
724
723
  * @param {SourceCode} sourceCode A SourceCode instance to get comments.
725
724
  * @returns {Token|null} The Block comment token containing the JSDoc comment for the given node or null if not found.
726
725
  * @private
727
726
  */
728
- function getJSDocComment(node, sourceCode) {
729
- /**
730
- * Checks for the presence of a JSDoc comment for the given node and returns it.
731
- * @param {ASTNode} astNode The AST node to get the comment for.
732
- * @returns {Token|null} The Block comment token containing the JSDoc comment for the given node or null if not found.
733
- * @private
734
- */
735
- function findJSDocComment(astNode) {
736
- const tokenBefore = sourceCode.getTokenBefore(astNode, {
737
- includeComments: true,
738
- });
727
+ function findJSDocComment(node, sourceCode) {
728
+ const tokenBefore = sourceCode.getTokenBefore(node, {
729
+ includeComments: true,
730
+ });
739
731
 
740
- if (
741
- tokenBefore &&
742
- isCommentToken(tokenBefore) &&
743
- tokenBefore.type === "Block" &&
744
- tokenBefore.value.charAt(0) === "*" &&
745
- astNode.loc.start.line - tokenBefore.loc.end.line <= 1
746
- ) {
747
- return tokenBefore;
748
- }
749
-
750
- return null;
732
+ if (
733
+ tokenBefore &&
734
+ tokenBefore.type === "Block" &&
735
+ tokenBefore.value.charAt(0) === "*" &&
736
+ node.loc.start.line - tokenBefore.loc.end.line <= 1
737
+ ) {
738
+ return tokenBefore;
751
739
  }
740
+
741
+ return null;
742
+ }
743
+
744
+ /**
745
+ * Retrieves the JSDoc comment for a given node.
746
+ * @param {ASTNode} node The node to get the comment for.
747
+ * @param {SourceCode} sourceCode A SourceCode instance to get comments.
748
+ * @returns {Token|null} The Block comment token containing the JSDoc comment for the given node or null if not found.
749
+ * @private
750
+ */
751
+ function getJSDocComment(node, sourceCode) {
752
752
  let parent = node.parent;
753
753
 
754
754
  switch (node.type) {
755
755
  case "ClassDeclaration":
756
756
  case "FunctionDeclaration":
757
- return findJSDocComment(looksLikeExport(parent) ? parent : node);
757
+ return findJSDocComment(
758
+ isExportDeclaration(parent) ? parent : node,
759
+ sourceCode,
760
+ );
758
761
 
759
762
  case "ClassExpression":
760
- return findJSDocComment(parent.parent);
763
+ return findJSDocComment(parent.parent, sourceCode);
761
764
 
762
765
  case "ArrowFunctionExpression":
763
766
  case "FunctionExpression":
@@ -783,11 +786,11 @@ function getJSDocComment(node, sourceCode) {
783
786
  parent.type !== "FunctionDeclaration" &&
784
787
  parent.type !== "Program"
785
788
  ) {
786
- return findJSDocComment(parent);
789
+ return findJSDocComment(parent, sourceCode);
787
790
  }
788
791
  }
789
792
 
790
- return findJSDocComment(node);
793
+ return findJSDocComment(node, sourceCode);
791
794
 
792
795
  // falls through
793
796
  default:
@@ -115,7 +115,7 @@ export namespace AST {
115
115
  end: ESTree.Position;
116
116
  }
117
117
 
118
- type Range = [number, number];
118
+ type Range = SourceRange;
119
119
 
120
120
  interface Program extends ESTree.Program {
121
121
  comments: ESTree.Comment[];
@@ -125,6 +125,11 @@ export namespace AST {
125
125
  }
126
126
  }
127
127
 
128
+ interface JSXIdentifier extends ESTree.BaseNode {
129
+ type: "JSXIdentifier";
130
+ name: string;
131
+ }
132
+
128
133
  export namespace Scope {
129
134
  interface ScopeManager {
130
135
  scopes: Scope[];
@@ -177,7 +182,7 @@ export namespace Scope {
177
182
  }
178
183
 
179
184
  interface Reference {
180
- identifier: ESTree.Identifier;
185
+ identifier: ESTree.Identifier | JSXIdentifier;
181
186
  from: Scope;
182
187
  resolved: Variable | null;
183
188
  writeExpr: ESTree.Node | null;
@@ -1256,9 +1261,8 @@ export namespace ESLint {
1256
1261
  foundWarnings: number;
1257
1262
  }
1258
1263
 
1259
- interface LintResultData {
1264
+ interface LintResultData extends ResultsMeta {
1260
1265
  cwd: string;
1261
- maxWarningsExceeded?: MaxWarningsExceeded | undefined;
1262
1266
  rulesMeta: {
1263
1267
  [ruleId: string]: Rule.RuleMetaData;
1264
1268
  };
@@ -1290,6 +1294,14 @@ export namespace ESLint {
1290
1294
  * Metadata about results for formatters.
1291
1295
  */
1292
1296
  interface ResultsMeta {
1297
+ /**
1298
+ * Whether or not to use color in the formatter output.
1299
+ * - If `--color` was set, this property is `true`.
1300
+ * - If `--no-color` was set, it is `false`.
1301
+ * - If neither option was provided, the property is omitted.
1302
+ */
1303
+ color?: boolean | undefined;
1304
+
1293
1305
  /**
1294
1306
  * Present if the maxWarnings threshold was exceeded.
1295
1307
  */
@@ -1301,9 +1313,9 @@ export namespace ESLint {
1301
1313
  /**
1302
1314
  * Used to call the underlying formatter.
1303
1315
  * @param results An array of lint results to format.
1304
- * @param resultsMeta An object with an optional `maxWarningsExceeded` property that will be
1316
+ * @param resultsMeta An object with optional `color` and `maxWarningsExceeded` properties that will be
1305
1317
  * passed to the underlying formatter function along with other properties set by ESLint.
1306
- * This argument can be omitted if `maxWarningsExceeded` is not needed.
1318
+ * This argument can be omitted if `color` and `maxWarningsExceeded` are not needed.
1307
1319
  * @return The formatter output.
1308
1320
  */
1309
1321
  format(
@@ -1332,9 +1344,10 @@ export namespace ESLint {
1332
1344
 
1333
1345
  // #endregion
1334
1346
 
1335
- export function loadESLint(options?: {
1336
- useFlatConfig?: boolean | undefined;
1337
- }): Promise<typeof ESLint>;
1347
+ /**
1348
+ * Loads the correct `ESLint` constructor.
1349
+ */
1350
+ export function loadESLint(): Promise<typeof ESLint>;
1338
1351
 
1339
1352
  // #region RuleTester
1340
1353
 
@@ -1342,6 +1355,9 @@ export class RuleTester {
1342
1355
  static describe: ((...args: any) => any) | null;
1343
1356
  static it: ((...args: any) => any) | null;
1344
1357
  static itOnly: ((...args: any) => any) | null;
1358
+ static setDefaultConfig(config: Linter.Config): void;
1359
+ static getDefaultConfig(): Linter.Config;
1360
+ static resetDefaultConfig(): void;
1345
1361
 
1346
1362
  constructor(config?: Linter.Config);
1347
1363
 
@@ -1351,6 +1367,26 @@ export class RuleTester {
1351
1367
  tests: {
1352
1368
  valid: Array<string | RuleTester.ValidTestCase>;
1353
1369
  invalid: RuleTester.InvalidTestCase[];
1370
+ /**
1371
+ * Additional assertions for the "error" matchers of invalid test cases to enforce consistency.
1372
+ */
1373
+ assertionOptions?: {
1374
+ /**
1375
+ * If true, each `errors` block must check the expected error
1376
+ * message, either via a string in the `errors` array, or via
1377
+ * `message`/`messageId` in an errors object.
1378
+ * `"message"`/`"messageId"` can be used to further limit the
1379
+ * message assertions to the respective versions.
1380
+ */
1381
+ requireMessage?: boolean | "message" | "messageId";
1382
+ /**
1383
+ * If true, each `errors` block must be an array of objects,
1384
+ * that each check all location properties `line`, `column`,
1385
+ * `endLine`, `endColumn`, the later may be omitted, if the
1386
+ * error does not contain them.
1387
+ */
1388
+ requireLocation?: boolean;
1389
+ };
1354
1390
  },
1355
1391
  ): void;
1356
1392
 
@@ -1360,14 +1396,22 @@ export class RuleTester {
1360
1396
  }
1361
1397
 
1362
1398
  export namespace RuleTester {
1363
- interface ValidTestCase {
1399
+ interface ValidTestCase
1400
+ extends Omit<
1401
+ Linter.Config,
1402
+ | "name"
1403
+ | "basePath"
1404
+ | "files"
1405
+ | "ignores"
1406
+ | "linterOptions"
1407
+ | "plugins"
1408
+ | "rules"
1409
+ > {
1364
1410
  name?: string;
1365
1411
  code: string;
1366
- options?: any;
1412
+ options?: any[];
1367
1413
  filename?: string | undefined;
1368
1414
  only?: boolean;
1369
- languageOptions?: Linter.LanguageOptions | undefined;
1370
- settings?: { [name: string]: any } | undefined;
1371
1415
  before?: () => void;
1372
1416
  after?: () => void;
1373
1417
  }
@@ -1380,7 +1424,7 @@ export namespace RuleTester {
1380
1424
  }
1381
1425
 
1382
1426
  interface InvalidTestCase extends ValidTestCase {
1383
- errors: number | Array<TestCaseError | string>;
1427
+ errors: number | Array<TestCaseError | string | RegExp>;
1384
1428
  output?: string | null | undefined;
1385
1429
  }
1386
1430
 
@@ -1392,7 +1436,7 @@ export namespace RuleTester {
1392
1436
  column?: number | undefined;
1393
1437
  endLine?: number | undefined;
1394
1438
  endColumn?: number | undefined;
1395
- suggestions?: SuggestionOutput[] | undefined;
1439
+ suggestions?: SuggestionOutput[] | number | undefined;
1396
1440
  }
1397
1441
  }
1398
1442
 
@@ -31,4 +31,4 @@ import { Rule } from "./index.js";
31
31
  export const builtinRules: Map<string, Rule.RuleModule>;
32
32
 
33
33
  /** @deprecated */
34
- export function shouldUseFlatConfig(): Promise<boolean>;
34
+ export function shouldUseFlatConfig(): Promise<true>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint",
3
- "version": "10.0.0-alpha.1",
3
+ "version": "10.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
  "type": "commonjs",
@@ -107,7 +107,7 @@
107
107
  "bugs": "https://github.com/eslint/eslint/issues/",
108
108
  "dependencies": {
109
109
  "@eslint-community/eslint-utils": "^4.8.0",
110
- "@eslint-community/regexpp": "^4.12.1",
110
+ "@eslint-community/regexpp": "^4.12.2",
111
111
  "@eslint/config-array": "^0.23.0",
112
112
  "@eslint/config-helpers": "^0.5.0",
113
113
  "@eslint/core": "^1.0.0",
@@ -117,7 +117,6 @@
117
117
  "@humanwhocodes/retry": "^0.4.2",
118
118
  "@types/estree": "^1.0.6",
119
119
  "ajv": "^6.12.4",
120
- "chalk": "^4.0.0",
121
120
  "cross-spawn": "^7.0.6",
122
121
  "debug": "^4.3.2",
123
122
  "escape-string-regexp": "^4.0.0",
@@ -144,7 +143,7 @@
144
143
  "@babel/preset-env": "^7.4.3",
145
144
  "@cypress/webpack-preprocessor": "^6.0.2",
146
145
  "@eslint/json": "^0.14.0",
147
- "@eslint/eslintrc": "^3.3.1",
146
+ "@eslint/eslintrc": "^3.3.3",
148
147
  "@trunkio/launcher": "^1.3.4",
149
148
  "@types/esquery": "^1.5.4",
150
149
  "@types/node": "^22.13.14",