Skip to content

Commit abb7c4d

Browse files
fix(typescript-eslint): infer tsconfigRootDir with v8 API (typescript-eslint#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
});
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+
}
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)
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