260 lines
8.0 KiB
Markdown
260 lines
8.0 KiB
Markdown
# 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` 通过
|