Files
2026Technology-Competition/docs/superpowers/specs/2026-08-08-pmd-auxclasspath-design.md
T

181 lines
9.9 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.
# PMD 依赖 Classpath 自动探测 — 设计 Spec
日期:2026-08-08
状态:待审批
关联流程:① 用户提出 → ② 需求澄清 → ③ 方案设计
## 1. 背景与根因
PMD 做类型感知分析(如 `UnusedImports``UnnecessaryFullyQualifiedName` 等规则)时,需要能解析源码中引用的外部类型,即需要"辅助 classpath"aux classpath)。当前插件调用 PMD 时未提供该 classpath
1. `src/adapters/pmd.ts:104``spawn('java', args, { cwd })` 未设置 `env`,依赖 classpath 无从传入。
2. `jars/pmd/PmdRunner.java` 也未读取任何 aux classpath 来源——经源码与编译产物 `PmdRunner.class` 双重验证,其既无 `getenv` 调用,也未调用 PMD 的 `PMDConfiguration.prependAuxClasspath(String)`pmd-core-7.26.0 中该方法存在,`javap` 已验证)。
因此即使插件侧把 classpath 设置进环境变量,PmdRunner 当前也不会消费它,需两端同时修改。
## 2. 共识(Stage ② 确认)
- **classpath 来源**:自动探测 Maven / Gradle 构建系统,不新增手动配置项。
- **探测优先级**`pom.xml``build.gradle``build.gradle.kts`,取第一个命中项;三者都没有则不设置 aux classpath(保持现状行为,不报错)。
- **缓存策略**:按 workingDir 缓存探测结果;以构建文件路径 + mtime 作为失效键,构建文件变更后自动重新探测。
- **多模块支持**:按模块根独立探测(workingDir 即模块根),不跨模块合并 classpath。
- **失败处理**:探测失败(工具未装 / 命令报错 / 超时)降级为不带 classpath 继续审查,仅日志提示,不影响可用性。
- **classpath 范围**runtimeClasspath 超集(compile + runtime + 传递依赖)。
## 3. 方案
### 3.1 架构概览
新增独立服务 `AuxClasspathResolver`(位于基础层 `src/services/`),对 PMD 适配器透明暴露 `resolve(workingDir) => Promise<string>`。数据流:
```
PmdAdapter.run()
├─ AuxClasspathResolver.resolve(workingDir)
│ ├─ 探测构建文件(pom.xml/build.gradle(.kts)),无 → 返回 ''
│ ├─ 缓存命中(构建文件路径+mtime 未变)→ 返回缓存 classpath
│ ├─ 未命中 → 运行 mvn/gradle 生成 classpath → 更新缓存(成功/失败均缓存)
└─ spawn('java', args, { cwd, env: { ...process.env, PMD_AUXCP } })
└─ PmdRunner.main() 读 PMD_AUXCP → config.prependAuxClasspath(aux)
```
classpath 以 `PMD_AUXCP` 环境变量经 `spawn``env` 传入子进程;`PmdRunner.java` 读取后注入 `PMDConfiguration`
### 3.2 文件变更清单
| 文件 | 变更 | 说明 |
|------|------|------|
| `src/services/auxClasspath.ts` | 新建 | `AuxClasspathResolver`:探测 + 缓存 + Maven/Gradle 调用 |
| `src/adapters/pmd.ts` | 修改 | `run()` 中解析 aux classpath`execPmd()` 的 spawn 传入 `env` |
| `jars/pmd/PmdRunner.java` | 修改 | 读取 `PMD_AUXCP`(兼容 `PMD_AUX_CLASSPATH`),调用 `prependAuxClasspath` |
| `jars/pmd/PmdRunner.class` | 重编译 | `javac -cp "jars/pmd/lib/*" -d jars/pmd/ jars/pmd/PmdRunner.java` |
| `package.json` | 修改 | 新增开关 `vscode-code-reviewer.pmd.autoAuxClasspath`(默认 true,见 §3.4 |
| `src/config/linter.ts` | 修改 | 新增 `getPMDAutoAuxClasspath()` 读取开关 |
### 3.3 关键接口
#### 3.3.1 `AuxClasspathResolver``src/services/auxClasspath.ts`
```ts
export interface AuxClasspathInfo {
classpath: string; // '' 表示无可用 classpath
source?: 'maven' | 'gradle';
error?: string; // 探测失败时的错误信息(仅日志用)
}
export class AuxClasspathResolver {
resolve(workingDir: string): Promise<AuxClasspathInfo>;
}
```
内部实现要点:
- **缓存结构**`Map<string, { buildFile: string; mtimeMs: number; info: AuxClasspathInfo }>`,键为 `workingDir`;外加 `Map<string, Promise<AuxClasspathInfo>>` 作 in-flight 去重,避免并发检查重复运行构建命令。
- **失效键**`buildFile 路径 + stat.mtimeMs`。每次 `resolve` 重新探测构建文件并 `stat`mtime 变化即重跑。
- **探测工具**
- Maven:优先 `mvnw`/`mvnw.cmd`,回退 `mvn`/`mvn.cmd`Windows 用 `.cmd`,避免 `spawn` PATHEXT 问题)。
- Gradle:优先 `gradlew`/`gradlew.bat`,回退 `gradle`/`gradle.bat`
- **Maven 命令**(写临时文件再读取,避免解析 stdout 噪音):
```
<mvn> -B -q -Dmdep.outputFile=<tempFile> dependency:build-classpath
```
读取 `tempFile` 内容 trim 后作为 classpath。插件默认 `includeScope=runtime``dependency:build-classpath` 默认值,符合 runtimeClasspath 超集共识)。
- **Gradle 命令**init 脚本注册一次性任务,打印 root 工程 `sourceSets.main.runtimeClasspath.asPath`):
```
<gradle> -q -I <initScript> _printRuntimeClasspath
```
init 脚本内容(Groovy,兼容 Groovy DSL 与 Kotlin DSL 项目):
```groovy
allprojects { proj ->
if (proj == rootProject) {
proj.tasks.register('_printRuntimeClasspath') {
doLast {
if (proj.plugins.hasPlugin('java')) {
def main = proj.sourceSets.findByName('main')
if (main != null) {
println main.runtimeClasspath.asPath
}
}
}
}
}
}
```
取 stdout 末行非空行作为 classpath。
- **超时**`spawn` + 手动计时(Maven 120s / Gradle 120s),超时 `proc.kill()` 并视为失败。
- **失败语义**:捕获所有异常/非零退出,返回 `{ classpath: '', error: message }`,并同样写入缓存(mtime 未变前不重跑,避免每次保存文件重复失败告警)。
#### 3.3.2 `PmdRunner.java` 改动
```java
String auxcp = System.getenv("PMD_AUXCP");
if (auxcp == null || auxcp.isEmpty()) {
auxcp = System.getenv("PMD_AUX_CLASSPATH"); // 兼容 PMD CLI 约定
}
if (auxcp != null && !auxcp.isEmpty()) {
config.prependAuxClasspath(auxcp);
}
```
放在 `config.addRuleSet(rulesetPath)` 之后、`PmdAnalysis.create(config)` 之前。
#### 3.3.3 `src/adapters/pmd.ts` 改动
- `PmdAdapter` 增加私有字段 `private auxResolver = new AuxClasspathResolver();`。
- `run()` 中 `classpath` 计算后、`execPmd` 前:
```ts
const auxInfo = await this.auxResolver.resolve(workingDir);
if (auxInfo.error) { console.warn(`[code-reviewer] PMD aux classpath resolve failed: ${auxInfo.error}`); }
const result = await this.execPmd(javaArgs, stdin, workingDir, auxInfo.classpath);
```
- `execPmd` 增加第 4 参数 `auxClasspath: string``spawn` 改为:
```ts
const env = { ...process.env };
if (auxClasspath && auxClasspath.trim() !== '') { env.PMD_AUXCP = auxClasspath; }
const proc = spawn('java', args, { cwd, env });
```
- Java 与 JSP 两条路径共用该逻辑(两者均可能受益于类型解析)。
### 3.4 新增配置项(待审批确认)
为自动探测提供安全阀:
```jsonc
"vscode-code-reviewer.pmd.autoAuxClasspath": {
"type": "boolean",
"default": true,
"description": "自动探测 Maven/Gradle 依赖并传给 PMD 作为辅助 classpath"
}
```
默认开启;置 `false` 时 `AuxClasspathResolver` 直接返回空,不做任何构建调用。若审批不通过可整体砍掉此项,不影响核心修复。
### 3.5 错误处理与降级
- 无构建文件 / 工具缺失 / 命令失败 / 超时 → `{ classpath: '' }`PMD 以无 aux classpath 运行(等同现状)。
- 失败信息仅 `console.warn`,不上浮错误弹窗、不改变 `AdapterResult.status`(保持 `'ok'`,避免误报 execution-failed 干扰现有审查流程)。
- `mvn/gradle` 输出含 Windows 盘符路径或空格时作为单个 env 变量值传递,无 shell 插值风险(`spawn` 不经 shell`env` 值原样透传)。
### 3.6 验证方式
1. `npm test`pretest 自动 `compile` + `lint`;当前仓库无 test 用例文件,adapter.test.ts 已移出,测试仅验证扩展可激活)。
2. 手工端到端(Windows 本机):
- 构造含 `pom.xml` 的最小 Maven 项目,被测 Java 源码引用第三方类型;确认 `.class` 的 PMD 类型敏感规则(如 `UnusedImports`)在改动前后行为差异、且无 `PMD exited with code` 报错。
- 构造含 `build.gradle` 项目跑同用例。
- 断言 `_printRuntimeClasspath` 任务输出非空;验证缓存:连续两次 check 只跑一次构建命令(改 `pom.xml` mtime 后重跑)。
- 无构建文件目录:行为与改动前一致。
3. `javac` 重编译 `PmdRunner.java` 成功,并核对 `PmdRunner.class` 中包含 `PMD_AUXCP` 字符串与 `prependAuxClasspath` 调用(`strings`/`javap` 验证)。
## 4. 影响范围
- 仅影响 PMD 适配器(Java + JSP)调用路径;无构建文件或开关关闭时行为与现状完全一致。
- 不改变规则集选择、诊断严重度映射、AI 审查流程。
- 新增基础层服务文件 1 个、PMD 适配器与 PmdRunner 修改、配置项 1 个;无 i18n 文案变更(失败仅 console 日志)。
- 首次探测某项目会额外运行一次 `mvn/gradle`(秒级),之后走缓存;可通过开关关闭。
## 5. 风险
- **Gradle init 脚本兼容性**`build.gradle.kts` 项目与 Groovy init 脚本组合在 Gradle 8/9 下可用(init 脚本恒为 Groovy,通过 Project API 访问 sourceSets);若项目配置特殊(无 java 插件、sourceSets 重命名),输出为空 → 降级无 classpath,不阻塞审查。
- **Maven 依赖解析失败**:私有仓库/离线场景 `dependency:build-classpath` 可能失败 → 降级,不影响审查。
- **首次延迟**:新项目首次检查多耗时(构建探测),后续缓存;可接受,且有开关兜底。
- **classpath 大小**:大型项目 runtimeClasspath 字符串可能很长,Windows 单环境变量上限约 32KB;超限时降级为无 classpath(读回为空/截断判断),不影响审查可用性。