From f36817108a7399defbc9a29508d25e1419cae745 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 3 Jul 2025 14:55:45 -0400 Subject: [PATCH 01/13] feat(typescript-estree): infer tsconfigRootDir from call stack --- .../src/getRootDirFromStack.ts | 16 ++++++++ packages/typescript-eslint/src/index.ts | 34 +++++++++++++++- .../tests/getRootDirFromStack.test.ts | 32 +++++++++++++++ .../typescript-eslint/tsconfig.build.json | 3 ++ packages/typescript-eslint/tsconfig.json | 3 ++ .../typescript-estree/src/clear-caches.ts | 2 + packages/typescript-estree/src/index.ts | 1 + .../candidateTSConfigRootDirs.ts | 30 ++++++++++++++ .../src/parseSettings/createParseSettings.ts | 3 +- .../lib/candidateTSConfigRootDirs.test.ts | 39 +++++++++++++++++++ .../tests/lib/createParseSettings.test.ts | 36 +++++++++++++++++ 11 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 packages/typescript-eslint/src/getRootDirFromStack.ts create mode 100644 packages/typescript-eslint/tests/getRootDirFromStack.test.ts create mode 100644 packages/typescript-estree/src/parseSettings/candidateTSConfigRootDirs.ts create mode 100644 packages/typescript-estree/tests/lib/candidateTSConfigRootDirs.test.ts diff --git a/packages/typescript-eslint/src/getRootDirFromStack.ts b/packages/typescript-eslint/src/getRootDirFromStack.ts new file mode 100644 index 000000000000..25d87fb4f992 --- /dev/null +++ b/packages/typescript-eslint/src/getRootDirFromStack.ts @@ -0,0 +1,16 @@ +import { fileURLToPath } from 'node:url'; + +export function getRootDirFromStack(stack: string): string | undefined { + for (const line of stack.split('\n').map(line => line.trim())) { + const candidate = /(\S+)eslint\.config\.(c|m)?(j|t)s/.exec(line)?.[1]; + if (!candidate) { + continue; + } + + return candidate.startsWith('file://') + ? fileURLToPath(candidate) + : candidate; + } + + return undefined; +} diff --git a/packages/typescript-eslint/src/index.ts b/packages/typescript-eslint/src/index.ts index 86870c855cd0..2de6143d0b7a 100644 --- a/packages/typescript-eslint/src/index.ts +++ b/packages/typescript-eslint/src/index.ts @@ -3,8 +3,10 @@ import type { TSESLint } from '@typescript-eslint/utils'; import pluginBase from '@typescript-eslint/eslint-plugin'; import rawPlugin from '@typescript-eslint/eslint-plugin/use-at-your-own-risk/raw-plugin'; +import { addCandidateTSConfigRootDir } from '@typescript-eslint/typescript-estree'; import { config } from './config-helper'; +import { getRootDirFromStack } from './getRootDirFromStack'; export const parser: TSESLint.FlatConfig.Parser = rawPlugin.parser; @@ -36,7 +38,7 @@ export const plugin: TSESLint.FlatConfig.Plugin = pluginBase as Omit< 'configs' >; -export const configs = { +export const configs = createConfigsGetters({ /** * Enables each the rules provided as a part of typescript-eslint. Note that many rules are not applicable in all codebases, or are meant to be configured. * @see {@link https://typescript-eslint.io/users/configs#all} @@ -120,7 +122,35 @@ export const configs = { */ stylisticTypeCheckedOnly: rawPlugin.flatConfigs['flat/stylistic-type-checked-only'], -}; +}); + +function createConfigsGetters(values: T): T { + const configs = {}; + + Object.defineProperties( + configs, + Object.fromEntries( + Object.entries(values).map(([key, value]: [string, unknown]) => [ + key, + { + get: () => { + const candidateRootDir = getRootDirFromStack( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + new Error().stack!, + ); + if (candidateRootDir) { + addCandidateTSConfigRootDir(candidateRootDir); + } + + return value; + }, + }, + ]), + ), + ); + + return configs as T; +} export type Config = TSESLint.FlatConfig.ConfigFile; diff --git a/packages/typescript-eslint/tests/getRootDirFromStack.test.ts b/packages/typescript-eslint/tests/getRootDirFromStack.test.ts new file mode 100644 index 000000000000..293a0cfa27a0 --- /dev/null +++ b/packages/typescript-eslint/tests/getRootDirFromStack.test.ts @@ -0,0 +1,32 @@ +import { getRootDirFromStack } from '../src/getRootDirFromStack'; + +describe(getRootDirFromStack, () => { + it('returns undefined when no file path seems to be an ESLint config', () => { + const actual = getRootDirFromStack( + [ + `Error`, + ' at file:///index.js', + ' at ModuleJob.run', + 'at async NodeHfs.walk(...)', + ].join('\n'), + ); + + expect(actual).toBeUndefined(); + }); + + it.each(['cjs', 'cts', 'js', 'mjs', 'mts', 'ts'])( + 'returns the path to the config file when in a Unix system and its extension is %s', + extension => { + const actual = getRootDirFromStack( + [ + `Error`, + ` at file:///path/to/file/eslint.config.${extension}`, + ' at ModuleJob.run', + 'at async NodeHfs.walk(...)', + ].join('\n'), + ); + + expect(actual).toBe('/path/to/file/'); + }, + ); +}); diff --git a/packages/typescript-eslint/tsconfig.build.json b/packages/typescript-eslint/tsconfig.build.json index c41d1d4c30ed..8f377becd3cd 100644 --- a/packages/typescript-eslint/tsconfig.build.json +++ b/packages/typescript-eslint/tsconfig.build.json @@ -2,6 +2,9 @@ "extends": "../../tsconfig.build.json", "compilerOptions": {}, "references": [ + { + "path": "../typescript-estree/tsconfig.build.json" + }, { "path": "../utils/tsconfig.build.json" }, diff --git a/packages/typescript-eslint/tsconfig.json b/packages/typescript-eslint/tsconfig.json index d12794a533bf..f94f467e0b00 100644 --- a/packages/typescript-eslint/tsconfig.json +++ b/packages/typescript-eslint/tsconfig.json @@ -3,6 +3,9 @@ "files": [], "include": [], "references": [ + { + "path": "../typescript-estree" + }, { "path": "../utils" }, diff --git a/packages/typescript-estree/src/clear-caches.ts b/packages/typescript-estree/src/clear-caches.ts index 5e9867d3beab..3d5897953b70 100644 --- a/packages/typescript-estree/src/clear-caches.ts +++ b/packages/typescript-estree/src/clear-caches.ts @@ -3,6 +3,7 @@ import { clearDefaultProjectMatchedFiles, clearProgramCache as clearProgramCacheOriginal, } from './parser'; +import { clearCandidateTSConfigRootDirs } from './parseSettings/candidateTSConfigRootDirs'; import { clearTSConfigMatchCache, clearTSServerProjectService, @@ -17,6 +18,7 @@ import { clearGlobCache } from './parseSettings/resolveProjectList'; * - In custom lint tooling that iteratively lints one project at a time to prevent OOMs. */ export function clearCaches(): void { + clearCandidateTSConfigRootDirs(); clearDefaultProjectMatchedFiles(); clearProgramCacheOriginal(); clearWatchCaches(); diff --git a/packages/typescript-estree/src/index.ts b/packages/typescript-estree/src/index.ts index 39e921e8762a..a3beef50759c 100644 --- a/packages/typescript-estree/src/index.ts +++ b/packages/typescript-estree/src/index.ts @@ -16,6 +16,7 @@ export type { ParserServicesWithTypeInformation, TSESTreeOptions, } from './parser-options'; +export { addCandidateTSConfigRootDir } from './parseSettings/candidateTSConfigRootDirs'; export { simpleTraverse } from './simple-traverse'; export * from './ts-estree'; export { typescriptVersionIsAtLeast } from './version-check'; diff --git a/packages/typescript-estree/src/parseSettings/candidateTSConfigRootDirs.ts b/packages/typescript-estree/src/parseSettings/candidateTSConfigRootDirs.ts new file mode 100644 index 000000000000..37b38dc1b435 --- /dev/null +++ b/packages/typescript-estree/src/parseSettings/candidateTSConfigRootDirs.ts @@ -0,0 +1,30 @@ +const candidateTSConfigRootDirs = new Set(); + +export function addCandidateTSConfigRootDir(candidate: string): void { + candidateTSConfigRootDirs.add(candidate); +} + +export function clearCandidateTSConfigRootDirs(): void { + candidateTSConfigRootDirs.clear(); +} + +export function getInferredTSConfigRootDir(): string { + const entries = [...candidateTSConfigRootDirs]; + + switch (entries.length) { + case 0: + return process.cwd(); + + case 1: + return entries[0]; + + default: + throw new Error( + [ + 'No tsconfigRootDir was set, and multiple candidate TSConfigRootDirs are present:', + ...entries.map(candidate => ` - ${candidate}`), + "You'll need to explicitly set tsconfigRootDir in your parser options.", + ].join('\n'), + ); + } +} diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index 678b771ac3a5..6710e36b870f 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -15,6 +15,7 @@ import type { MutableParseSettings } from './index'; import { ensureAbsolutePath } from '../create-program/shared'; import { validateDefaultProjectForFilesGlob } from '../create-program/validateDefaultProjectForFilesGlob'; import { isSourceFile } from '../source-files'; +import { getInferredTSConfigRootDir } from './candidateTSConfigRootDirs'; import { DEFAULT_TSCONFIG_CACHE_DURATION_SECONDS, ExpiringCache, @@ -52,7 +53,7 @@ export function createParseSettings( const tsconfigRootDir = typeof tsestreeOptions.tsconfigRootDir === 'string' ? tsestreeOptions.tsconfigRootDir - : process.cwd(); + : getInferredTSConfigRootDir(); const passedLoggerFn = typeof tsestreeOptions.loggerFn === 'function'; const filePath = ensureAbsolutePath( typeof tsestreeOptions.filePath === 'string' && diff --git a/packages/typescript-estree/tests/lib/candidateTSConfigRootDirs.test.ts b/packages/typescript-estree/tests/lib/candidateTSConfigRootDirs.test.ts new file mode 100644 index 000000000000..9f59d6f507f9 --- /dev/null +++ b/packages/typescript-estree/tests/lib/candidateTSConfigRootDirs.test.ts @@ -0,0 +1,39 @@ +import { addCandidateTSConfigRootDir } from '../../src'; +import { + clearCandidateTSConfigRootDirs, + getInferredTSConfigRootDir, +} from '../../src/parseSettings/candidateTSConfigRootDirs'; + +describe(getInferredTSConfigRootDir, () => { + beforeEach(() => { + clearCandidateTSConfigRootDirs(); + }); + + it('returns process.cwd() when there are no candidates', () => { + const actual = getInferredTSConfigRootDir(); + + expect(actual).toBe(process.cwd()); + }); + + it('returns the candidate when there is one candidate', () => { + const candidate = 'a/b/c'; + addCandidateTSConfigRootDir(candidate); + + const actual = getInferredTSConfigRootDir(); + + expect(actual).toBe(candidate); + }); + + it('throws an error when there are multiple candidates', () => { + addCandidateTSConfigRootDir('a'); + addCandidateTSConfigRootDir('b'); + + expect(() => getInferredTSConfigRootDir()) + .toThrowErrorMatchingInlineSnapshot(` + [Error: No tsconfigRootDir was set, and multiple candidate TSConfigRootDirs are present: + - a + - b + You'll need to explicitly set tsconfigRootDir in your parser options.] + `); + }); +}); diff --git a/packages/typescript-estree/tests/lib/createParseSettings.test.ts b/packages/typescript-estree/tests/lib/createParseSettings.test.ts index ccb210fd8dbd..d3f5fea99819 100644 --- a/packages/typescript-estree/tests/lib/createParseSettings.test.ts +++ b/packages/typescript-estree/tests/lib/createParseSettings.test.ts @@ -1,3 +1,7 @@ +import { + addCandidateTSConfigRootDir, + clearCandidateTSConfigRootDirs, +} from '../../src/parseSettings/candidateTSConfigRootDirs'; import { createParseSettings } from '../../src/parseSettings/createParseSettings'; const projectService = { service: true }; @@ -61,4 +65,36 @@ describe(createParseSettings, () => { ); }); }); + + describe('tsconfigRootDir', () => { + beforeEach(() => { + clearCandidateTSConfigRootDirs(); + }); + + it('uses the provided tsconfigRootDir when it exists and no candidates exist', () => { + const tsconfigRootDir = 'a/b/c'; + + const parseSettings = createParseSettings('', { tsconfigRootDir }); + + expect(parseSettings.tsconfigRootDir).toBe(tsconfigRootDir); + }); + + it('uses the provided tsconfigRootDir when it exists and a candidate exists', () => { + addCandidateTSConfigRootDir('candidate'); + const tsconfigRootDir = 'a/b/c'; + + const parseSettings = createParseSettings('', { tsconfigRootDir }); + + expect(parseSettings.tsconfigRootDir).toBe(tsconfigRootDir); + }); + + it('uses the inferred candidate when no tsconfigRootDir is provided and a candidate exists', () => { + const tsconfigRootDir = 'a/b/c'; + addCandidateTSConfigRootDir(tsconfigRootDir); + + const parseSettings = createParseSettings(''); + + expect(parseSettings.tsconfigRootDir).toBe(tsconfigRootDir); + }); + }); }); From 88edcc3fcbd81301d7d143251e9285715de6df3d Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 3 Jul 2025 15:41:01 -0400 Subject: [PATCH 02/13] rename --- ...RootDirFromStack.ts => getTsconfigRootDirFromStack.ts} | 2 +- packages/typescript-eslint/src/index.ts | 4 ++-- ...mStack.test.ts => getTsconfigRootDirFromStack.test.ts} | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) rename packages/typescript-eslint/src/{getRootDirFromStack.ts => getTsconfigRootDirFromStack.ts} (81%) rename packages/typescript-eslint/tests/{getRootDirFromStack.test.ts => getTsconfigRootDirFromStack.test.ts} (75%) diff --git a/packages/typescript-eslint/src/getRootDirFromStack.ts b/packages/typescript-eslint/src/getTsconfigRootDirFromStack.ts similarity index 81% rename from packages/typescript-eslint/src/getRootDirFromStack.ts rename to packages/typescript-eslint/src/getTsconfigRootDirFromStack.ts index 25d87fb4f992..7cc4e87a66fd 100644 --- a/packages/typescript-eslint/src/getRootDirFromStack.ts +++ b/packages/typescript-eslint/src/getTsconfigRootDirFromStack.ts @@ -1,6 +1,6 @@ import { fileURLToPath } from 'node:url'; -export function getRootDirFromStack(stack: string): string | undefined { +export function getTsconfigRootDirFromStack(stack: string): string | undefined { for (const line of stack.split('\n').map(line => line.trim())) { const candidate = /(\S+)eslint\.config\.(c|m)?(j|t)s/.exec(line)?.[1]; if (!candidate) { diff --git a/packages/typescript-eslint/src/index.ts b/packages/typescript-eslint/src/index.ts index 2de6143d0b7a..91ac90534ccb 100644 --- a/packages/typescript-eslint/src/index.ts +++ b/packages/typescript-eslint/src/index.ts @@ -6,7 +6,7 @@ import rawPlugin from '@typescript-eslint/eslint-plugin/use-at-your-own-risk/raw import { addCandidateTSConfigRootDir } from '@typescript-eslint/typescript-estree'; import { config } from './config-helper'; -import { getRootDirFromStack } from './getRootDirFromStack'; +import { getTsconfigRootDirFromStack } from './getTsconfigRootDirFromStack'; export const parser: TSESLint.FlatConfig.Parser = rawPlugin.parser; @@ -134,7 +134,7 @@ function createConfigsGetters(values: T): T { key, { get: () => { - const candidateRootDir = getRootDirFromStack( + const candidateRootDir = getTsconfigRootDirFromStack( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion new Error().stack!, ); diff --git a/packages/typescript-eslint/tests/getRootDirFromStack.test.ts b/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts similarity index 75% rename from packages/typescript-eslint/tests/getRootDirFromStack.test.ts rename to packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts index 293a0cfa27a0..63766760984e 100644 --- a/packages/typescript-eslint/tests/getRootDirFromStack.test.ts +++ b/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts @@ -1,8 +1,8 @@ -import { getRootDirFromStack } from '../src/getRootDirFromStack'; +import { getTsconfigRootDirFromStack } from '../src/getTsconfigRootDirFromStack'; -describe(getRootDirFromStack, () => { +describe(getTsconfigRootDirFromStack, () => { it('returns undefined when no file path seems to be an ESLint config', () => { - const actual = getRootDirFromStack( + const actual = getTsconfigRootDirFromStack( [ `Error`, ' at file:///index.js', @@ -17,7 +17,7 @@ describe(getRootDirFromStack, () => { it.each(['cjs', 'cts', 'js', 'mjs', 'mts', 'ts'])( 'returns the path to the config file when in a Unix system and its extension is %s', extension => { - const actual = getRootDirFromStack( + const actual = getTsconfigRootDirFromStack( [ `Error`, ` at file:///path/to/file/eslint.config.${extension}`, From c42ce4ca985ef53dd501fb8e21e5881610908af8 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 4 Jul 2025 07:45:48 -0500 Subject: [PATCH 03/13] Adjust for Windows --- .../tests/getTsconfigRootDirFromStack.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts b/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts index 63766760984e..ffbbd94e3448 100644 --- a/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts +++ b/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts @@ -1,3 +1,4 @@ +import path from 'node:path'; import { getTsconfigRootDirFromStack } from '../src/getTsconfigRootDirFromStack'; describe(getTsconfigRootDirFromStack, () => { @@ -15,18 +16,23 @@ describe(getTsconfigRootDirFromStack, () => { }); it.each(['cjs', 'cts', 'js', 'mjs', 'mts', 'ts'])( - 'returns the path to the config file when in a Unix system and its extension is %s', + 'returns the path to the config file when in a Posix system and its extension is %s', extension => { + const expected = + process.platform === 'win32' + ? 'C:\\path\\to\\file\\' + : '/path/to/file/'; + const actual = getTsconfigRootDirFromStack( [ `Error`, - ` at file:///path/to/file/eslint.config.${extension}`, + ` at ${path.join(expected, `eslint.config.${extension}`)}`, ' at ModuleJob.run', 'at async NodeHfs.walk(...)', ].join('\n'), ); - expect(actual).toBe('/path/to/file/'); + expect(actual).toBe(expected); }, ); }); From 2856485cf14ee9b9d43570f73b2117ac7d6b5dc4 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 4 Jul 2025 09:02:49 -0400 Subject: [PATCH 04/13] lint --- .../tests/getTsconfigRootDirFromStack.test.ts | 1 + packages/website/data/recent-blog-posts.json | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 packages/website/data/recent-blog-posts.json diff --git a/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts b/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts index ffbbd94e3448..3f1ca139b301 100644 --- a/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts +++ b/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts @@ -1,4 +1,5 @@ import path from 'node:path'; + import { getTsconfigRootDirFromStack } from '../src/getTsconfigRootDirFromStack'; describe(getTsconfigRootDirFromStack, () => { diff --git a/packages/website/data/recent-blog-posts.json b/packages/website/data/recent-blog-posts.json new file mode 100644 index 000000000000..9bc0ece682e5 --- /dev/null +++ b/packages/website/data/recent-blog-posts.json @@ -0,0 +1,23 @@ +[ + { + "date": "2025-05-29T00:00:00.000Z", + "description": "How typescript-eslint's new \"Project Service\" makes typed linting easier to configure, especially for large projects.", + "readingTime": 7, + "slug": "project-service", + "title": "Typed Linting with Project Service" + }, + { + "date": "2025-01-21T00:00:00.000Z", + "description": "How typescript-eslint expands on TypeScript's type safety to catch explicit and implicit `any`s.", + "readingTime": 9, + "slug": "avoiding-anys", + "title": "Avoiding `any`s with Linting and TypeScript" + }, + { + "date": "2024-09-30T00:00:00.000Z", + "description": "Explaining what linting with type information means, why it's so powerful, and some of the useful rules you can enable that use it.", + "readingTime": 8, + "slug": "typed-linting", + "title": "Typed Linting: The Most Powerful TypeScript Linting Ever" + } +] From 5474282cd4186d153e90c6261fa6db17ad986a6d Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 4 Jul 2025 09:27:45 -0400 Subject: [PATCH 05/13] Unit tested the candidate detection --- .../src/getTsconfigRootDirFromStack.ts | 2 +- packages/typescript-eslint/src/index.ts | 5 +- .../typescript-eslint/tests/configs.test.ts | 49 +++++++++++++++++++ .../tests/getTsconfigRootDirFromStack.test.ts | 8 +-- packages/typescript-estree/src/index.ts | 5 +- 5 files changed, 61 insertions(+), 8 deletions(-) diff --git a/packages/typescript-eslint/src/getTsconfigRootDirFromStack.ts b/packages/typescript-eslint/src/getTsconfigRootDirFromStack.ts index 7cc4e87a66fd..01378b1618f3 100644 --- a/packages/typescript-eslint/src/getTsconfigRootDirFromStack.ts +++ b/packages/typescript-eslint/src/getTsconfigRootDirFromStack.ts @@ -1,6 +1,6 @@ import { fileURLToPath } from 'node:url'; -export function getTsconfigRootDirFromStack(stack: string): string | undefined { +export function getTSConfigRootDirFromStack(stack: string): string | undefined { for (const line of stack.split('\n').map(line => line.trim())) { const candidate = /(\S+)eslint\.config\.(c|m)?(j|t)s/.exec(line)?.[1]; if (!candidate) { diff --git a/packages/typescript-eslint/src/index.ts b/packages/typescript-eslint/src/index.ts index 91ac90534ccb..9892ede6065a 100644 --- a/packages/typescript-eslint/src/index.ts +++ b/packages/typescript-eslint/src/index.ts @@ -6,7 +6,7 @@ import rawPlugin from '@typescript-eslint/eslint-plugin/use-at-your-own-risk/raw import { addCandidateTSConfigRootDir } from '@typescript-eslint/typescript-estree'; import { config } from './config-helper'; -import { getTsconfigRootDirFromStack } from './getTsconfigRootDirFromStack'; +import { getTSConfigRootDirFromStack } from './getTSConfigRootDirFromStack'; export const parser: TSESLint.FlatConfig.Parser = rawPlugin.parser; @@ -133,8 +133,9 @@ function createConfigsGetters(values: T): T { Object.entries(values).map(([key, value]: [string, unknown]) => [ key, { + enumerable: true, get: () => { - const candidateRootDir = getTsconfigRootDirFromStack( + const candidateRootDir = getTSConfigRootDirFromStack( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion new Error().stack!, ); diff --git a/packages/typescript-eslint/tests/configs.test.ts b/packages/typescript-eslint/tests/configs.test.ts index fea7a41196f4..cac7e4f46bd6 100644 --- a/packages/typescript-eslint/tests/configs.test.ts +++ b/packages/typescript-eslint/tests/configs.test.ts @@ -4,9 +4,25 @@ import type { } from '@typescript-eslint/utils/ts-eslint'; import rules from '@typescript-eslint/eslint-plugin/use-at-your-own-risk/rules'; +import { clearCandidateTSConfigRootDirs } from '@typescript-eslint/typescript-estree'; import tseslint from '../src/index.js'; +vi.mock('@typescript-eslint/typescript-estree', async () => ({ + ...(await vi.importActual('@typescript-eslint/typescript-estree')), + get addCandidateTSConfigRootDir() { + return mockAddCandidateTSConfigRootDir; + }, +})); + +const mockGetTSConfigRootDirFromStack = vi.fn(); + +vi.mock('../src/getTSConfigRootDirFromStack', () => ({ + get getTSConfigRootDirFromStack() { + return mockGetTSConfigRootDirFromStack; + }, +})); + const RULE_NAME_PREFIX = '@typescript-eslint/'; const EXTENSION_RULES = Object.entries(rules) .filter(([, rule]) => rule.meta.docs.extendsBaseRule) @@ -384,3 +400,36 @@ describe('stylistic-type-checked-only.ts', () => { }, ); }); + +const mockAddCandidateTSConfigRootDir = vi.fn(); + +describe('Candidate tsconfigRootDirs', () => { + beforeEach(() => { + clearCandidateTSConfigRootDirs(); + mockAddCandidateTSConfigRootDir.mockClear(); + }); + + describe.each(Object.keys(tseslint.configs))('%s', configKey => { + it('does not populate a candidate tsconfigRootDir when accessed and one cannot be inferred from the stack', () => { + mockGetTSConfigRootDirFromStack.mockReturnValue(undefined); + + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + tseslint.configs[configKey as keyof typeof tseslint.configs]; + + expect(mockAddCandidateTSConfigRootDir).not.toHaveBeenCalled(); + }); + + it('populates a candidate tsconfigRootDir when accessed and one can be inferred from the stack', () => { + const tsconfigRootDir = 'a/b/c/'; + + mockGetTSConfigRootDirFromStack.mockReturnValue(tsconfigRootDir); + + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + tseslint.configs[configKey as keyof typeof tseslint.configs]; + + expect(mockAddCandidateTSConfigRootDir).toHaveBeenCalledWith( + tsconfigRootDir, + ); + }); + }); +}); diff --git a/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts b/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts index 3f1ca139b301..1a8898faf059 100644 --- a/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts +++ b/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts @@ -1,10 +1,10 @@ import path from 'node:path'; -import { getTsconfigRootDirFromStack } from '../src/getTsconfigRootDirFromStack'; +import { getTSConfigRootDirFromStack } from '../src/getTSConfigRootDirFromStack'; -describe(getTsconfigRootDirFromStack, () => { +describe(getTSConfigRootDirFromStack, () => { it('returns undefined when no file path seems to be an ESLint config', () => { - const actual = getTsconfigRootDirFromStack( + const actual = getTSConfigRootDirFromStack( [ `Error`, ' at file:///index.js', @@ -24,7 +24,7 @@ describe(getTsconfigRootDirFromStack, () => { ? 'C:\\path\\to\\file\\' : '/path/to/file/'; - const actual = getTsconfigRootDirFromStack( + const actual = getTSConfigRootDirFromStack( [ `Error`, ` at ${path.join(expected, `eslint.config.${extension}`)}`, diff --git a/packages/typescript-estree/src/index.ts b/packages/typescript-estree/src/index.ts index a3beef50759c..654ea73982d2 100644 --- a/packages/typescript-estree/src/index.ts +++ b/packages/typescript-estree/src/index.ts @@ -16,7 +16,10 @@ export type { ParserServicesWithTypeInformation, TSESTreeOptions, } from './parser-options'; -export { addCandidateTSConfigRootDir } from './parseSettings/candidateTSConfigRootDirs'; +export { + addCandidateTSConfigRootDir, + clearCandidateTSConfigRootDirs, +} from './parseSettings/candidateTSConfigRootDirs'; export { simpleTraverse } from './simple-traverse'; export * from './ts-estree'; export { typescriptVersionIsAtLeast } from './version-check'; From 4d7aae939f0664b5e276b7b20fd55522d6ea23c6 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 4 Jul 2025 09:32:13 -0400 Subject: [PATCH 06/13] mv --- ...TsconfigRootDirFromStack.ts => getTSConfigRootDirFromStack.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/typescript-eslint/src/{getTsconfigRootDirFromStack.ts => getTSConfigRootDirFromStack.ts} (100%) diff --git a/packages/typescript-eslint/src/getTsconfigRootDirFromStack.ts b/packages/typescript-eslint/src/getTSConfigRootDirFromStack.ts similarity index 100% rename from packages/typescript-eslint/src/getTsconfigRootDirFromStack.ts rename to packages/typescript-eslint/src/getTSConfigRootDirFromStack.ts From 3a94f405de52ce38bfbd128f9ef68a5f42828aa7 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 4 Jul 2025 09:38:23 -0400 Subject: [PATCH 07/13] rm packages/website/data/recent-blog-posts.json --- packages/website/data/recent-blog-posts.json | 23 -------------------- 1 file changed, 23 deletions(-) delete mode 100644 packages/website/data/recent-blog-posts.json diff --git a/packages/website/data/recent-blog-posts.json b/packages/website/data/recent-blog-posts.json deleted file mode 100644 index 9bc0ece682e5..000000000000 --- a/packages/website/data/recent-blog-posts.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - { - "date": "2025-05-29T00:00:00.000Z", - "description": "How typescript-eslint's new \"Project Service\" makes typed linting easier to configure, especially for large projects.", - "readingTime": 7, - "slug": "project-service", - "title": "Typed Linting with Project Service" - }, - { - "date": "2025-01-21T00:00:00.000Z", - "description": "How typescript-eslint expands on TypeScript's type safety to catch explicit and implicit `any`s.", - "readingTime": 9, - "slug": "avoiding-anys", - "title": "Avoiding `any`s with Linting and TypeScript" - }, - { - "date": "2024-09-30T00:00:00.000Z", - "description": "Explaining what linting with type information means, why it's so powerful, and some of the useful rules you can enable that use it.", - "readingTime": 8, - "slug": "typed-linting", - "title": "Typed Linting: The Most Powerful TypeScript Linting Ever" - } -] From 571870885c023e2740b916e186689972e6992a20 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 4 Jul 2025 10:03:43 -0400 Subject: [PATCH 08/13] chore: have website lint depend on its build --- packages/website/package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/website/package.json b/packages/website/package.json index 117723cf3ace..528850f495fd 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -113,7 +113,10 @@ } }, "lint": { - "command": "eslint" + "command": "eslint", + "dependsOn": [ + "build" + ] }, "start": { "command": "docusaurus start", From 9266c7d9ea20373d1a40878595024e4a61245d7c Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 4 Jul 2025 10:15:14 -0400 Subject: [PATCH 09/13] Add file:/// testing back --- .../tests/getTsconfigRootDirFromStack.test.ts | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts b/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts index 1a8898faf059..57a3b0b5f2a3 100644 --- a/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts +++ b/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts @@ -2,12 +2,14 @@ import path from 'node:path'; import { getTSConfigRootDirFromStack } from '../src/getTSConfigRootDirFromStack'; +const isWindows = process.platform === 'win32'; + describe(getTSConfigRootDirFromStack, () => { it('returns undefined when no file path seems to be an ESLint config', () => { const actual = getTSConfigRootDirFromStack( [ `Error`, - ' at file:///index.js', + ' at file:///other.config.js', ' at ModuleJob.run', 'at async NodeHfs.walk(...)', ].join('\n'), @@ -16,13 +18,26 @@ describe(getTSConfigRootDirFromStack, () => { expect(actual).toBeUndefined(); }); + it.runIf(!isWindows)( + 'returns a Posix config file path when a file:// path to an ESLint config is in the stack', + () => { + const actual = getTSConfigRootDirFromStack( + [ + `Error`, + ' at file:///path/to/file/eslint.config.js', + ' at ModuleJob.run', + 'at async NodeHfs.walk(...)', + ].join('\n'), + ); + + expect(actual).toBe('/path/to/file/'); + }, + ); + it.each(['cjs', 'cts', 'js', 'mjs', 'mts', 'ts'])( - 'returns the path to the config file when in a Posix system and its extension is %s', + 'returns the path to the config file when its extension is %s', extension => { - const expected = - process.platform === 'win32' - ? 'C:\\path\\to\\file\\' - : '/path/to/file/'; + const expected = isWindows ? 'C:\\path\\to\\file\\' : '/path/to/file/'; const actual = getTSConfigRootDirFromStack( [ From 7c0a5fc8a27bcd7979b993908b1d91b2d3950053 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 9 Jul 2025 17:26:44 -0400 Subject: [PATCH 10/13] Add doc link --- .../src/parseSettings/candidateTSConfigRootDirs.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/typescript-estree/src/parseSettings/candidateTSConfigRootDirs.ts b/packages/typescript-estree/src/parseSettings/candidateTSConfigRootDirs.ts index 37b38dc1b435..13e19c8aac8a 100644 --- a/packages/typescript-estree/src/parseSettings/candidateTSConfigRootDirs.ts +++ b/packages/typescript-estree/src/parseSettings/candidateTSConfigRootDirs.ts @@ -24,6 +24,7 @@ export function getInferredTSConfigRootDir(): string { 'No tsconfigRootDir was set, and multiple candidate TSConfigRootDirs are present:', ...entries.map(candidate => ` - ${candidate}`), "You'll need to explicitly set tsconfigRootDir in your parser options.", + 'See: https://typescript-eslint.io/packages/parser/#tsconfigrootdir', ].join('\n'), ); } From 13879b7c87c7e472b55d5f6a013bb767585f8fb6 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 9 Jul 2025 17:27:28 -0400 Subject: [PATCH 11/13] dep --- packages/typescript-estree/package.json | 1 + yarn.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/typescript-estree/package.json b/packages/typescript-estree/package.json index 96bbcff65e32..fd0b95462a45 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -55,6 +55,7 @@ "@typescript-eslint/project-service": "8.35.1", "@typescript-eslint/tsconfig-utils": "8.35.1", "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/typescript-estree": "8.35.1", "@typescript-eslint/visitor-keys": "8.35.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", diff --git a/yarn.lock b/yarn.lock index 400a0f938286..d72e9977bb4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6008,6 +6008,7 @@ __metadata: "@typescript-eslint/project-service": 8.35.1 "@typescript-eslint/tsconfig-utils": 8.35.1 "@typescript-eslint/types": 8.35.1 + "@typescript-eslint/typescript-estree": 8.35.1 "@typescript-eslint/visitor-keys": 8.35.1 "@vitest/coverage-v8": ^3.1.3 debug: ^4.3.4 From 37d822b1d15785fe9ff151d792f1d3d48e67608a Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 9 Jul 2025 18:27:58 -0400 Subject: [PATCH 12/13] Heh, wrong paackage --- packages/typescript-eslint/package.json | 1 + packages/typescript-estree/package.json | 1 - yarn.lock | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/typescript-eslint/package.json b/packages/typescript-eslint/package.json index f68c8c9991b7..6c0144e06b20 100644 --- a/packages/typescript-eslint/package.json +++ b/packages/typescript-eslint/package.json @@ -52,6 +52,7 @@ "dependencies": { "@typescript-eslint/eslint-plugin": "8.36.0", "@typescript-eslint/parser": "8.36.0", + "@typescript-eslint/typescript-estree": "8.36.0", "@typescript-eslint/utils": "8.36.0" }, "peerDependencies": { diff --git a/packages/typescript-estree/package.json b/packages/typescript-estree/package.json index d794c4139a02..b6a2ff8b2468 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -55,7 +55,6 @@ "@typescript-eslint/project-service": "8.36.0", "@typescript-eslint/tsconfig-utils": "8.36.0", "@typescript-eslint/types": "8.36.0", - "@typescript-eslint/typescript-estree": "8.36.0", "@typescript-eslint/visitor-keys": "8.36.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", diff --git a/yarn.lock b/yarn.lock index 160d31b9b942..08bde6338a88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6214,7 +6214,6 @@ __metadata: "@typescript-eslint/project-service": 8.36.0 "@typescript-eslint/tsconfig-utils": 8.36.0 "@typescript-eslint/types": 8.36.0 - "@typescript-eslint/typescript-estree": 8.36.0 "@typescript-eslint/visitor-keys": 8.36.0 "@vitest/coverage-v8": ^3.1.3 debug: ^4.3.4 @@ -19803,6 +19802,7 @@ __metadata: dependencies: "@typescript-eslint/eslint-plugin": 8.36.0 "@typescript-eslint/parser": 8.36.0 + "@typescript-eslint/typescript-estree": 8.36.0 "@typescript-eslint/utils": 8.36.0 "@vitest/coverage-v8": ^3.1.3 eslint: "*" From 718b8810c48e4c7f8e39a288c437e63c6799f11b Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 9 Jul 2025 18:36:08 -0400 Subject: [PATCH 13/13] yarn test -u --- .../tests/lib/candidateTSConfigRootDirs.test.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/typescript-estree/tests/lib/candidateTSConfigRootDirs.test.ts b/packages/typescript-estree/tests/lib/candidateTSConfigRootDirs.test.ts index 9f59d6f507f9..7536d56a205d 100644 --- a/packages/typescript-estree/tests/lib/candidateTSConfigRootDirs.test.ts +++ b/packages/typescript-estree/tests/lib/candidateTSConfigRootDirs.test.ts @@ -30,10 +30,11 @@ describe(getInferredTSConfigRootDir, () => { expect(() => getInferredTSConfigRootDir()) .toThrowErrorMatchingInlineSnapshot(` - [Error: No tsconfigRootDir was set, and multiple candidate TSConfigRootDirs are present: - - a - - b - You'll need to explicitly set tsconfigRootDir in your parser options.] - `); + [Error: No tsconfigRootDir was set, and multiple candidate TSConfigRootDirs are present: + - a + - b + You'll need to explicitly set tsconfigRootDir in your parser options. + See: https://typescript-eslint.io/packages/parser/#tsconfigrootdir] + `); }); }); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy