eslint 9.0.0-beta.2 → 9.0.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.
Files changed (38) hide show
  1. package/README.md +8 -12
  2. package/bin/eslint.js +14 -1
  3. package/lib/cli.js +30 -11
  4. package/lib/config/flat-config-schema.js +1 -2
  5. package/lib/eslint/eslint-helpers.js +5 -1
  6. package/lib/eslint/eslint.js +16 -1
  7. package/lib/linter/code-path-analysis/code-path-analyzer.js +0 -1
  8. package/lib/linter/index.js +1 -3
  9. package/lib/linter/linter.js +184 -40
  10. package/lib/linter/timing.js +16 -8
  11. package/lib/options.js +25 -1
  12. package/lib/rule-tester/index.js +3 -1
  13. package/lib/rule-tester/rule-tester.js +18 -2
  14. package/lib/rules/camelcase.js +3 -5
  15. package/lib/rules/constructor-super.js +98 -99
  16. package/lib/rules/no-fallthrough.js +41 -16
  17. package/lib/rules/no-lone-blocks.js +1 -1
  18. package/lib/rules/no-this-before-super.js +28 -9
  19. package/lib/rules/no-unused-vars.js +179 -29
  20. package/lib/rules/no-useless-return.js +7 -2
  21. package/lib/rules/use-isnan.js +2 -2
  22. package/lib/rules/utils/lazy-loading-rule-map.js +1 -1
  23. package/lib/rules/utils/unicode/index.js +9 -4
  24. package/lib/shared/runtime-info.js +1 -0
  25. package/lib/shared/stats.js +30 -0
  26. package/lib/shared/types.js +34 -0
  27. package/lib/source-code/index.js +3 -1
  28. package/lib/source-code/source-code.js +165 -1
  29. package/lib/source-code/token-store/backward-token-cursor.js +3 -3
  30. package/lib/source-code/token-store/cursors.js +4 -2
  31. package/lib/source-code/token-store/forward-token-comment-cursor.js +3 -3
  32. package/lib/source-code/token-store/forward-token-cursor.js +3 -3
  33. package/messages/plugin-conflict.js +1 -1
  34. package/messages/plugin-invalid.js +1 -1
  35. package/messages/plugin-missing.js +1 -1
  36. package/package.json +12 -8
  37. package/lib/cli-engine/xml-escape.js +0 -34
  38. package/lib/shared/deprecation-warnings.js +0 -58
@@ -30,7 +30,6 @@ const
30
30
  } = require("@eslint/eslintrc/universal"),
31
31
  Traverser = require("../shared/traverser"),
32
32
  { SourceCode } = require("../source-code"),
33
- CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"),
34
33
  applyDisableDirectives = require("./apply-disable-directives"),
35
34
  ConfigCommentParser = require("./config-comment-parser"),
36
35
  NodeEventGenerator = require("./node-event-generator"),
@@ -42,6 +41,7 @@ const
42
41
  ruleReplacements = require("../../conf/replacements.json");
43
42
  const { getRuleFromConfig } = require("../config/flat-config-helpers");
44
43
  const { FlatConfigArray } = require("../config/flat-config-array");
44
+ const { startTime, endTime } = require("../shared/stats");
45
45
  const { RuleValidator } = require("../config/rule-validator");
46
46
  const { assertIsRuleSeverity } = require("../config/flat-config-schema");
47
47
  const { normalizeSeverityToString } = require("../shared/severity");
@@ -53,13 +53,13 @@ const commentParser = new ConfigCommentParser();
53
53
  const DEFAULT_ERROR_LOC = { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } };
54
54
  const parserSymbol = Symbol.for("eslint.RuleTester.parser");
55
55
  const { LATEST_ECMA_VERSION } = require("../../conf/ecma-version");
56
+ const STEP_KIND_VISIT = 1;
57
+ const STEP_KIND_CALL = 2;
56
58
 
57
59
  //------------------------------------------------------------------------------
58
60
  // Typedefs
59
61
  //------------------------------------------------------------------------------
60
62
 
61
- /** @typedef {InstanceType<import("../cli-engine/config-array").ConfigArray>} ConfigArray */
62
- /** @typedef {InstanceType<import("../cli-engine/config-array").ExtractedConfig>} ExtractedConfig */
63
63
  /** @typedef {import("../shared/types").ConfigData} ConfigData */
64
64
  /** @typedef {import("../shared/types").Environment} Environment */
65
65
  /** @typedef {import("../shared/types").GlobalConf} GlobalConf */
@@ -69,6 +69,7 @@ const { LATEST_ECMA_VERSION } = require("../../conf/ecma-version");
69
69
  /** @typedef {import("../shared/types").LanguageOptions} LanguageOptions */
70
70
  /** @typedef {import("../shared/types").Processor} Processor */
71
71
  /** @typedef {import("../shared/types").Rule} Rule */
72
+ /** @typedef {import("../shared/types").Times} Times */
72
73
 
73
74
  /* eslint-disable jsdoc/valid-types -- https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser/issues/4#issuecomment-778805577 */
74
75
  /**
@@ -93,6 +94,7 @@ const { LATEST_ECMA_VERSION } = require("../../conf/ecma-version");
93
94
  * @property {SourceCode|null} lastSourceCode The `SourceCode` instance that the last `verify()` call used.
94
95
  * @property {SuppressedLintMessage[]} lastSuppressedMessages The `SuppressedLintMessage[]` instance that the last `verify()` call produced.
95
96
  * @property {Map<string, Parser>} parserMap The loaded parsers.
97
+ * @property {Times} times The times spent on applying a rule to a file (see `stats` option).
96
98
  * @property {Rules} ruleMap The loaded rules.
97
99
  */
98
100
 
@@ -737,6 +739,7 @@ function normalizeVerifyOptions(providedOptions, config) {
737
739
  : null,
738
740
  reportUnusedDisableDirectives,
739
741
  disableFixes: Boolean(providedOptions.disableFixes),
742
+ stats: providedOptions.stats,
740
743
  ruleFilter
741
744
  };
742
745
  }
@@ -826,6 +829,36 @@ function stripUnicodeBOM(text) {
826
829
  return text;
827
830
  }
828
831
 
832
+ /**
833
+ * Store time measurements in map
834
+ * @param {number} time Time measurement
835
+ * @param {Object} timeOpts Options relating which time was measured
836
+ * @param {WeakMap<Linter, LinterInternalSlots>} slots Linter internal slots map
837
+ * @returns {void}
838
+ */
839
+ function storeTime(time, timeOpts, slots) {
840
+ const { type, key } = timeOpts;
841
+
842
+ if (!slots.times) {
843
+ slots.times = { passes: [{}] };
844
+ }
845
+
846
+ const passIndex = slots.fixPasses;
847
+
848
+ if (passIndex > slots.times.passes.length - 1) {
849
+ slots.times.passes.push({});
850
+ }
851
+
852
+ if (key) {
853
+ slots.times.passes[passIndex][type] ??= {};
854
+ slots.times.passes[passIndex][type][key] ??= { total: 0 };
855
+ slots.times.passes[passIndex][type][key].total += time;
856
+ } else {
857
+ slots.times.passes[passIndex][type] ??= { total: 0 };
858
+ slots.times.passes[passIndex][type].total += time;
859
+ }
860
+ }
861
+
829
862
  /**
830
863
  * Get the options for a rule (not including severity), if any
831
864
  * @param {Array|number} ruleConfig rule configuration
@@ -987,23 +1020,17 @@ function createRuleListeners(rule, ruleContext) {
987
1020
  * @param {string | undefined} cwd cwd of the cli
988
1021
  * @param {string} physicalFilename The full path of the file on disk without any code block information
989
1022
  * @param {Function} ruleFilter A predicate function to filter which rules should be executed.
1023
+ * @param {boolean} stats If true, stats are collected appended to the result
1024
+ * @param {WeakMap<Linter, LinterInternalSlots>} slots InternalSlotsMap of linter
990
1025
  * @returns {LintMessage[]} An array of reported problems
1026
+ * @throws {Error} If traversal into a node fails.
991
1027
  */
992
- function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageOptions, settings, filename, disableFixes, cwd, physicalFilename, ruleFilter) {
1028
+ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageOptions, settings, filename, disableFixes, cwd, physicalFilename, ruleFilter,
1029
+ stats, slots) {
993
1030
  const emitter = createEmitter();
994
- const nodeQueue = [];
995
- let currentNode = sourceCode.ast;
996
-
997
- Traverser.traverse(sourceCode.ast, {
998
- enter(node, parent) {
999
- node.parent = parent;
1000
- nodeQueue.push({ isEntering: true, node });
1001
- },
1002
- leave(node) {
1003
- nodeQueue.push({ isEntering: false, node });
1004
- },
1005
- visitorKeys: sourceCode.visitorKeys
1006
- });
1031
+
1032
+ // must happen first to assign all node.parent properties
1033
+ const eventQueue = sourceCode.traverse();
1007
1034
 
1008
1035
  /*
1009
1036
  * Create a frozen object with the ruleContext properties and methods that are shared by all rules.
@@ -1098,7 +1125,14 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO
1098
1125
  )
1099
1126
  );
1100
1127
 
1101
- const ruleListeners = timing.enabled ? timing.time(ruleId, createRuleListeners)(rule, ruleContext) : createRuleListeners(rule, ruleContext);
1128
+ const ruleListenersReturn = (timing.enabled || stats)
1129
+ ? timing.time(ruleId, createRuleListeners, stats)(rule, ruleContext) : createRuleListeners(rule, ruleContext);
1130
+
1131
+ const ruleListeners = stats ? ruleListenersReturn.result : ruleListenersReturn;
1132
+
1133
+ if (stats) {
1134
+ storeTime(ruleListenersReturn.tdiff, { type: "rules", key: ruleId }, slots);
1135
+ }
1102
1136
 
1103
1137
  /**
1104
1138
  * Include `ruleId` in error logs
@@ -1108,7 +1142,15 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO
1108
1142
  function addRuleErrorHandler(ruleListener) {
1109
1143
  return function ruleErrorHandler(...listenerArgs) {
1110
1144
  try {
1111
- return ruleListener(...listenerArgs);
1145
+ const ruleListenerReturn = ruleListener(...listenerArgs);
1146
+
1147
+ const ruleListenerResult = stats ? ruleListenerReturn.result : ruleListenerReturn;
1148
+
1149
+ if (stats) {
1150
+ storeTime(ruleListenerReturn.tdiff, { type: "rules", key: ruleId }, slots);
1151
+ }
1152
+
1153
+ return ruleListenerResult;
1112
1154
  } catch (e) {
1113
1155
  e.ruleId = ruleId;
1114
1156
  throw e;
@@ -1122,9 +1164,8 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO
1122
1164
 
1123
1165
  // add all the selectors from the rule as listeners
1124
1166
  Object.keys(ruleListeners).forEach(selector => {
1125
- const ruleListener = timing.enabled
1126
- ? timing.time(ruleId, ruleListeners[selector])
1127
- : ruleListeners[selector];
1167
+ const ruleListener = (timing.enabled || stats)
1168
+ ? timing.time(ruleId, ruleListeners[selector], stats) : ruleListeners[selector];
1128
1169
 
1129
1170
  emitter.on(
1130
1171
  selector,
@@ -1133,25 +1174,34 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO
1133
1174
  });
1134
1175
  });
1135
1176
 
1136
- // only run code path analyzer if the top level node is "Program", skip otherwise
1137
- const eventGenerator = nodeQueue[0].node.type === "Program"
1138
- ? new CodePathAnalyzer(new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys }))
1139
- : new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys });
1177
+ const eventGenerator = new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys });
1140
1178
 
1141
- nodeQueue.forEach(traversalInfo => {
1142
- currentNode = traversalInfo.node;
1179
+ for (const step of eventQueue) {
1180
+ switch (step.kind) {
1181
+ case STEP_KIND_VISIT: {
1182
+ try {
1183
+ if (step.phase === 1) {
1184
+ eventGenerator.enterNode(step.target);
1185
+ } else {
1186
+ eventGenerator.leaveNode(step.target);
1187
+ }
1188
+ } catch (err) {
1189
+ err.currentNode = step.target;
1190
+ throw err;
1191
+ }
1192
+ break;
1193
+ }
1143
1194
 
1144
- try {
1145
- if (traversalInfo.isEntering) {
1146
- eventGenerator.enterNode(currentNode);
1147
- } else {
1148
- eventGenerator.leaveNode(currentNode);
1195
+ case STEP_KIND_CALL: {
1196
+ emitter.emit(step.target, ...step.args);
1197
+ break;
1149
1198
  }
1150
- } catch (err) {
1151
- err.currentNode = currentNode;
1152
- throw err;
1199
+
1200
+ default:
1201
+ throw new Error(`Invalid traversal step found: "${step.type}".`);
1153
1202
  }
1154
- });
1203
+
1204
+ }
1155
1205
 
1156
1206
  return lintingProblems;
1157
1207
  }
@@ -1237,7 +1287,6 @@ function assertEslintrcConfig(linter) {
1237
1287
  }
1238
1288
  }
1239
1289
 
1240
-
1241
1290
  //------------------------------------------------------------------------------
1242
1291
  // Public Interface
1243
1292
  //------------------------------------------------------------------------------
@@ -1343,12 +1392,25 @@ class Linter {
1343
1392
  });
1344
1393
 
1345
1394
  if (!slots.lastSourceCode) {
1395
+ let t;
1396
+
1397
+ if (options.stats) {
1398
+ t = startTime();
1399
+ }
1400
+
1346
1401
  const parseResult = parse(
1347
1402
  text,
1348
1403
  languageOptions,
1349
1404
  options.filename
1350
1405
  );
1351
1406
 
1407
+ if (options.stats) {
1408
+ const time = endTime(t);
1409
+ const timeOpts = { type: "parse" };
1410
+
1411
+ storeTime(time, timeOpts, slots);
1412
+ }
1413
+
1352
1414
  if (!parseResult.success) {
1353
1415
  return [parseResult.error];
1354
1416
  }
@@ -1399,7 +1461,9 @@ class Linter {
1399
1461
  options.disableFixes,
1400
1462
  slots.cwd,
1401
1463
  providedOptions.physicalFilename,
1402
- null
1464
+ null,
1465
+ options.stats,
1466
+ slots
1403
1467
  );
1404
1468
  } catch (err) {
1405
1469
  err.message += `\nOccurred while linting ${options.filename}`;
@@ -1627,12 +1691,24 @@ class Linter {
1627
1691
  const settings = config.settings || {};
1628
1692
 
1629
1693
  if (!slots.lastSourceCode) {
1694
+ let t;
1695
+
1696
+ if (options.stats) {
1697
+ t = startTime();
1698
+ }
1699
+
1630
1700
  const parseResult = parse(
1631
1701
  text,
1632
1702
  languageOptions,
1633
1703
  options.filename
1634
1704
  );
1635
1705
 
1706
+ if (options.stats) {
1707
+ const time = endTime(t);
1708
+
1709
+ storeTime(time, { type: "parse" }, slots);
1710
+ }
1711
+
1636
1712
  if (!parseResult.success) {
1637
1713
  return [parseResult.error];
1638
1714
  }
@@ -1842,7 +1918,9 @@ class Linter {
1842
1918
  options.disableFixes,
1843
1919
  slots.cwd,
1844
1920
  providedOptions.physicalFilename,
1845
- options.ruleFilter
1921
+ options.ruleFilter,
1922
+ options.stats,
1923
+ slots
1846
1924
  );
1847
1925
  } catch (err) {
1848
1926
  err.message += `\nOccurred while linting ${options.filename}`;
@@ -2082,6 +2160,22 @@ class Linter {
2082
2160
  return internalSlotsMap.get(this).lastSourceCode;
2083
2161
  }
2084
2162
 
2163
+ /**
2164
+ * Gets the times spent on (parsing, fixing, linting) a file.
2165
+ * @returns {LintTimes} The times.
2166
+ */
2167
+ getTimes() {
2168
+ return internalSlotsMap.get(this).times ?? { passes: [] };
2169
+ }
2170
+
2171
+ /**
2172
+ * Gets the number of autofix passes that were made in the last run.
2173
+ * @returns {number} The number of autofix passes.
2174
+ */
2175
+ getFixPassCount() {
2176
+ return internalSlotsMap.get(this).fixPasses ?? 0;
2177
+ }
2178
+
2085
2179
  /**
2086
2180
  * Gets the list of SuppressedLintMessage produced in the last running.
2087
2181
  * @returns {SuppressedLintMessage[]} The list of SuppressedLintMessage
@@ -2158,6 +2252,7 @@ class Linter {
2158
2252
  currentText = text;
2159
2253
  const debugTextDescription = options && options.filename || `${text.slice(0, 10)}...`;
2160
2254
  const shouldFix = options && typeof options.fix !== "undefined" ? options.fix : true;
2255
+ const stats = options?.stats;
2161
2256
 
2162
2257
  /**
2163
2258
  * This loop continues until one of the following is true:
@@ -2168,15 +2263,46 @@ class Linter {
2168
2263
  * That means anytime a fix is successfully applied, there will be another pass.
2169
2264
  * Essentially, guaranteeing a minimum of two passes.
2170
2265
  */
2266
+ const slots = internalSlotsMap.get(this);
2267
+
2268
+ // Remove lint times from the last run.
2269
+ if (stats) {
2270
+ delete slots.times;
2271
+ slots.fixPasses = 0;
2272
+ }
2273
+
2171
2274
  do {
2172
2275
  passNumber++;
2276
+ let tTotal;
2277
+
2278
+ if (stats) {
2279
+ tTotal = startTime();
2280
+ }
2173
2281
 
2174
2282
  debug(`Linting code for ${debugTextDescription} (pass ${passNumber})`);
2175
2283
  messages = this.verify(currentText, config, options);
2176
2284
 
2177
2285
  debug(`Generating fixed text for ${debugTextDescription} (pass ${passNumber})`);
2286
+ let t;
2287
+
2288
+ if (stats) {
2289
+ t = startTime();
2290
+ }
2291
+
2178
2292
  fixedResult = SourceCodeFixer.applyFixes(currentText, messages, shouldFix);
2179
2293
 
2294
+ if (stats) {
2295
+
2296
+ if (fixedResult.fixed) {
2297
+ const time = endTime(t);
2298
+
2299
+ storeTime(time, { type: "fix" }, slots);
2300
+ slots.fixPasses++;
2301
+ } else {
2302
+ storeTime(0, { type: "fix" }, slots);
2303
+ }
2304
+ }
2305
+
2180
2306
  /*
2181
2307
  * stop if there are any syntax errors.
2182
2308
  * 'fixedResult.output' is a empty string.
@@ -2191,6 +2317,13 @@ class Linter {
2191
2317
  // update to use the fixed output instead of the original text
2192
2318
  currentText = fixedResult.output;
2193
2319
 
2320
+ if (stats) {
2321
+ tTotal = endTime(tTotal);
2322
+ const passIndex = slots.times.passes.length - 1;
2323
+
2324
+ slots.times.passes[passIndex].total = tTotal;
2325
+ }
2326
+
2194
2327
  } while (
2195
2328
  fixedResult.fixed &&
2196
2329
  passNumber < MAX_AUTOFIX_PASSES
@@ -2201,7 +2334,18 @@ class Linter {
2201
2334
  * the most up-to-date information.
2202
2335
  */
2203
2336
  if (fixedResult.fixed) {
2337
+ let tTotal;
2338
+
2339
+ if (stats) {
2340
+ tTotal = startTime();
2341
+ }
2342
+
2204
2343
  fixedResult.messages = this.verify(currentText, config, options);
2344
+
2345
+ if (stats) {
2346
+ storeTime(0, { type: "fix" }, slots);
2347
+ slots.times.passes.at(-1).total = endTime(tTotal);
2348
+ }
2205
2349
  }
2206
2350
 
2207
2351
  // ensure the last result properly reflects if fixes were done
@@ -5,6 +5,8 @@
5
5
 
6
6
  "use strict";
7
7
 
8
+ const { startTime, endTime } = require("../shared/stats");
9
+
8
10
  //------------------------------------------------------------------------------
9
11
  // Helpers
10
12
  //------------------------------------------------------------------------------
@@ -128,21 +130,27 @@ module.exports = (function() {
128
130
  * Time the run
129
131
  * @param {any} key key from the data object
130
132
  * @param {Function} fn function to be called
133
+ * @param {boolean} stats if 'stats' is true, return the result and the time difference
131
134
  * @returns {Function} function to be executed
132
135
  * @private
133
136
  */
134
- function time(key, fn) {
135
- if (typeof data[key] === "undefined") {
136
- data[key] = 0;
137
- }
137
+ function time(key, fn, stats) {
138
138
 
139
139
  return function(...args) {
140
- let t = process.hrtime();
140
+
141
+ const t = startTime();
141
142
  const result = fn(...args);
143
+ const tdiff = endTime(t);
144
+
145
+ if (enabled) {
146
+ if (typeof data[key] === "undefined") {
147
+ data[key] = 0;
148
+ }
149
+
150
+ data[key] += tdiff;
151
+ }
142
152
 
143
- t = process.hrtime(t);
144
- data[key] += t[0] * 1e3 + t[1] / 1e6;
145
- return result;
153
+ return stats ? { result, tdiff } : result;
146
154
  };
147
155
  }
148
156
 
package/lib/options.js CHANGED
@@ -60,6 +60,7 @@ const optionator = require("optionator");
60
60
  * @property {boolean} [passOnNoPatterns=false] When set to true, missing patterns cause
61
61
  * the linting operation to short circuit and not report any failures.
62
62
  * @property {string[]} _ Positional filenames or patterns
63
+ * @property {boolean} [stats] Report additional statistics
63
64
  */
64
65
 
65
66
  //------------------------------------------------------------------------------
@@ -103,6 +104,16 @@ module.exports = function(usingFlatConfig) {
103
104
  };
104
105
  }
105
106
 
107
+ let inspectConfigFlag;
108
+
109
+ if (usingFlatConfig) {
110
+ inspectConfigFlag = {
111
+ option: "inspect-config",
112
+ type: "Boolean",
113
+ description: "Open the config inspector with the current configuration"
114
+ };
115
+ }
116
+
106
117
  let extFlag;
107
118
 
108
119
  if (!usingFlatConfig) {
@@ -143,6 +154,17 @@ module.exports = function(usingFlatConfig) {
143
154
  };
144
155
  }
145
156
 
157
+ let statsFlag;
158
+
159
+ if (usingFlatConfig) {
160
+ statsFlag = {
161
+ option: "stats",
162
+ type: "Boolean",
163
+ default: "false",
164
+ description: "Add statistics to the lint report"
165
+ };
166
+ }
167
+
146
168
  let warnIgnoredFlag;
147
169
 
148
170
  if (usingFlatConfig) {
@@ -173,6 +195,7 @@ module.exports = function(usingFlatConfig) {
173
195
  ? "Use this configuration instead of eslint.config.js, eslint.config.mjs, or eslint.config.cjs"
174
196
  : "Use this configuration, overriding .eslintrc.* config options if present"
175
197
  },
198
+ inspectConfigFlag,
176
199
  envFlag,
177
200
  extFlag,
178
201
  {
@@ -400,7 +423,8 @@ module.exports = function(usingFlatConfig) {
400
423
  option: "print-config",
401
424
  type: "path::String",
402
425
  description: "Print the configuration for the given file"
403
- }
426
+ },
427
+ statsFlag
404
428
  ].filter(value => !!value)
405
429
  });
406
430
  };
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
 
3
+ const RuleTester = require("./rule-tester");
4
+
3
5
  module.exports = {
4
- RuleTester: require("./rule-tester")
6
+ RuleTester
5
7
  };
@@ -136,6 +136,15 @@ const suggestionObjectParameters = new Set([
136
136
  ]);
137
137
  const friendlySuggestionObjectParameterList = `[${[...suggestionObjectParameters].map(key => `'${key}'`).join(", ")}]`;
138
138
 
139
+ /*
140
+ * Ignored test case properties when checking for test case duplicates.
141
+ */
142
+ const duplicationIgnoredParameters = new Set([
143
+ "name",
144
+ "errors",
145
+ "output"
146
+ ]);
147
+
139
148
  const forbiddenMethods = [
140
149
  "applyInlineConfig",
141
150
  "applyLanguageOptions",
@@ -848,7 +857,7 @@ class RuleTester {
848
857
 
849
858
  /**
850
859
  * Check if this test case is a duplicate of one we have seen before.
851
- * @param {Object} item test case object
860
+ * @param {string|Object} item test case object
852
861
  * @param {Set<string>} seenTestCases set of serialized test cases we have seen so far (managed by this function)
853
862
  * @returns {void}
854
863
  * @private
@@ -863,7 +872,14 @@ class RuleTester {
863
872
  return;
864
873
  }
865
874
 
866
- const serializedTestCase = stringify(item);
875
+ const normalizedItem = typeof item === "string" ? { code: item } : item;
876
+ const serializedTestCase = stringify(normalizedItem, {
877
+ replacer(key, value) {
878
+
879
+ // "this" is the currently stringified object --> only ignore top-level properties
880
+ return (normalizedItem !== this || !duplicationIgnoredParameters.has(key)) ? value : void 0;
881
+ }
882
+ });
867
883
 
868
884
  assert(
869
885
  !seenTestCases.has(serializedTestCase),
@@ -47,11 +47,9 @@ module.exports = {
47
47
  },
48
48
  allow: {
49
49
  type: "array",
50
- items: [
51
- {
52
- type: "string"
53
- }
54
- ],
50
+ items: {
51
+ type: "string"
52
+ },
55
53
  minItems: 0,
56
54
  uniqueItems: true
57
55
  }