diff --git a/AGENTS.md b/AGENTS.md index bc164eb..d77032a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,8 @@ - **涉及文件**:文件路径列表 - **使用模型**:当前使用的模型名 +**排除项**:打包工程(生产打包 VSIX,`npm run package-prod` / `vsce package`)不需要记录 + ## 阶段执行指引 - Stage ① @用户提出:load skill stage-1-propose - Stage ② @需求澄清:load skill stage-2-clarify diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e27fdc..4d8a8e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to the "vscode-code-reviewer" extension will be documented i Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. -## [Unreleased] +## [1.3.0] - 2026-08-09 -- Initial release \ No newline at end of file +### 修复 + +- 修复 `codeReviewer.addCustomRule` 空壳命令:改为打开设置面板,进入真正的规则添加流程(抽取 `openSetupPanel`,与 `openSetup` 共用) +- 修复插件停用资源清理:清空待执行的延迟分析定时器,显式释放诊断集合,消除停用后回调抛错隐患 +- 修复 PMD 退出码判定:去掉 `stdout.length > 0` 兜底,非零退出按 stderr 报错,不再静默吞错 +- 修复 SQLFluff 项目配置检测遗漏:全量支持 `setup.cfg` / `tox.ini` / `pep8.ini` / `pyproject.toml`,按配置段内容感知匹配,避免项目配置被内置临时配置覆盖 +- 修复 AI 翻译结果配对错位:由数组下标改为按 `originalRuleId` 匹配,AI 返回重排或漏译时不再错挂诊断 +- 修复启动/打开文件时分析错误未被捕获:`runStaticAndApply` 增加 try-catch 保护,消除未处理 Promise 拒绝 + +### 清理 + +- 删除 orchestrator 未使用的 `getAdapterMap()` / `getAdaptersByIds()` 死方法 +- 删除 pmd.ts 未使用的 `jarPathChecked` 死字段 +- 移除 SQLFluff 不支持的 `.sqlfluff.ini` 检测死代码 + +## [Unreleased] \ No newline at end of file diff --git a/_AI_USAGE_LOG.md b/_AI_USAGE_LOG.md index f0b0cfc..a40d10c 100644 --- a/_AI_USAGE_LOG.md +++ b/_AI_USAGE_LOG.md @@ -190,4 +190,14 @@ | 2026-08-08 22:36 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | maxTokens 统一走配置:fixer(4096→getAIMaxTokens)、import-service(8192→getAIMaxTokens)、setupView 连接测试(1024→getAIMaxTokens),各文件补 import,lint/compile/test 全通过 | 中间产物:无 | src/fixer/fixer.ts src/rules/import-service.ts src/views/setupView.ts | deepseek-v4-flash | | 2026-08-08 23:40 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 | PMD 依赖 classpath 自动探测设计:核实用户所述「PmdRunner 已支持 PMD_AUXCP」不成立(源码+编译 class 均无 getenv),设计两端修改方案(AuxClasspathResolver 服务 + PmdRunner 读 PMD_AUXCP),共识含探测优先级/缓存/多模块/失败降级/runtimeClasspath 范围,新增 autoAuxClasspath 开关待审批 | 中间产物:无 | docs/superpowers/specs/2026-08-08-pmd-auxclasspath-design.md | deepseek-v4-flash | | 2026-08-08 23:47 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 实现 PMD 依赖 classpath 自动探测:新建 src/services/auxClasspath.ts(AuxClasspathResolver:pom.xml→build.gradle→build.gradle.kts 优先级探测、按 workingDir+构建文件 mtime 缓存、in-flight 去重、mvn dependency:build-classpath/Gradle init 脚本 _printRuntimeClasspath、120s 超时、失败降级);pmd.ts run() 解析 aux 并在 execPmd spawn 传 env.PMD_AUXCP;PmdRunner.java 读 PMD_AUXCP/PMD_AUX_CLASSPATH 调 prependAuxClasspath 并重编译 .class;package.json + linter.ts 新增 pmd.autoAuxClasspath 开关(默认 true)。lint 0 error / compile 通过 / npm test 79 通过;本机无 mvn/gradle,用 vscode stub 冒烟验证无构建降级、工具缺失降级(spawn EINVAL 同步抛被 try/catch 捕获)、缓存命中(cached-same=true)、开关关闭直返;PmdRunner 带 PMD_AUXCP 实测正常输出 JSON | 中间产物:auxClasspath.ts 初版 proc.stdout 类型报 TS18047(ChildProcess|null 未收窄),改 const child = proc + stdout/stderr 可选链;冒烟脚本曾误用 process.argv[1](脚本路径)导致探测不到 pom.xml,改 argv[2] 重测 | src/services/auxClasspath.ts(新建) src/adapters/pmd.ts jars/pmd/PmdRunner.java jars/pmd/PmdRunner.class package.json src/config/linter.ts docs/superpowers/specs/2026-08-08-pmd-auxclasspath-design.md | deepseek-v4-flash | -| 2026-08-08 23:49 | ① 用户提出 → ⑤ 编码实现 → ⑥ 审查验证 | 打包生产 VSIX:npm run package-prod(esbuild build + vsce package),产物 vscode-code-reviewer-1.2.0.vsix(68.91MB);确认 out/extension.js 已包含 PMD_AUXCP 逻辑、jars/ 含重编译的 PmdRunner.class | 中间产物:无 | vscode-code-reviewer-1.2.0.vsix | deepseek-v4-flash | \ No newline at end of file +| 2026-08-09 13:28 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 修复审查报告 3 条结论:①「comand 拼写错误」不成立(全库无匹配);②「mariadb 缺引号」不成立(package.json:258 引号完整,node 解析 JSON OK);③「addCustomRule 空壳命令」成立——改为打开设置面板:抽取 openSetupPanel 辅助函数,openSetup 与 addCustomRule 共用,addCustomRule 不再弹提示而是进入真正的规则添加流程(setupView.addRule)。lint(0 error,仅 jsp.ts 既有 2 warning)compile 通过 | 中间产物:方案询问阶段用户从「改为打开设置面板 / 完整向导 / 去掉注册」三选一,选「改为打开设置面板」 | src/activation/commands.ts | deepseek-v4-flash | +| 2026-08-09 13:34 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 | AGENTS.md 日志规则新增排除项:打包工程(生产打包 VSIX,npm run package-prod / vsce package)不记录日志;_AI_USAGE_LOG.md 删除 2026-08-08 23:49 纯打包 VSIX 条目,保留混合条目 | 中间产物:初版将「排除项」误加在字段列表内(会歧义为日志字段),调整为字段列表后的独立段落 | AGENTS.md _AI_USAGE_LOG.md | deepseek-v4-flash | +| 2026-08-09 13:39 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 审查报告 #4 deactivate 资源清理:①「markers.dispose() 未调用」实为伪命题(markers 已 push context.subscriptions,VSCode 停用自动 dispose,diagnosticMarkers.ts:47 已释放集合);②「analysisTimers 未清理」属实——deactivate() 新增清空所有 pending 定时器(clearTimeout + Map.clear)并显式 markers?.dispose() 双保险,消除停用后 orchestrator=undefined 时定时器回调抛 TypeError 的隐患。lint 0 error / compile 通过。审批询问未获明确答复,按推荐方案执行 | 中间产物:无 | src/extension.ts | deepseek-v4-flash | +| 2026-08-09 13:46 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 审查报告 #5 PMD 退出码判定:核实「stdout.length > 0 可能掩盖错误」为潜在风险——code===4 是套用标准 PMD CLI 语义的误导性残留(PmdRunner.java 成功恒退 0、从不退 4);PmdRunner 非零退出时 stdout 必为空,故暂未触发,但兜底会吞掉「失败+stdout 有内容」的错误且 parsePmdOutput 解析失败静默返回 []。经用户确认兜底作用后审批通过,去掉 execPmd 成功条件中的 stdout.length > 0,改为 code===0 || code===4,与设计文档一致。对照确认 sqlfluff 同款兜底为必需(sqlfluff 检出违规退 1 但 stdout 有合法 JSON),未动。lint 0 error / compile 通过 | 中间产物:无 | src/adapters/pmd.ts | deepseek-v4-flash | +| 2026-08-09 14:01 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 审查报告 #6 SQLFluff 项目配置检测遗漏:核实属实——按已装 sqlfluff 4.2.2 源码 loader.py 支持的文件为 pyproject.toml > .sqlfluff > pep8.ini > tox.ini > setup.cfg,且 .sqlfluff.ini 在本版本无任何支持(原代码该项为死代码)。hasProjectSqlfluffConfig 改为内容感知全量检测:.sqlfluff 恒为配置;setup.cfg/tox.ini/pep8.ini 含 [sqlfluff] 段才算;pyproject.toml 含 [tool.sqlfluff](覆盖 [tool.sqlfluff.core])才算,避免普通 Python 项目 setup.cfg 被误判有配置而跳过内置精选规则集。用本机 sqlfluff 4.2.2 实证:setup.cfg 与 pyproject.toml 配置均被 CLI 正确识别(rules=LT01 仅报 LT01)。lint 0 error / compile 通过 | 中间产物:实证测试曾因 PowerShell Set-Content 写入 UTF-8 BOM 致 configparser 报 MissingSectionHeaderError,改 .NET WriteAllText 无 BOM 重测 | src/adapters/sqlfluff.ts | deepseek-v4-flash | +| 2026-08-09 14:04 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 审查报告 #7 orchestrator 未调用方法:核实属实——getAdapterMap()(language→adapter 映射)与 getAdaptersByIds()(按 ID 批量查询)在全部源码零调用,仅定义处与设计 spec 08-phase3-orchestrator.md 提及;实际查适配器走 getLinterForLanguage + this.adapters.find()。经审批删除两个死方法。lint 0 error / compile 通过 | 中间产物:编辑失误——先误将 getAdapterMap 重复插入 runStaticAnalysis 前形成两份定义,随即检查发现并一次性修正删除两处 getAdapterMap 与 getAdaptersByIds,终稿经 Read 确认仅剩 runStaticAnalysis | src/orchestrator/orchestrator.ts | deepseek-v4-flash | +| 2026-08-09 14:06 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 审查报告 #8 pmd.ts 未使用字段:核实属实——jarPathChecked 仅声明(pmd.ts:14)从未读写,本意可能是缓存 jar 路径校验状态,实际缓存由 pmdDir 字段承担,属遗留死代码。经审批删除该字段。lint 0 error / compile 通过 | 中间产物:无 | src/adapters/pmd.ts | deepseek-v4-flash | +| 2026-08-09 14:23 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 审查报告 #9 多适配器并行能力:判定不成立(设计而非缺陷)——runStaticAnalysis 按语言单 linter 分派是有文档依据的设计(design.md §4 调度逻辑、linters. 单选配置),5 适配器按语言各自工作非闲置;「SQL 同时跑 SQLFluff 和自定义规则」举例有误(自定义规则走 AI 引擎,与静态分析本就并行);多引擎文件走组合适配器(JspAdapter)。未改行为。应用户要求,在 orchestrator.ts 构造函数适配器注册处追加设计意图注释(单语言单 linter + 组合适配器,避免读者误判为 bug),措辞用「单语言单 linter 设计」而非「预留」。lint 0 error / compile 通过 | 中间产物:注释措辞方案二选一——初拟「预留」被否(暗示多 linter 为计划中功能),改「单语言单 linter 设计 + 组合适配器」 | src/orchestrator/orchestrator.ts | deepseek-v4-flash | +| 2026-08-09 14:29 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 审查报告 #10 AI 翻译按索引配对:核实属实(低频健壮性缺陷)——merger.ts 原 translatedDiagnostics[i] 按下标与 staticDiagnostics[i] 配对,if(td) 只挡缺失不挡错位;schema 的 originalRuleId 字段与 AI prompt 本就要求按规则 ID 返回,但 merger 未接线。经审批改为按 originalRuleId 匹配(同 ruleId 多条顺序消费),重排/漏译不再挂错。新增 2 个 merger 测试(错序不漏译挂错、重复 ruleId 顺序消费),npm test 81 passing | 中间产物:首个测试断言顺序错误——未考虑 merger 按 severity+line 排序,断言 linterDiagnostics[1]/[2] 的 message 写反,失败后修正断言重跑通过 | src/merger/merger.ts src/test/merger.test.ts | deepseek-v4-flash | +| 2026-08-09 14:31 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 审查报告 #11 启动分析未捕获错误:核实属实且更严重——void analyzeOpenDocuments()/void runStaticAndApply()/scheduleAnalysis 定时器三处均为未捕获 Promise 拒绝路径,runStaticAndApply 内部无 try-catch,markers.apply 无保护;纠正审查中「runStaticAnalysis 自身有 try-catch」的事实错误(orchestrator.ts 直接 await adapter.check(),无 try-catch,唯一防线是各适配器内部 catch)。经审批在 runStaticAndApply 包 try-catch + console.error 单点保护,覆盖三处调用。lint 0 error / compile / npm test 81 passing | 中间产物:无 | src/extension.ts | deepseek-v4-flash | +| 2026-08-09 14:34 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 版本升级 1.2.0 → 1.3.0:package.json version 更新;CHANGELOG.md 由模板「Unreleased-Initial release」改为 1.3.0 正式条目(修复 6 项 + 清理 3 项);整合审查报告 #3-#11 全部修复作为本版本内容 | 中间产物:无 | package.json CHANGELOG.md | deepseek-v4-flash | \ No newline at end of file diff --git a/package.json b/package.json index 0cc70b5..eb465ba 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vscode-code-reviewer", "displayName": "净码特工 · Code Purifier", "description": "AI 驱动的代码审查与规范检查一体化 VSCode 插件", - "version": "1.2.0", + "version": "1.3.0", "engines": { "vscode": "^1.120.0" }, diff --git a/src/activation/commands.ts b/src/activation/commands.ts index 018a2c1..7140a96 100644 --- a/src/activation/commands.ts +++ b/src/activation/commands.ts @@ -17,6 +17,20 @@ import type { CustomRule } from '../types'; let currentReport: MergedReport | null = null; +async function openSetupPanel(): Promise { + try { + await vscode.commands.executeCommand('workbench.view.extension.code-reviewer'); + } catch { + const action = await vscode.window.showErrorMessage( + t('setup.openSetupFail'), + t('setup.openSettingsJson') + ); + if (action === t('setup.openSettingsJson')) { + await vscode.commands.executeCommand('workbench.action.openSettingsJson'); + } + } +} + export function registerCommands( context: vscode.ExtensionContext, orchestrator: Orchestrator, @@ -224,13 +238,13 @@ export function registerCommands( ); context.subscriptions.push( - vscode.commands.registerCommand('codeReviewer.addCustomRule', () => { + vscode.commands.registerCommand('codeReviewer.addCustomRule', async () => { const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; if (!workspaceRoot) { vscode.window.showWarningMessage(t('setup.noWorkspace')); return; } - vscode.window.showInformationMessage(t('setup.manageRulesHint')); + await openSetupPanel(); }) ); @@ -248,17 +262,7 @@ export function registerCommands( context.subscriptions.push( vscode.commands.registerCommand('codeReviewer.openSetup', async () => { - try { - await vscode.commands.executeCommand('workbench.view.extension.code-reviewer'); - } catch { - const action = await vscode.window.showErrorMessage( - t('setup.openSetupFail'), - t('setup.openSettingsJson') - ); - if (action === t('setup.openSettingsJson')) { - await vscode.commands.executeCommand('workbench.action.openSettingsJson'); - } - } + await openSetupPanel(); }) ); diff --git a/src/adapters/pmd.ts b/src/adapters/pmd.ts index 217d4cc..e982f0f 100644 --- a/src/adapters/pmd.ts +++ b/src/adapters/pmd.ts @@ -11,7 +11,6 @@ export class PmdAdapter implements LinterAdapter { id = 'pmd'; supportedLanguages = ['java']; private pmdDir: string | null = null; - private jarPathChecked = false; private auxResolver = new AuxClasspathResolver(); private resolvePmdDir(): string { @@ -117,7 +116,7 @@ export class PmdAdapter implements LinterAdapter { 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) { + if (code === 0 || code === 4) { resolve(stdout); } else { reject(new Error(stderr || `PMD exited with code ${code}`)); diff --git a/src/adapters/sqlfluff.ts b/src/adapters/sqlfluff.ts index 2da2d16..a78a899 100644 --- a/src/adapters/sqlfluff.ts +++ b/src/adapters/sqlfluff.ts @@ -52,11 +52,19 @@ export function buildPRSMessage(description: string, dialect: string): string { } function hasProjectSqlfluffConfig(workspaceRoot: string): boolean { - const candidates = ['.sqlfluff', '.sqlfluff.ini']; + const candidates: Array<{ file: string; marker: string | null }> = [ + { file: '.sqlfluff', marker: null }, + { file: 'setup.cfg', marker: '[sqlfluff]' }, + { file: 'tox.ini', marker: '[sqlfluff]' }, + { file: 'pep8.ini', marker: '[sqlfluff]' }, + { file: 'pyproject.toml', marker: '[tool.sqlfluff]' }, + ]; for (const candidate of candidates) { - if (fs.existsSync(path.join(workspaceRoot, candidate))) { - return true; - } + const filePath = path.join(workspaceRoot, candidate.file); + if (!fs.existsSync(filePath)) { continue; } + if (candidate.marker === null) { return true; } + const content = fs.readFileSync(filePath, 'utf-8'); + if (content.includes(candidate.marker)) { return true; } } return false; } diff --git a/src/extension.ts b/src/extension.ts index d66f2de..0025c55 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -12,13 +12,17 @@ let orchestrator: Orchestrator; let markers: DiagnosticMarkers; async function runStaticAndApply(document: vscode.TextDocument): Promise { - const version = document.version; - const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? ''; - const workingDir = workspaceRoot || vscode.Uri.joinPath(document.uri, '..').fsPath; - const result = await orchestrator.runStaticAnalysis(document, workingDir); - const current = vscode.workspace.textDocuments.find(d => d.uri.toString() === document.uri.toString()); - if (!current || current.version !== version) { return; } - markers.apply(document.uri, result.diagnostics); + try { + const version = document.version; + const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? ''; + const workingDir = workspaceRoot || vscode.Uri.joinPath(document.uri, '..').fsPath; + const result = await orchestrator.runStaticAnalysis(document, workingDir); + const current = vscode.workspace.textDocuments.find(d => d.uri.toString() === document.uri.toString()); + if (!current || current.version !== version) { return; } + markers.apply(document.uri, result.diagnostics); + } catch (err) { + console.error('[code-reviewer] static analysis failed:', err); + } } const analysisTimers = new Map(); @@ -113,5 +117,10 @@ export function activate(context: vscode.ExtensionContext) { } export function deactivate() { + for (const timer of analysisTimers.values()) { + clearTimeout(timer); + } + analysisTimers.clear(); + markers?.dispose(); orchestrator = undefined!; } diff --git a/src/merger/merger.ts b/src/merger/merger.ts index c0df5e0..14ce696 100644 --- a/src/merger/merger.ts +++ b/src/merger/merger.ts @@ -66,9 +66,17 @@ export function mergeResults(input: MergeInput): MergedReport { d => d.range.start.line ); + const translationsByRule = new Map(); + for (const td of input.translatedDiagnostics) { + const list = translationsByRule.get(td.originalRuleId) ?? []; + list.push(td); + translationsByRule.set(td.originalRuleId, list); + } + const linterDiagnostics = sortBySeverityAndLine( - input.staticDiagnostics.map((d, i) => { - const td = input.translatedDiagnostics[i]; + input.staticDiagnostics.map(d => { + const list = translationsByRule.get(d.ruleId); + const td = list?.shift(); if (td) { return { ...d, message: td.translatedMessage, suggestion: td.translatedSuggestion || d.suggestion }; } diff --git a/src/orchestrator/orchestrator.ts b/src/orchestrator/orchestrator.ts index a782815..5a645b2 100644 --- a/src/orchestrator/orchestrator.ts +++ b/src/orchestrator/orchestrator.ts @@ -18,6 +18,8 @@ export class Orchestrator { private adapters: LinterAdapter[]; constructor() { + // 单语言单 linter 设计:按 linters. 单选配置分派一个适配器; + // 需多引擎的文件走组合适配器(如 JspAdapter = PMD + ESLint + Stylelint)。 this.adapters = [ new ESLintAdapter(), new PmdAdapter(), @@ -27,16 +29,6 @@ export class Orchestrator { ]; } - getAdapterMap(): Map { - const map = new Map(); - for (const adapter of this.adapters) { - for (const lang of adapter.supportedLanguages) { - map.set(lang, adapter); - } - } - return map; - } - async runStaticAnalysis( document: vscode.TextDocument, workingDir: string @@ -81,8 +73,4 @@ export class Orchestrator { duration: Date.now() - startTime, }; } - - getAdaptersByIds(ids: string[]): LinterAdapter[] { - return ids.map(id => this.adapters.find(a => a.id === id)).filter(Boolean) as LinterAdapter[]; - } }