diff --git a/packages/typescript-eslint/src/getTSConfigRootDirFromStack.ts b/packages/typescript-eslint/src/getTSConfigRootDirFromStack.ts index 01378b1618f..b4ac708ea3b 100644 --- a/packages/typescript-eslint/src/getTSConfigRootDirFromStack.ts +++ b/packages/typescript-eslint/src/getTSConfigRootDirFromStack.ts @@ -1,15 +1,40 @@ -import { fileURLToPath } from 'node:url'; +import path from 'node:path'; -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) { +/** + * Infers the `tsconfigRootDir` from the current call stack, using the V8 API. + * + * See https://v8.dev/docs/stack-trace-api + * + * This API is implemented in Deno and Bun as well. + */ +export function getTSConfigRootDirFromStack(): string | undefined { + function getStack(): NodeJS.CallSite[] { + const stackTraceLimit = Error.stackTraceLimit; + Error.stackTraceLimit = Infinity; + const prepareStackTrace = Error.prepareStackTrace; + Error.prepareStackTrace = (_, structuredStackTrace) => structuredStackTrace; + + const dummyObject: { stack?: NodeJS.CallSite[] } = {}; + Error.captureStackTrace(dummyObject, getTSConfigRootDirFromStack); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- stack is set by captureStackTrace + const rv = dummyObject.stack!; + + Error.prepareStackTrace = prepareStackTrace; + Error.stackTraceLimit = stackTraceLimit; + + return rv; + } + + for (const callSite of getStack()) { + const stackFrameFilePath = callSite.getFileName(); + if (!stackFrameFilePath) { continue; } - return candidate.startsWith('file://') - ? fileURLToPath(candidate) - : candidate; + const parsedPath = path.parse(stackFrameFilePath); + if (/^eslint\.config\.(c|m)?(j|t)s$/.test(parsedPath.base)) { + return parsedPath.dir; + } } return undefined; diff --git a/packages/typescript-eslint/src/index.ts b/packages/typescript-eslint/src/index.ts index 9892ede6065..b123d80b4db 100644 --- a/packages/typescript-eslint/src/index.ts +++ b/packages/typescript-eslint/src/index.ts @@ -135,10 +135,7 @@ function createConfigsGetters(values: T): T { { enumerable: true, get: () => { - const candidateRootDir = getTSConfigRootDirFromStack( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - new Error().stack!, - ); + const candidateRootDir = getTSConfigRootDirFromStack(); if (candidateRootDir) { addCandidateTSConfigRootDir(candidateRootDir); } diff --git a/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts b/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts index 57a3b0b5f2a..a9c169785c6 100644 --- a/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts +++ b/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts @@ -1,54 +1,28 @@ -import path from 'node:path'; - import { getTSConfigRootDirFromStack } from '../src/getTSConfigRootDirFromStack'; - -const isWindows = process.platform === 'win32'; +import * as normalFolder from './path-test-fixtures/tsconfigRootDirInference-normal/normal-folder/eslint.config.cjs'; +import * as notEslintConfig from './path-test-fixtures/tsconfigRootDirInference-not-eslint-config/not-an-eslint.config.cjs'; +import * as folderThatHasASpace from './path-test-fixtures/tsconfigRootDirInference-space/folder that has a space/eslint.config.cjs'; describe(getTSConfigRootDirFromStack, () => { - it('returns undefined when no file path seems to be an ESLint config', () => { - const actual = getTSConfigRootDirFromStack( - [ - `Error`, - ' at file:///other.config.js', - ' at ModuleJob.run', - 'at async NodeHfs.walk(...)', - ].join('\n'), - ); - - expect(actual).toBeUndefined(); + it('does stack analysis right for normal folder', () => { + expect(normalFolder.get()).toBe(normalFolder.dirname()); }); - 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 its extension is %s', - extension => { - const expected = isWindows ? 'C:\\path\\to\\file\\' : '/path/to/file/'; + it('does stack analysis right for folder that has a space', () => { + expect(folderThatHasASpace.get()).toBe(folderThatHasASpace.dirname()); + }); - const actual = getTSConfigRootDirFromStack( - [ - `Error`, - ` at ${path.join(expected, `eslint.config.${extension}`)}`, - ' at ModuleJob.run', - 'at async NodeHfs.walk(...)', - ].join('\n'), - ); + it("doesn't get tricked by a file that is not an ESLint config", () => { + expect(notEslintConfig.get()).toBeUndefined(); + }); - expect(actual).toBe(expected); - }, - ); + it('should work in the presence of a messed up strack trace string', () => { + const prepareStackTrace = Error.prepareStackTrace; + const dummyFunction = () => {}; + Error.prepareStackTrace = dummyFunction; + expect(new Error().stack).toBeUndefined(); + expect(normalFolder.get()).toBe(normalFolder.dirname()); + expect(Error.prepareStackTrace).toBe(dummyFunction); + Error.prepareStackTrace = prepareStackTrace; + }); }); diff --git a/packages/typescript-eslint/tests/path-test-fixtures/tsconfigRootDirInference-normal/normal-folder/eslint.config.cts b/packages/typescript-eslint/tests/path-test-fixtures/tsconfigRootDirInference-normal/normal-folder/eslint.config.cts new file mode 100644 index 00000000000..39678798c6d --- /dev/null +++ b/packages/typescript-eslint/tests/path-test-fixtures/tsconfigRootDirInference-normal/normal-folder/eslint.config.cts @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { getTSConfigRootDirFromStack } from './../../../../src/getTSConfigRootDirFromStack'; + +export function get() { + return getTSConfigRootDirFromStack(); +} + +export function dirname() { + return __dirname; +} diff --git a/packages/typescript-eslint/tests/path-test-fixtures/tsconfigRootDirInference-not-eslint-config/not-an-eslint.config.cts b/packages/typescript-eslint/tests/path-test-fixtures/tsconfigRootDirInference-not-eslint-config/not-an-eslint.config.cts new file mode 100644 index 00000000000..16f159971d9 --- /dev/null +++ b/packages/typescript-eslint/tests/path-test-fixtures/tsconfigRootDirInference-not-eslint-config/not-an-eslint.config.cts @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { getTSConfigRootDirFromStack } from '../../../src/getTSConfigRootDirFromStack'; + +export function get() { + return getTSConfigRootDirFromStack(); +} + +export function dirname() { + return __dirname; +} diff --git a/packages/typescript-eslint/tests/path-test-fixtures/tsconfigRootDirInference-space/folder that has a space/eslint.config.cts b/packages/typescript-eslint/tests/path-test-fixtures/tsconfigRootDirInference-space/folder that has a space/eslint.config.cts new file mode 100644 index 00000000000..ba723627ffa --- /dev/null +++ b/packages/typescript-eslint/tests/path-test-fixtures/tsconfigRootDirInference-space/folder that has a space/eslint.config.cts @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { getTSConfigRootDirFromStack } from '../../../../src/getTSConfigRootDirFromStack'; + +export function get() { + return getTSConfigRootDirFromStack(); +} + +export function dirname() { + return __dirname; +}