From f6d60a763646901842423e7dfa034f37d11c3b3e Mon Sep 17 00:00:00 2001 From: Sean Doherty Date: Sat, 16 May 2026 18:15:46 -0500 Subject: [PATCH] Fix diagnostic cache key collisions --- .../src/diagnosticCacheKey.ts | 27 ++++++++++ apps/vscode-extension/src/diagnostics.ts | 8 +-- .../src/provider/webviewViewProvider.ts | 3 +- .../src/test/suite/diagnosticCacheKey.test.ts | 53 +++++++++++++++++++ 4 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 apps/vscode-extension/src/diagnosticCacheKey.ts create mode 100644 apps/vscode-extension/src/test/suite/diagnosticCacheKey.test.ts diff --git a/apps/vscode-extension/src/diagnosticCacheKey.ts b/apps/vscode-extension/src/diagnosticCacheKey.ts new file mode 100644 index 0000000..67e3ad6 --- /dev/null +++ b/apps/vscode-extension/src/diagnosticCacheKey.ts @@ -0,0 +1,27 @@ +import type { + Diagnostic, + DiagnosticRelatedInformation, +} from "vscode-languageserver-types"; + +export function getDiagnosticCacheKey(diagnostic: Diagnostic): string { + return JSON.stringify({ + message: diagnostic.message, + code: diagnostic.code, + range: diagnostic.range, + relatedInformation: + diagnostic.relatedInformation?.map(getRelatedInformationCacheKey) ?? [], + }); +} + +function getRelatedInformationCacheKey({ + location, + message, +}: DiagnosticRelatedInformation) { + return { + message, + location: { + uri: location.uri, + range: location.range, + }, + }; +} diff --git a/apps/vscode-extension/src/diagnostics.ts b/apps/vscode-extension/src/diagnostics.ts index 023b957..049c003 100644 --- a/apps/vscode-extension/src/diagnostics.ts +++ b/apps/vscode-extension/src/diagnostics.ts @@ -19,6 +19,7 @@ import { } from "./formattedDiagnosticsStore"; import { logger } from "./logger"; import { enabledCommands } from "./commands/enabledCommands"; +import { getDiagnosticCacheKey } from "./diagnosticCacheKey"; /** * The list of diagnostic sources that pretty-ts-errors supports @@ -84,7 +85,7 @@ export function registerOnDidChangeDiagnostics(context: ExtensionContext) { const CACHE_SIZE_MAX = 100; /** - * A local cache that maps TS diagnostics as `string` to their formatted `MarkdownString` counter part. + * A local cache that maps TS diagnostics to their formatted `MarkdownString` counter part. * @see https://github.com/yoavbls/pretty-ts-errors/pull/62 * * One reason this cache is critical is because the TypeScript Language Features extension is very noisy and will constantly push all diagnostics for a file, @@ -103,8 +104,9 @@ async function getFormattedDiagnostic( // formatDiagnosticForHover converts message based on LSP Diagnostic type, not VSCode Diagnostic type, so it can be used in other IDEs. // Here we convert VSCode Diagnostic to LSP Diagnostic to make formatDiagnosticForHover recognize it. const lspDiagnostic = converter.asDiagnostic(diagnostic); + const cacheKey = getDiagnosticCacheKey(lspDiagnostic); - let formattedMessage = cache.get(diagnostic.message); + let formattedMessage = cache.get(cacheKey); if (!formattedMessage) { const formattedDiagnostic = await prettifyDiagnosticForHover(lspDiagnostic); const markdownString = new MarkdownString(formattedDiagnostic); @@ -117,7 +119,7 @@ async function getFormattedDiagnostic( const firstCacheKey = cache.keys().next().value!; cache.delete(firstCacheKey); } - cache.set(diagnostic.message, formattedMessage); + cache.set(cacheKey, formattedMessage); } return { diff --git a/apps/vscode-extension/src/provider/webviewViewProvider.ts b/apps/vscode-extension/src/provider/webviewViewProvider.ts index 518614a..bdac761 100644 --- a/apps/vscode-extension/src/provider/webviewViewProvider.ts +++ b/apps/vscode-extension/src/provider/webviewViewProvider.ts @@ -19,6 +19,7 @@ import { } from "@pretty-ts-errors/vscode-formatter"; import { SUPPORTED_LANGUAGE_IDS } from "../supportedLanguageIds"; import { logger } from "../logger"; +import { getDiagnosticCacheKey } from "../diagnosticCacheKey"; const NO_DIAGNOSTICS_MESSAGE = "Select code with an error to show the prettified diagnostic in this view."; @@ -81,7 +82,7 @@ export function registerWebviewViewProvider(context: ExtensionContext) { async function diagnosticToItem( formattedDiagnostic: FormattedDiagnostic ): Promise { - const cacheKey = formattedDiagnostic.lspDiagnostic.message; + const cacheKey = getDiagnosticCacheKey(formattedDiagnostic.lspDiagnostic); let html = sidebarHtmlCache.get(cacheKey); if (!html) { html = await prettifyDiagnosticForSidebar( diff --git a/apps/vscode-extension/src/test/suite/diagnosticCacheKey.test.ts b/apps/vscode-extension/src/test/suite/diagnosticCacheKey.test.ts new file mode 100644 index 0000000..4b18e3b --- /dev/null +++ b/apps/vscode-extension/src/test/suite/diagnosticCacheKey.test.ts @@ -0,0 +1,53 @@ +import { strict as assert } from "node:assert"; +import type { Diagnostic } from "vscode-languageserver-types"; +import { getDiagnosticCacheKey } from "../../diagnosticCacheKey"; + +suite("getDiagnosticCacheKey", () => { + test("distinguishes duplicate messages that link to different symbols", () => { + const firstKey = getDiagnosticCacheKey( + diagnosticWithRelatedInformation("file:///workspace/example.ts", 1) + ); + const secondKey = getDiagnosticCacheKey( + diagnosticWithRelatedInformation("file:///workspace/other.ts", 1) + ); + + assert.notEqual(firstKey, secondKey); + }); + + test("distinguishes duplicate messages at different ranges", () => { + const firstKey = getDiagnosticCacheKey( + diagnosticWithRelatedInformation("file:///workspace/example.ts", 1) + ); + const secondKey = getDiagnosticCacheKey( + diagnosticWithRelatedInformation("file:///workspace/example.ts", 8) + ); + + assert.notEqual(firstKey, secondKey); + }); +}); + +function diagnosticWithRelatedInformation( + symbolUri: string, + startCharacter: number +): Diagnostic { + return { + message: "Cannot find name 'Person'.", + code: 2304, + range: { + start: { line: 0, character: startCharacter }, + end: { line: 0, character: startCharacter + 6 }, + }, + relatedInformation: [ + { + message: "'Person' is declared here.", + location: { + uri: symbolUri, + range: { + start: { line: 3, character: 12 }, + end: { line: 3, character: 18 }, + }, + }, + }, + ], + }; +}