diff --git a/apps/oxlint/src-js/package/rule_tester.ts b/apps/oxlint/src-js/package/rule_tester.ts index d01d1dd3396f9..53c5aef1d34db 100644 --- a/apps/oxlint/src-js/package/rule_tester.ts +++ b/apps/oxlint/src-js/package/rule_tester.ts @@ -111,10 +111,19 @@ function getItOnly(): ItFn { /** * Configuration for `RuleTester`. */ -export type Config = Record; +export interface Config { + /** + * ESLint compatibility mode. + * If `true`, column offsets in diagnostics are incremented by 1, to match ESLint's behavior. + */ + eslintCompat?: boolean; + [key: string]: unknown; +} // Default shared config -const DEFAULT_SHARED_CONFIG: Config = {}; +const DEFAULT_SHARED_CONFIG: Config = { + eslintCompat: false, +}; // `RuleTester` uses this config as its default. Can be overwritten via `RuleTester.setDefaultConfig()`. let sharedConfig: Config = DEFAULT_SHARED_CONFIG; @@ -134,6 +143,11 @@ interface TestCase { options?: Options; before?: (this: this) => void; after?: (this: this) => void; + /** + * `true` to enable ESLint compatibility mode. + * See `Config` type. + */ + eslintCompat?: boolean; } /** @@ -429,7 +443,7 @@ function assertInvalidTestCasePasses(test: InvalidTestCase, plugin: Plugin, conf } else { // `error` is an error object assertInvalidTestCaseMessageIsCorrect(diagnostic, error, messages); - assertInvalidTestCaseLocationIsCorrect(diagnostic, error); + assertInvalidTestCaseLocationIsCorrect(diagnostic, error, config); // TODO: Test suggestions } @@ -521,9 +535,14 @@ function assertInvalidTestCaseMessageIsCorrect( * Assert that location reported by rule under test matches the expected location. * @param diagnostic - Diagnostic emitted by rule under test * @param error - Error object from test case + * @param config - Config for this test case * @throws {AssertionError} If diagnostic's location does not match expected location */ -function assertInvalidTestCaseLocationIsCorrect(diagnostic: Diagnostic, error: Error) { +function assertInvalidTestCaseLocationIsCorrect( + diagnostic: Diagnostic, + error: Error, + config: Config, +) { interface Location { line?: number; column?: number; @@ -534,13 +553,15 @@ function assertInvalidTestCaseLocationIsCorrect(diagnostic: Diagnostic, error: E const actualLocation: Location = {}; const expectedLocation: Location = {}; + const columnOffset = config.eslintCompat === true ? 1 : 0; + if (hasOwn(error, "line")) { actualLocation.line = diagnostic.line; expectedLocation.line = error.line; } if (hasOwn(error, "column")) { - actualLocation.column = diagnostic.column; + actualLocation.column = diagnostic.column + columnOffset; expectedLocation.column = error.column; } @@ -550,7 +571,7 @@ function assertInvalidTestCaseLocationIsCorrect(diagnostic: Diagnostic, error: E } if (hasOwn(error, "endColumn")) { - actualLocation.endColumn = diagnostic.endColumn; + actualLocation.endColumn = diagnostic.endColumn + columnOffset; expectedLocation.endColumn = error.endColumn; } @@ -654,7 +675,19 @@ function createConfigForRun(config: Config | null): Config { * @returns Merged config */ function createConfigForTest(test: TestCase, config: Config): Config { - // TODO: Merge properties of `test` into `config` + let isCloned = false; + function clone(): void { + if (!isCloned) { + config = { ...config }; + isCloned = true; + } + } + + // TODO: Merge more properties of `test` into `config` + if (hasOwn(test, "eslintCompat")) { + clone(); + config.eslintCompat = test.eslintCompat; + } return config; } diff --git a/apps/oxlint/test/rule_tester.test.ts b/apps/oxlint/test/rule_tester.test.ts index b353c12bf84bd..811587a0d459b 100644 --- a/apps/oxlint/test/rule_tester.test.ts +++ b/apps/oxlint/test/rule_tester.test.ts @@ -816,4 +816,101 @@ describe("RuleTester", () => { expect(test.after.mock.contexts[0]).toBe(test); } }); + + describe("ESLint compat mode", () => { + it("enabled globally", () => { + RuleTester.setDefaultConfig({ eslintCompat: true }); + + const tester = new RuleTester(); + tester.run("no-foo", simpleRule, { + valid: [], + invalid: [ + { + code: "foo;", + errors: [ + { + message: "No foo!", + line: 1, + column: 1, + endLine: 1, + endColumn: 4, + }, + ], + }, + ], + }); + expect(runCases()).toEqual([null]); + }); + + it("enabled in `RuleTester` options", () => { + const tester = new RuleTester({ eslintCompat: true }); + tester.run("no-foo", simpleRule, { + valid: [], + invalid: [ + { + code: "foo;", + errors: [ + { + message: "No foo!", + line: 1, + column: 1, + endLine: 1, + endColumn: 4, + }, + ], + }, + ], + }); + expect(runCases()).toEqual([null]); + }); + + it("enabled in in individual test cases", () => { + const tester = new RuleTester(); + tester.run("no-foo", simpleRule, { + valid: [], + invalid: [ + { + code: "foo = 1;", + // Default: eslintCompat: false + errors: [ + { + message: "No foo!", + line: 1, + column: 0, + endLine: 1, + endColumn: 3, + }, + ], + }, + { + code: "foo = 1;", + eslintCompat: false, + errors: [ + { + message: "No foo!", + line: 1, + column: 0, + endLine: 1, + endColumn: 3, + }, + ], + }, + { + code: "foo = 1;", + eslintCompat: true, + errors: [ + { + message: "No foo!", + line: 1, + column: 1, // 1 not 0 + endLine: 1, + endColumn: 4, // 4 not 3 + }, + ], + }, + ], + }); + expect(runCases()).toEqual([null, null, null]); + }); + }); });