From 69fc25a15501a7dbb921bce661911b1ed1c12fc2 Mon Sep 17 00:00:00 2001 From: David Michon Date: Tue, 11 Nov 2025 01:36:38 +0000 Subject: [PATCH 1/4] [eslint-plugin] Fix calculation of project folder in ESLint --- eslint/eslint-plugin/src/LintUtilities.ts | 38 ++++++++++++++----- .../src/no-external-local-imports.ts | 16 +++++--- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/eslint/eslint-plugin/src/LintUtilities.ts b/eslint/eslint-plugin/src/LintUtilities.ts index 3921ac3b7c0..fb610ba7a99 100644 --- a/eslint/eslint-plugin/src/LintUtilities.ts +++ b/eslint/eslint-plugin/src/LintUtilities.ts @@ -4,7 +4,7 @@ import * as path from 'node:path'; import { ESLintUtils, TSESTree, type TSESLint } from '@typescript-eslint/utils'; -import type { Program } from 'typescript'; +import type { CompilerOptions, Program } from 'typescript'; export interface IParsedImportSpecifier { loader?: string; @@ -33,23 +33,41 @@ export function getFilePathFromContext(context: TSESLint.RuleContext ): string | undefined { - let rootDirectory: string | undefined; + /* + * Precedence of root directory resolution: + * 1. parserOptions.tsconfigRootDir if available (since set by repo maintainer) + * 2. tsconfig.json directory if available (but might be in a subfolder) + * 3. TS Program current directory if available + * 4. ESLint working directory (probably wrong, but better than nothing?) + */ + const tsConfigRootDir: string | undefined = context.parserOptions?.tsconfigRootDir; + if (tsConfigRootDir) { + return tsConfigRootDir; + } + try { - // First attempt to get the root directory from the tsconfig baseUrl, then the program current directory const program: Program | null | undefined = ( context.sourceCode?.parserServices ?? ESLintUtils.getParserServices(context) ).program; - rootDirectory = program?.getCompilerOptions().baseUrl ?? program?.getCurrentDirectory(); + const compilerOptions: CompilerOptions | undefined = program?.getCompilerOptions(); + + const tsConfigPath: string | undefined = compilerOptions?.configFilePath as string | undefined; + if (tsConfigPath) { + const tsConfigDir: string = path.dirname(tsConfigPath); + return tsConfigDir; + } + + // Next, try to get the current directory from the TS program + const rootDirectory: string | undefined = program?.getCurrentDirectory(); + if (rootDirectory) { + return rootDirectory; + } } catch { // Ignore the error if we cannot retrieve a TS program } - // Fall back to the parserOptions.tsconfigRootDir if available, otherwise the eslint working directory - if (!rootDirectory) { - rootDirectory = context.parserOptions?.tsconfigRootDir ?? context.getCwd?.(); - } - - return rootDirectory; + // Last resort: use ESLint's current working directory + return context.getCwd?.(); } export function parseImportSpecifierFromExpression( diff --git a/eslint/eslint-plugin/src/no-external-local-imports.ts b/eslint/eslint-plugin/src/no-external-local-imports.ts index 9fb8c3fd3f8..d32bafbc336 100644 --- a/eslint/eslint-plugin/src/no-external-local-imports.ts +++ b/eslint/eslint-plugin/src/no-external-local-imports.ts @@ -19,15 +19,15 @@ export const noExternalLocalImportsRule: RuleModule = { type: 'problem', messages: { [MESSAGE_ID]: - 'The specified import target is not under the root directory. Ensure that ' + - 'all local import targets are either under the "rootDir" specified in your tsconfig.json (if one ' + - 'exists) or under the package directory.' + 'The specified import target "{{ importAbsolutePath }}" is not under the root directory, "{{ rootDirectory }}". Ensure that ' + + 'all local import targets are either under the "parserOptions.tsconfigRootDir" specified in your eslint.config.js (if one ' + + 'exists) or else not under the folder that contains your tsconfig.json.' }, schema: [], docs: { description: - 'Prevents referencing relative imports that are either not under the "rootDir" specified in ' + - 'the tsconfig.json (if one exists) or not under the package directory.', + 'Prevents referencing relative imports that are either not under the "parserOptions.tsconfigRootDir" specified in ' + + 'your eslint.config.js (if one exists) or else not under the folder that contains your tsconfig.json.', url: 'https://www.npmjs.com/package/@rushstack/eslint-plugin' } }, @@ -54,7 +54,11 @@ export const noExternalLocalImportsRule: RuleModule = { const relativePathToRoot: string = path.relative(importAbsolutePath, rootDirectory); if (!_relativePathRegex.test(relativePathToRoot)) { - context.report({ node: importExpression, messageId: MESSAGE_ID }); + context.report({ + node: importExpression, + messageId: MESSAGE_ID, + data: { importAbsolutePath, rootDirectory } + }); } }; From 6ac670b717d35255f234b18e3931bbe381318ae0 Mon Sep 17 00:00:00 2001 From: David Michon Date: Tue, 11 Nov 2025 01:38:02 +0000 Subject: [PATCH 2/4] rush change --- .../eslint-plugin/fix-lint-dir_2025-11-11-01-37.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@rushstack/eslint-plugin/fix-lint-dir_2025-11-11-01-37.json diff --git a/common/changes/@rushstack/eslint-plugin/fix-lint-dir_2025-11-11-01-37.json b/common/changes/@rushstack/eslint-plugin/fix-lint-dir_2025-11-11-01-37.json new file mode 100644 index 00000000000..3f5bb04f5e1 --- /dev/null +++ b/common/changes/@rushstack/eslint-plugin/fix-lint-dir_2025-11-11-01-37.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/eslint-plugin", + "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.", + "type": "patch" + } + ], + "packageName": "@rushstack/eslint-plugin" +} \ No newline at end of file From 40e34952ea19b72da6765cdecba310d9ffcfb918 Mon Sep 17 00:00:00 2001 From: David Michon Date: Tue, 11 Nov 2025 01:43:43 +0000 Subject: [PATCH 3/4] Fixup --- eslint/eslint-plugin/src/no-external-local-imports.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint/eslint-plugin/src/no-external-local-imports.ts b/eslint/eslint-plugin/src/no-external-local-imports.ts index d32bafbc336..e4ae7bae204 100644 --- a/eslint/eslint-plugin/src/no-external-local-imports.ts +++ b/eslint/eslint-plugin/src/no-external-local-imports.ts @@ -21,7 +21,7 @@ export const noExternalLocalImportsRule: RuleModule = { [MESSAGE_ID]: 'The specified import target "{{ importAbsolutePath }}" is not under the root directory, "{{ rootDirectory }}". Ensure that ' + 'all local import targets are either under the "parserOptions.tsconfigRootDir" specified in your eslint.config.js (if one ' + - 'exists) or else not under the folder that contains your tsconfig.json.' + 'exists) or else under the folder that contains your tsconfig.json.' }, schema: [], docs: { From fbf0aa68b942a12383a6b21cc8f1e57d16b7dbea Mon Sep 17 00:00:00 2001 From: David Michon Date: Tue, 11 Nov 2025 02:24:19 +0000 Subject: [PATCH 4/4] [heft-lint] Set parserOptions.tsconfigRootDir --- .../fix-lint-dir_2025-11-11-02-24.json | 10 ++++++++++ heft-plugins/heft-lint-plugin/src/Eslint.ts | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 common/changes/@rushstack/heft-lint-plugin/fix-lint-dir_2025-11-11-02-24.json diff --git a/common/changes/@rushstack/heft-lint-plugin/fix-lint-dir_2025-11-11-02-24.json b/common/changes/@rushstack/heft-lint-plugin/fix-lint-dir_2025-11-11-02-24.json new file mode 100644 index 00000000000..b454ed5b8c8 --- /dev/null +++ b/common/changes/@rushstack/heft-lint-plugin/fix-lint-dir_2025-11-11-02-24.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-lint-plugin", + "comment": "Ensure that `parserOptions.tsconfigRootDir` is set for use by custom lint rules.", + "type": "patch" + } + ], + "packageName": "@rushstack/heft-lint-plugin" +} \ No newline at end of file diff --git a/heft-plugins/heft-lint-plugin/src/Eslint.ts b/heft-plugins/heft-lint-plugin/src/Eslint.ts index 4d68afeebbe..6cbd4b5727d 100644 --- a/heft-plugins/heft-lint-plugin/src/Eslint.ts +++ b/heft-plugins/heft-lint-plugin/src/Eslint.ts @@ -157,7 +157,9 @@ export class Eslint extends LinterBase 9.28.0 - toJSON: parserOptionsToJson + toJSON: parserOptionsToJson, + // ESlint's merge logic for parserOptions is a "replace", so we need to set this again + tsconfigRootDir: buildFolderPath }; if (this._eslintPackageVersion.minor < 28) { overrideParserOptions = Object.defineProperties(overrideParserOptions, {