Skip to content

Commit abb7c4d

Browse files
fix(typescript-eslint): infer tsconfigRootDir with v8 API (#11412)
* infer tsconfigRootDir with v8 API * fix up test * fixup * ensure that test works with broken stack trace * remove unnecessary defensive coding * remove AI-looking comment
1 parent 5ec8c58 commit abb7c4d

File tree

6 files changed

+84
-58
lines changed

6 files changed

+84
-58
lines changed

packages/typescript-eslint/src/getTSConfigRootDirFromStack.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,40 @@
1-
import { fileURLToPath } from 'node:url';
1+
import path from 'node:path';
22

3-
export function getTSConfigRootDirFromStack(stack: string): string | undefined {
4-
for (const line of stack.split('\n').map(line => line.trim())) {
5-
const candidate = /(\S+)eslint\.config\.(c|m)?(j|t)s/.exec(line)?.[1];
6-
if (!candidate) {
3+
/**
4+
* Infers the `tsconfigRootDir` from the current call stack, using the V8 API.
5+
*
6+
* See https://v8.dev/docs/stack-trace-api
7+
*
8+
* This API is implemented in Deno and Bun as well.
9+
*/
10+
export function getTSConfigRootDirFromStack(): string | undefined {
11+
function getStack(): NodeJS.CallSite[] {
12+
const stackTraceLimit = Error.stackTraceLimit;
13+
Error.stackTraceLimit = Infinity;
14+
const prepareStackTrace = Error.prepareStackTrace;
15+
Error.prepareStackTrace = (_, structuredStackTrace) => structuredStackTrace;
16+
17+
const dummyObject: { stack?: NodeJS.CallSite[] } = {};
18+
Error.captureStackTrace(dummyObject, getTSConfigRootDirFromStack);
19+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- stack is set by captureStackTrace
20+
const rv = dummyObject.stack!;
21+
22+
Error.prepareStackTrace = prepareStackTrace;
23+
Error.stackTraceLimit = stackTraceLimit;
24+
25+
return rv;
26+
}
27+
28+
for (const callSite of getStack()) {
29+
const stackFrameFilePath = callSite.getFileName();
30+
if (!stackFrameFilePath) {
731
continue;
832
}
933

10-
return candidate.startsWith('file://')
11-
? fileURLToPath(candidate)
12-
: candidate;
34+
const parsedPath = path.parse(stackFrameFilePath);
35+
if (/^eslint\.config\.(c|m)?(j|t)s$/.test(parsedPath.base)) {
36+
return parsedPath.dir;
37+
}
1338
}
1439

1540
return undefined;

packages/typescript-eslint/src/index.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,7 @@ function createConfigsGetters<T extends object>(values: T): T {
135135
{
136136
enumerable: true,
137137
get: () => {
138-
const candidateRootDir = getTSConfigRootDirFromStack(
139-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
140-
new Error().stack!,
141-
);
138+
const candidateRootDir = getTSConfigRootDirFromStack();
142139
if (candidateRootDir) {
143140
addCandidateTSConfigRootDir(candidateRootDir);
144141
}
Lines changed: 20 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,28 @@
1-
import path from 'node:path';
2-
31
import { getTSConfigRootDirFromStack } from '../src/getTSConfigRootDirFromStack';
4-
5-
const isWindows = process.platform === 'win32';
2+
import * as normalFolder from './path-test-fixtures/tsconfigRootDirInference-normal/normal-folder/eslint.config.cjs';
3+
import * as notEslintConfig from './path-test-fixtures/tsconfigRootDirInference-not-eslint-config/not-an-eslint.config.cjs';
4+
import * as folderThatHasASpace from './path-test-fixtures/tsconfigRootDirInference-space/folder that has a space/eslint.config.cjs';
65

76
describe(getTSConfigRootDirFromStack, () => {
8-
it('returns undefined when no file path seems to be an ESLint config', () => {
9-
const actual = getTSConfigRootDirFromStack(
10-
[
11-
`Error`,
12-
' at file:///other.config.js',
13-
' at ModuleJob.run',
14-
'at async NodeHfs.walk(...)',
15-
].join('\n'),
16-
);
17-
18-
expect(actual).toBeUndefined();
7+
it('does stack analysis right for normal folder', () => {
8+
expect(normalFolder.get()).toBe(normalFolder.dirname());
199
});
2010

21-
it.runIf(!isWindows)(
22-
'returns a Posix config file path when a file:// path to an ESLint config is in the stack',
23-
() => {
24-
const actual = getTSConfigRootDirFromStack(
25-
[
26-
`Error`,
27-
' at file:///path/to/file/eslint.config.js',
28-
' at ModuleJob.run',
29-
'at async NodeHfs.walk(...)',
30-
].join('\n'),
31-
);
32-
33-
expect(actual).toBe('/path/to/file/');
34-
},
35-
);
36-
37-
it.each(['cjs', 'cts', 'js', 'mjs', 'mts', 'ts'])(
38-
'returns the path to the config file when its extension is %s',
39-
extension => {
40-
const expected = isWindows ? 'C:\\path\\to\\file\\' : '/path/to/file/';
11+
it('does stack analysis right for folder that has a space', () => {
12+
expect(folderThatHasASpace.get()).toBe(folderThatHasASpace.dirname());
13+
});
4114

42-
const actual = getTSConfigRootDirFromStack(
43-
[
44-
`Error`,
45-
` at ${path.join(expected, `eslint.config.${extension}`)}`,
46-
' at ModuleJob.run',
47-
'at async NodeHfs.walk(...)',
48-
].join('\n'),
49-
);
15+
it("doesn't get tricked by a file that is not an ESLint config", () => {
16+
expect(notEslintConfig.get()).toBeUndefined();
17+
});
5018

51-
expect(actual).toBe(expected);
52-
},
53-
);
19+
it('should work in the presence of a messed up strack trace string', () => {
20+
const prepareStackTrace = Error.prepareStackTrace;
21+
const dummyFunction = () => {};
22+
Error.prepareStackTrace = dummyFunction;
23+
expect(new Error().stack).toBeUndefined();
24+
expect(normalFolder.get()).toBe(normalFolder.dirname());
25+
expect(Error.prepareStackTrace).toBe(dummyFunction);
26+
Error.prepareStackTrace = prepareStackTrace;
27+
});
5428
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
2+
import { getTSConfigRootDirFromStack } from './../../../../src/getTSConfigRootDirFromStack';
3+
4+
export function get() {
5+
return getTSConfigRootDirFromStack();
6+
}
7+
8+
export function dirname() {
9+
return __dirname;
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
2+
import { getTSConfigRootDirFromStack } from '../../../src/getTSConfigRootDirFromStack';
3+
4+
export function get() {
5+
return getTSConfigRootDirFromStack();
6+
}
7+
8+
export function dirname() {
9+
return __dirname;
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
2+
import { getTSConfigRootDirFromStack } from '../../../../src/getTSConfigRootDirFromStack';
3+
4+
export function get() {
5+
return getTSConfigRootDirFromStack();
6+
}
7+
8+
export function dirname() {
9+
return __dirname;
10+
}

0 commit comments

Comments
 (0)