Skip to content

Commit a871bb8

Browse files
authored
[eslint-plugin] Fix no-external-local-imports in editor (#5441)
* [eslint-plugin] Fix calculation of project folder in ESLint * [heft-lint] Set parserOptions.tsconfigRootDir --------- Co-authored-by: David Michon <dmichon-msft@users.noreply.github.com>
1 parent cbcfacb commit a871bb8

5 files changed

Lines changed: 61 additions & 17 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@rushstack/eslint-plugin",
5+
"comment": "Fix calculation of project root folder when using the ESLint extension in VS Code. Report the paths being compared in 'no-external-local-imports' rule violations.",
6+
"type": "patch"
7+
}
8+
],
9+
"packageName": "@rushstack/eslint-plugin"
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@rushstack/heft-lint-plugin",
5+
"comment": "Ensure that `parserOptions.tsconfigRootDir` is set for use by custom lint rules.",
6+
"type": "patch"
7+
}
8+
],
9+
"packageName": "@rushstack/heft-lint-plugin"
10+
}

eslint/eslint-plugin/src/LintUtilities.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import * as path from 'node:path';
55

66
import { ESLintUtils, TSESTree, type TSESLint } from '@typescript-eslint/utils';
7-
import type { Program } from 'typescript';
7+
import type { CompilerOptions, Program } from 'typescript';
88

99
export interface IParsedImportSpecifier {
1010
loader?: string;
@@ -33,23 +33,41 @@ export function getFilePathFromContext(context: TSESLint.RuleContext<string, unk
3333
export function getRootDirectoryFromContext(
3434
context: TSESLint.RuleContext<string, unknown[]>
3535
): string | undefined {
36-
let rootDirectory: string | undefined;
36+
/*
37+
* Precedence of root directory resolution:
38+
* 1. parserOptions.tsconfigRootDir if available (since set by repo maintainer)
39+
* 2. tsconfig.json directory if available (but might be in a subfolder)
40+
* 3. TS Program current directory if available
41+
* 4. ESLint working directory (probably wrong, but better than nothing?)
42+
*/
43+
const tsConfigRootDir: string | undefined = context.parserOptions?.tsconfigRootDir;
44+
if (tsConfigRootDir) {
45+
return tsConfigRootDir;
46+
}
47+
3748
try {
38-
// First attempt to get the root directory from the tsconfig baseUrl, then the program current directory
3949
const program: Program | null | undefined = (
4050
context.sourceCode?.parserServices ?? ESLintUtils.getParserServices(context)
4151
).program;
42-
rootDirectory = program?.getCompilerOptions().baseUrl ?? program?.getCurrentDirectory();
52+
const compilerOptions: CompilerOptions | undefined = program?.getCompilerOptions();
53+
54+
const tsConfigPath: string | undefined = compilerOptions?.configFilePath as string | undefined;
55+
if (tsConfigPath) {
56+
const tsConfigDir: string = path.dirname(tsConfigPath);
57+
return tsConfigDir;
58+
}
59+
60+
// Next, try to get the current directory from the TS program
61+
const rootDirectory: string | undefined = program?.getCurrentDirectory();
62+
if (rootDirectory) {
63+
return rootDirectory;
64+
}
4365
} catch {
4466
// Ignore the error if we cannot retrieve a TS program
4567
}
4668

47-
// Fall back to the parserOptions.tsconfigRootDir if available, otherwise the eslint working directory
48-
if (!rootDirectory) {
49-
rootDirectory = context.parserOptions?.tsconfigRootDir ?? context.getCwd?.();
50-
}
51-
52-
return rootDirectory;
69+
// Last resort: use ESLint's current working directory
70+
return context.getCwd?.();
5371
}
5472

5573
export function parseImportSpecifierFromExpression(

eslint/eslint-plugin/src/no-external-local-imports.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ export const noExternalLocalImportsRule: RuleModule = {
1919
type: 'problem',
2020
messages: {
2121
[MESSAGE_ID]:
22-
'The specified import target is not under the root directory. Ensure that ' +
23-
'all local import targets are either under the "rootDir" specified in your tsconfig.json (if one ' +
24-
'exists) or under the package directory.'
22+
'The specified import target "{{ importAbsolutePath }}" is not under the root directory, "{{ rootDirectory }}". Ensure that ' +
23+
'all local import targets are either under the "parserOptions.tsconfigRootDir" specified in your eslint.config.js (if one ' +
24+
'exists) or else under the folder that contains your tsconfig.json.'
2525
},
2626
schema: [],
2727
docs: {
2828
description:
29-
'Prevents referencing relative imports that are either not under the "rootDir" specified in ' +
30-
'the tsconfig.json (if one exists) or not under the package directory.',
29+
'Prevents referencing relative imports that are either not under the "parserOptions.tsconfigRootDir" specified in ' +
30+
'your eslint.config.js (if one exists) or else not under the folder that contains your tsconfig.json.',
3131
url: 'https://www.npmjs.com/package/@rushstack/eslint-plugin'
3232
}
3333
},
@@ -54,7 +54,11 @@ export const noExternalLocalImportsRule: RuleModule = {
5454

5555
const relativePathToRoot: string = path.relative(importAbsolutePath, rootDirectory);
5656
if (!_relativePathRegex.test(relativePathToRoot)) {
57-
context.report({ node: importExpression, messageId: MESSAGE_ID });
57+
context.report({
58+
node: importExpression,
59+
messageId: MESSAGE_ID,
60+
data: { importAbsolutePath, rootDirectory }
61+
});
5862
}
5963
};
6064

heft-plugins/heft-lint-plugin/src/Eslint.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,9 @@ export class Eslint extends LinterBase<TEslint.ESLint.LintResult | TEslintLegacy
157157
let overrideParserOptions: TEslint.Linter.ParserOptions = {
158158
programs: [tsProgram],
159159
// Used by stableStringify and ESLint > 9.28.0
160-
toJSON: parserOptionsToJson
160+
toJSON: parserOptionsToJson,
161+
// ESlint's merge logic for parserOptions is a "replace", so we need to set this again
162+
tsconfigRootDir: buildFolderPath
161163
};
162164
if (this._eslintPackageVersion.minor < 28) {
163165
overrideParserOptions = Object.defineProperties(overrideParserOptions, {

0 commit comments

Comments
 (0)