From f78d811f84b1dbf9a7d8fecd51d6a7f87f086dcc Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 9 Mar 2026 11:09:13 +0900 Subject: [PATCH] fix(lsp): include file paths in directory diagnostics output --- src/tools/lsp/directory-diagnostics.test.ts | 72 ++++++++++++++++++++- src/tools/lsp/directory-diagnostics.ts | 18 ++++-- 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/src/tools/lsp/directory-diagnostics.test.ts b/src/tools/lsp/directory-diagnostics.test.ts index 4c932e036..ca3f2d3a7 100644 --- a/src/tools/lsp/directory-diagnostics.test.ts +++ b/src/tools/lsp/directory-diagnostics.test.ts @@ -1,12 +1,56 @@ -import { describe, expect, it } from "bun:test" +import { beforeEach, describe, expect, it, mock } from "bun:test" import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs" import { join } from "path" import os from "os" -import { isDirectoryPath } from "./lsp-client-wrapper" -import { aggregateDiagnosticsForDirectory } from "./directory-diagnostics" +import type { Diagnostic } from "./types" + +const diagnosticsMock = mock(async (_filePath: string) => ({ items: [] as Diagnostic[] })) +const getClientMock = mock(async () => ({ diagnostics: diagnosticsMock })) +const releaseClientMock = mock(() => {}) + +mock.module("./config", () => ({ + findServerForExtension: (extension: string) => ({ + status: "found" as const, + server: { + id: "test-server", + command: ["test-server"], + extensions: [extension], + priority: 1, + }, + }), + getLanguageId: () => "typescript", +})) + +mock.module("./lsp-server", () => ({ + lspManager: { + getClient: getClientMock, + releaseClient: releaseClientMock, + }, +})) + +const { isDirectoryPath } = await import("./lsp-client-wrapper") +const { aggregateDiagnosticsForDirectory } = await import("./directory-diagnostics") + +function createDiagnostic(message: string): Diagnostic { + return { + message, + severity: 1, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 1 }, + }, + } +} describe("directory diagnostics", () => { + beforeEach(() => { + diagnosticsMock.mockReset() + diagnosticsMock.mockImplementation(async (_filePath: string) => ({ items: [] })) + getClientMock.mockClear() + releaseClientMock.mockClear() + }) + describe("isDirectoryPath", () => { it("returns true for existing directory", () => { const tmp = mkdtempSync(join(os.tmpdir(), "omo-isdir-")) @@ -52,5 +96,27 @@ describe("directory diagnostics", () => { "Directory does not exist" ) }) + + it("#given diagnostics from multiple files #when aggregating directory diagnostics #then each entry includes the source file path", async () => { + const tmp = mkdtempSync(join(os.tmpdir(), "omo-aggr-files-")) + try { + const firstFile = join(tmp, "first.ts") + const secondFile = join(tmp, "second.ts") + + writeFileSync(firstFile, "export const first = true\n") + writeFileSync(secondFile, "export const second = true\n") + + diagnosticsMock.mockImplementation(async (filePath: string) => ({ + items: [createDiagnostic(`problem in ${filePath}`)], + })) + + const result = await aggregateDiagnosticsForDirectory(tmp, ".ts") + + expect(result).toContain(`${firstFile}: error at 1:0: problem in ${firstFile}`) + expect(result).toContain(`${secondFile}: error at 1:0: problem in ${secondFile}`) + } finally { + rmSync(tmp, { recursive: true, force: true }) + } + }) }) }) diff --git a/src/tools/lsp/directory-diagnostics.ts b/src/tools/lsp/directory-diagnostics.ts index a99450fa8..b3dd96106 100644 --- a/src/tools/lsp/directory-diagnostics.ts +++ b/src/tools/lsp/directory-diagnostics.ts @@ -11,6 +11,11 @@ import type { Diagnostic } from "./types" const SKIP_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]) +type FileDiagnostic = { + filePath: string + diagnostic: Diagnostic +} + function collectFilesWithExtension(dir: string, extension: string, maxFiles: number): string[] { const files: string[] = [] @@ -95,7 +100,7 @@ export async function aggregateDiagnosticsForDirectory( const root = findWorkspaceRoot(absDir) - const allDiagnostics: Diagnostic[] = [] + const allDiagnostics: FileDiagnostic[] = [] const fileErrors: { file: string; error: string }[] = [] let client: LSPClient @@ -106,7 +111,12 @@ export async function aggregateDiagnosticsForDirectory( try { const result = await client.diagnostics(file) const filtered = filterDiagnosticsBySeverity(result.items, severity) - allDiagnostics.push(...filtered) + allDiagnostics.push( + ...filtered.map((diagnostic) => ({ + filePath: file, + diagnostic, + })) + ) } catch (e) { fileErrors.push({ file, @@ -138,8 +148,8 @@ export async function aggregateDiagnosticsForDirectory( if (displayDiagnostics.length > 0) { lines.push("") - for (const diag of displayDiagnostics) { - lines.push(formatDiagnostic(diag)) + for (const { filePath, diagnostic } of displayDiagnostics) { + lines.push(`${filePath}: ${formatDiagnostic(diagnostic)}`) } if (wasDiagCapped) { lines.push(