164 lines
4.4 KiB
TypeScript
164 lines
4.4 KiB
TypeScript
import { existsSync, lstatSync, readdirSync, type Stats } from "fs"
|
|
import { extname, join, resolve } from "path"
|
|
|
|
import { findServerForExtension } from "./config"
|
|
import { findWorkspaceRoot, formatServerLookupError } from "./lsp-client-wrapper"
|
|
import { filterDiagnosticsBySeverity, formatDiagnostic } from "./lsp-formatters"
|
|
import { LSPClient } from "./lsp-client"
|
|
import { lspManager } from "./lsp-server"
|
|
import { DEFAULT_MAX_DIAGNOSTICS, DEFAULT_MAX_DIRECTORY_FILES } from "./constants"
|
|
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[] = []
|
|
|
|
function walk(currentDir: string): void {
|
|
if (files.length >= maxFiles) return
|
|
|
|
let entries: string[] = []
|
|
try {
|
|
entries = readdirSync(currentDir)
|
|
} catch {
|
|
return
|
|
}
|
|
|
|
for (const entry of entries) {
|
|
if (files.length >= maxFiles) return
|
|
|
|
const fullPath = join(currentDir, entry)
|
|
|
|
let stat: Stats | undefined
|
|
try {
|
|
stat = lstatSync(fullPath)
|
|
} catch {
|
|
continue
|
|
}
|
|
|
|
if (!stat || stat.isSymbolicLink()) {
|
|
continue
|
|
}
|
|
|
|
if (stat.isDirectory()) {
|
|
if (!SKIP_DIRECTORIES.has(entry)) {
|
|
walk(fullPath)
|
|
}
|
|
} else if (stat.isFile()) {
|
|
if (extname(fullPath) === extension) {
|
|
files.push(fullPath)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
walk(dir)
|
|
return files
|
|
}
|
|
|
|
export async function aggregateDiagnosticsForDirectory(
|
|
directory: string,
|
|
extension: string,
|
|
severity?: "error" | "warning" | "information" | "hint" | "all",
|
|
maxFiles: number = DEFAULT_MAX_DIRECTORY_FILES
|
|
): Promise<string> {
|
|
if (!extension.startsWith(".")) {
|
|
throw new Error(
|
|
`Extension must start with a dot (e.g., ".ts", not "${extension}"). ` +
|
|
`Use ".${extension}" instead.`
|
|
)
|
|
}
|
|
|
|
const absDir = resolve(directory)
|
|
if (!existsSync(absDir)) {
|
|
throw new Error(`Directory does not exist: ${absDir}`)
|
|
}
|
|
|
|
const serverResult = findServerForExtension(extension)
|
|
if (serverResult.status !== "found") {
|
|
throw new Error(formatServerLookupError(serverResult))
|
|
}
|
|
|
|
const server = serverResult.server
|
|
const allFiles = collectFilesWithExtension(absDir, extension, maxFiles + 1)
|
|
const wasCapped = allFiles.length > maxFiles
|
|
const filesToProcess = allFiles.slice(0, maxFiles)
|
|
|
|
if (filesToProcess.length === 0) {
|
|
return [
|
|
`Directory: ${absDir}`,
|
|
`Extension: ${extension}`,
|
|
`Files scanned: 0`,
|
|
`No files found with extension "${extension}".`,
|
|
].join("\n")
|
|
}
|
|
|
|
const root = findWorkspaceRoot(absDir)
|
|
|
|
const allDiagnostics: FileDiagnostic[] = []
|
|
const fileErrors: { file: string; error: string }[] = []
|
|
|
|
let client: LSPClient
|
|
try {
|
|
client = await lspManager.getClient(root, server)
|
|
|
|
for (const file of filesToProcess) {
|
|
try {
|
|
const result = await client.diagnostics(file)
|
|
const filtered = filterDiagnosticsBySeverity(result.items, severity)
|
|
allDiagnostics.push(
|
|
...filtered.map((diagnostic) => ({
|
|
filePath: file,
|
|
diagnostic,
|
|
}))
|
|
)
|
|
} catch (e) {
|
|
fileErrors.push({
|
|
file,
|
|
error: e instanceof Error ? e.message : String(e),
|
|
})
|
|
}
|
|
}
|
|
} finally {
|
|
lspManager.releaseClient(root, server.id)
|
|
}
|
|
|
|
const displayDiagnostics = allDiagnostics.slice(0, DEFAULT_MAX_DIAGNOSTICS)
|
|
const wasDiagCapped = allDiagnostics.length > DEFAULT_MAX_DIAGNOSTICS
|
|
|
|
const lines: string[] = [
|
|
`Directory: ${absDir}`,
|
|
`Extension: ${extension}`,
|
|
`Files scanned: ${filesToProcess.length}${wasCapped ? ` (capped at ${maxFiles})` : ""}`,
|
|
`Files with errors: ${fileErrors.length}`,
|
|
`Total diagnostics: ${allDiagnostics.length}`,
|
|
]
|
|
|
|
if (fileErrors.length > 0) {
|
|
lines.push("", "File processing errors:")
|
|
for (const { file, error } of fileErrors) {
|
|
lines.push(` ${file}: ${error}`)
|
|
}
|
|
}
|
|
|
|
if (displayDiagnostics.length > 0) {
|
|
lines.push("")
|
|
for (const { filePath, diagnostic } of displayDiagnostics) {
|
|
lines.push(`${filePath}: ${formatDiagnostic(diagnostic)}`)
|
|
}
|
|
if (wasDiagCapped) {
|
|
lines.push(
|
|
"",
|
|
`... (${allDiagnostics.length - DEFAULT_MAX_DIAGNOSTICS} more diagnostics not shown)`
|
|
)
|
|
}
|
|
}
|
|
|
|
return lines.join("\n")
|
|
}
|