Files
2026Technology-Competition/docs/superpowers/specs/implementation-steps/06-phase2.5-pmd-adapter.md
T

260 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <filePath|- for stdin> <rulesetPath>");
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
<?xml version="1.0"?>
<ruleset name="Java Rules"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
<description>Java Code Review Rules</description>
<rule ref="category/java/bestpractices.xml"/>
<rule ref="category/java/codestyle.xml"/>
<rule ref="category/java/design.xml"/>
<rule ref="category/java/errorprone.xml"/>
<rule ref="category/java/performance.xml"/>
<rule ref="category/java/security.xml"/>
</ruleset>
```
---
## 3. `jars/pmd/pmd-jsp-ruleset.xml`
```xml
<?xml version="1.0"?>
<ruleset name="JSP Rules"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
<description>JSP Code Review Rules</description>
<rule ref="category/jsp/bestpractices.xml"/>
<rule ref="category/jsp/codestyle.xml"/>
<rule ref="category/jsp/design.xml"/>
<rule ref="category/jsp/errorprone.xml"/>
</ruleset>
```
---
## 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<AdapterResult> {
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<string> {
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` 通过