# Step 06 — Phase 2.5: PMD 适配器 **依赖**: Step 02 **参考设计**: §3.3, §3.4 ## 目标 实现 Java 的 PMD 适配器。通过 Java 子进程调用 `PmdRunner` 包装器,支持 stdin 传入代码(虚拟文档)。 ## 新建文件 | # | 文件 | 说明 | |---|------|------| | 1 | `src/adapters/pmd.ts` | `PmdAdapter` | | 2 | `jars/pmd/PmdRunner.java` | PMD Java 包装器(stdin + JSON 输出) | | 3 | `jars/pmd/pmd-java-ruleset.xml` | Java 规则集 | | 4 | `jars/pmd/pmd-jsp-ruleset.xml` | JSP 规则集 | ## 目录结构 ``` jars/pmd/ ├── lib/ # PMD 依赖 JAR(需下载) ├── PmdRunner.java # 包装器 ├── pmd-java-ruleset.xml └── pmd-jsp-ruleset.xml ``` --- ## 1. `jars/pmd/PmdRunner.java` ```java import java.io.*; import java.nio.file.*; import net.sourceforge.pmd.*; import net.sourceforge.pmd.renderers.*; public class PmdRunner { public static void main(String[] args) throws Exception { if (args.length < 2) { System.err.println("Usage: PmdRunner "); System.exit(1); return; } String filePath = args[0]; String rulesetPath = args[1]; Path tempFile = null; if ("-".equals(filePath)) { String code = new String(System.in.readAllBytes()); tempFile = Files.createTempFile("pmd-stdin-", ".java"); Files.writeString(tempFile, code); filePath = tempFile.toString(); } try { PMDConfiguration config = new PMDConfiguration(); config.setInputFilePath(Path.of(filePath)); config.addRuleSet(Path.of(rulesetPath)); config.setReportFormat("json"); StringWriter writer = new StringWriter(); config.setReportWriter(writer); PmdAnalysis pmd = PmdAnalysis.create(config); pmd.performAnalysis(); System.out.print(writer.toString()); } finally { if (tempFile != null) { Files.deleteIfExists(tempFile); } } } } ``` **编译命令**(classpath 需指向 `jars/pmd/lib/*`): ```bash javac -cp "jars/pmd/lib/*" -d jars/pmd/ jars/pmd/PmdRunner.java ``` --- ## 2. `jars/pmd/pmd-java-ruleset.xml` ```xml Java Code Review Rules ``` --- ## 3. `jars/pmd/pmd-jsp-ruleset.xml` ```xml JSP Code Review Rules ``` --- ## 4. `src/adapters/pmd.ts` ```typescript import * as vscode from 'vscode'; import * as path from 'path'; import * as child_process from 'child_process'; import { LinterAdapter, LinterDiagnostic, AdapterResult } from '../types'; import { getLinterConfig } from '../config'; export class PmdAdapter implements LinterAdapter { id = 'pmd'; supportedLanguages = ['java']; private getPmdLibClasspath(): string { const extRoot = vscode.extensions.getExtension?.('vscode-code-reviewer')?.extensionPath ?? path.join(__dirname, '..', '..'); const pmdLib = path.join(extRoot, 'jars', 'pmd', 'lib'); return path.join(pmdLib, '*'); } private getPmdRunnerClasspath(): string { const extRoot = vscode.extensions.getExtension?.('vscode-code-reviewer')?.extensionPath ?? path.join(__dirname, '..', '..'); return path.join(extRoot, 'jars', 'pmd'); } async check(document: vscode.TextDocument, workingDir: string): Promise { try { const config = getLinterConfig(); const ruleset = config.pmdRulesetPath || 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')) { return { diagnostics: [], status: 'tool-unavailable', errorMessage: 'Java 11+ 未安装或不在 PATH 中' }; } return { diagnostics: [], status: 'execution-failed', errorMessage: message }; } } private execPmd(args: string[], stdinInput: string | null, cwd: string): Promise { return new Promise((resolve, reject) => { const proc = child_process.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 { child_process.execSync('java -version 2>&1', { stdio: 'ignore' }); return true; } catch { return false; } } } ``` --- ## 关键逻辑 - 虚拟文档通过 stdin 传入代码(`PmdRunner "-" ruleset`) - 真实文件传文件路径(`PmdRunner filePath ruleset`) - classpath 使用 `;` 分隔(Windows 目标平台) - isVirtual 判断依据:`document.uri.scheme === 'untitled'` - PMD exit code 0 和 4 都视为成功(4 表示有 violations 但执行成功) - isAvailable 检测 Java 是否可用 --- ## 验收 - [ ] 4 个文件创建完成 - [ ] PMD JAR 依赖已下载到 `jars/pmd/lib/` - [ ] `PmdRunner.java` 编译成功 - [ ] `npm run compile` 通过 - [ ] `npm run lint` 通过