import * as vscode from 'vscode'; import * as path from 'path'; import { existsSync } from 'fs'; import { execSync, spawn } from 'child_process'; import type { LinterAdapter, LinterDiagnostic, AdapterResult } from '../types'; import { getPMDRulesetPath } from '../config'; import { t } from '../i18n/messages'; export class PmdAdapter implements LinterAdapter { id = 'pmd'; supportedLanguages = ['java']; private pmdDir: string | null = null; private resolvePmdDir(): string { if (this.pmdDir) { return this.pmdDir; } const candidates: string[] = []; try { const ext = vscode.extensions.getExtension?.('vscode-code-reviewer'); if (ext?.extensionPath) { candidates.push(path.join(ext.extensionPath, 'jars', 'pmd')); candidates.push(path.join(ext.extensionPath, 'out', 'jars', 'pmd')); } } catch { /* ignore */ } candidates.push(path.join(path.resolve(__dirname, '..'), 'jars', 'pmd')); candidates.push(path.join(path.resolve(__dirname, '..', '..'), 'jars', 'pmd')); for (const dir of candidates) { if (existsSync(path.join(dir, 'PmdRunner.class'))) { this.pmdDir = dir; return dir; } } this.pmdDir = candidates[0]; return this.pmdDir; } private getPmdLibClasspath(): string { return path.join(this.resolvePmdDir(), 'lib', '*'); } private getPmdRunnerClasspath(): string { return this.resolvePmdDir(); } async check(document: vscode.TextDocument, workingDir: string): Promise { try { const ruleset = getPMDRulesetPath() || path.join(this.getPmdRunnerClasspath(), 'pmd-java-ruleset.xml'); const classpath = `${this.getPmdLibClasspath()};${this.getPmdRunnerClasspath()}`; const isVirtual = document.uri.scheme === 'untitled'; const fileArg = isVirtual ? '-' : document.uri.fsPath; const javaArgs = ['-cp', classpath, 'PmdRunner', fileArg, ruleset]; const result = await this.execPmd(javaArgs, isVirtual ? document.getText() : null, workingDir); const diagnostics = this.parsePmdOutput(result); return { diagnostics, status: 'ok' }; } catch (err) { const message = err instanceof Error ? err.message : String(err); if (message.includes('ENOENT') || message.includes('java not found') || message.includes('Cannot find')) { return { diagnostics: [], status: 'tool-unavailable', errorMessage: t('adapter.javaNotInstalled') }; } return { diagnostics: [], status: 'execution-failed', errorMessage: message }; } } private execPmd(args: string[], stdinInput: string | null, cwd: string): Promise { return new Promise((resolve, reject) => { const proc = spawn('java', args, { cwd }); let stdout = ''; let stderr = ''; proc.stdout.on('data', (data: Buffer) => { stdout += data.toString(); }); proc.stderr.on('data', (data: Buffer) => { stderr += data.toString(); }); proc.on('close', (code) => { if (code === 0 || code === 4 || stdout.length > 0) { resolve(stdout); } else { reject(new Error(stderr || `PMD exited with code ${code}`)); } }); proc.on('error', reject); if (stdinInput !== null) { proc.stdin.write(stdinInput); proc.stdin.end(); } }); } private parsePmdOutput(output: string): LinterDiagnostic[] { if (!output.trim()) { return []; } try { const data = JSON.parse(output); const diagnostics: LinterDiagnostic[] = []; for (const file of data.files ?? []) { for (const violation of file.violations ?? []) { const line = Math.max(0, (violation.beginline ?? 1) - 1); const col = Math.max(0, (violation.begincolumn ?? 1) - 1); const endCol = Math.max(col, (violation.endcolumn ?? col + 1) - 1); const range = new vscode.Range(line, col, line, endCol); diagnostics.push({ severity: this.mapPriority(violation.priority), ruleId: `pmd:${violation.rule}`, message: violation.description ?? '', range, }); } } return diagnostics; } catch { return []; } } private mapPriority(priority: number): 'error' | 'warning' | 'info' { if (priority <= 2) { return 'error'; } if (priority === 3) { return 'warning'; } return 'info'; } isAvailable(): boolean { try { execSync('java -version 2>&1', { stdio: 'ignore' }); return true; } catch { return false; } } }