📄 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:
filenamestring | undefined
Returns: Linter
Calls:
path.isAbsolutepath.normalize(filename).startsWithpath.parsepath.resolveprocess.cwdthis.#lintersByBasePath.getthis.#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
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
RuleTester.resetDefaultConfig(): void¶
Reset the configuration to the initial configuration of the tester removing any changes made until now.
Raw JSDoc
Calls:
merge (from lodash.merge)
RuleTester.only(item: string | ValidTestCase<Options>): ValidTestCase<Options>¶
Adds the only property to a test to run it in isolation.
Code
RuleTester.#normalizeTests(rawTests: RunTests<MessageIds, Options>): NormalizedRunTests<MessageIds, Options>¶
Define a rule for one particular run of tests.
Calls:
deepMerge (from @typescript-eslint/utils/eslint-utils)path.joinprocess.cwdgetFilenamerawTests.invalid.maprawTests.valid .map(test => { if (typeof test === 'string') { return { code: test }; } return test; }) .mapcomplex_call_12631complex_call_13031Object.keyssatisfiesAllDependencyConstraints (from ./utils/dependencyConstraints)normalizedTests.valid.mapnormalizedTests.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:
namestringruleAnyRuleModule
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.strictEqualcomplex_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
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.
Calls:
this.verifyAssertionOptionssatisfiesAllDependencyConstraints (from ./utils/dependencyConstraints)constructor.describeSkipconstructor.itassert.equalREQUIRED_SCENARIOS.forEachscenarioErrors.push[Test Scenarios for rule ${ruleName} is invalid:, ...scenarioErrors, ].jointhis.#normalizeTestsconstructor.describenormalizedTests.valid.forEachcomplex_call_18554complex_call_18752getTestMethodsanitize (from ./utils/validationHelpers)this.#runHookthis.#testValidTemplatenormalizedTests.invalid.forEachcomplex_call_19369complex_call_19575this.#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
Calls:
this.defineRulemerge (from lodash.merge)cloneDeeplyExcludesParent (from ./utils/cloneDeeplyExcludesParent)hasOwnProperty (from ./utils/hasOwnProperty)assert.okassert (from node:assert)Array.isArraywrapParser (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].joinajv.compilevalidate (from ./utils/config-validator)omitCustomConfigProperties (from ./utils/omitCustomConfigProperties)this.#getLinterForFilenameforbiddenMethods.forEachthrowForbiddenMethodErrorlinter.verifymessages.findSourceCodeFixer.applyFixesoutputs.pushlinter .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
Calls:
assert.okcheckDuplicateTestCasethis.runRuleForItemassert.strictEqualutil.formatutil.inspectassertASTDidntChange
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.
Calls:
assert.okArray.isArrayassert.failcheckDuplicateTestCasehasOwnProperty (from ./utils/hasOwnProperty)Object.keys(rule.meta.messages) .map(key =>'${key}') .jointhis.runRuleForItemresult.messages.mapmessage.ruleId.startsWithmessage.ruleId.sliceseenMessageIndices.getseenMessageIndices.hasseenMessageIndices.setassert.strictEqualutil.formatutil.inspectmessages.someassert (from node:assert)assertMessageMatchesObject.keys(error).forEachERROR_OBJECT_PARAMETERS.hasgetUnsubstitutedMessagePlaceholdersunsubstitutedPlaceholders.map(name =>'${name}').joininterpolate (from ./utils/interpolate)getMessagePlaceholderslocationProperties.filtermissingKeys.joinBooleanerror.suggestions.forEachObject.keys(expectedSuggestion).forEachSUGGESTION_OBJECT_PARAMETERS.hasSourceCodeFixer.applyFixesthis.#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, ].joinassert.notStrictEqualassert.deepStrictEqualassertASTDidntChange
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:
methodNameany: The name of the method to forbid.prototypeany: The prototype with the original method to call.
Returns: undefined
The function that throws the error.
Raw JSDoc
Calls:
forbiddenMethodCalls.getcalled.hascalled.addoriginal.apply
Internal Comments:
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:
messageany: Reported message
Returns: undefined
Array of placeholder names
Raw JSDoc
Calls:
getPlaceholderMatcher (from ./utils/interpolate)Array.frommessage.matchAllname.trim
Code
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:
messageany: The reported messagerawany: The raw message specified in the rule meta.messagesdataany: 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:
getMessagePlaceholdersObject.keysunsubstituted.filterknown.includesprovided.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
Calls:
assert.deepStrictEqual
Code
checkDuplicateTestCase(item: unknown, seenTestCases: Set<unknown>): void¶
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.hasseenTestCases.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
Calls:
assert.okexpected.testassert.strictEqual
Internal Comments:
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:
originalFilenamestring | undefinedtestOptionsParserOptions | undefined
Returns: string
Calls:
deepMerge (from @typescript-eslint/utils/eslint-utils)path.joinprocess.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:
testT
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:
testT
Returns: T
Calls:
satisfiesAllDependencyConstraints (from ./utils/dependencyConstraints)
Code
getTestMethod(test: ValidTestCase<TSUtils.NoInfer<Options>>): 'it' | 'itOnly' | 'itSkip'¶
Parameters:
testValidTestCase<TSUtils.NoInfer<Options>>
Returns: 'it' | 'itOnly' | 'itSkip'
Code
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¶
ForbiddenMethodName¶
Generated by Syntax Scribe