feat: PMD 依赖 classpath 自动探测 + SQLFluff AL06 参数修复 + 诊断消息前缀 Code Purifier + AI maxTokens 统一走配置
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
# 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(读回为空/截断判断),不影响审查可用性。
|
||||
@@ -0,0 +1,90 @@
|
||||
# SQLFluff AL06 生效修复 — 设计 Spec
|
||||
|
||||
日期:2026-08-08
|
||||
状态:待审批
|
||||
关联流程:① 用户提出 → ② 需求澄清 → ③ 方案设计
|
||||
|
||||
## 1. 背景与根因
|
||||
|
||||
用户反馈 SQLFluff 的 AL06(`aliasing.length`,强制表别名长度)未生效。
|
||||
|
||||
经代码调研 + 官方文档核实 + 本机 sqlfluff 4.2.2 实测,根因为:
|
||||
|
||||
1. 内置规则集 `rules = core,AM03,...`(`src/rules/builtin-rules.ts:94`)中的 `core` 组已包含 AL06。
|
||||
- 佐证:static-rules.json 中 32 条 P0 规则与 SQLFluff 官方 core 组完全一致,AL06 在其中。
|
||||
2. AL06 默认参数 `min_alias_length = None`、`max_alias_length = None`(官方默认配置),无限制时永不产生 violation,
|
||||
表现为"未生效"。
|
||||
3. 实测:`SELECT a FROM orders AS this_is_a_very_long_table_alias_x;` 在 `--rules core` 下仅报 AL05,无 AL06。
|
||||
|
||||
### 结论
|
||||
|
||||
AL06 不是"未启用",而是"已启用但缺少长度参数"。修复方向是为内置 SQLFluff 配置补充
|
||||
`[sqlfluff:rules:aliasing.length]` 参数段,而非向 `BUILTIN_SQLFLUFF_RULES` 添加 AL06。
|
||||
|
||||
## 2. 共识(Stage ② 确认)
|
||||
|
||||
- 场景:插件默认内置配置(无全局 configFile、无项目 .sqlfluff)下。
|
||||
- 阈值:`max_alias_length = 30`(与 Oracle 标识符 30 字符上限对齐,本项目 sql/plsql 方言兜底为 oracle);不设 `min_alias_length`。
|
||||
- 范围:不做其它 AL 规则调整;不改 `BUILTIN_SQLFLUFF_RULES` 字符串(避免 i18n 计数文案连锁变更)。
|
||||
|
||||
## 3. 方案
|
||||
|
||||
### 3.1 架构概览
|
||||
|
||||
无需架构调整。改动局限于 `src/rules/builtin-rules.ts` 中两个配置生成函数,使生成的
|
||||
配置文件同时携带 `[sqlfluff] rules` 与 `[sqlfluff:rules:aliasing.length] max_alias_length = 30`。
|
||||
|
||||
数据流不变:
|
||||
|
||||
```
|
||||
SqlFluffAdapter.check()
|
||||
└─ 无全局/项目配置时
|
||||
└─ buildBuiltinSqlfluffConfig(dialect) ──写临时文件──> sqlfluff lint --config ...
|
||||
```
|
||||
|
||||
### 3.2 文件变更清单
|
||||
|
||||
| 文件 | 变更 | 说明 |
|
||||
|------|------|------|
|
||||
| `src/rules/builtin-rules.ts` | 修改 | `buildBuiltinSqlfluffConfig()` 输出追加参数段 |
|
||||
| `src/rules/builtin-rules.ts` | 修改 | `buildSqlfluffProjectConfigText()` 输出同步追加参数段(设置面板生成的项目 .sqlfluff 模板与内置保持一致) |
|
||||
|
||||
无新建、无删除。
|
||||
|
||||
### 3.3 关键接口
|
||||
|
||||
两函数签名不变:
|
||||
|
||||
```ts
|
||||
export function buildBuiltinSqlfluffConfig(dialect: string): string
|
||||
export function buildSqlfluffProjectConfigText(dialect: string, lang: Language): string
|
||||
```
|
||||
|
||||
新增配置段内容(两处一致):
|
||||
|
||||
```ini
|
||||
[sqlfluff:rules:aliasing.length]
|
||||
max_alias_length = 30
|
||||
```
|
||||
|
||||
### 3.4 配置优先级影响
|
||||
|
||||
- 全局 `sqlfluff.configFile` / 项目 `.sqlfluff` 存在时仍优先(`sqlfluff.ts:141-149`),本次改动不影响这两条路径。
|
||||
- 仅内置兜底配置获得 AL06 长度限制,行为符合"与插件内置规则一致"的产品语义。
|
||||
|
||||
### 3.5 验证方式
|
||||
|
||||
1. `npm test`(lint → compile → test)。
|
||||
2. 手工端到端:`sqlfluff lint --rules core --config <生成配置>` 对超长别名 SQL 应报 AL06;现有 4.2.2 实测基线已确认改前不报。
|
||||
3. 可选:新增单测断言 `buildBuiltinSqlfluffConfig` 输出包含 `max_alias_length = 30`。
|
||||
|
||||
## 4. 影响范围
|
||||
|
||||
- 仅影响未配置任何外部 SQLFluff 配置的默认路径。
|
||||
- 不触发 i18n 文案、static-rules.json、package.json 变更。
|
||||
- 诊断严重度:AL06 tier 为 P0 → 保持 error(`tierToSeverity`,sqlfluff.ts:38)。
|
||||
|
||||
## 5. 风险
|
||||
|
||||
- SQLFluff 低版本(< 4.x)对 `[sqlfluff:rules:aliasing.length]` 段名兼容性:暂无低版本支持承诺,目标版本 4.2.2+,风险低。
|
||||
- 长别名在 Oracle 中本就受限(30 字符),阈值不会产生误报。
|
||||
Reference in New Issue
Block a user