Skip to content

⬅️ Back to Table of Contents

📄 src/RuleTester

📊 Analysis Summary

Metric Count
🔧 Functions 23
🧱 Classes 1
📦 Imports 44
📊 Variables & Constants 9
📑 Type Aliases 2

📚 Table of Contents

🛠️ File Location:

📂 packages/rule-tester/src/RuleTester.ts

📦 Imports

Name Source
TSESTree @typescript-eslint/utils
TSUtils @typescript-eslint/utils
AnyRuleCreateFunction @typescript-eslint/utils/ts-eslint
AnyRuleModule @typescript-eslint/utils/ts-eslint
ParserOptions @typescript-eslint/utils/ts-eslint
RuleListener @typescript-eslint/utils/ts-eslint
RuleModule @typescript-eslint/utils/ts-eslint
deepMerge @typescript-eslint/utils/eslint-utils
Linter @typescript-eslint/utils/ts-eslint
assert node:assert
path node:path
util node:util
SourceCode eslint
stringify json-stable-stringify-without-jsonify
merge lodash.merge
InvalidTestCase ./types
NormalizedRunTests ./types
RuleTesterConfig ./types
RunTests ./types
SuggestionOutput ./types
TestCaseError ./types
TesterConfigWithDefaults ./types
ValidTestCase ./types
AssertionOptions ./types/AssertionOptions
TestFramework ./TestFramework
ajvBuilder ./utils/ajv
cloneDeeplyExcludesParent ./utils/cloneDeeplyExcludesParent
validate ./utils/config-validator
satisfiesAllDependencyConstraints ./utils/dependencyConstraints
freezeDeeply ./utils/freezeDeeply
getRuleOptionsSchema ./utils/getRuleOptionsSchema
hasOwnProperty ./utils/hasOwnProperty
getPlaceholderMatcher ./utils/interpolate
interpolate ./utils/interpolate
omitCustomConfigProperties ./utils/omitCustomConfigProperties
isSerializable ./utils/serialization
ERROR_OBJECT_PARAMETERS ./utils/validationHelpers
FRIENDLY_ERROR_OBJECT_PARAMETER_LIST ./utils/validationHelpers
FRIENDLY_SUGGESTION_OBJECT_PARAMETER_LIST ./utils/validationHelpers
REQUIRED_SCENARIOS ./utils/validationHelpers
RULE_TESTER_PARAMETERS ./utils/validationHelpers
sanitize ./utils/validationHelpers
SUGGESTION_OBJECT_PARAMETERS ./utils/validationHelpers
wrapParser ./utils/validationHelpers

Variables & Constants

Name Type Kind Value Exported
RULE_TESTER_PLUGIN "@rule-tester" const '@rule-tester'
RULE_TESTER_PLUGIN_PREFIX "@rule-tester/" const ${RULE_TESTER_PLUGIN}/
TYPESCRIPT_ESLINT_PARSER "@typescript-eslint/parser" const '@typescript-eslint/parser'
DUPLICATE_PARSER_ERROR_MESSAGE "Do not set the parser at the test le... const `Do not set the parser at the test level unless you want to use a parser othe...
defaultParser any const require(TYPESCRIPT_ESLINT_PARSER) as typeof ParserType
testerDefaultConfig Readonly<TesterConfigWithDefaults> const { defaultFilenames: { ts: 'file.ts', tsx: 'react.tsx' }, languageOptions: { p...
defaultConfig Mutable<Required<Pick<RuleTesterConfi... let/var deepMerge( {}, testerDefaultConfig, ) as TesterConfigWithDefaults
forbiddenMethods readonly ["applyInlineConfig", "apply... const [ 'applyInlineConfig', 'applyLanguageOptions', 'finalize', ] as const
forbiddenMethodCalls Map<"applyInlineConfig" \| "applyLang... const new Map( forbiddenMethods.map(methodName => [methodName, new WeakSet()]), )

Functions

RuleTester.#getLinterForFilename(filename: string | undefined): Linter

Parameters:

  • filename string | undefined

Returns: Linter

Calls:

  • path.isAbsolute
  • path.normalize(filename).startsWith
  • path.parse
  • path.resolve
  • process.cwd
  • this.#lintersByBasePath.get
  • this.#lintersByBasePath.set

Internal Comments:

// For an absolute path (`/foo.ts`), or a path that steps
// up (`../foo.ts`), resolve the path relative to the base
// path (using the current working directory if the parser
// options did not specify a base path) and use the file's
// root as the base path so that the file is under the base
// path. For any other path, which would just be a plain
// file name (`foo.ts`), don't change the base path.

Code
#getLinterForFilename(filename: string | undefined): Linter {
    let basePath: string | undefined =
      this.#testerConfig.languageOptions.parserOptions?.tsconfigRootDir;
    // For an absolute path (`/foo.ts`), or a path that steps
    // up (`../foo.ts`), resolve the path relative to the base
    // path (using the current working directory if the parser
    // options did not specify a base path) and use the file's
    // root as the base path so that the file is under the base
    // path. For any other path, which would just be a plain
    // file name (`foo.ts`), don't change the base path.
    if (
      filename != null &&
      (path.isAbsolute(filename) || path.normalize(filename).startsWith('..'))
    ) {
      basePath = path.parse(
        path.resolve(basePath ?? process.cwd(), filename),
      ).root;
    }

    let linterForBasePath = this.#lintersByBasePath.get(basePath);
    if (!linterForBasePath) {
      linterForBasePath = new Linter({
        configType: 'flat',
        cwd: basePath,
      });

      this.#lintersByBasePath.set(basePath, linterForBasePath);
    }
    return linterForBasePath;
  }

RuleTester.setDefaultConfig(config: RuleTesterConfig): void

Set the configuration to use for all future tests

Raw JSDoc
/**
   * Set the configuration to use for all future tests
   */

Calls:

  • deepMerge (from @typescript-eslint/utils/eslint-utils)

Internal Comments:

// Make sure the rules object exists since it is assumed to exist later (x3)
// @ts-expect-error -- no index signature

Code
static setDefaultConfig(config: RuleTesterConfig): void {
    if (typeof config !== 'object' || config == null) {
      throw new TypeError(
        'RuleTester.setDefaultConfig: config must be an object',
      );
    }
    // Make sure the rules object exists since it is assumed to exist later
    defaultConfig = deepMerge(
      defaultConfig,
      // @ts-expect-error -- no index signature
      config,
    ) as TesterConfigWithDefaults;
  }

RuleTester.getDefaultConfig(): Readonly<RuleTesterConfig>

Get the current configuration used for all tests

Raw JSDoc
/**
   * Get the current configuration used for all tests
   */
Code
static getDefaultConfig(): Readonly<RuleTesterConfig> {
    return defaultConfig;
  }

RuleTester.resetDefaultConfig(): void

Reset the configuration to the initial configuration of the tester removing any changes made until now.

Raw JSDoc
/**
   * Reset the configuration to the initial configuration of the tester removing
   * any changes made until now.
   */

Calls:

  • merge (from lodash.merge)
Code
static resetDefaultConfig(): void {
    defaultConfig = merge({}, testerDefaultConfig);
  }

RuleTester.only(item: string | ValidTestCase<Options>): ValidTestCase<Options>

Adds the only property to a test to run it in isolation.

Raw JSDoc
/**
   * Adds the `only` property to a test to run it in isolation.
   */
Code
static only<Options extends readonly unknown[]>(
    item: string | ValidTestCase<Options>,
  ): ValidTestCase<Options>;

RuleTester.#normalizeTests(rawTests: RunTests<MessageIds, Options>): NormalizedRunTests<MessageIds, Options>

Define a rule for one particular run of tests.

Raw JSDoc
/**
   * Define a rule for one particular run of tests.
   */

Calls:

  • deepMerge (from @typescript-eslint/utils/eslint-utils)
  • path.join
  • process.cwd
  • getFilename
  • rawTests.invalid.map
  • rawTests.valid .map(test => { if (typeof test === 'string') { return { code: test }; } return test; }) .map
  • complex_call_12631
  • complex_call_13031
  • Object.keys
  • satisfiesAllDependencyConstraints (from ./utils/dependencyConstraints)
  • normalizedTests.valid.map
  • normalizedTests.invalid.map

Internal Comments:

/*
    Automatically add a filename to the tests to enable type-aware tests to "just work".
    This saves users having to verbosely and manually add the filename to every
    single test case.
    Hugely helps with the string-based valid test cases as it means they don't
    need to be made objects!
    */ (x2)
// Re-running simulates --fix mode, which implies an isolated program (x2)
// (i.e. parseAndGenerateServicesCalls[test.filename] > 1). (x2)
// convenience iterator to make it easy to loop all tests without a concat (x2)
// if there is an `only: true` - don't try apply constraints - assume that
// we are in "local development" mode rather than "CI validation" mode
/*
    Mark all unsatisfactory tests as `skip: true`.
    We do this instead of just omitting the tests entirely because it gives the
    test framework the opportunity to log the test as skipped rather than the test
    just disappearing without a trace.
    */ (x2)

Code
#normalizeTests<
    MessageIds extends string,
    Options extends readonly unknown[],
  >(
    rawTests: RunTests<MessageIds, Options>,
  ): NormalizedRunTests<MessageIds, Options> {
    /*
    Automatically add a filename to the tests to enable type-aware tests to "just work".
    This saves users having to verbosely and manually add the filename to every
    single test case.
    Hugely helps with the string-based valid test cases as it means they don't
    need to be made objects!
    */
    const getFilename = (
      originalFilename: string | undefined,
      testOptions: ParserOptions | undefined,
    ): string => {
      const resolvedOptions = deepMerge(
        this.#testerConfig.languageOptions.parserOptions,
        testOptions,
      ) as ParserOptions;
      const filename =
        originalFilename ??
        (resolvedOptions.ecmaFeatures?.jsx
          ? this.#testerConfig.defaultFilenames.tsx
          : this.#testerConfig.defaultFilenames.ts);
      if (resolvedOptions.project) {
        return path.join(
          resolvedOptions.tsconfigRootDir ?? process.cwd(),
          filename,
        );
      }
      return filename;
    };
    const normalizeTest = <
      MessageIds extends string,
      Options extends readonly unknown[],
      T extends InvalidTestCase<MessageIds, Options> | ValidTestCase<Options>,
    >(
      test: T,
    ): T => {
      const { languageOptions = {} } = test;
      if (languageOptions.parser === parser) {
        throw new Error(DUPLICATE_PARSER_ERROR_MESSAGE);
      }
      return {
        ...test,
        filename: getFilename(test.filename, languageOptions.parserOptions),
        languageOptions: {
          ...languageOptions,
          parserOptions: {
            // Re-running simulates --fix mode, which implies an isolated program
            // (i.e. parseAndGenerateServicesCalls[test.filename] > 1).
            disallowAutomaticSingleRunInference: true,
            ...languageOptions.parserOptions,
          },
        },
      };
    };

    const normalizedTests = {
      invalid: rawTests.invalid.map(normalizeTest),
      valid: rawTests.valid
        .map(test => {
          if (typeof test === 'string') {
            return { code: test };
          }
          return test;
        })
        .map(normalizeTest),
    };

    // convenience iterator to make it easy to loop all tests without a concat
    const allTestsIterator = {
      *[Symbol.iterator](): Generator<ValidTestCase<Options>, void> {
        for (const testCase of normalizedTests.valid) {
          yield testCase;
        }
        for (const testCase of normalizedTests.invalid) {
          yield testCase;
        }
      },
    };

    const hasOnly = ((): boolean => {
      for (const test of allTestsIterator) {
        if (test.only) {
          return true;
        }
      }
      return false;
    })();
    if (hasOnly) {
      // if there is an `only: true` - don't try apply constraints - assume that
      // we are in "local development" mode rather than "CI validation" mode
      return normalizedTests;
    }

    const hasConstraints = ((): boolean => {
      for (const test of allTestsIterator) {
        if (
          test.dependencyConstraints &&
          Object.keys(test.dependencyConstraints).length > 0
        ) {
          return true;
        }
      }
      return false;
    })();
    if (!hasConstraints) {
      return normalizedTests;
    }

    /*
    Mark all unsatisfactory tests as `skip: true`.
    We do this instead of just omitting the tests entirely because it gives the
    test framework the opportunity to log the test as skipped rather than the test
    just disappearing without a trace.
    */
    const maybeMarkAsOnly = <
      T extends InvalidTestCase<MessageIds, Options> | ValidTestCase<Options>,
    >(
      test: T,
    ): T => {
      return {
        ...test,
        skip: !satisfiesAllDependencyConstraints(test.dependencyConstraints),
      };
    };
    normalizedTests.valid = normalizedTests.valid.map(maybeMarkAsOnly);
    normalizedTests.invalid = normalizedTests.invalid.map(maybeMarkAsOnly);

    return normalizedTests;
  }

RuleTester.defineRule(name: string, rule: AnyRuleModule): void

Parameters:

  • name string
  • rule AnyRuleModule

Returns: void

Calls:

  • freezeDeeply (from ./utils/freezeDeeply)
  • complex_call_14536

Internal Comments:

// Create a wrapper rule that freezes the `context` properties. (x2)
// eslint-disable-next-line @typescript-eslint/no-deprecated -- For compatibility with ESLint 8 (x3)

Code
defineRule(name: string, rule: AnyRuleModule): void {
    this.#rules[name] = {
      ...rule,
      name,
      // Create a wrapper rule that freezes the `context` properties.
      create(context): RuleListener {
        freezeDeeply(context.options);
        freezeDeeply(context.settings);
        // eslint-disable-next-line @typescript-eslint/no-deprecated -- For compatibility with ESLint 8
        freezeDeeply(context.parserOptions);

        return (typeof rule === 'function' ? rule : rule.create)(context);
      },
    };
  }

RuleTester.#runHook(item: InvalidTestCase<MessageIds, Options> | …, prop: keyof Pick<typeof item, 'after' | 'befo…): void

Runs a hook on the given item when it's assigned to the given property

Throws:

  • Error: If the property is not a function or that function throws an error

Raw JSDoc

/**
   * Runs a hook on the given item when it's assigned to the given property
   * @throws {Error} If the property is not a function or that function throws an error
   */

Calls:

  • hasOwnProperty (from ./utils/hasOwnProperty)
  • assert.strictEqual
  • complex_call_15202
Code
#runHook<MessageIds extends string, Options extends readonly unknown[]>(
    item: InvalidTestCase<MessageIds, Options> | ValidTestCase<Options>,
    prop: keyof Pick<typeof item, 'after' | 'before'>,
  ): void {
    if (hasOwnProperty(item, prop)) {
      assert.strictEqual(
        typeof item[prop],
        'function',
        `Optional test case property '${prop}' must be a function`,
      );
      item[prop]();
    }
  }

RuleTester.verifyAssertionOptions(assertionOption: AssertionOptions | undefined): void

Verifies that the assertion options are valid

Throws:

  • Error: if the options aren't the correct type
Raw JSDoc
/**
   * Verifies that the assertion options are valid
   * @throws {Error} if the options aren't the correct type
   */
Code
private verifyAssertionOptions(
    assertionOption: AssertionOptions | undefined,
  ): void {
    if (assertionOption == null) {
      return;
    }

    const dataOption = assertionOption.requireData;

    if (
      dataOption != null &&
      typeof dataOption !== 'boolean' &&
      dataOption !== 'error' &&
      dataOption !== 'suggestion'
    ) {
      throw new Error(
        "The assertion option `requireData` should be of type boolean | 'error' | 'suggestion'",
      );
    }

    const locationOption = assertionOption.requireLocation;

    if (locationOption != null && typeof locationOption !== 'boolean') {
      throw new Error(
        'The assertion option `requireLocation` should be of type boolean',
      );
    }
  }

RuleTester.run(ruleName: string, rule: RuleModule<MessageIds, Options>, test: RunTests<TSUtils.NoInfer<MessageIds>, T…): void

Adds a new rule test to execute.

Raw JSDoc
/**
   * Adds a new rule test to execute.
   */

Calls:

  • this.verifyAssertionOptions
  • satisfiesAllDependencyConstraints (from ./utils/dependencyConstraints)
  • constructor.describeSkip
  • constructor.it
  • assert.equal
  • REQUIRED_SCENARIOS.forEach
  • scenarioErrors.push
  • [Test Scenarios for rule ${ruleName} is invalid:, ...scenarioErrors, ].join
  • this.#normalizeTests
  • constructor.describe
  • normalizedTests.valid.forEach
  • complex_call_18554
  • complex_call_18752
  • getTestMethod
  • sanitize (from ./utils/validationHelpers)
  • this.#runHook
  • this.#testValidTemplate
  • normalizedTests.invalid.forEach
  • complex_call_19369
  • complex_call_19575
  • this.#testInvalidTemplate

Internal Comments:

// for frameworks like mocha or jest that have a "skip" version of their function (x4)
// we can provide a nice skipped test! (x4)
// some frameworks error if there are no assertions (x4)
// don't run any tests because we don't match the base constraint
/*
     * This creates a test suite and pipes all supplied info through
     * one of the templates above.
     */ (x4)
// no need to pass no infer type parameter down to private methods

Code
run<MessageIds extends string, Options extends readonly unknown[]>(
    ruleName: string,
    rule: RuleModule<MessageIds, Options>,
    test: RunTests<TSUtils.NoInfer<MessageIds>, TSUtils.NoInfer<Options>>,
  ): void {
    this.verifyAssertionOptions(test.assertionOptions);

    const constructor = this.constructor as typeof RuleTester;

    if (
      this.#testerConfig.dependencyConstraints &&
      !satisfiesAllDependencyConstraints(
        this.#testerConfig.dependencyConstraints,
      )
    ) {
      // for frameworks like mocha or jest that have a "skip" version of their function
      // we can provide a nice skipped test!
      constructor.describeSkip(ruleName, () => {
        constructor.it(
          'All tests skipped due to unsatisfied constructor dependency constraints',
          () => {
            // some frameworks error if there are no assertions
            assert.equal(true, true);
          },
        );
      });

      // don't run any tests because we don't match the base constraint
      return;
    }

    if (!test || typeof test !== 'object') {
      throw new TypeError(
        `Test Scenarios for rule ${ruleName} : Could not find test scenario object`,
      );
    }

    const scenarioErrors: string[] = [];
    REQUIRED_SCENARIOS.forEach(scenarioType => {
      if (!test[scenarioType]) {
        scenarioErrors.push(
          `Could not find any ${scenarioType} test scenarios`,
        );
      }
    });

    if (scenarioErrors.length > 0) {
      throw new Error(
        [
          `Test Scenarios for rule ${ruleName} is invalid:`,
          ...scenarioErrors,
        ].join('\n'),
      );
    }

    const seenValidTestCases = new Set<string>();
    const seenInvalidTestCases = new Set<string>();
    const normalizedTests = this.#normalizeTests(test);

    function getTestMethod(
      test: ValidTestCase<TSUtils.NoInfer<Options>>,
    ): 'it' | 'itOnly' | 'itSkip' {
      if (test.skip) {
        return 'itSkip';
      }
      if (test.only) {
        return 'itOnly';
      }
      return 'it';
    }

    /*
     * This creates a test suite and pipes all supplied info through
     * one of the templates above.
     */
    constructor.describe(ruleName, () => {
      if (normalizedTests.valid.length) {
        constructor.describe('valid', () => {
          normalizedTests.valid.forEach(valid => {
            const testName = ((): string => {
              if (valid.name == null || valid.name.length === 0) {
                return valid.code;
              }
              return valid.name;
            })();
            constructor[getTestMethod(valid)](sanitize(testName), () => {
              try {
                this.#runHook(valid, 'before');
                this.#testValidTemplate(
                  ruleName,
                  rule,
                  valid,
                  seenValidTestCases,
                );
              } finally {
                this.#runHook(valid, 'after');
              }
            });
          });
        });
      }

      if (normalizedTests.invalid.length) {
        constructor.describe('invalid', () => {
          normalizedTests.invalid.forEach(invalid => {
            const name = ((): string => {
              if (invalid.name == null || invalid.name.length === 0) {
                return invalid.code;
              }
              return invalid.name;
            })();
            constructor[getTestMethod(invalid)](sanitize(name), () => {
              try {
                this.#runHook(invalid, 'before');
                this.#testInvalidTemplate(
                  ruleName,
                  rule,
                  // no need to pass no infer type parameter down to private methods
                  invalid,
                  seenInvalidTestCases,
                  test.assertionOptions,
                );
              } finally {
                this.#runHook(invalid, 'after');
              }
            });
          });
        });
      }
    });
  }

RuleTester.runRuleForItem(…): { afterAST: TSESTree.Program; beforeAST: TSESTree.Program; …

Run the rule for the given item

Throws:

  • Error: If an invalid schema. Use

Tags: @private instead of #private to expose it for testing purposes

Raw JSDoc
/**
   * Run the rule for the given item
   * @throws {Error} If an invalid schema.
   * Use @private instead of #private to expose it for testing purposes
   */

Calls:

  • this.defineRule
  • merge (from lodash.merge)
  • cloneDeeplyExcludesParent (from ./utils/cloneDeeplyExcludesParent)
  • hasOwnProperty (from ./utils/hasOwnProperty)
  • assert.ok
  • assert (from node:assert)
  • Array.isArray
  • wrapParser (from ./utils/validationHelpers)
  • getRuleOptionsSchema (from ./utils/getRuleOptionsSchema)
  • ajv.validateSchema
  • `ajv.errors .map(error => { const field = error.dataPath[0] === '.' ? error.dataPath.slice(1) : error.dataPath;
        return `\t${field}: ${error.message}`;
      })
      .join`
    
    • [Schema for rule ${ruleName} is invalid:, errors].join
    • ajv.compile
    • validate (from ./utils/config-validator)
    • omitCustomConfigProperties (from ./utils/omitCustomConfigProperties)
    • this.#getLinterForFilename
    • forbiddenMethods.forEach
    • throwForbiddenMethodError
    • linter.verify
    • messages.find
    • SourceCodeFixer.applyFixes
    • outputs.push
    • linter .verify(fixedResult.output, configWithoutCustomKeys, filename) .find
    • [ 'A fatal parsing error occurred in autofix.',Error: ${errorMessageInFix?.message}, 'Autofix output:', fixedResult.output, ].join

Internal Comments:

/**
             * Setup AST getters.
             * The goal is to check whether or not AST was modified when
             * running the rule under test.
             */ (x2)
// Unlike other properties, we don't want to spread props between different parsers. (x5)
/*
       * Assumes everything on the item is a config except for the
       * parameters used by this tester
       */ (x2)
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete (x2)
/*
       * Create the config object from the tester config and this item
       * specific configurations.
       */ (x3)
// no space after comma to match eslint core
/*
       * `ajv.validateSchema` checks for errors in the structure of the schema (by comparing the schema against a "meta-schema"),
       * and it reports those errors individually. However, there are other types of schema errors that only occur when compiling
       * the schema (e.g. using invalid defaults in a schema), and only one of these errors can be reported at a time. As a result,
       * the schema is compiled here separately from checking for `validateSchema` errors.
       */
// Verify the code. (x2)
// Verify if autofix makes a syntax error or not. (x2)
// is definitely assigned within the `@rule-tester/validate-ast` rule (x4)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion (x4)

Code
private runRuleForItem<
    MessageIds extends string,
    Options extends readonly unknown[],
  >(
    ruleName: string,
    rule: RuleModule<MessageIds, Options>,
    item: InvalidTestCase<MessageIds, Options> | ValidTestCase<Options>,
  ): {
    afterAST: TSESTree.Program;
    beforeAST: TSESTree.Program;
    config: RuleTesterConfig;
    filename?: string;
    messages: Linter.LintMessage[];
    outputs: string[];
  } {
    this.defineRule(ruleName, rule);

    let config: TesterConfigWithDefaults = merge({}, this.#testerConfig, {
      files: ['**'],
      plugins: {
        [RULE_TESTER_PLUGIN]: {
          rules: {
            /**
             * Setup AST getters.
             * The goal is to check whether or not AST was modified when
             * running the rule under test.
             */
            'validate-ast': {
              create(): RuleListener {
                return {
                  Program(node: TSESTree.Program): void {
                    beforeAST = cloneDeeplyExcludesParent(node);
                  },
                  'Program:exit'(node: TSESTree.Program): void {
                    afterAST = node;
                  },
                };
              },
            },
            ...this.#rules,
          },
        },
      },
    });

    // Unlike other properties, we don't want to spread props between different parsers.
    config.languageOptions.parser =
      item.languageOptions?.parser ?? this.#testerConfig.languageOptions.parser;

    let code;
    let filename;
    let beforeAST: TSESTree.Program;
    let afterAST: TSESTree.Program;

    if (typeof item === 'string') {
      code = item;
    } else {
      code = item.code;

      /*
       * Assumes everything on the item is a config except for the
       * parameters used by this tester
       */
      const itemConfig: Record<string, unknown> = { ...item };

      for (const parameter of RULE_TESTER_PARAMETERS) {
        // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
        delete itemConfig[parameter];
      }

      /*
       * Create the config object from the tester config and this item
       * specific configurations.
       */
      config = merge(config, itemConfig);
    }

    if (hasOwnProperty(item, 'only')) {
      assert.ok(
        typeof item.only === 'boolean',
        "Optional test case property 'only' must be a boolean",
      );
    }
    if (hasOwnProperty(item, 'filename')) {
      assert.ok(
        typeof item.filename === 'string',
        "Optional test case property 'filename' must be a string",
      );
      filename = item.filename;
    }

    const prefixedRuleName = `${RULE_TESTER_PLUGIN_PREFIX}${ruleName}`;

    if (hasOwnProperty(item, 'options')) {
      assert(Array.isArray(item.options), 'options must be an array');
      config.rules[prefixedRuleName] = ['error', ...item.options];
    } else {
      config.rules[prefixedRuleName] = 'error';
    }

    config.languageOptions ??= {};
    config.languageOptions.parser ??= defaultParser;
    config.languageOptions.parser = wrapParser(config.languageOptions.parser);

    const schema = getRuleOptionsSchema(rule);

    if (schema) {
      ajv.validateSchema(schema);

      if (ajv.errors) {
        const errors = ajv.errors
          .map(error => {
            const field =
              error.dataPath[0] === '.'
                ? error.dataPath.slice(1)
                : error.dataPath;

            return `\t${field}: ${error.message}`;
          })
          .join('\n');

        throw new Error(
          [`Schema for rule ${ruleName} is invalid:`, errors].join(
            // no space after comma to match eslint core
            ',',
          ),
        );
      }

      /*
       * `ajv.validateSchema` checks for errors in the structure of the schema (by comparing the schema against a "meta-schema"),
       * and it reports those errors individually. However, there are other types of schema errors that only occur when compiling
       * the schema (e.g. using invalid defaults in a schema), and only one of these errors can be reported at a time. As a result,
       * the schema is compiled here separately from checking for `validateSchema` errors.
       */
      try {
        ajv.compile(schema);
      } catch (err) {
        throw new Error(
          `Schema for rule ${ruleName} is invalid: ${(err as Error).message}`,
          { cause: err },
        );
      }
    }

    validate(config, RULE_TESTER_PLUGIN, id => (id === ruleName ? rule : null));

    // Verify the code.
    let initialMessages: Linter.LintMessage[] | null = null;
    let messages: Linter.LintMessage[];
    let fixedResult: SourceCodeFixer.AppliedFixes;
    let passNumber = 0;
    const outputs: string[] = [];
    const configWithoutCustomKeys = omitCustomConfigProperties(config);
    const linter = this.#getLinterForFilename(filename);

    do {
      passNumber++;

      const SourceCodePrototype = SourceCode.prototype as Record<
        ForbiddenMethodName,
        ForbiddenFunction
      >;

      const { applyInlineConfig, applyLanguageOptions, finalize } =
        SourceCodePrototype;

      try {
        forbiddenMethods.forEach(methodName => {
          SourceCodePrototype[methodName] = throwForbiddenMethodError(
            methodName,
            SourceCodePrototype,
          );
        });

        const actualConfig = merge(configWithoutCustomKeys, {
          languageOptions: {
            ...configWithoutCustomKeys.languageOptions,
            parserOptions: {
              ecmaVersion: 'latest',
              sourceType: 'module',
              ...configWithoutCustomKeys.languageOptions?.parserOptions,
            },
          },
          linterOptions: {
            reportUnusedDisableDirectives: 1,
            ...configWithoutCustomKeys.linterOptions,
          },
        });
        messages = linter.verify(code, actualConfig, filename);
      } finally {
        SourceCodePrototype.applyInlineConfig = applyInlineConfig;
        SourceCodePrototype.applyLanguageOptions = applyLanguageOptions;
        SourceCodePrototype.finalize = finalize;
      }

      initialMessages ??= messages;

      if (messages.length === 0) {
        break;
      }

      const fatalErrorMessage = messages.find(m => m.fatal);
      assert(
        !fatalErrorMessage,
        `A fatal parsing error occurred: ${fatalErrorMessage?.message}`,
      );

      fixedResult = SourceCodeFixer.applyFixes(code, messages);
      if (fixedResult.output === code) {
        break;
      }
      code = fixedResult.output;
      outputs.push(code);

      // Verify if autofix makes a syntax error or not.
      const errorMessageInFix = linter
        .verify(fixedResult.output, configWithoutCustomKeys, filename)
        .find(m => m.fatal);

      assert(
        !errorMessageInFix,
        [
          'A fatal parsing error occurred in autofix.',
          `Error: ${errorMessageInFix?.message}`,
          'Autofix output:',
          fixedResult.output,
        ].join('\n'),
      );
    } while (fixedResult.fixed && passNumber < 10);

    return {
      config,
      filename,
      messages: initialMessages,
      outputs,
      // is definitely assigned within the `@rule-tester/validate-ast` rule
      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
      beforeAST: beforeAST!,
      // is definitely assigned within the `@rule-tester/validate-ast` rule
      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
      afterAST: cloneDeeplyExcludesParent(afterAST!),
    };
  }

RuleTester.#testValidTemplate(ruleName: string, rule: RuleModule<MessageIds, Options>, itemIn: string | ValidTestCase<Options>, seenValidTestCases: Set<string>): void

Check if the template is valid or not all valid cases go through this

Raw JSDoc
/**
   * Check if the template is valid or not
   * all valid cases go through this
   */

Calls:

  • assert.ok
  • checkDuplicateTestCase
  • this.runRuleForItem
  • assert.strictEqual
  • util.format
  • util.inspect
  • assertASTDidntChange
Code
#testValidTemplate<
    MessageIds extends string,
    Options extends readonly unknown[],
  >(
    ruleName: string,
    rule: RuleModule<MessageIds, Options>,
    itemIn: string | ValidTestCase<Options>,
    seenValidTestCases: Set<string>,
  ): void {
    const item: ValidTestCase<Options> =
      typeof itemIn === 'object' ? itemIn : { code: itemIn };

    assert.ok(
      typeof item.code === 'string',
      "Test case must specify a string value for 'code'",
    );
    if (item.name) {
      assert.ok(
        typeof item.name === 'string',
        "Optional test case property 'name' must be a string",
      );
    }

    checkDuplicateTestCase(item, seenValidTestCases);

    const result = this.runRuleForItem(ruleName, rule, item);
    const messages = result.messages;

    assert.strictEqual(
      messages.length,
      0,
      util.format(
        'Should have no errors but had %d: %s',
        messages.length,
        util.inspect(messages),
      ),
    );

    assertASTDidntChange(result.beforeAST, result.afterAST);
  }

RuleTester.#testInvalidTemplate(…): void

Check if the template is invalid or not all invalid cases go through this.

Raw JSDoc
/**
   * Check if the template is invalid or not
   * all invalid cases go through this.
   */

Calls:

  • assert.ok
  • Array.isArray
  • assert.fail
  • checkDuplicateTestCase
  • hasOwnProperty (from ./utils/hasOwnProperty)
  • Object.keys(rule.meta.messages) .map(key =>'${key}') .join
  • this.runRuleForItem
  • result.messages.map
  • message.ruleId.startsWith
  • message.ruleId.slice
  • seenMessageIndices.get
  • seenMessageIndices.has
  • seenMessageIndices.set
  • assert.strictEqual
  • util.format
  • util.inspect
  • messages.some
  • assert (from node:assert)
  • assertMessageMatches
  • Object.keys(error).forEach
  • ERROR_OBJECT_PARAMETERS.has
  • getUnsubstitutedMessagePlaceholders
  • unsubstitutedPlaceholders.map(name =>'${name}').join
  • interpolate (from ./utils/interpolate)
  • getMessagePlaceholders
  • locationProperties.filter
  • missingKeys.join
  • Boolean
  • error.suggestions.forEach
  • Object.keys(expectedSuggestion).forEach
  • SUGGESTION_OBJECT_PARAMETERS.has
  • SourceCodeFixer.applyFixes
  • this.#getLinterForFilename( item.filename, ) .verify( codeWithAppliedSuggestion, omitCustomConfigProperties(result.config), result.filename, ) .find
  • [ 'A fatal parsing error occurred in suggestion fix.',Error: ${errorMessageInSuggestion?.message}, 'Suggestion output:', codeWithAppliedSuggestion, ].join
  • assert.notStrictEqual
  • assert.deepStrictEqual
  • assertASTDidntChange

Internal Comments:

// console.log({ messages });
// Just an error message. (x3)
/*
           * Error object.
           * This may have a message, messageId, data, node type, line, and/or
           * column.
           */ (x6)
// @ts-expect-error -- we purposely don't define `message` on our types as the current standard is `messageId` (x4)
/*
               *  if data was provided, then directly compare the returned message to a synthetic
               *  interpolated message using the same message ID and data provided in the test.
               *  See https://github.com/eslint/eslint/issues/9890 for context.
               */ (x2)
// Support asserting there are no suggestions (x2)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion (x2)
// It is possible that error.suggestions is a number (x2)
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions (x2)
// @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId` (x3)
// Verify if suggestion fix makes a syntax error or not. (x2)
// Message was an unexpected type (x4)

Code
#testInvalidTemplate<
    MessageIds extends string,
    Options extends readonly unknown[],
  >(
    ruleName: string,
    rule: RuleModule<MessageIds, Options>,
    item: InvalidTestCase<MessageIds, Options>,
    seenInvalidTestCases: Set<string>,
    assertionOptions: AssertionOptions = {},
  ): void {
    assert.ok(
      typeof item.code === 'string',
      "Test case must specify a string value for 'code'",
    );
    if (item.name) {
      assert.ok(
        typeof item.name === 'string',
        "Optional test case property 'name' must be a string",
      );
    }
    assert.ok(
      item.errors || item.errors === 0,
      `Did not specify errors for an invalid test of ${ruleName}`,
    );

    if (Array.isArray(item.errors) && item.errors.length === 0) {
      assert.fail('Invalid cases must have at least one error');
    }

    checkDuplicateTestCase(item, seenInvalidTestCases);

    const ruleHasMetaMessages =
      hasOwnProperty(rule, 'meta') && hasOwnProperty(rule.meta, 'messages');
    const friendlyIDList = ruleHasMetaMessages
      ? `[${Object.keys(rule.meta.messages)
          .map(key => `'${key}'`)
          .join(', ')}]`
      : null;

    const result = this.runRuleForItem(ruleName, rule, item);
    const messages = result.messages.map(message => ({
      ...message,
      ...(message.ruleId && {
        ruleId: message.ruleId.startsWith(RULE_TESTER_PLUGIN_PREFIX)
          ? message.ruleId.slice(RULE_TESTER_PLUGIN_PREFIX.length)
          : message.ruleId,
      }),
    }));

    for (const message of messages) {
      if (hasOwnProperty(message, 'suggestions')) {
        const seenMessageIndices = new Map<string, number>();

        for (let i = 0; i < message.suggestions.length; i += 1) {
          const suggestionMessage = message.suggestions[i].desc;
          const previous = seenMessageIndices.get(suggestionMessage);

          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.`,
          );
          seenMessageIndices.set(suggestionMessage, i);
        }
      }
    }

    if (typeof item.errors === 'number') {
      if (item.errors === 0) {
        assert.fail("Invalid cases must have 'error' value greater than 0");
      }

      assert.strictEqual(
        messages.length,
        item.errors,
        util.format(
          'Should have %d error%s but had %d: %s',
          item.errors,
          item.errors === 1 ? '' : 's',
          messages.length,
          util.inspect(messages),
        ),
      );
    } else {
      assert.strictEqual(
        messages.length,
        item.errors.length,
        util.format(
          'Should have %d error%s but had %d: %s',
          item.errors.length,
          item.errors.length === 1 ? '' : 's',
          messages.length,
          util.inspect(messages),
        ),
      );

      const hasMessageOfThisRule = messages.some(m => m.ruleId === ruleName);

      const { requireData = false, requireLocation = false } = assertionOptions;

      // console.log({ messages });
      for (let i = 0, l = item.errors.length; i < l; i++) {
        const error = item.errors[i];
        const message = messages[i];

        assert(
          hasMessageOfThisRule,
          'Error rule name should be the same as the name of the rule being tested',
        );

        if (typeof error === 'string' || error instanceof RegExp) {
          // Just an error message.
          assertMessageMatches(message.message, error);
          assert.ok(
            message.suggestions == null,
            `Error at index ${i} has suggestions. Please convert the test error into an object and specify 'suggestions' property on it to test suggestions.`,
          );
        } else if (typeof error === 'object' && error != null) {
          /*
           * Error object.
           * This may have a message, messageId, data, node type, line, and/or
           * column.
           */

          Object.keys(error).forEach(propertyName => {
            assert.ok(
              ERROR_OBJECT_PARAMETERS.has(propertyName),
              `Invalid error property name '${propertyName}'. Expected one of ${FRIENDLY_ERROR_OBJECT_PARAMETER_LIST}.`,
            );
          });

          // @ts-expect-error -- we purposely don't define `message` on our types as the current standard is `messageId`
          if (hasOwnProperty(error, 'message')) {
            assert.ok(
              !hasOwnProperty(error, 'messageId'),
              "Error should not specify both 'message' and a 'messageId'.",
            );
            assert.ok(
              !hasOwnProperty(error, 'data'),
              "Error should not specify both 'data' and 'message'.",
            );
            assertMessageMatches(
              message.message,
              // @ts-expect-error -- we purposely don't define `message` on our types as the current standard is `messageId`
              error.message as unknown,
            );
          } else if (hasOwnProperty(error, 'messageId')) {
            assert.ok(
              ruleHasMetaMessages,
              "Error can not use 'messageId' if rule under test doesn't define 'meta.messages'.",
            );
            if (!hasOwnProperty(rule.meta.messages, error.messageId)) {
              assert(
                false,
                `Invalid messageId '${error.messageId}'. Expected one of ${friendlyIDList}.`,
              );
            }
            assert.strictEqual(
              message.messageId,
              error.messageId,
              `messageId '${message.messageId}' does not match expected messageId '${error.messageId}'.`,
            );

            const unsubstitutedPlaceholders =
              getUnsubstitutedMessagePlaceholders(
                message.message,
                rule.meta.messages[message.messageId],
                error.data,
              );

            assert.ok(
              unsubstitutedPlaceholders.length === 0,
              `The reported message has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(', ')}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? 'values' : 'value'} via the 'data' property in the context.report() call.`,
            );

            if (hasOwnProperty(error, 'data')) {
              /*
               *  if data was provided, then directly compare the returned message to a synthetic
               *  interpolated message using the same message ID and data provided in the test.
               *  See https://github.com/eslint/eslint/issues/9890 for context.
               */
              const unformattedOriginalMessage =
                rule.meta.messages[error.messageId];
              const rehydratedMessage = interpolate(
                unformattedOriginalMessage,
                error.data,
              );

              assert.strictEqual(
                message.message,
                rehydratedMessage,
                `Hydrated message "${rehydratedMessage}" does not match "${message.message}"`,
              );
            } else {
              const requiresDataProperty =
                requireData === true || requireData === 'error';
              const hasPlaceholders =
                getMessagePlaceholders(rule.meta.messages[error.messageId])
                  .length > 0;
              assert.ok(
                !requiresDataProperty || !hasPlaceholders,
                `Error should specify the 'data' property as the referenced message has placeholders.`,
              );
            }
          } else {
            assert.fail(
              "Test error must specify either a 'messageId' or 'message'.",
            );
          }

          if (hasOwnProperty(error, 'line')) {
            assert.strictEqual(
              message.line,
              error.line,
              `Error line should be ${error.line}`,
            );
          }

          if (hasOwnProperty(error, 'column')) {
            assert.strictEqual(
              message.column,
              error.column,
              `Error column should be ${error.column}`,
            );
          }

          if (hasOwnProperty(error, 'endLine')) {
            assert.strictEqual(
              message.endLine,
              error.endLine,
              `Error endLine should be ${error.endLine}`,
            );
          }

          if (hasOwnProperty(error, 'endColumn')) {
            assert.strictEqual(
              message.endColumn,
              error.endColumn,
              `Error endColumn should be ${error.endColumn}`,
            );
          }

          if (requireLocation) {
            const locationProperties: (keyof Pick<
              TestCaseError<MessageIds>,
              'column' | 'endColumn' | 'endLine' | 'line'
            >)[] = ['line', 'column', 'endLine', 'endColumn'];

            const missingKeys = locationProperties.filter(
              key =>
                !hasOwnProperty(error, key) && hasOwnProperty(message, key),
            );
            assert.ok(
              missingKeys.length === 0,
              `Error is missing expected location properties: ${missingKeys.join(', ')}`,
            );
          }

          assert.ok(
            !message.suggestions || hasOwnProperty(error, 'suggestions'),
            `Error at index ${i} has suggestions. Please specify 'suggestions' property on the test error object.`,
          );
          if (hasOwnProperty(error, 'suggestions')) {
            // Support asserting there are no suggestions
            const expectsSuggestions = Array.isArray(error.suggestions)
              ? error.suggestions.length > 0
              : Boolean(error.suggestions);
            const hasSuggestions = message.suggestions != null;
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            const messageSuggestions = message.suggestions!;

            if (!hasSuggestions && expectsSuggestions) {
              assert.ok(
                !error.suggestions,
                `Error should have suggestions on error with message: "${message.message}"`,
              );
            } else if (hasSuggestions) {
              assert.ok(
                expectsSuggestions,
                `Error should have no suggestions on error with message: "${message.message}"`,
              );
              if (typeof error.suggestions === 'number') {
                assert.strictEqual(
                  messageSuggestions.length,
                  error.suggestions,
                  // It is possible that error.suggestions is a number
                  // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
                  `Error should have ${error.suggestions} suggestions. Instead found ${messageSuggestions.length} suggestions`,
                );
              } else if (Array.isArray(error.suggestions)) {
                assert.strictEqual(
                  messageSuggestions.length,
                  error.suggestions.length,
                  `Error should have ${error.suggestions.length} suggestions. Instead found ${messageSuggestions.length} suggestions`,
                );

                error.suggestions.forEach(
                  (expectedSuggestion: SuggestionOutput<MessageIds>, index) => {
                    assert.ok(
                      typeof expectedSuggestion === 'object' &&
                        expectedSuggestion != null,
                      "Test suggestion in 'suggestions' array must be an object.",
                    );
                    Object.keys(expectedSuggestion).forEach(propertyName => {
                      assert.ok(
                        SUGGESTION_OBJECT_PARAMETERS.has(propertyName),
                        `Invalid suggestion property name '${propertyName}'. Expected one of ${FRIENDLY_SUGGESTION_OBJECT_PARAMETER_LIST}.`,
                      );
                    });

                    const actualSuggestion = messageSuggestions[index];
                    const suggestionPrefix = `Error Suggestion at index ${index}:`;

                    // @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId`
                    if (hasOwnProperty(expectedSuggestion, 'desc')) {
                      // @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId`
                      const expectedDesc = expectedSuggestion.desc as string;

                      assert.ok(
                        !hasOwnProperty(expectedSuggestion, 'data'),
                        `${suggestionPrefix} Test should not specify both 'desc' and 'data'.`,
                      );
                      assert.ok(
                        !hasOwnProperty(expectedSuggestion, 'messageId'),
                        `${suggestionPrefix} Test should not specify both 'desc' and 'messageId'.`,
                      );
                      assert.strictEqual(
                        actualSuggestion.desc,
                        expectedDesc,
                        `${suggestionPrefix} desc should be "${expectedDesc}" but got "${actualSuggestion.desc}" instead.`,
                      );
                    } else if (
                      hasOwnProperty(expectedSuggestion, 'messageId')
                    ) {
                      assert.ok(
                        ruleHasMetaMessages,
                        `${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.`,
                      );
                      assert.ok(
                        hasOwnProperty(
                          rule.meta.messages,
                          expectedSuggestion.messageId,
                        ),
                        `${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.`,
                      );
                      assert.strictEqual(
                        actualSuggestion.messageId,
                        expectedSuggestion.messageId,
                        `${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.`,
                      );

                      const rawSuggestionMessage =
                        rule.meta.messages[expectedSuggestion.messageId];

                      const unsubstitutedPlaceholders =
                        getUnsubstitutedMessagePlaceholders(
                          actualSuggestion.desc,
                          rawSuggestionMessage,
                          expectedSuggestion.data,
                        );

                      assert.ok(
                        unsubstitutedPlaceholders.length === 0,
                        `The message of the suggestion has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(', ')}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? 'values' : 'value'} via the 'data' property for the suggestion in the context.report() call.`,
                      );

                      if (hasOwnProperty(expectedSuggestion, 'data')) {
                        const unformattedMetaMessage =
                          rule.meta.messages[expectedSuggestion.messageId];
                        const rehydratedDesc = interpolate(
                          unformattedMetaMessage,
                          expectedSuggestion.data,
                        );

                        assert.strictEqual(
                          actualSuggestion.desc,
                          rehydratedDesc,
                          `${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".`,
                        );
                      } else {
                        const requiresDataProperty =
                          requireData === true || requireData === 'suggestion';
                        const hasPlaceholders =
                          getMessagePlaceholders(rawSuggestionMessage).length >
                          0;
                        assert.ok(
                          !requiresDataProperty || !hasPlaceholders,
                          `${suggestionPrefix} Suggestion should specify the 'data' property as the referenced message has placeholders.`,
                        );
                      }
                    } else if (hasOwnProperty(expectedSuggestion, 'data')) {
                      assert.fail(
                        `${suggestionPrefix} Test must specify 'messageId' if 'data' is used.`,
                      );
                    } else {
                      assert.fail(
                        `${suggestionPrefix} Test must specify either 'messageId' or 'desc'.`,
                      );
                    }

                    assert.ok(
                      hasOwnProperty(expectedSuggestion, 'output'),
                      `${suggestionPrefix} The "output" property is required.`,
                    );
                    const codeWithAppliedSuggestion =
                      SourceCodeFixer.applyFixes(item.code, [
                        actualSuggestion,
                      ]).output;

                    // Verify if suggestion fix makes a syntax error or not.
                    const errorMessageInSuggestion = this.#getLinterForFilename(
                      item.filename,
                    )
                      .verify(
                        codeWithAppliedSuggestion,
                        omitCustomConfigProperties(result.config),
                        result.filename,
                      )
                      .find(m => m.fatal);

                    assert(
                      !errorMessageInSuggestion,
                      [
                        'A fatal parsing error occurred in suggestion fix.',
                        `Error: ${errorMessageInSuggestion?.message}`,
                        'Suggestion output:',
                        codeWithAppliedSuggestion,
                      ].join('\n'),
                    );

                    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}"`,
                    );
                    assert.notStrictEqual(
                      expectedSuggestion.output,
                      item.code,
                      `The output of a suggestion should differ from the original source code for suggestion at index: ${index} on error with message: "${message.message}"`,
                    );
                  },
                );
              } else {
                assert.fail(
                  "Test error object property 'suggestions' should be an array or a number",
                );
              }
            }
          }
        } else {
          // Message was an unexpected type
          assert.fail(
            `Error should be a string, object, or RegExp, but found (${util.inspect(
              message,
            )})`,
          );
        }
      }
    }

    if (hasOwnProperty(item, 'output')) {
      if (item.output == null) {
        if (result.outputs.length) {
          assert.strictEqual(
            result.outputs[0],
            item.code,
            'Expected no autofixes to be suggested.',
          );
        }
      } else if (typeof item.output === 'string') {
        assert(result.outputs.length > 0, 'Expected autofix to be suggested.');
        assert.strictEqual(
          result.outputs[0],
          item.output,
          'Output is incorrect.',
        );
        if (result.outputs.length) {
          assert.deepStrictEqual(
            result.outputs,
            [item.output],
            'Multiple autofixes are required due to overlapping fix ranges - please use the array form of output to declare all of the expected autofix passes.',
          );
        }
      } else {
        assert(result.outputs.length > 0, 'Expected autofix to be suggested.');
        assert.deepStrictEqual(
          result.outputs,
          item.output,
          'Outputs do not match.',
        );
      }
    } else if (result.outputs.length) {
      assert.strictEqual(
        result.outputs[0],
        item.code,
        "The rule fixed the code. Please add 'output' property.",
      );
    }

    assertASTDidntChange(result.beforeAST, result.afterAST);
  }

throwForbiddenMethodError(methodName: MethodName, prototype: Prototype): Prototype[MethodName]

Function to replace forbidden SourceCode methods. Allows just one call per method.

Parameters:

  • methodName any: The name of the method to forbid.
  • prototype any: The prototype with the original method to call.

Returns: undefined The function that throws the error.

Raw JSDoc
/**
 * Function to replace forbidden `SourceCode` methods. Allows just one call per method.
 * @param methodName The name of the method to forbid.
 * @param prototype The prototype with the original method to call.
 * @returns The function that throws the error.
 */

Calls:

  • forbiddenMethodCalls.get
  • called.has
  • called.add
  • original.apply

Internal Comments:

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion (x2)

Code
function throwForbiddenMethodError<
  MethodName extends ForbiddenMethodName,
  Prototype extends Record<MethodName, ForbiddenFunction>,
>(methodName: MethodName, prototype: Prototype): Prototype[MethodName] {
  const original = prototype[methodName];

  return function (this: WeakKey, ...args: Parameters<Prototype[MethodName]>) {
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    const called = forbiddenMethodCalls.get(methodName)!;

    if (!called.has(this)) {
      called.add(this);

      return original.apply(this, args);
    }

    throw new Error(
      `\`SourceCode#${methodName}()\` cannot be called inside a rule.`,
    );
  } as Prototype[MethodName];
}

getMessagePlaceholders(message: string): string[]

Extracts names of {{ placeholders }} from the reported message.

Parameters:

  • message any: Reported message

Returns: undefined Array of placeholder names

Raw JSDoc
/**
 * Extracts names of {{ placeholders }} from the reported message.
 * @param message Reported message
 * @returns Array of placeholder names
 */

Calls:

  • getPlaceholderMatcher (from ./utils/interpolate)
  • Array.from
  • message.matchAll
  • name.trim
Code
function getMessagePlaceholders(message: string): string[] {
  const matcher = getPlaceholderMatcher();

  return Array.from(message.matchAll(matcher), ([, name]) => name.trim());
}

getUnsubstitutedMessagePlaceholders(message: string, raw: string, data: Record<string, unknown>): string[]

Returns the placeholders in the reported messages but only includes the placeholders available in the raw message and not in the provided data.

Parameters:

  • message any: The reported message
  • raw any: The raw message specified in the rule meta.messages
  • data any: The passed

Returns: undefined Missing placeholder names

Raw JSDoc
/**
 * Returns the placeholders in the reported messages but
 * only includes the placeholders available in the raw message and not in the provided data.
 * @param message The reported message
 * @param raw The raw message specified in the rule meta.messages
 * @param data The passed
 * @returns Missing placeholder names
 */

Calls:

  • getMessagePlaceholders
  • Object.keys
  • unsubstituted.filter
  • known.includes
  • provided.includes

Internal Comments:

// Remove false positives by only counting placeholders in the raw message, which were not provided in the data matcher or added with a data property (x2)

Code
function getUnsubstitutedMessagePlaceholders(
  message: string,
  raw: string,
  data: Record<string, unknown> = {},
): string[] {
  const unsubstituted = getMessagePlaceholders(message);

  if (unsubstituted.length === 0) {
    return [];
  }

  // Remove false positives by only counting placeholders in the raw message, which were not provided in the data matcher or added with a data property
  const known = getMessagePlaceholders(raw);
  const provided = Object.keys(data);

  return unsubstituted.filter(
    name => known.includes(name) && !provided.includes(name),
  );
}

assertASTDidntChange(beforeAST: unknown, afterAST: unknown): void

Check if the AST was changed

Raw JSDoc
/**
 * Check if the AST was changed
 */

Calls:

  • assert.deepStrictEqual
Code
function assertASTDidntChange(beforeAST: unknown, afterAST: unknown): void {
  assert.deepStrictEqual(beforeAST, afterAST, 'Rule should not modify AST.');
}

checkDuplicateTestCase(item: unknown, seenTestCases: Set<unknown>): void

Check if this test case is a duplicate of one we have seen before.

Raw JSDoc
/**
 * Check if this test case is a duplicate of one we have seen before.
 */

Calls:

  • isSerializable (from ./utils/serialization)
  • stringify (from json-stable-stringify-without-jsonify)
  • assert (from node:assert)
  • seenTestCases.has
  • seenTestCases.add

Internal Comments:

/*
     * If we can't serialize a test case (because it contains a function, RegExp, etc), skip the check.
     * This might happen with properties like: options, plugins, settings, languageOptions.parser, languageOptions.parserOptions.
     */

Code
function checkDuplicateTestCase(
  item: unknown,
  seenTestCases: Set<unknown>,
): void {
  if (!isSerializable(item)) {
    /*
     * If we can't serialize a test case (because it contains a function, RegExp, etc), skip the check.
     * This might happen with properties like: options, plugins, settings, languageOptions.parser, languageOptions.parserOptions.
     */
    return;
  }

  const serializedTestCase = stringify(item);

  assert(
    !seenTestCases.has(serializedTestCase),
    'detected duplicate test case',
  );
  seenTestCases.add(serializedTestCase);
}

assertMessageMatches(actual: string, expected: string | RegExp): void

Asserts that the message matches its expected value. If the expected value is a regular expression, it is checked against the actual value.

Raw JSDoc
/**
 * Asserts that the message matches its expected value. If the expected
 * value is a regular expression, it is checked against the actual
 * value.
 */

Calls:

  • assert.ok
  • expected.test
  • assert.strictEqual

Internal Comments:

// assert.js doesn't have a built-in RegExp match function (x4)

Code
function assertMessageMatches(actual: string, expected: string | RegExp): void {
  if (expected instanceof RegExp) {
    // assert.js doesn't have a built-in RegExp match function
    assert.ok(
      expected.test(actual),
      `Expected '${actual}' to match ${expected}`,
    );
  } else {
    assert.strictEqual(actual, expected);
  }
}

Internal helpers

Declared inside another function in this file.

getFilename(originalFilename: string | undefined, testOptions: ParserOptions | undefined): string

Parameters:

  • originalFilename string | undefined
  • testOptions ParserOptions | undefined

Returns: string

Calls:

  • deepMerge (from @typescript-eslint/utils/eslint-utils)
  • path.join
  • process.cwd
Code
(
      originalFilename: string | undefined,
      testOptions: ParserOptions | undefined,
    ): string => {
      const resolvedOptions = deepMerge(
        this.#testerConfig.languageOptions.parserOptions,
        testOptions,
      ) as ParserOptions;
      const filename =
        originalFilename ??
        (resolvedOptions.ecmaFeatures?.jsx
          ? this.#testerConfig.defaultFilenames.tsx
          : this.#testerConfig.defaultFilenames.ts);
      if (resolvedOptions.project) {
        return path.join(
          resolvedOptions.tsconfigRootDir ?? process.cwd(),
          filename,
        );
      }
      return filename;
    }

normalizeTest(test: T): T

Parameters:

  • test T

Returns: T

Calls:

  • getFilename

Internal Comments:

// Re-running simulates --fix mode, which implies an isolated program (x2)
// (i.e. parseAndGenerateServicesCalls[test.filename] > 1). (x2)

Code
<
      MessageIds extends string,
      Options extends readonly unknown[],
      T extends InvalidTestCase<MessageIds, Options> | ValidTestCase<Options>,
    >(
      test: T,
    ): T => {
      const { languageOptions = {} } = test;
      if (languageOptions.parser === parser) {
        throw new Error(DUPLICATE_PARSER_ERROR_MESSAGE);
      }
      return {
        ...test,
        filename: getFilename(test.filename, languageOptions.parserOptions),
        languageOptions: {
          ...languageOptions,
          parserOptions: {
            // Re-running simulates --fix mode, which implies an isolated program
            // (i.e. parseAndGenerateServicesCalls[test.filename] > 1).
            disallowAutomaticSingleRunInference: true,
            ...languageOptions.parserOptions,
          },
        },
      };
    }

maybeMarkAsOnly(test: T): T

Parameters:

  • test T

Returns: T

Calls:

  • satisfiesAllDependencyConstraints (from ./utils/dependencyConstraints)
Code
<
      T extends InvalidTestCase<MessageIds, Options> | ValidTestCase<Options>,
    >(
      test: T,
    ): T => {
      return {
        ...test,
        skip: !satisfiesAllDependencyConstraints(test.dependencyConstraints),
      };
    }

getTestMethod(test: ValidTestCase<TSUtils.NoInfer<Options>>): 'it' | 'itOnly' | 'itSkip'

Parameters:

  • test ValidTestCase<TSUtils.NoInfer<Options>>

Returns: 'it' | 'itOnly' | 'itSkip'

Code
function getTestMethod(
      test: ValidTestCase<TSUtils.NoInfer<Options>>,
    ): 'it' | 'itOnly' | 'itSkip' {
      if (test.skip) {
        return 'itSkip';
      }
      if (test.only) {
        return 'itOnly';
      }
      return 'it';
    }

Classes

RuleTester

Extends: TestFramework

Methods (15) — full entries under Functions

Method Signature
#getLinterForFilename (filename: string \| undefined): Linter
setDefaultConfig (config: RuleTesterConfig): void
getDefaultConfig (): Readonly<RuleTesterConfig>
resetDefaultConfig (): void
only (item: string \| ValidTestCase<Options>): ValidTestCase<Options>
only (item: InvalidTestCase<MessageIds, Options>): InvalidTestCase<MessageIds, Options>
only (item: string \| InvalidTestCase<MessageIds, Options> \| ValidTestCase<Options>): InvalidTestCase...
#normalizeTests (rawTests: RunTests<MessageIds, Options>): NormalizedRunTests<MessageIds, Options>
defineRule (name: string, rule: AnyRuleModule): void
#runHook (item: InvalidTestCase<MessageIds, Options> \| ValidTestCase<Options>, prop: keyof Pick<typeof it...
verifyAssertionOptions (assertionOption: AssertionOptions \| undefined): void
run (ruleName: string, rule: RuleModule<MessageIds, Options>, test: RunTests<TSUtils.NoInfer<MessageI...
runRuleForItem (ruleName: string, rule: RuleModule<MessageIds, Options>, item: InvalidTestCase<MessageIds, Optio...
#testValidTemplate (ruleName: string, rule: RuleModule<MessageIds, Options>, itemIn: string \| ValidTestCase<Options...
#testInvalidTemplate (ruleName: string, rule: RuleModule<MessageIds, Options>, item: InvalidTestCase<MessageIds, Optio...

Type Aliases

ForbiddenFunction

type ForbiddenFunction = (this: WeakKey, ...args: never[]) => unknown;

ForbiddenMethodName

type ForbiddenMethodName = (typeof forbiddenMethods)[number];

Generated by Syntax Scribe