From 73fd81f68d8c414d4ad1a3473abd0c10c97a0daa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=B3=C3=B0i=20Karlsson?= Date: Thu, 21 Aug 2025 15:35:55 +0200 Subject: [PATCH 1/3] Support realpaths with true-defaulting config --- $shared/settings.ts | 1 + client/src/client.ts | 1 + server/src/eslint.ts | 22 +++++++++++----------- server/src/eslintServer.ts | 10 +++++----- server/src/paths.ts | 31 +++++++++++++++++++------------ 5 files changed, 37 insertions(+), 28 deletions(-) diff --git a/$shared/settings.ts b/$shared/settings.ts index 0faed5a5..fcb2b169 100644 --- a/$shared/settings.ts +++ b/$shared/settings.ts @@ -168,6 +168,7 @@ export type ConfigurationSettings = { packageManager: PackageManagers; useESLintClass: boolean; useFlatConfig?: boolean | undefined; + useRealpaths: boolean; experimental?: { useFlatConfig: boolean; }; diff --git a/client/src/client.ts b/client/src/client.ts index 561322d5..b72fbc16 100644 --- a/client/src/client.ts +++ b/client/src/client.ts @@ -678,6 +678,7 @@ export namespace ESLintClient { packageManager: config.get('packageManager', 'npm'), useESLintClass: config.get('useESLintClass', false), useFlatConfig: useFlatConfig === null ? undefined : useFlatConfig, + useRealpaths: config.get('useRealpaths', true), experimental: { useFlatConfig: config.get('experimental.useFlatConfig', false), }, diff --git a/server/src/eslint.ts b/server/src/eslint.ts index 567f5aee..c7e4db87 100644 --- a/server/src/eslint.ts +++ b/server/src/eslint.ts @@ -478,11 +478,11 @@ export type SaveRuleConfigItem = { offRules: Set; onRules: Set; */ export namespace SaveRuleConfigs { - export let inferFilePath: (documentOrUri: string | TextDocument | URI | undefined) => string | undefined; + export let inferFilePath: (documentOrUri: string | TextDocument | URI | undefined, useRealpaths: boolean) => string | undefined; const saveRuleConfigCache = new LRUCache(128); export async function get(uri: string, settings: TextDocumentSettings & { library: ESLintModule }): Promise { - const filePath = inferFilePath(uri); + const filePath = inferFilePath(uri, settings.useRealpaths); let result = saveRuleConfigCache.get(uri); if (filePath === undefined || result === null) { return undefined; @@ -755,7 +755,7 @@ export namespace ESLint { let connection: ProposedFeatures.Connection; let documents: TextDocuments; - let inferFilePath: (documentOrUri: string | TextDocument | URI | undefined) => string | undefined; + let inferFilePath: (documentOrUri: string | TextDocument | URI | undefined, useRealpaths: boolean) => string | undefined; let loadNodeModule: (moduleName: string) => T | undefined; const languageId2ParserRegExp: Map = function createLanguageId2ParserRegExp() { @@ -821,7 +821,7 @@ export namespace ESLint { const document2Settings: Map> = new Map>(); const formatterRegistrations: Map> = new Map(); - export function initialize($connection: ProposedFeatures.Connection, $documents: TextDocuments, $inferFilePath: (documentOrUri: string | TextDocument | URI | undefined) => string | undefined, $loadNodeModule: (moduleName: string) => T | undefined) { + export function initialize($connection: ProposedFeatures.Connection, $documents: TextDocuments, $inferFilePath: (documentOrUri: string | TextDocument | URI | undefined, useRealpaths: boolean) => string | undefined, $loadNodeModule: (moduleName: string) => T | undefined) { connection = $connection; documents = $documents; inferFilePath = $inferFilePath; @@ -868,8 +868,8 @@ export namespace ESLint { return settings; } settings.resolvedGlobalPackageManagerPath = GlobalPaths.get(settings.packageManager); - const filePath = inferFilePath(document); - const workspaceFolderPath = settings.workspaceFolder !== undefined ? inferFilePath(settings.workspaceFolder.uri) : undefined; + const filePath = inferFilePath(document, settings.useRealpaths); + const workspaceFolderPath = settings.workspaceFolder !== undefined ? inferFilePath(settings.workspaceFolder.uri, settings.useRealpaths) : undefined; let assumeFlatConfig:boolean = false; const hasUserDefinedWorkingDirectories: boolean = configuration.workingDirectory !== undefined; const workingDirectoryConfig = configuration.workingDirectory ?? { mode: ModeEnum.location }; @@ -1095,7 +1095,7 @@ export namespace ESLint { if (!isFile) { formatterRegistrations.set(uri, connection.client.register(DocumentFormattingRequest.type, options)); } else { - const filePath = inferFilePath(uri)!; + const filePath = inferFilePath(uri, settings.useRealpaths)!; await ESLint.withClass(async (eslintClass) => { if (!await eslintClass.isPathIgnored(filePath)) { formatterRegistrations.set(uri, connection.client.register(DocumentFormattingRequest.type, options)); @@ -1181,14 +1181,14 @@ export namespace ESLint { if (uri.scheme !== 'file') { if (settings.workspaceFolder !== undefined) { const ext = LanguageDefaults.getExtension(document.languageId); - const workspacePath = inferFilePath(settings.workspaceFolder.uri); + const workspacePath = inferFilePath(settings.workspaceFolder.uri, settings.useRealpaths); if (workspacePath !== undefined && ext !== undefined) { return path.join(workspacePath, `test.${ext}`); } } return undefined; } else { - return inferFilePath(uri); + return inferFilePath(uri, settings.useRealpaths); } } @@ -1416,14 +1416,14 @@ export namespace ESLint { missingModuleReported.clear(); } - function tryHandleMissingModule(error: any, document: TextDocument, library: ESLintModule): Status | undefined { + function tryHandleMissingModule(error: any, document: TextDocument, library: ESLintModule, settings: TextDocumentSettings): Status | undefined { if (!error.message) { return undefined; } function handleMissingModule(plugin: string, module: string, error: ESLintError): Status { if (!missingModuleReported.has(plugin)) { - const fsPath = inferFilePath(document); + const fsPath = inferFilePath(document, settings.useRealpaths); missingModuleReported.set(plugin, library); if (error.messageTemplate === 'plugin-missing') { connection.console.error([ diff --git a/server/src/eslintServer.ts b/server/src/eslintServer.ts index b6d7183e..d9cd31c9 100644 --- a/server/src/eslintServer.ts +++ b/server/src/eslintServer.ts @@ -116,20 +116,20 @@ process.on('uncaughtException', (error: any) => { * cell document it uses the file path from the notebook with a corresponding * extension (e.g. TypeScript -> ts) */ -function inferFilePath(documentOrUri: string | TextDocument | URI | undefined): string | undefined { +function inferFilePath(documentOrUri: string | TextDocument | URI | undefined, useRealpaths: boolean): string | undefined { if (!documentOrUri) { return undefined; } const uri = getUri(documentOrUri); if (uri.scheme === 'file') { - return getFileSystemPath(uri); + return getFileSystemPath(uri, useRealpaths); } const notebookDocument = notebooks.findNotebookDocumentForCell(uri.toString()); if (notebookDocument !== undefined ) { const notebookUri = URI.parse(notebookDocument.uri); if (notebookUri.scheme === 'file') { - const filePath = getFileSystemPath(uri); + const filePath = getFileSystemPath(uri, useRealpaths); if (filePath !== undefined) { const textDocument = documents.get(uri.toString()); if (textDocument !== undefined) { @@ -299,7 +299,7 @@ connection.onDidChangeWatchedFiles(async (params) => { SaveRuleConfigs.clear(); await Promise.all(params.changes.map(async (change) => { - const fsPath = inferFilePath(change.uri); + const fsPath = inferFilePath(change.uri, false); if (fsPath === undefined || fsPath.length === 0 || isUNC(fsPath)) { return; } @@ -756,7 +756,7 @@ async function computeAllFixes(identifier: VersionedTextDocumentIdentifier, mode if (settings.validate !== Validate.on || !TextDocumentSettings.hasLibrary(settings) || (mode === AllFixesMode.format && !settings.format)) { return []; } - const filePath = inferFilePath(textDocument); + const filePath = inferFilePath(textDocument, settings.useRealpaths); const problems = CodeActions.get(uri); const originalContent = textDocument.getText(); let start = Date.now(); diff --git a/server/src/paths.ts b/server/src/paths.ts index 38d6ee45..c4277f4c 100644 --- a/server/src/paths.ts +++ b/server/src/paths.ts @@ -76,7 +76,7 @@ export function isUNC(path: string): boolean { return true; } -export function getFileSystemPath(uri: URI): string { +export function getFileSystemPath(uri: URI, useRealpaths: boolean): string { let result = uri.fsPath; if (process.platform === 'win32' && result.length >= 2 && result[1] === ':') { // Node by default uses an upper case drive letter and ESLint uses @@ -84,22 +84,29 @@ export function getFileSystemPath(uri: URI): string { // if the drive letter is lower case in th URI. Ensure upper case. result = result[0].toUpperCase() + result.substr(1); } - if (process.platform === 'win32' || process.platform === 'darwin') { - try { - const realpath = fs.realpathSync.native(result); - // Only use the real path if only the casing has changed. - if (realpath.toLowerCase() === result.toLowerCase()) { - result = realpath; - } - } catch { - // Silently ignore errors from `fs.realpathSync` to handle scenarios where - // the file being linted is not yet written to disk. This occurs in editors - // such as Neovim for non-written buffers. + if (useRealpaths) { + result = tryRealpath(result); + } else if (process.platform === 'win32' || process.platform === 'darwin') { + const realpath = tryRealpath(result); + // Only use the real path if only the casing has changed. + if (realpath.toLowerCase() === result.toLowerCase()) { + result = realpath; } } return result; } +function tryRealpath(path: string): string { + try { + return fs.realpathSync.native(path); + } catch (error) { + // Silently ignore errors from `fs.realpathSync` to handle scenarios where + // the file being linted is not yet written to disk. This occurs in editors + // such as Neovim for non-written buffers. + return path; + } +} + export function normalizePath(path: string): string; export function normalizePath(path: undefined): undefined; export function normalizePath(path: string | undefined): string | undefined { From 7e9c45b5d6d339e91ed18e085dbbe6cd08043154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=B3=C3=B0i=20Karlsson?= Date: Thu, 21 Aug 2025 22:44:42 +0200 Subject: [PATCH 2/3] Add to package.json configuration --- client/src/client.ts | 2 +- package.json | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/client/src/client.ts b/client/src/client.ts index b72fbc16..29284453 100644 --- a/client/src/client.ts +++ b/client/src/client.ts @@ -963,7 +963,7 @@ export namespace ESLintClient { } if (detail !== undefined && languageStatus.detail !== detail) { - languageStatus.detail = detail; + languageStatus.detail = detail; } if (languageStatus.severity !== severity) { languageStatus.severity = severity; diff --git a/package.json b/package.json index 9b849a0d..5f08e596 100644 --- a/package.json +++ b/package.json @@ -196,6 +196,12 @@ "deprecationMessage": "Use ESLint version 8.57 or later and `eslint.useFlatConfig` instead.", "description": "Enables support of experimental Flat Config (aka eslint.config.js). Requires ESLint version >= 8.21 < 8.57.0)." }, + "eslint.useRealpaths": { + "scope": "resource", + "type": "boolean", + "default": true, + "description": "Whether ESLint should use real paths when resolving files. This is useful when working with symlinks or when the casing of file paths is inconsistent." + }, "eslint.workingDirectories": { "scope": "resource", "type": "array", From f8e1843cd62c9a0c3ce5bac967848a063864cdfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=B3=C3=B0i=20Karlsson?= Date: Mon, 1 Sep 2025 17:02:34 +0200 Subject: [PATCH 3/3] Default to false --- client/src/client.ts | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/client.ts b/client/src/client.ts index 29284453..1e412bc0 100644 --- a/client/src/client.ts +++ b/client/src/client.ts @@ -678,7 +678,7 @@ export namespace ESLintClient { packageManager: config.get('packageManager', 'npm'), useESLintClass: config.get('useESLintClass', false), useFlatConfig: useFlatConfig === null ? undefined : useFlatConfig, - useRealpaths: config.get('useRealpaths', true), + useRealpaths: config.get('useRealpaths', false), experimental: { useFlatConfig: config.get('experimental.useFlatConfig', false), }, diff --git a/package.json b/package.json index 5f08e596..8ed10981 100644 --- a/package.json +++ b/package.json @@ -199,7 +199,7 @@ "eslint.useRealpaths": { "scope": "resource", "type": "boolean", - "default": true, + "default": false, "description": "Whether ESLint should use real paths when resolving files. This is useful when working with symlinks or when the casing of file paths is inconsistent." }, "eslint.workingDirectories": {