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

9.9 KiB
Raw Blame History

PMD 依赖 Classpath 自动探测 — 设计 Spec

日期:2026-08-08 状态:待审批 关联流程:① 用户提出 → ② 需求澄清 → ③ 方案设计

1. 背景与根因

PMD 做类型感知分析(如 UnusedImportsUnnecessaryFullyQualifiedName 等规则)时,需要能解析源码中引用的外部类型,即需要"辅助 classpath"aux classpath)。当前插件调用 PMD 时未提供该 classpath

  1. src/adapters/pmd.ts:104spawn('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.xmlbuild.gradlebuild.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 环境变量经 spawnenv 传入子进程;PmdRunner.java 读取后注入 PMDConfiguration

3.2 文件变更清单

文件 变更 说明
src/services/auxClasspath.ts 新建 AuxClasspathResolver:探测 + 缓存 + Maven/Gradle 调用
src/adapters/pmd.ts 修改 run() 中解析 aux classpathexecPmd() 的 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 AuxClasspathResolversrc/services/auxClasspath.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 重新探测构建文件并 statmtime 变化即重跑。
  • 探测工具
    • Maven:优先 mvnw/mvnw.cmd,回退 mvn/mvn.cmdWindows 用 .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=runtimedependency:build-classpath 默认值,符合 runtimeClasspath 超集共识)。
  • Gradle 命令(init 脚本注册一次性任务,打印 root 工程 sourceSets.main.runtimeClasspath.asPath):
    <gradle> -q -I <initScript> _printRuntimeClasspath
    
    init 脚本内容(Groovy,兼容 Groovy DSL 与 Kotlin DSL 项目):
    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 改动

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 前:
    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: stringspawn 改为:
    const env = { ...process.env };
    if (auxClasspath && auxClasspath.trim() !== '') { env.PMD_AUXCP = auxClasspath; }
    const proc = spawn('java', args, { cwd, env });
    
  • Java 与 JSP 两条路径共用该逻辑(两者均可能受益于类型解析)。

3.4 新增配置项(待审批确认)

为自动探测提供安全阀:

"vscode-code-reviewer.pmd.autoAuxClasspath": {
  "type": "boolean",
  "default": true,
  "description": "自动探测 Maven/Gradle 依赖并传给 PMD 作为辅助 classpath"
}

默认开启;置 falseAuxClasspathResolver 直接返回空,不做任何构建调用。若审批不通过可整体砍掉此项,不影响核心修复。

3.5 错误处理与降级

  • 无构建文件 / 工具缺失 / 命令失败 / 超时 → { classpath: '' }PMD 以无 aux classpath 运行(等同现状)。
  • 失败信息仅 console.warn,不上浮错误弹窗、不改变 AdapterResult.status(保持 'ok',避免误报 execution-failed 干扰现有审查流程)。
  • mvn/gradle 输出含 Windows 盘符路径或空格时作为单个 env 变量值传递,无 shell 插值风险(spawn 不经 shellenv 值原样透传)。

3.6 验证方式

  1. npm testpretest 自动 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(读回为空/截断判断),不影响审查可用性。