eslint 8.56.0 → 9.0.0-alpha.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 (50) hide show
  1. package/README.md +2 -2
  2. package/conf/rule-type-list.json +3 -1
  3. package/lib/api.js +1 -1
  4. package/lib/cli-engine/cli-engine.js +13 -2
  5. package/lib/cli-engine/formatters/formatters-meta.json +1 -29
  6. package/lib/cli.js +32 -9
  7. package/lib/config/default-config.js +3 -0
  8. package/lib/config/flat-config-array.js +0 -20
  9. package/lib/config/flat-config-helpers.js +41 -20
  10. package/lib/config/flat-config-schema.js +35 -25
  11. package/lib/config/rule-validator.js +27 -4
  12. package/lib/eslint/eslint-helpers.js +32 -12
  13. package/lib/eslint/eslint.js +856 -373
  14. package/lib/eslint/index.js +2 -2
  15. package/lib/eslint/legacy-eslint.js +722 -0
  16. package/lib/linter/apply-disable-directives.js +33 -5
  17. package/lib/linter/config-comment-parser.js +1 -1
  18. package/lib/linter/linter.js +91 -96
  19. package/lib/linter/rules.js +6 -15
  20. package/lib/options.js +9 -1
  21. package/lib/rule-tester/rule-tester.js +240 -272
  22. package/lib/rules/index.js +0 -2
  23. package/lib/rules/no-constant-binary-expression.js +1 -1
  24. package/lib/rules/no-constructor-return.js +1 -1
  25. package/lib/rules/no-empty-static-block.js +1 -1
  26. package/lib/rules/no-extra-semi.js +1 -1
  27. package/lib/rules/no-implicit-coercion.js +17 -1
  28. package/lib/rules/no-inner-declarations.js +1 -1
  29. package/lib/rules/no-invalid-regexp.js +1 -1
  30. package/lib/rules/no-mixed-spaces-and-tabs.js +1 -1
  31. package/lib/rules/no-new-native-nonconstructor.js +1 -1
  32. package/lib/rules/no-new-symbol.js +8 -1
  33. package/lib/rules/no-sequences.js +1 -0
  34. package/lib/rules/no-unused-private-class-members.js +1 -1
  35. package/lib/shared/config-validator.js +44 -11
  36. package/lib/shared/types.js +1 -1
  37. package/lib/source-code/source-code.js +1 -79
  38. package/lib/unsupported-api.js +3 -5
  39. package/package.json +9 -11
  40. package/lib/cli-engine/formatters/checkstyle.js +0 -60
  41. package/lib/cli-engine/formatters/compact.js +0 -60
  42. package/lib/cli-engine/formatters/jslint-xml.js +0 -41
  43. package/lib/cli-engine/formatters/junit.js +0 -82
  44. package/lib/cli-engine/formatters/tap.js +0 -95
  45. package/lib/cli-engine/formatters/unix.js +0 -58
  46. package/lib/cli-engine/formatters/visualstudio.js +0 -63
  47. package/lib/eslint/flat-eslint.js +0 -1142
  48. package/lib/rule-tester/flat-rule-tester.js +0 -1122
  49. package/lib/rules/require-jsdoc.js +0 -122
  50. package/lib/rules/valid-jsdoc.js +0 -516
@@ -1,68 +1,39 @@
1
1
  /**
2
- * @fileoverview Mocha test wrapper
2
+ * @fileoverview Mocha/Jest test wrapper
3
3
  * @author Ilya Volodin
4
4
  */
5
5
  "use strict";
6
6
 
7
7
  /* globals describe, it -- Mocha globals */
8
8
 
9
- /*
10
- * This is a wrapper around mocha to allow for DRY unittests for eslint
11
- * Format:
12
- * RuleTester.run("{ruleName}", {
13
- * valid: [
14
- * "{code}",
15
- * { code: "{code}", options: {options}, globals: {globals}, parser: "{parser}", settings: {settings} }
16
- * ],
17
- * invalid: [
18
- * { code: "{code}", errors: {numErrors} },
19
- * { code: "{code}", errors: ["{errorMessage}"] },
20
- * { code: "{code}", options: {options}, globals: {globals}, parser: "{parser}", settings: {settings}, errors: [{ message: "{errorMessage}", type: "{errorNodeType}"}] }
21
- * ]
22
- * });
23
- *
24
- * Variables:
25
- * {code} - String that represents the code to be tested
26
- * {options} - Arguments that are passed to the configurable rules.
27
- * {globals} - An object representing a list of variables that are
28
- * registered as globals
29
- * {parser} - String representing the parser to use
30
- * {settings} - An object representing global settings for all rules
31
- * {numErrors} - If failing case doesn't need to check error message,
32
- * this integer will specify how many errors should be
33
- * received
34
- * {errorMessage} - Message that is returned by the rule on failure
35
- * {errorNodeType} - AST node type that is returned by they rule as
36
- * a cause of the failure.
37
- */
38
-
39
9
  //------------------------------------------------------------------------------
40
10
  // Requirements
41
11
  //------------------------------------------------------------------------------
42
12
 
43
13
  const
44
14
  assert = require("assert"),
45
- path = require("path"),
46
15
  util = require("util"),
47
- merge = require("lodash.merge"),
48
16
  equal = require("fast-deep-equal"),
49
- Traverser = require("../../lib/shared/traverser"),
50
- { getRuleOptionsSchema, validate } = require("../shared/config-validator"),
17
+ Traverser = require("../shared/traverser"),
18
+ { getRuleOptionsSchema } = require("../config/flat-config-helpers"),
51
19
  { Linter, SourceCodeFixer, interpolate } = require("../linter"),
52
20
  CodePath = require("../linter/code-path-analysis/code-path");
53
21
 
22
+ const { FlatConfigArray } = require("../config/flat-config-array");
23
+ const { defaultConfig } = require("../config/default-config");
24
+
54
25
  const ajv = require("../shared/ajv")({ strictDefaults: true });
55
26
 
56
- const espreePath = require.resolve("espree");
57
27
  const parserSymbol = Symbol.for("eslint.RuleTester.parser");
58
-
59
28
  const { SourceCode } = require("../source-code");
29
+ const { ConfigArraySymbol } = require("@humanwhocodes/config-array");
60
30
 
61
31
  //------------------------------------------------------------------------------
62
32
  // Typedefs
63
33
  //------------------------------------------------------------------------------
64
34
 
65
35
  /** @typedef {import("../shared/types").Parser} Parser */
36
+ /** @typedef {import("../shared/types").LanguageOptions} LanguageOptions */
66
37
  /** @typedef {import("../shared/types").Rule} Rule */
67
38
 
68
39
 
@@ -72,12 +43,9 @@ const { SourceCode } = require("../source-code");
72
43
  * @property {string} [name] Name for the test case.
73
44
  * @property {string} code Code for the test case.
74
45
  * @property {any[]} [options] Options for the test case.
46
+ * @property {LanguageOptions} [languageOptions] The language options to use in the test case.
75
47
  * @property {{ [name: string]: any }} [settings] Settings for the test case.
76
48
  * @property {string} [filename] The fake filename for the test case. Useful for rules that make assertion about filenames.
77
- * @property {string} [parser] The absolute path for the parser.
78
- * @property {{ [name: string]: any }} [parserOptions] Options for the parser.
79
- * @property {{ [name: string]: "readonly" | "writable" | "off" }} [globals] The additional global variables.
80
- * @property {{ [name: string]: boolean }} [env] Environments for the test case.
81
49
  * @property {boolean} [only] Run only this test case or the subset of test cases with this property.
82
50
  */
83
51
 
@@ -91,10 +59,7 @@ const { SourceCode } = require("../source-code");
91
59
  * @property {any[]} [options] Options for the test case.
92
60
  * @property {{ [name: string]: any }} [settings] Settings for the test case.
93
61
  * @property {string} [filename] The fake filename for the test case. Useful for rules that make assertion about filenames.
94
- * @property {string} [parser] The absolute path for the parser.
95
- * @property {{ [name: string]: any }} [parserOptions] Options for the parser.
96
- * @property {{ [name: string]: "readonly" | "writable" | "off" }} [globals] The additional global variables.
97
- * @property {{ [name: string]: boolean }} [env] Environments for the test case.
62
+ * @property {LanguageOptions} [languageOptions] The language options to use in the test case.
98
63
  * @property {boolean} [only] Run only this test case or the subset of test cases with this property.
99
64
  */
100
65
 
@@ -120,7 +85,12 @@ const { SourceCode } = require("../source-code");
120
85
  * the initial default configuration
121
86
  */
122
87
  const testerDefaultConfig = { rules: {} };
123
- let defaultConfig = { rules: {} };
88
+
89
+ /*
90
+ * RuleTester uses this config as its default. This can be overwritten via
91
+ * setDefaultConfig().
92
+ */
93
+ let sharedDefaultConfig = { rules: {} };
124
94
 
125
95
  /*
126
96
  * List every parameters possible on a test case that are not related to eslint
@@ -169,36 +139,10 @@ const forbiddenMethods = [
169
139
  "finalize"
170
140
  ];
171
141
 
172
- const hasOwnProperty = Function.call.bind(Object.hasOwnProperty);
142
+ /** @type {Map<string,WeakSet>} */
143
+ const forbiddenMethodCalls = new Map(forbiddenMethods.map(methodName => ([methodName, new WeakSet()])));
173
144
 
174
- const DEPRECATED_SOURCECODE_PASSTHROUGHS = {
175
- getSource: "getText",
176
- getSourceLines: "getLines",
177
- getAllComments: "getAllComments",
178
- getNodeByRangeIndex: "getNodeByRangeIndex",
179
-
180
- // getComments: "getComments", -- already handled by a separate error
181
- getCommentsBefore: "getCommentsBefore",
182
- getCommentsAfter: "getCommentsAfter",
183
- getCommentsInside: "getCommentsInside",
184
- getJSDocComment: "getJSDocComment",
185
- getFirstToken: "getFirstToken",
186
- getFirstTokens: "getFirstTokens",
187
- getLastToken: "getLastToken",
188
- getLastTokens: "getLastTokens",
189
- getTokenAfter: "getTokenAfter",
190
- getTokenBefore: "getTokenBefore",
191
- getTokenByRangeStart: "getTokenByRangeStart",
192
- getTokens: "getTokens",
193
- getTokensAfter: "getTokensAfter",
194
- getTokensBefore: "getTokensBefore",
195
- getTokensBetween: "getTokensBetween",
196
-
197
- getScope: "getScope",
198
- getAncestors: "getAncestors",
199
- getDeclaredVariables: "getDeclaredVariables",
200
- markVariableAsUsed: "markVariableAsUsed"
201
- };
145
+ const hasOwnProperty = Function.call.bind(Object.hasOwnProperty);
202
146
 
203
147
  /**
204
148
  * Clones a given value deeply.
@@ -330,76 +274,6 @@ function wrapParser(parser) {
330
274
  };
331
275
  }
332
276
 
333
- /**
334
- * Function to replace `SourceCode.prototype.getComments`.
335
- * @returns {void}
336
- * @throws {Error} Deprecation message.
337
- */
338
- function getCommentsDeprecation() {
339
- throw new Error(
340
- "`SourceCode#getComments()` is deprecated and will be removed in a future major version. Use `getCommentsBefore()`, `getCommentsAfter()`, and `getCommentsInside()` instead."
341
- );
342
- }
343
-
344
- /**
345
- * Function to replace forbidden `SourceCode` methods.
346
- * @param {string} methodName The name of the method to forbid.
347
- * @returns {Function} The function that throws the error.
348
- */
349
- function throwForbiddenMethodError(methodName) {
350
- return () => {
351
- throw new Error(
352
- `\`SourceCode#${methodName}()\` cannot be called inside a rule.`
353
- );
354
- };
355
- }
356
-
357
- /**
358
- * Emit a deprecation warning if function-style format is being used.
359
- * @param {string} ruleName Name of the rule.
360
- * @returns {void}
361
- */
362
- function emitLegacyRuleAPIWarning(ruleName) {
363
- if (!emitLegacyRuleAPIWarning[`warned-${ruleName}`]) {
364
- emitLegacyRuleAPIWarning[`warned-${ruleName}`] = true;
365
- process.emitWarning(
366
- `"${ruleName}" rule is using the deprecated function-style format and will stop working in ESLint v9. Please use object-style format: https://eslint.org/docs/latest/extend/custom-rules`,
367
- "DeprecationWarning"
368
- );
369
- }
370
- }
371
-
372
- /**
373
- * Emit a deprecation warning if rule has options but is missing the "meta.schema" property
374
- * @param {string} ruleName Name of the rule.
375
- * @returns {void}
376
- */
377
- function emitMissingSchemaWarning(ruleName) {
378
- if (!emitMissingSchemaWarning[`warned-${ruleName}`]) {
379
- emitMissingSchemaWarning[`warned-${ruleName}`] = true;
380
- process.emitWarning(
381
- `"${ruleName}" rule has options but is missing the "meta.schema" property and will stop working in ESLint v9. Please add a schema: https://eslint.org/docs/latest/extend/custom-rules#options-schemas`,
382
- "DeprecationWarning"
383
- );
384
- }
385
- }
386
-
387
- /**
388
- * Emit a deprecation warning if a rule uses a deprecated `context` method.
389
- * @param {string} ruleName Name of the rule.
390
- * @param {string} methodName The name of the method on `context` that was used.
391
- * @returns {void}
392
- */
393
- function emitDeprecatedContextMethodWarning(ruleName, methodName) {
394
- if (!emitDeprecatedContextMethodWarning[`warned-${ruleName}-${methodName}`]) {
395
- emitDeprecatedContextMethodWarning[`warned-${ruleName}-${methodName}`] = true;
396
- process.emitWarning(
397
- `"${ruleName}" rule is using \`context.${methodName}()\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.${DEPRECATED_SOURCECODE_PASSTHROUGHS[methodName]}()\` instead.`,
398
- "DeprecationWarning"
399
- );
400
- }
401
- }
402
-
403
277
  /**
404
278
  * Emit a deprecation warning if rule uses CodePath#currentSegments.
405
279
  * @param {string} ruleName Name of the rule.
@@ -416,20 +290,40 @@ function emitCodePathCurrentSegmentsWarning(ruleName) {
416
290
  }
417
291
 
418
292
  /**
419
- * Emit a deprecation warning if `context.parserServices` is used.
420
- * @param {string} ruleName Name of the rule.
421
- * @returns {void}
293
+ * Function to replace forbidden `SourceCode` methods. Allows just one call per method.
294
+ * @param {string} methodName The name of the method to forbid.
295
+ * @param {Function} prototype The prototype with the original method to call.
296
+ * @returns {Function} The function that throws the error.
422
297
  */
423
- function emitParserServicesWarning(ruleName) {
424
- if (!emitParserServicesWarning[`warned-${ruleName}`]) {
425
- emitParserServicesWarning[`warned-${ruleName}`] = true;
426
- process.emitWarning(
427
- `"${ruleName}" rule is using \`context.parserServices\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.parserServices\` instead.`,
428
- "DeprecationWarning"
298
+ function throwForbiddenMethodError(methodName, prototype) {
299
+
300
+ const original = prototype[methodName];
301
+
302
+ return function(...args) {
303
+
304
+ const called = forbiddenMethodCalls.get(methodName);
305
+
306
+ /* eslint-disable no-invalid-this -- needed to operate as a method. */
307
+ if (!called.has(this)) {
308
+ called.add(this);
309
+
310
+ return original.apply(this, args);
311
+ }
312
+ /* eslint-enable no-invalid-this -- not needed past this point */
313
+
314
+ throw new Error(
315
+ `\`SourceCode#${methodName}()\` cannot be called inside a rule.`
429
316
  );
430
- }
317
+ };
431
318
  }
432
319
 
320
+ const metaSchemaDescription = `
321
+ \t- If the rule has options, set \`meta.schema\` to an array or non-empty object to enable options validation.
322
+ \t- If the rule doesn't have options, omit \`meta.schema\` to enforce that no options can be passed to the rule.
323
+ \t- You can also set \`meta.schema\` to \`false\` to opt-out of options validation (not recommended).
324
+
325
+ \thttps://eslint.org/docs/latest/extend/custom-rules#options-schemas
326
+ `;
433
327
 
434
328
  //------------------------------------------------------------------------------
435
329
  // Public Interface
@@ -479,26 +373,20 @@ class RuleTester {
479
373
  * Creates a new instance of RuleTester.
480
374
  * @param {Object} [testerConfig] Optional, extra configuration for the tester
481
375
  */
482
- constructor(testerConfig) {
376
+ constructor(testerConfig = {}) {
483
377
 
484
378
  /**
485
379
  * The configuration to use for this tester. Combination of the tester
486
380
  * configuration and the default configuration.
487
381
  * @type {Object}
488
382
  */
489
- this.testerConfig = merge(
490
- {},
491
- defaultConfig,
383
+ this.testerConfig = [
384
+ sharedDefaultConfig,
492
385
  testerConfig,
493
386
  { rules: { "rule-tester/validate-ast": "error" } }
494
- );
387
+ ];
495
388
 
496
- /**
497
- * Rule definitions to define before tests.
498
- * @type {Object}
499
- */
500
- this.rules = {};
501
- this.linter = new Linter();
389
+ this.linter = new Linter({ configType: "flat" });
502
390
  }
503
391
 
504
392
  /**
@@ -511,10 +399,10 @@ class RuleTester {
511
399
  if (typeof config !== "object" || config === null) {
512
400
  throw new TypeError("RuleTester.setDefaultConfig: config must be an object");
513
401
  }
514
- defaultConfig = config;
402
+ sharedDefaultConfig = config;
515
403
 
516
404
  // Make sure the rules object exists since it is assumed to exist later
517
- defaultConfig.rules = defaultConfig.rules || {};
405
+ sharedDefaultConfig.rules = sharedDefaultConfig.rules || {};
518
406
  }
519
407
 
520
408
  /**
@@ -522,7 +410,7 @@ class RuleTester {
522
410
  * @returns {Object} the current configuration
523
411
  */
524
412
  static getDefaultConfig() {
525
- return defaultConfig;
413
+ return sharedDefaultConfig;
526
414
  }
527
415
 
528
416
  /**
@@ -531,7 +419,11 @@ class RuleTester {
531
419
  * @returns {void}
532
420
  */
533
421
  static resetDefaultConfig() {
534
- defaultConfig = merge({}, testerDefaultConfig);
422
+ sharedDefaultConfig = {
423
+ rules: {
424
+ ...testerDefaultConfig.rules
425
+ }
426
+ };
535
427
  }
536
428
 
537
429
 
@@ -602,29 +494,17 @@ class RuleTester {
602
494
  this[IT_ONLY] = value;
603
495
  }
604
496
 
605
- /**
606
- * Define a rule for one particular run of tests.
607
- * @param {string} name The name of the rule to define.
608
- * @param {Function | Rule} rule The rule definition.
609
- * @returns {void}
610
- */
611
- defineRule(name, rule) {
612
- if (typeof rule === "function") {
613
- emitLegacyRuleAPIWarning(name);
614
- }
615
- this.rules[name] = rule;
616
- }
617
497
 
618
498
  /**
619
499
  * Adds a new rule test to execute.
620
500
  * @param {string} ruleName The name of the rule to run.
621
- * @param {Function | Rule} rule The rule to test.
501
+ * @param {Rule} rule The rule to test.
622
502
  * @param {{
623
503
  * valid: (ValidTestCase | string)[],
624
504
  * invalid: InvalidTestCase[]
625
505
  * }} test The collection of tests to run.
626
- * @throws {TypeError|Error} If non-object `test`, or if a required
627
- * scenario of the given type is missing.
506
+ * @throws {TypeError|Error} If `rule` is not an object with a `create` method,
507
+ * or if non-object `test`, or if a required scenario of the given type is missing.
628
508
  * @returns {void}
629
509
  */
630
510
  run(ruleName, rule, test) {
@@ -632,7 +512,12 @@ class RuleTester {
632
512
  const testerConfig = this.testerConfig,
633
513
  requiredScenarios = ["valid", "invalid"],
634
514
  scenarioErrors = [],
635
- linter = this.linter;
515
+ linter = this.linter,
516
+ ruleId = `rule-to-test/${ruleName}`;
517
+
518
+ if (!rule || typeof rule !== "object" || typeof rule.create !== "function") {
519
+ throw new TypeError("Rule must be an object with a `create` method");
520
+ }
636
521
 
637
522
  if (!test || typeof test !== "object") {
638
523
  throw new TypeError(`Test Scenarios for rule ${ruleName} : Could not find test scenario object`);
@@ -650,54 +535,55 @@ class RuleTester {
650
535
  ].concat(scenarioErrors).join("\n"));
651
536
  }
652
537
 
653
- if (typeof rule === "function") {
654
- emitLegacyRuleAPIWarning(ruleName);
655
- }
538
+ const baseConfig = [
539
+ { files: ["**"] }, // Make sure the default config matches for all files
540
+ {
541
+ plugins: {
656
542
 
657
- linter.defineRule(ruleName, Object.assign({}, rule, {
658
-
659
- // Create a wrapper rule that freezes the `context` properties.
660
- create(context) {
661
- freezeDeeply(context.options);
662
- freezeDeeply(context.settings);
663
- freezeDeeply(context.parserOptions);
664
-
665
- // wrap all deprecated methods
666
- const newContext = Object.create(
667
- context,
668
- Object.fromEntries(Object.keys(DEPRECATED_SOURCECODE_PASSTHROUGHS).map(methodName => [
669
- methodName,
670
- {
671
- value(...args) {
672
-
673
- // emit deprecation warning
674
- emitDeprecatedContextMethodWarning(ruleName, methodName);
675
-
676
- // call the original method
677
- return context[methodName].call(this, ...args);
678
- },
679
- enumerable: true
680
- }
681
- ]))
682
- );
543
+ // copy root plugin over
544
+ "@": {
683
545
 
684
- // emit warning about context.parserServices
685
- const parserServices = context.parserServices;
546
+ /*
547
+ * Parsers are wrapped to detect more errors, so this needs
548
+ * to be a new object for each call to run(), otherwise the
549
+ * parsers will be wrapped multiple times.
550
+ */
551
+ parsers: {
552
+ ...defaultConfig[0].plugins["@"].parsers
553
+ },
686
554
 
687
- Object.defineProperty(newContext, "parserServices", {
688
- get() {
689
- emitParserServicesWarning(ruleName);
690
- return parserServices;
691
- }
692
- });
555
+ /*
556
+ * The rules key on the default plugin is a proxy to lazy-load
557
+ * just the rules that are needed. So, don't create a new object
558
+ * here, just use the default one to keep that performance
559
+ * enhancement.
560
+ */
561
+ rules: defaultConfig[0].plugins["@"].rules
562
+ },
563
+ "rule-to-test": {
564
+ rules: {
565
+ [ruleName]: Object.assign({}, rule, {
693
566
 
694
- Object.freeze(newContext);
567
+ // Create a wrapper rule that freezes the `context` properties.
568
+ create(context) {
569
+ freezeDeeply(context.options);
570
+ freezeDeeply(context.settings);
571
+ freezeDeeply(context.parserOptions);
695
572
 
696
- return (typeof rule === "function" ? rule : rule.create)(newContext);
697
- }
698
- }));
573
+ // freezeDeeply(context.languageOptions);
699
574
 
700
- linter.defineRules(this.rules);
575
+ return rule.create(context);
576
+ }
577
+ })
578
+ }
579
+ }
580
+ },
581
+ languageOptions: {
582
+ ...defaultConfig[0].languageOptions
583
+ }
584
+ },
585
+ ...defaultConfig.slice(1)
586
+ ];
701
587
 
702
588
  /**
703
589
  * Run the rule for the given item
@@ -707,8 +593,26 @@ class RuleTester {
707
593
  * @private
708
594
  */
709
595
  function runRuleForItem(item) {
710
- let config = merge({}, testerConfig),
711
- code, filename, output, beforeAST, afterAST;
596
+ const configs = new FlatConfigArray(testerConfig, { baseConfig });
597
+
598
+ /*
599
+ * Modify the returned config so that the parser is wrapped to catch
600
+ * access of the start/end properties. This method is called just
601
+ * once per code snippet being tested, so each test case gets a clean
602
+ * parser.
603
+ */
604
+ configs[ConfigArraySymbol.finalizeConfig] = function(...args) {
605
+
606
+ // can't do super here :(
607
+ const proto = Object.getPrototypeOf(this);
608
+ const calculatedConfig = proto[ConfigArraySymbol.finalizeConfig].apply(this, args);
609
+
610
+ // wrap the parser to catch start/end property access
611
+ calculatedConfig.languageOptions.parser = wrapParser(calculatedConfig.languageOptions.parser);
612
+ return calculatedConfig;
613
+ };
614
+
615
+ let code, filename, output, beforeAST, afterAST;
712
616
 
713
617
  if (typeof item === "string") {
714
618
  code = item;
@@ -725,64 +629,93 @@ class RuleTester {
725
629
  delete itemConfig[parameter];
726
630
  }
727
631
 
632
+ // wrap any parsers
633
+ if (itemConfig.languageOptions && itemConfig.languageOptions.parser) {
634
+
635
+ const parser = itemConfig.languageOptions.parser;
636
+
637
+ if (parser && typeof parser !== "object") {
638
+ throw new Error("Parser must be an object with a parse() or parseForESLint() method.");
639
+ }
640
+
641
+ }
642
+
728
643
  /*
729
644
  * Create the config object from the tester config and this item
730
645
  * specific configurations.
731
646
  */
732
- config = merge(
733
- config,
734
- itemConfig
735
- );
647
+ configs.push(itemConfig);
736
648
  }
737
649
 
738
650
  if (item.filename) {
739
651
  filename = item.filename;
740
652
  }
741
653
 
654
+ let ruleConfig = 1;
655
+
742
656
  if (hasOwnProperty(item, "options")) {
743
657
  assert(Array.isArray(item.options), "options must be an array");
744
- if (
745
- item.options.length > 0 &&
746
- typeof rule === "object" &&
747
- (
748
- !rule.meta || (rule.meta && (typeof rule.meta.schema === "undefined" || rule.meta.schema === null))
749
- )
750
- ) {
751
- emitMissingSchemaWarning(ruleName);
658
+ ruleConfig = [1, ...item.options];
659
+ }
660
+
661
+ configs.push({
662
+ rules: {
663
+ [ruleId]: ruleConfig
752
664
  }
753
- config.rules[ruleName] = [1].concat(item.options);
754
- } else {
755
- config.rules[ruleName] = 1;
665
+ });
666
+
667
+ let schema;
668
+
669
+ try {
670
+ schema = getRuleOptionsSchema(rule);
671
+ } catch (err) {
672
+ err.message += metaSchemaDescription;
673
+ throw err;
756
674
  }
757
675
 
758
- const schema = getRuleOptionsSchema(rule);
676
+ /*
677
+ * Check and throw an error if the schema is an empty object (`schema:{}`), because such schema
678
+ * doesn't validate or enforce anything and is therefore considered a possible error. If the intent
679
+ * was to skip options validation, `schema:false` should be set instead (explicit opt-out).
680
+ *
681
+ * For this purpose, a schema object is considered empty if it doesn't have any own enumerable string-keyed
682
+ * properties. While `ajv.compile()` does use enumerable properties from the prototype chain as well,
683
+ * it caches compiled schemas by serializing only own enumerable properties, so it's generally not a good idea
684
+ * to use inherited properties in schemas because schemas that differ only in inherited properties would end up
685
+ * having the same cache entry that would be correct for only one of them.
686
+ *
687
+ * At this point, `schema` can only be an object or `null`.
688
+ */
689
+ if (schema && Object.keys(schema).length === 0) {
690
+ throw new Error(`\`schema: {}\` is a no-op${metaSchemaDescription}`);
691
+ }
759
692
 
760
693
  /*
761
694
  * Setup AST getters.
762
695
  * The goal is to check whether or not AST was modified when
763
696
  * running the rule under test.
764
697
  */
765
- linter.defineRule("rule-tester/validate-ast", {
766
- create() {
767
- return {
768
- Program(node) {
769
- beforeAST = cloneDeeplyExcludesParent(node);
770
- },
771
- "Program:exit"(node) {
772
- afterAST = node;
698
+ configs.push({
699
+ plugins: {
700
+ "rule-tester": {
701
+ rules: {
702
+ "validate-ast": {
703
+ create() {
704
+ return {
705
+ Program(node) {
706
+ beforeAST = cloneDeeplyExcludesParent(node);
707
+ },
708
+ "Program:exit"(node) {
709
+ afterAST = node;
710
+ }
711
+ };
712
+ }
713
+ }
773
714
  }
774
- };
715
+ }
775
716
  }
776
717
  });
777
718
 
778
- if (typeof config.parser === "string") {
779
- assert(path.isAbsolute(config.parser), "Parsers provided as strings to RuleTester must be absolute paths");
780
- } else {
781
- config.parser = espreePath;
782
- }
783
-
784
- linter.defineParser(config.parser, wrapParser(require(config.parser)));
785
-
786
719
  if (schema) {
787
720
  ajv.validateSchema(schema);
788
721
 
@@ -809,15 +742,21 @@ class RuleTester {
809
742
  }
810
743
  }
811
744
 
812
- validate(config, "rule-tester", id => (id === ruleName ? rule : null));
745
+ // check for validation errors
746
+ try {
747
+ configs.normalizeSync();
748
+ configs.getConfig("test.js");
749
+ } catch (error) {
750
+ error.message = `ESLint configuration in rule-tester is invalid: ${error.message}`;
751
+ throw error;
752
+ }
813
753
 
814
754
  // Verify the code.
815
- const { getComments, applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype;
755
+ const { applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype;
816
756
  const originalCurrentSegments = Object.getOwnPropertyDescriptor(CodePath.prototype, "currentSegments");
817
757
  let messages;
818
758
 
819
759
  try {
820
- SourceCode.prototype.getComments = getCommentsDeprecation;
821
760
  Object.defineProperty(CodePath.prototype, "currentSegments", {
822
761
  get() {
823
762
  emitCodePathCurrentSegmentsWarning(ruleName);
@@ -826,18 +765,18 @@ class RuleTester {
826
765
  });
827
766
 
828
767
  forbiddenMethods.forEach(methodName => {
829
- SourceCode.prototype[methodName] = throwForbiddenMethodError(methodName);
768
+ SourceCode.prototype[methodName] = throwForbiddenMethodError(methodName, SourceCode.prototype);
830
769
  });
831
770
 
832
- messages = linter.verify(code, config, filename);
771
+ messages = linter.verify(code, configs, filename);
833
772
  } finally {
834
- SourceCode.prototype.getComments = getComments;
835
773
  Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments);
836
774
  SourceCode.prototype.applyInlineConfig = applyInlineConfig;
837
775
  SourceCode.prototype.applyLanguageOptions = applyLanguageOptions;
838
776
  SourceCode.prototype.finalize = finalize;
839
777
  }
840
778
 
779
+
841
780
  const fatalErrorMessage = messages.find(m => m.fatal);
842
781
 
843
782
  assert(!fatalErrorMessage, `A fatal parsing error occurred: ${fatalErrorMessage && fatalErrorMessage.message}`);
@@ -845,7 +784,7 @@ class RuleTester {
845
784
  // Verify if autofix makes a syntax error or not.
846
785
  if (messages.some(m => m.fix)) {
847
786
  output = SourceCodeFixer.applyFixes(code, messages).output;
848
- const errorMessageInFix = linter.verify(output, config, filename).find(m => m.fatal);
787
+ const errorMessageInFix = linter.verify(output, configs, filename).find(m => m.fatal);
849
788
 
850
789
  assert(!errorMessageInFix, [
851
790
  "A fatal parsing error occurred in autofix.",
@@ -861,7 +800,9 @@ class RuleTester {
861
800
  messages,
862
801
  output,
863
802
  beforeAST,
864
- afterAST: cloneDeeplyExcludesParent(afterAST)
803
+ afterAST: cloneDeeplyExcludesParent(afterAST),
804
+ configs,
805
+ filename
865
806
  };
866
807
  }
867
808
 
@@ -950,6 +891,22 @@ class RuleTester {
950
891
  const result = runRuleForItem(item);
951
892
  const messages = result.messages;
952
893
 
894
+ for (const message of messages) {
895
+ if (hasOwnProperty(message, "suggestions")) {
896
+
897
+ /** @type {Map<string, number>} */
898
+ const seenMessageIndices = new Map();
899
+
900
+ for (let i = 0; i < message.suggestions.length; i += 1) {
901
+ const suggestionMessage = message.suggestions[i].desc;
902
+ const previous = seenMessageIndices.get(suggestionMessage);
903
+
904
+ assert.ok(!seenMessageIndices.has(suggestionMessage), `Suggestion message '${suggestionMessage}' reported from suggestion ${i} was previously reported by suggestion ${previous}. Suggestion messages should be unique within an error.`);
905
+ seenMessageIndices.set(suggestionMessage, i);
906
+ }
907
+ }
908
+ }
909
+
953
910
  if (typeof item.errors === "number") {
954
911
 
955
912
  if (item.errors === 0) {
@@ -972,7 +929,7 @@ class RuleTester {
972
929
  )
973
930
  );
974
931
 
975
- const hasMessageOfThisRule = messages.some(m => m.ruleId === ruleName);
932
+ const hasMessageOfThisRule = messages.some(m => m.ruleId === ruleId);
976
933
 
977
934
  for (let i = 0, l = item.errors.length; i < l; i++) {
978
935
  const error = item.errors[i];
@@ -1131,6 +1088,17 @@ class RuleTester {
1131
1088
  if (hasOwnProperty(expectedSuggestion, "output")) {
1132
1089
  const codeWithAppliedSuggestion = SourceCodeFixer.applyFixes(item.code, [actualSuggestion]).output;
1133
1090
 
1091
+ // Verify if suggestion fix makes a syntax error or not.
1092
+ const errorMessageInSuggestion =
1093
+ linter.verify(codeWithAppliedSuggestion, result.configs, result.filename).find(m => m.fatal);
1094
+
1095
+ assert(!errorMessageInSuggestion, [
1096
+ "A fatal parsing error occurred in suggestion fix.",
1097
+ `Error: ${errorMessageInSuggestion && errorMessageInSuggestion.message}`,
1098
+ "Suggestion output:",
1099
+ codeWithAppliedSuggestion
1100
+ ].join("\n"));
1101
+
1134
1102
  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}"`);
1135
1103
  }
1136
1104
  });