Skip to content

fix(typescript-eslint): infer tsconfigRootDir with v8 API #11412

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions packages/typescript-eslint/src/getTSConfigRootDirFromStack.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
5 changes: 1 addition & 4 deletions packages/typescript-eslint/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,7 @@ function createConfigsGetters<T extends object>(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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
});
});
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Loading
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