From ef3176ac3bcb6d900dc5c7f6b85ee7e76c63ea5f Mon Sep 17 00:00:00 2001 From: reduckted Date: Wed, 23 Oct 2024 22:01:07 +1000 Subject: [PATCH 1/5] Use cwd to set the base path for absolute and relative file names. --- packages/rule-tester/src/RuleTester.ts | 95 ++++++++++++++++---------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index 1cbd5e2cd226..5f7b941c905f 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -166,7 +166,7 @@ function getUnsubstitutedMessagePlaceholders( } export class RuleTester extends TestFramework { - readonly #linter: Linter; + readonly #lintersByBasePath: Map; readonly #rules: Record = {}; readonly #testerConfig: TesterConfigWithDefaults; @@ -184,31 +184,7 @@ export class RuleTester extends TestFramework { rules: { [`${RULE_TESTER_PLUGIN_PREFIX}validate-ast`]: 'error' }, }); - this.#linter = (() => { - const linter = new Linter({ - configType: 'flat', - cwd: this.#testerConfig.languageOptions.parserOptions?.tsconfigRootDir, - }); - - // This nonsense is a workaround for https://github.com/jestjs/jest/issues/14840 - // see also https://github.com/typescript-eslint/typescript-eslint/issues/8942 - // - // For some reason rethrowing exceptions skirts around the circular JSON error. - const oldVerify = linter.verify.bind(linter); - linter.verify = ( - ...args: Parameters - ): ReturnType => { - try { - return oldVerify(...args); - } catch (error) { - throw new Error('Caught an error while linting', { - cause: error, - }); - } - }; - - return linter; - })(); + this.#lintersByBasePath = new Map(); // make sure that the parser doesn't hold onto file handles between tests // on linux (i.e. our CI env), there can be very a limited number of watch handles available @@ -222,6 +198,56 @@ export class RuleTester extends TestFramework { }); } + #getLinterForFilename(filename: string | undefined): Linter { + let basePath: string | undefined = + this.#testerConfig.languageOptions.parserOptions?.tsconfigRootDir; + if (filename !== undefined) { + // 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.startsWith('/') || filename.startsWith('..')) { + basePath = path.parse( + path.resolve(basePath ?? process.cwd(), filename), + ).root; + } + } + + let linterForBasePath = this.#lintersByBasePath.get(basePath); + if (!linterForBasePath) { + linterForBasePath = (() => { + const linter = new Linter({ + configType: 'flat', + cwd: basePath, + }); + + // This nonsense is a workaround for https://github.com/jestjs/jest/issues/14840 + // see also https://github.com/typescript-eslint/typescript-eslint/issues/8942 + // + // For some reason rethrowing exceptions skirts around the circular JSON error. + const oldVerify = linter.verify.bind(linter); + linter.verify = ( + ...args: Parameters + ): ReturnType => { + try { + return oldVerify(...args); + } catch (error) { + throw new Error('Caught an error while linting', { + cause: error, + }); + } + }; + + return linter; + })(); + this.#lintersByBasePath.set(basePath, linterForBasePath); + } + return linterForBasePath; + } + /** * Set the configuration to use for all future tests */ @@ -738,6 +764,7 @@ export class RuleTester extends TestFramework { let passNumber = 0; const outputs: string[] = []; const configWithoutCustomKeys = omitCustomConfigProperties(config); + const linter = this.#getLinterForFilename(filename); do { passNumber++; @@ -767,15 +794,7 @@ export class RuleTester extends TestFramework { ...configWithoutCustomKeys.linterOptions, }, }); - messages = this.#linter.verify( - code, - // ESLint uses an internal FlatConfigArray that extends @humanwhocodes/config-array. - Object.assign([], { - basePath: filename ? path.parse(filename).root : '', - getConfig: () => actualConfig, - }), - filename, - ); + messages = linter.verify(code, actualConfig, filename); } finally { SourceCode.prototype.applyInlineConfig = applyInlineConfig; SourceCode.prototype.applyLanguageOptions = applyLanguageOptions; @@ -802,7 +821,7 @@ export class RuleTester extends TestFramework { outputs.push(code); // Verify if autofix makes a syntax error or not. - const errorMessageInFix = this.#linter + const errorMessageInFix = linter .verify(fixedResult.output, configWithoutCustomKeys, filename) .find(m => m.fatal); @@ -1255,7 +1274,9 @@ export class RuleTester extends TestFramework { ]).output; // Verify if suggestion fix makes a syntax error or not. - const errorMessageInSuggestion = this.#linter + const errorMessageInSuggestion = this.#getLinterForFilename( + undefined, + ) .verify( codeWithAppliedSuggestion, omitCustomConfigProperties(result.config), From 50ed82be65c5bb618d3ddc9cad1a62dbcbfead48 Mon Sep 17 00:00:00 2001 From: reduckted Date: Wed, 23 Oct 2024 22:27:41 +1000 Subject: [PATCH 2/5] Fixed lint errors --- packages/rule-tester/src/RuleTester.ts | 27 +++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index 5f7b941c905f..bd2f6bb3e4b8 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -201,19 +201,20 @@ export class RuleTester extends TestFramework { #getLinterForFilename(filename: string | undefined): Linter { let basePath: string | undefined = this.#testerConfig.languageOptions.parserOptions?.tsconfigRootDir; - if (filename !== undefined) { - // 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.startsWith('/') || filename.startsWith('..')) { - basePath = path.parse( - path.resolve(basePath ?? process.cwd(), filename), - ).root; - } + // 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 !== undefined && + (filename.startsWith('/') || filename.startsWith('..')) + ) { + basePath = path.parse( + path.resolve(basePath ?? process.cwd(), filename), + ).root; } let linterForBasePath = this.#lintersByBasePath.get(basePath); From 538f4cb418e84936015daf57f6d3522d5b25a3ce Mon Sep 17 00:00:00 2001 From: reduckted Date: Wed, 23 Oct 2024 22:40:26 +1000 Subject: [PATCH 3/5] Improved code coverage. --- packages/rule-tester/tests/filename.test.ts | 32 +++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/rule-tester/tests/filename.test.ts b/packages/rule-tester/tests/filename.test.ts index 1c68f4ba196d..03e2e99e2c3f 100644 --- a/packages/rule-tester/tests/filename.test.ts +++ b/packages/rule-tester/tests/filename.test.ts @@ -2,8 +2,6 @@ import { RuleTester } from '@typescript-eslint/rule-tester'; import { ESLintUtils } from '@typescript-eslint/utils'; -const ruleTester = new RuleTester(); - const rule = ESLintUtils.RuleCreator.withoutDocs({ meta: { docs: { @@ -24,6 +22,8 @@ const rule = ESLintUtils.RuleCreator.withoutDocs({ }); describe('rule tester filename', () => { + const ruleTester = new RuleTester(); + ruleTester.run('absolute path', rule, { invalid: [ { @@ -45,4 +45,32 @@ describe('rule tester filename', () => { ], valid: [], }); + + const ruleTesterWithRootDir = new RuleTester({ + languageOptions: { + parserOptions: { tsconfigRootDir: '/some/path/that/totally/exists/' }, + }, + }); + + ruleTesterWithRootDir.run('absolute path with root dir', rule, { + invalid: [ + { + code: '_', + errors: [{ messageId: 'foo' }], + filename: '/an-absolute-path/foo.js', + }, + ], + valid: [], + }); + + ruleTesterWithRootDir.run('relative path with root dir', rule, { + invalid: [ + { + code: '_', + errors: [{ messageId: 'foo' }], + filename: '../foo.js', + }, + ], + valid: [], + }); }); From 72ff7d54967ed1d22e6e60123b64376d31035c9e Mon Sep 17 00:00:00 2001 From: reduckted Date: Fri, 25 Oct 2024 20:04:15 +1000 Subject: [PATCH 4/5] Used relative import to fix code coverage. --- packages/rule-tester/tests/filename.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/rule-tester/tests/filename.test.ts b/packages/rule-tester/tests/filename.test.ts index 03e2e99e2c3f..5e75287922ab 100644 --- a/packages/rule-tester/tests/filename.test.ts +++ b/packages/rule-tester/tests/filename.test.ts @@ -1,7 +1,8 @@ /* eslint-disable perfectionist/sort-objects */ -import { RuleTester } from '@typescript-eslint/rule-tester'; import { ESLintUtils } from '@typescript-eslint/utils'; +import { RuleTester } from '../src/RuleTester'; + const rule = ESLintUtils.RuleCreator.withoutDocs({ meta: { docs: { From 1e47a9ec0473088730505a62b04ce4deb40fb00c Mon Sep 17 00:00:00 2001 From: reduckted Date: Mon, 28 Oct 2024 21:41:27 +1000 Subject: [PATCH 5/5] Used test filename when verifying suggestion, and increased code coverage. --- packages/rule-tester/src/RuleTester.ts | 2 +- packages/rule-tester/tests/filename.test.ts | 92 +++++++++++++++++---- 2 files changed, 75 insertions(+), 19 deletions(-) diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index bd2f6bb3e4b8..725e2a9460bc 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -1276,7 +1276,7 @@ export class RuleTester extends TestFramework { // Verify if suggestion fix makes a syntax error or not. const errorMessageInSuggestion = this.#getLinterForFilename( - undefined, + item.filename, ) .verify( codeWithAppliedSuggestion, diff --git a/packages/rule-tester/tests/filename.test.ts b/packages/rule-tester/tests/filename.test.ts index 5e75287922ab..7068aa7fd3b5 100644 --- a/packages/rule-tester/tests/filename.test.ts +++ b/packages/rule-tester/tests/filename.test.ts @@ -1,5 +1,7 @@ /* eslint-disable perfectionist/sort-objects */ -import { ESLintUtils } from '@typescript-eslint/utils'; +import type { TSESLint } from '@typescript-eslint/utils'; + +import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils'; import { RuleTester } from '../src/RuleTester'; @@ -10,35 +12,46 @@ const rule = ESLintUtils.RuleCreator.withoutDocs({ }, messages: { foo: 'It works', + createError: 'Create error', }, schema: [], type: 'problem', + hasSuggestions: true, }, defaultOptions: [], create: context => ({ Program(node): void { - context.report({ node, messageId: 'foo' }); + context.report({ + node, + messageId: 'foo', + suggest: + node.body.length === 1 && + node.body[0].type === AST_NODE_TYPES.EmptyStatement + ? [ + { + messageId: 'createError', + fix(fixer): TSESLint.RuleFix { + return fixer.replaceText(node, '//'); + }, + }, + ] + : [], + }); }, }), }); describe('rule tester filename', () => { - const ruleTester = new RuleTester(); - - ruleTester.run('absolute path', rule, { + new RuleTester().run('without tsconfigRootDir', rule, { invalid: [ { + name: 'absolute path', code: '_', errors: [{ messageId: 'foo' }], filename: '/an-absolute-path/foo.js', }, - ], - valid: [], - }); - - ruleTester.run('relative path', rule, { - invalid: [ { + name: 'relative path above project', code: '_', errors: [{ messageId: 'foo' }], filename: '../foo.js', @@ -47,31 +60,74 @@ describe('rule tester filename', () => { valid: [], }); - const ruleTesterWithRootDir = new RuleTester({ + new RuleTester({ languageOptions: { parserOptions: { tsconfigRootDir: '/some/path/that/totally/exists/' }, }, - }); - - ruleTesterWithRootDir.run('absolute path with root dir', rule, { + }).run('with tsconfigRootDir', rule, { invalid: [ { + name: 'absolute path', code: '_', errors: [{ messageId: 'foo' }], filename: '/an-absolute-path/foo.js', }, + { + name: 'relative path above project', + code: '_', + errors: [{ messageId: 'foo' }], + filename: '../foo.js', + }, ], valid: [], }); +}); - ruleTesterWithRootDir.run('relative path with root dir', rule, { +describe('rule tester suggestion syntax error checks', () => { + new RuleTester().run('verifies suggestion with absolute path', rule, { invalid: [ { - code: '_', - errors: [{ messageId: 'foo' }], + code: ';', + errors: [ + { + messageId: 'foo', + suggestions: [{ messageId: 'createError', output: '//' }], + }, + ], + filename: '/an-absolute-path/foo.js', + }, + ], + valid: [], + }); + + new RuleTester().run('verifies suggestion with relative path', rule, { + invalid: [ + { + code: ';', + errors: [ + { + messageId: 'foo', + suggestions: [{ messageId: 'createError', output: '//' }], + }, + ], filename: '../foo.js', }, ], valid: [], }); + + new RuleTester().run('verifies suggestion with no path', rule, { + invalid: [ + { + code: ';', + errors: [ + { + messageId: 'foo', + suggestions: [{ messageId: 'createError', output: '//' }], + }, + ], + }, + ], + valid: [], + }); });