- CodeGuard → Code Purifier / 净码特工(displayName、命令、配置标题) - 新增 ai.maxTokens 配置项,所有 Provider 及 fixer 传入 maxTokens - AI 引擎增强:repairJsonEscapes + JSON 解析 fallback + 详细错误信息 - 设置面板规则管理改为文件级(list/delete .yaml),addRule 改为 AI 从 Markdown 生成 YAML - yaml-parser 简化:移除 config.yaml 的 enable/disable 过滤逻辑 - 审查报告面板:errorBanner 优先显示具体错误、lint 诊断显示 suggestion、移除 translatedDiagnostics 独立渲染 - merger 中 translatedDiagnostics 覆盖原始 lint 诊断 message/suggestion - HTML linter 配置项、测试用例重写、typescript-eslint 移入 dependencies
45 KiB
Code Reviewer — VSCode Extension Design Spec
1. 概述
VSCode 代码审查与规范检查一体化工具。集成多语言静态分析器与 AI 智能审查,为开发团队提供代码质量保障。
核心能力
- 多语言代码静态分析(ESLint、PMD、Stylelint、sql-lint 等)
- AI 辅助深度审查与问题解释/翻译
- 自定义规则检查(YAML 定义 + AI 语义评估)
- 自动修复建议与批量修复(快照撤销)
- Webview 审查报告面板
2. 架构总览
插件分为四层:
UI 层 — Webview 审查面板 / Inline Diagnostic / Code Action
核心层 — Orchestrator(编排器)+ AI 审查引擎
适配层 — Linter 适配器(统一 LinterAdapter 接口)
基础层 — 配置管理 / 规则管理 / 报告导出
所有 linter 统一实现 LinterAdapter 接口,通过 Orchestrator 调度,结果聚合后通过 Webview 面板展示。
3. 适配器层
3.1 设计决策
| 决策项 | 结论 | 说明 |
|---|---|---|
| 支持语言 | Java / JS/TS / CSS / SQL / JSP | 不支持 Python、Go |
| 注册方式 | 硬编码(方案 A) | 适配器数量少,无需过度设计 |
| 审查粒度 | 单文件 | check() 接收单个 TextDocument |
| 目标平台 | Windows | PMD classpath 分隔符使用 ; |
3.2 统一接口
interface LinterDiagnostic {
severity: 'error' | 'warning' | 'info';
ruleId: string; // 格式: "linter名:规则ID"
message: string;
range: vscode.Range;
suggestion?: string;
}
interface AdapterResult {
diagnostics: LinterDiagnostic[];
status: 'ok' | 'tool-unavailable' | 'execution-failed';
errorMessage?: string;
}
interface LinterAdapter {
id: string;
supportedLanguages: string[];
check(document: vscode.TextDocument, workingDir: string): Promise<AdapterResult>;
isAvailable(): boolean;
}
错误状态说明:
| 状态 | 含义 | 用户感知 |
|---|---|---|
ok |
检查成功 | 正常显示结果 |
tool-unavailable |
工具未安装/未找到 | 提示用户安装对应工具 |
execution-failed |
工具已安装但执行出错 | 显示错误信息,引导排查 |
3.3 适配器清单
| 适配器 | 语言 | 实现方式 | 特殊处理 |
|---|---|---|---|
| ESLint | JS/TS | eslint npm 包 lintText() |
直接接收代码文本,支持虚拟文档 |
| PMD | Java | Java 子进程调用 | 支持 stdin 传入代码,支持虚拟文档 |
| Stylelint | CSS | stylelint npm 包 lint({ code }) |
直接接收代码文本,支持虚拟文档 |
| sql-lint | SQL | sqlfluff CLI 调用 | 支持 SQL 和 PL/SQL,方言映射 |
| JSP | JSP | 组合适配器(PMD + ESLint + Stylelint) | 提取内嵌代码块后分发检查 |
3.4 PMD 适配器特殊设计
PMD 是 Java 工具,需要特殊处理:
- JAR 文件管理:插件自带
jars/pmd/目录存放 PMD 依赖 - 规则集配置:支持自定义规则集 XML 文件
- Java 包装器:
PmdRunner.java简化调用,输出 JSON 格式 - 虚拟文档支持:通过 stdin 传入代码,无需真实文件
PmdRunner.java 核心逻辑(jars/pmd/PmdRunner.java):
参数: filePath (传 "-" 表示从 stdin 读取), ruleset
→ 构建 PMDConfiguration
→ 配置 JSON 渲染器
→ 若 filePath 为 "-",从 stdin 读代码 → 写入临时文件
→ 执行 PMD 分析
→ 输出 JSON 到 stdout
→ 清理临时文件
PmdAdapter.check() 虚拟文档处理:
if 虚拟文档 (uri.scheme === 'untitled')
→ java -cp "classpath;dist" PmdRunner "-" ruleset
→ stdin 传入 document.getText()
else
→ java -cp "classpath;dist" PmdRunner document.fileName ruleset
3.5 JSP 适配器设计
JSP 适配器是组合适配器,自身不做检查,而是将 JSP 文件拆分后交给其他适配器:
JSP 文件输入
│
├─ 1. 调用 PMD 检查 JSP 规范
│ → 使用 pmd-jsp-ruleset.xml
│
└─ 2. jsp-extractor 提取内嵌代码块
├─ <script> → JavaScript → ESLint
├─ <style> → CSS → Stylelint
└─ <% %> scriptlet → Java → PMD
│
└─ 行号偏移修正 → 合并结果
提取器(jsp-extractor.ts):
| 代码块类型 | 正则匹配 | 目标适配器 |
|---|---|---|
<script> 标签 |
/<script\b[^>]*>([\s\S]*?)<\/script\s*>/gi |
ESLint |
<style> 标签 |
/<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi |
Stylelint |
<% %> scriptlet |
/<%=?([\s\S]*?)%>/g |
PMD |
JspAdapter.check() 流程:
1. 调用 PmdAdapter.check(document) → JSP 规范检查
2. extractJspSections(document.getText()) → 提取内嵌代码块
3. 对每个 section:
a. 按 language 选择对应适配器
b. 创建虚拟文档 (vscode.workspace.openTextDocument)
c. 调用 adapter.check(virtualDoc)
d. 修正行号偏移 (section.lineOffset)
4. 合并所有结果
3.6 适配器注册
采用硬编码注册,在 Orchestrator 中维护适配器列表:
// orchestrator.ts
function getAdapters(): LinterAdapter[] {
return [
new ESLintAdapter(),
new PmdAdapter(),
new StylelintAdapter(),
new SqlLintAdapter(),
new JspAdapter(),
];
}
通过 linters.<language> 配置项控制每种语言使用的 linter(空字符串表示禁用)。
4. Orchestrator(编排器)
注册方式
采用硬编码注册,适配器列表在 orchestrator.ts 中静态定义(见 §3.6)。
执行策略
- 保存触发:文件保存时自动运行,debounce 500ms(可配置)
- 手动触发:命令面板、右键菜单、文件树右键
- 进度反馈:运行时显示
withProgress
调度逻辑
1. 获取当前文档语言 ID
2. 查询 linters.<language> 配置 → 确定使用的适配器
3. 调用适配器 check(document, workingDir)
4. 收集结果,区分 status
5. 返回聚合后的 diagnostics 列表
5. AI 审查引擎
5.1 设计决策
| 决策项 | 结论 | 说明 |
|---|---|---|
| 任务拆分 | 两并行请求 | 请求 A(自定义规则评估)+ 请求 B(翻译 + 深度审查) |
| 代码长度限制 | 暂不限制 | 后续可添加大文件截断 |
| 模型支持 | Provider 层抽象 | 支持 DeepSeek / OpenAI,统一接口 |
| 成本控制 | 暂不考虑 | 不做 Token 上限、调用次数限制 |
| 修复复用 | 复用 Provider | 单条修复和批量修复共用 Provider 层 |
| 错误感知 | 区分提示 | 不同错误类型显示不同提示信息 |
| Provider 与面板共用 | ai.provider 通用 | 面板下拉框直接读写 ai.provider |
5.2 Provider 层架构
采用策略模式,将不同模型的 API 调用封装为独立 Provider,上层只依赖统一接口。
src/ai/
├── providers/
│ ├── base.ts # Provider 接口/抽象基类
│ ├── deepseek.ts # DeepSeek 实现
│ └── openai.ts # OpenAI 实现
├── factory.ts # Provider 工厂
├── engine.ts # 审查引擎(业务逻辑)
└── schema.ts # 响应结构定义
Provider 接口:
// providers/base.ts
export interface ChatOptions {
model: string;
temperature: number;
timeoutMs: number;
}
export abstract class AIProvider {
abstract id: string;
abstract name: string;
constructor(
protected apiKey: string,
protected baseUrl: string
) {}
abstract chat(
systemPrompt: string,
userPrompt: string,
options: ChatOptions
): Promise<string>;
}
Provider 工厂:
// factory.ts
const registry: Record<string, new (apiKey: string, baseUrl: string) => AIProvider> = {
deepseek: DeepSeekProvider,
openai: OpenAIProvider,
};
export function createProvider(providerId: string, apiKey: string, baseUrl: string): AIProvider {
const Cls = registry[providerId];
if (!Cls) throw new Error(`Unknown provider: ${providerId}`);
return new Cls(apiKey, baseUrl);
}
5.3 两并行请求方案
请求 A(规则审查) 请求 B(翻译 + 深度审查)
┌──────────────────┐ ┌─────────────────────────┐
│ 自定义规则评估 │ │ 静态分析结果翻译 │
│ (需要完整代码) │ │ (需静态分析结果) │
└──────────────────┘ │ AI 深度审查 │
│ (需代码 + 静态分析) │
└─────────────────────────┘
请求分配理由:
| 任务 | 请求 | 理由 |
|---|---|---|
| 自定义规则评估 | A | 需完整代码,与深度审查关注点不同 |
| 静态分析翻译 | B | 与深度审查共享静态分析上下文,放一起省 Token |
| AI 深度审查 | B | 需代码 + 静态分析结果,与翻译同上下文 |
执行流程:
async function runAIReview(code: string, staticDiagnostics: LinterDiagnostic[], customRules: CustomRule[]) {
const [resultA, resultB] = await Promise.allSettled([
callCustomRuleReview(code, customRules), // 请求 A
callTranslateAndDeepReview(code, staticDiagnostics), // 请求 B
]);
return {
customRuleResults: resultA.status === 'fulfilled' ? resultA.value : [],
translatedDiagnostics: resultB.status === 'fulfilled' ? resultB.value.translated : [],
findings: resultB.status === 'fulfilled' ? resultB.value.findings : [],
degraded: resultA.status === 'rejected' || resultB.status === 'rejected',
error: collectErrors(resultA, resultB),
};
}
5.4 Prompt 设计
请求 A — 自定义规则评估:
System: 你是代码规则审查员,只评估以下自定义规则是否被违反。
理解语义而非文本匹配。
仅输出 JSON,格式:{ customRuleResults: [{ ruleId, line, severity, message }] }
User: ## 自定义规则
<规则列表>
## 代码(带行号)
<代码内容>
请求 B — 翻译 + 深度审查:
System: 你是资深代码审查专家,完成两个任务:
1. 将英文静态分析结果翻译为中文,并补充修复建议
2. 深度审查代码,发现静态分析未覆盖的问题
重点:安全漏洞、逻辑错误、性能问题、设计缺陷
不要重复静态分析已报告的问题
输出语言:<language>
仅输出 JSON
User: ## 代码(带行号)
<代码内容>
## 静态分析结果(英文)
<诊断列表>
5.5 AI 响应结构
// ai/schema.ts
interface AIResponse {
translatedDiagnostics: TranslatedDiagnostic[];
customRuleResults: CustomRuleResult[];
findings: AIFinding[];
}
interface TranslatedDiagnostic {
originalRuleId: string;
translatedMessage: string;
translatedSuggestion: string;
codeDiff?: string;
}
interface CustomRuleResult {
ruleId: string; // 格式: "custom:规则id"
line: number;
severity: 'error' | 'warning' | 'info';
message: string;
}
interface AIFinding {
ruleId: string; // kebab-case,如 no-hardcoded-secret
severity: 'error' | 'warning' | 'info';
category: 'bug' | 'performance' | 'security' | 'style' | 'design';
title: string;
description: string;
suggestion: string;
codeDiff?: string; // unified diff 格式
line: number;
}
5.6 错误处理与用户感知
| 错误类型 | 用户感知 | 处理方式 |
|---|---|---|
| 未配置 API Key | 面板显示「请先设置 API Key」+ 设置按钮 | 降级,静态分析结果正常显示 |
| API Key 无效 | 面板显示「API Key 无效,请重新设置」+ 设置按钮 | 降级,弹错误提示 |
| 网络超时 | 面板显示「AI 请求超时,可重试」+ 重试按钮 | 降级,静态分析结果正常显示 |
| 模型服务不可用 | 面板显示具体错误信息 | 降级 |
| JSON 解析失败 | 面板显示「AI 响应格式异常」+ 查看原始响应 | 降级,记录原始响应 |
| 部分请求失败 | 面板显示「部分 AI 功能不可用」+ 详情 | 部分降级 |
降级策略:
- 单请求失败不影响另一个请求(
Promise.allSettled) - 所有 AI 功能都失败时,纯静态分析结果仍然展示
- 失败信息在面板顶部以黄色/红色提示条展示
6. 插件贡献点
Commands
| 命令 | 功能 | 快捷键 |
|---|---|---|
vscode-code-reviewer.review |
运行完整审查(静态分析 + AI) | Ctrl+Shift+R |
vscode-code-reviewer.reviewSelection |
审查选中代码 | — |
vscode-code-reviewer.openPanel |
显示审查报告面板 | — |
vscode-code-reviewer.exportReport |
导出 Markdown 报告 | — |
vscode-code-reviewer.addCustomRule |
添加自定义规则 | — |
vscode-code-reviewer.fixIssue |
修复单条问题 | — |
vscode-code-reviewer.fixAll |
批量修复 | — |
vscode-code-reviewer.openSetup |
打开设置面板(侧边栏) | — |
Views
codeReviewer.setupView— 设置面板(侧边栏视图)
菜单集成
{
"menus": {
"editor/context": [
{
"command": "vscode-code-reviewer.review",
"group": "navigation"
},
{
"command": "vscode-code-reviewer.reviewSelection",
"when": "editorHasSelection"
}
]
}
}
视图容器
{
"viewsContainers": {
"activitybar": [
{
"id": "code-reviewer",
"title": "净码特工",
"icon": "images/icon.png"
}
]
},
"views": {
"code-reviewer": [
{
"type": "tree",
"id": "codeReviewer.setupView",
"name": "设置"
}
]
}
}
Configuration
以下配置项通过 VS Code 原生设置界面(Ctrl+,)修改。AI 基础配置、API Key 和自定义规则的启用/禁用则在侧边栏设置面板中管理。
AI 配置:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
vscode-code-reviewer.ai.provider |
enum | deepseek |
模型提供商:deepseek / openai |
vscode-code-reviewer.ai.model |
string | deepseek-chat |
模型名称 |
vscode-code-reviewer.ai.baseUrl |
string | https://api.deepseek.com/v1 |
API Base URL |
vscode-code-reviewer.ai.temperature |
number | 0.2 | AI 温度参数(建议 0.1-0.3) |
vscode-code-reviewer.ai.timeout |
number | 300 | AI 请求超时(秒) |
vscode-code-reviewer.ai.outputLanguage |
string | zh-CN |
输出语言:zh-CN / en / ja |
Linter 配置(每种语言单选,空字符串=禁用):
| 配置项 | 类型 | 默认值 | enum |
|---|---|---|---|
vscode-code-reviewer.linters.javascript |
enum | eslint |
"" / eslint |
vscode-code-reviewer.linters.typescript |
enum | eslint |
"" / eslint |
vscode-code-reviewer.linters.java |
enum | pmd |
"" / pmd |
vscode-code-reviewer.linters.jsp |
enum | jsp |
"" / jsp |
vscode-code-reviewer.linters.css |
enum | stylelint |
"" / stylelint |
vscode-code-reviewer.linters.sql |
enum | sql-lint |
"" / sql-lint |
vscode-code-reviewer.linters.plsql |
enum | sql-lint |
"" / sql-lint |
PMD 配置:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
vscode-code-reviewer.pmd.jarPath |
string | "" |
PMD jar 路径(空=使用插件内置) |
vscode-code-reviewer.pmd.rulesetPath |
string | "" |
Java 规则集 XML 路径(空=使用内置) |
vscode-code-reviewer.pmd.jspRulesetPath |
string | "" |
JSP 规则集 XML 路径(空=使用内置) |
其他:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
vscode-code-reviewer.sql-lint.configFile |
string | "" |
sqlfluff 配置文件路径 |
vscode-code-reviewer.fixer.contextLines |
number | 5 | AI 修复时提取的上下文行数 |
API Key 不在此配置,通过 VS Code
SecretStorage存储,在设置面板中管理。
7. 审查面板(Webview)
7.1 面板功能
采用 Webview 实现,分 Tab 展示三类审查结果:
| Tab | 来源 | 内容 |
|---|---|---|
| 🔧 静态分析 | Linter 适配器 | ESLint / PMD / Stylelint / sql-lint / JSP 的问题列表 |
| 📋 自定义规则 | AI 请求 A | custom:* 规则评估结果 |
| 🤖 AI 审查 | AI 请求 B | 翻译后的诊断 + 深度审查发现 |
每条问题支持:点击跳转到代码位置、单条修复、添加忽略注释。
7.2 交互设计
┌─────────────────────────────────────┐
│ 📋 代码审查报告 │
│ xxx.java · Java · 3.2s │
├─────────────────────────────────────┤
│ [总计:10] [错误:3] [警告:5] [建议:2] │
├─────────────────────────────────────┤
│ 🔧 静态分析 | 📋 自定义规则 | 🤖 AI │
├─────────────────────────────────────┤
│ ┌─────────────────────────────────┐ │
│ │ 🔴 PMD L23 [修复] [忽略]│ │
│ │ 避免重复字符串常量 │ │
│ │ NoScriptlets.md │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ 🟡 custom L45 [修复] [忽略]│ │
│ │ SQL 拼接风险 │ │
│ └─────────────────────────────────┘ │
├─────────────────────────────────────┤
│ [🔄 重新审查] [📄 导出] [⚙️ 设置] │
│ [↩ 撤销上次修复] │
└─────────────────────────────────────┘
降级提示:当 AI 请求部分/全部失败时,面板顶部显示黄色/红色提示条,静态分析结果正常展示。
7.3 消息通信
Webview 与扩展通过 postMessage 通信:
interface PanelMessage {
type: 'navigate' | 'rerun' | 'export' | 'settings' | 'fix' | 'fixAll';
line?: number;
ruleId?: string;
source?: 'linter' | 'custom' | 'ai';
}
// 扩展 → Webview:推送审查结果
interface PanelUpdate {
report: MergedReport;
degraded: boolean;
errors: string[];
hasSnapshot: boolean; // 撤销按钮状态
}
7.4 与结果合并器的关系
Linter 诊断 ─┐
AI 翻译 ─┤
自定义规则 ─┤→ Merger.mergeResults() → MergedReport → Webview 渲染
AI 深度审查 ─┤
执行错误 ─┘
用户操作 → PanelMessage → extension → 执行对应命令 → 更新面板
8. 自动修复模块
8.1 设计决策
| 决策项 | 结论 | 说明 |
|---|---|---|
| 上下文行数 | 动态调整 | 根据问题类型决定上下文范围,不固定 5 行 |
| 修复匹配策略 | 行号 + 代码匹配 | 先按行号匹配原文,失败则在文件中搜索 |
| 批量修复 | 预览后应用 | 先展示所有修改,用户确认后才执行 |
| 修复后验证 | 不做验证 | 不重新跑 linter 或编译,信任 AI 输出 |
| 撤销机制 | 快照 + 撤销按钮 | 修复前保存快照,面板底部提供"撤销上次修复" |
| 修复范围限定 | 不限定 | 用户自行判断哪些问题可修复 |
| Code Action | 复用 AI Provider | 单条修复与批量修复共用 §5.2 的 Provider 层 |
8.2 修复流程
用户触发修复(单条 / 批量)
↓
prepareContext() 获取代码上下文(动态行数,见 §8.3)
↓
generateFix() 调用 AI 生成修复方案(复用 §5.2 Provider)
↓
matchAndValidate() 匹配验证(行号 → 代码搜索,见 §8.4)
↓
applyFix() 应用修复到编辑器(单条 / 批量倒序)
↓
保存快照,更新撤销按钮状态
8.3 动态上下文策略
根据问题分类决定上下文的行数范围:
| 问题类型 | 上下文范围 | 说明 |
|---|---|---|
命名问题(naming) |
问题行 ± 2 行 | 只需那一行 |
代码风格(style) |
问题行 ± 5 行 | 需要少量上下文 |
逻辑错误(bug) |
整个函数/方法 | 需完整逻辑块 |
安全漏洞(security) |
整个函数/方法 | 需完整逻辑块 |
性能问题(performance) |
整个函数/方法 | 需完整逻辑块 |
interface FixableDiagnostic {
ruleId: string;
message: string;
line: number;
severity: string;
codeContext: string; // 带行号的代码片段
source: 'linter' | 'custom';
category: 'naming' | 'style' | 'bug' | 'security' | 'performance';
}
function prepareContext(document: vscode.TextDocument, diagnostic: LinterDiagnostic): FixableDiagnostic | null {
let startLine: number, endLine: number;
switch (diagnostic.category) {
case 'naming':
({ startLine, endLine } = rangeAround(diagnostic.line, 2)); break;
case 'style':
({ startLine, endLine } = rangeAround(diagnostic.line, 5)); break;
case 'bug':
case 'security':
case 'performance':
const funcRange = findEnclosingFunction(document, diagnostic.line);
startLine = funcRange?.start.line ?? diagnostic.line - 10;
endLine = funcRange?.end.line ?? diagnostic.line + 10;
break;
default:
({ startLine, endLine } = rangeAround(diagnostic.line, 5));
}
const codeContext = extractLines(document, startLine, endLine);
return { ...diagnostic, codeContext };
}
8.4 修复匹配策略
两阶段匹配,确保修复应用到正确位置:
阶段 1 — 行号匹配
提取问题行的原文 → 与 AI 返回的 originalText 首行对比
✓ 匹配 → 验证完整原文是否一致
✗ 不匹配 → 进入阶段 2
阶段 2 — 全文搜索
在文件中搜索 originalText
✓ 找到 → 通过 positionAt() 计算实际 range
✗ 未找到 → 标记为匹配失败
interface CodeFix {
startLine: number;
endLine: number;
originalText: string; // AI 认为要替换的原文
newText: string; // 修复后的新代码
}
function matchAndValidate(document: vscode.TextDocument, fix: CodeFix): { matched: boolean; actualRange?: vscode.Range } {
// 阶段 1:按行号匹配
const lineContent = document.lineAt(fix.startLine).text;
if (lineContent === fix.originalText.split('\n')[0]) {
const range = new vscode.Range(fix.startLine, 0, fix.endLine, document.lineAt(fix.endLine).text.length);
if (document.getText(range) === fix.originalText) {
return { matched: true, actualRange: range };
}
}
// 阶段 2:全文搜索
const index = document.getText().indexOf(fix.originalText);
if (index !== -1) {
return {
matched: true,
actualRange: new vscode.Range(
document.positionAt(index),
document.positionAt(index + fix.originalText.length)
),
};
}
return { matched: false };
}
失败处理:
- 单条修复失败 → 提示"代码已变更,无法定位问题位置"
- 批量修复部分失败 → 只应用成功匹配的修复,失败的在预览面板中标记
8.5 批量修复预览
批量修复不直接应用,先展示预览面板:
┌────────────────────────────────────────┐
│ 🔧 批量修复预览 │
├────────────────────────────────────────┤
│ 共 5 处修改,请确认后应用 │
│ │
│ ✓ L23 eslint:no-unused-vars │
│ - var unused = 1; │
│ + let count = 0; │
│ │
│ ✗ L67 custom:no-console │
│ 代码已变更,无法定位 │
│ │
│ [取消] [应用全部 (4 处)] │
└────────────────────────────────────────┘
应用顺序:按位置倒序执行(从文件末尾开始),避免行号偏移:
function applyBatchFixes(document: vscode.TextDocument, fixes: CodeFix[]): number {
const validFixes = fixes.filter(f => f.matched);
const sorted = [...validFixes].sort((a, b) => b.startLine - a.startLine);
let applied = 0;
for (const fix of sorted) {
if (applySingleFix(document, fix)) applied++;
}
return applied;
}
8.6 撤销机制
每次修复前保存当前文件内容的快照:
const snapshotStack: Map<string, string[]> = new Map();
function saveSnapshot(document: vscode.TextDocument): void {
const filePath = document.uri.fsPath;
if (!snapshotStack.has(filePath)) snapshotStack.set(filePath, []);
snapshotStack.get(filePath)!.push(document.getText());
}
function undoLastFix(document: vscode.TextDocument): boolean {
const stack = snapshotStack.get(document.uri.fsPath);
if (!stack || stack.length === 0) return false;
const previousContent = stack.pop()!;
const edit = new vscode.WorkspaceEdit();
edit.replace(document.uri, new vscode.Range(0, 0, document.lineCount, 0), previousContent);
return vscode.workspace.applyEdit(edit);
}
面板底部提供撤销按钮,上次修复成功时启用,无快照时禁用(灰色)。
9. 文件结构
vscode-code-reviewer/
├── src/
│ ├── extension.ts # 扩展入口
│ ├── orchestrator/
│ │ └── orchestrator.ts # 多 linter 调度与结果聚合
│ ├── adapters/ # 语言适配器(策略模式)
│ │ ├── adapter.ts # LinterAdapter 统一接口定义
│ │ ├── eslint.ts # JavaScript / TypeScript
│ │ ├── pmd.ts # Java
│ │ ├── stylelint.ts # CSS
│ │ ├── sql-lint.ts # SQL / PL/SQL
│ │ └── jsp.ts # JSP 组合适配器
│ ├── jsp/
│ │ └── jsp-extractor.ts # JSP 内嵌代码块提取器
│ ├── ai/ # AI 审查引擎(后续细化)
│ ├── fixer/ # 自动修复模块(§8)
│ ├── merger/ # 结果合并与报告(§14)
│ ├── rules/ # 自定义规则管理(后续细化)
│ ├── config/ # 配置管理(§13 设置面板)
│ ├── panel/ # UI 面板(§7 Webview 审查面板)
│ └── util/ # 工具函数
├── jars/ # 外部工具(PMD)
│ ├── pmd/lib/ # PMD 依赖库
│ ├── PmdRunner.java # PMD 调用包装器(支持 stdin)
│ ├── pmd-java-ruleset.xml # Java 规则集
│ └── pmd-jsp-ruleset.xml # JSP 规则集
├── images/ # 图标资源
├── scripts/ # 构建脚本
│ ├── build.mjs # esbuild 打包
│ ├── download-pmd.mjs # PMD 下载脚本
│ └── package-prod.mjs # 生产打包
├── .code-review/ # 自定义规则目录(后续细化)
│ ├── rules/ # 规则定义文件 (*.yaml)
│ └── config.yaml # 规则启用状态配置
├── package.json
├── tsconfig.json
└── eslint.config.mjs
10. 排除范围(非本期实现)
- 与 GitHub/GitLab PR Review API 集成
- 多人协作审查工作流
11. 技术栈
- VSCode Extension API (^1.120.0)
- TypeScript (ES2022, Node16 module)
- esbuild(打包构建)
- ESLint + typescript-eslint(自检)
- Mocha + @vscode/test-electron(测试)
- LLM API(openai 兼容接口)
- PMD 7.26.0(Java/JSP 静态分析)
- Java 11+(PMD 运行环境)
12. 自定义规则系统
12.1 设计决策
| 决策项 | 结论 | 说明 |
|---|---|---|
| 规则定义方式 | 纯描述(自然语言) | 不依赖正则,AI 理解语义后判断是否违反 |
| 规则执行方式 | 纯 AI 评估 | 规则随代码发给 AI,由 AI 判断是否触发 |
| 规则 ID 前缀 | 统一 custom: |
UI 展示为 custom:no-console,区别于 linter 规则 |
| 规则文件组织 | 多文件 + 文件夹 | .code-review/rules/*.yaml,按类别拆分 |
| 规则定位 | 项目级团队约定 | 与静态分析、AI 深度审查三者互补 |
| 严重程度 | 必须标注 | error / warning / info |
12.2 规则文件格式
自定义规则以 YAML 文件存储在工作区根目录 .code-review/rules/ 下,按类别拆分:
.code-review/
├── rules/
│ ├── security-rules.yaml # 安全类规则
│ ├── coding-conventions.yaml # 编码规范类规则
│ └── naming-conventions.yaml # 命名约定类规则(可选)
└── config.yaml # 启用状态配置
规则 YAML 字段说明:
| 字段 | 必填 | 类型 | 说明 |
|---|---|---|---|
id |
是 | string | 规则唯一标识(不含 custom: 前缀,运行时自动拼接) |
severity |
是 | string | error / warning / info |
description |
是 | string | 规则的自然语言描述(AI 评估依据) |
message |
是 | string | 触发时显示给用户的信息 |
languages |
否 | string[] | 生效的语言列表,为空表示对所有语言生效 |
模板文件 — security-rules.yaml:
- id: no-hardcoded-secret
severity: error
description: 禁止在代码中硬编码 API Key、密码等敏感信息
message: 检测到硬编码密钥,请使用环境变量或密钥管理工具
languages: [java, javascript, typescript]
- id: no-sql-injection
severity: error
description: 禁止使用字符串拼接的方式构造 SQL 语句
message: 使用参数化查询(PreparedStatement)替代字符串拼接
languages: [java]
模板文件 — coding-conventions.yaml:
- id: no-console-log
severity: warning
description: 生产代码不应保留 console.log 调试语句
message: 请使用日志框架替代 console.log
languages: [javascript, typescript]
- id: no-magic-numbers
severity: info
description: 禁止在代码中使用未命名的魔术数字
message: 请将魔法数字提取为命名常量
languages: [java, javascript, typescript]
12.3 启用/禁用配置
config.yaml:
# 各规则文件的启用状态
enabled:
- security-rules.yaml
- coding-conventions.yaml
# 各规则的单独启用/禁用(覆盖文件级设置)
rules:
no-magic-numbers:
enabled: false
加载逻辑(rules/yaml-parser.ts):
1. 扫描 .code-review/rules/*.yaml → 加载所有规则定义
2. 读取 .code-review/config.yaml
3. 按文件级 enabled 列表过滤
4. 按规则级 rules.<id>.enabled 覆盖
5. 返回激活的规则列表
12.4 与各组件的交互
自定义规则
│
├── 定义阶段 ── YAML 解析器 (rules/yaml-parser.ts)
│ → 解析 .code-review/rules/*.yaml
│ → 合并 config.yaml 启用状态
│
└── 执行阶段 ── AI 引擎 (ai/engine.ts 请求 A)
→ 将规则 description 注入 System Prompt
→ AI 评估代码是否违反规则
→ 返回 custom:规则ID 格式的结果
→ Merger 合并入最终报告
---
## 13. 设置面板
### 13.1 设计决策
| 决策项 | 结论 | 说明 |
|--------|------|------|
| 配置组织方式 | 按模块分组 | `aiConfig` / `linterConfig` / `fixerConfig` 三个命名空间 |
| API Key 存储 | 全局一个 SecretStorage | 切换 provider 时由用户更新 Key |
| 配置变更监听 | 不监听 | 下次审查时读取最新配置即可 |
| 配置验证 | 不做 | 依赖 `package.json` 的 enum/minimum 等约束 |
| 多根工作区 | 暂不考虑 | 取第一个工作区 |
| 设置面板形态 | 仅侧边栏视图 | 不另开独立面板 |
| provider 配置 | ai.provider 与面板共用 | 面板下拉框直接读写 `ai.provider` |
| linter 配置 | 单选 enum | `linters.<language>` 值只能是预设 linter 或空字符串(禁用) |
| 快速引导 | 三步渐进 | 步骤自动标记完成,引导用户完成初始配置 |
| 主按钮色彩 | 紫色 | 保存并测试连接按钮 `#7c3aed`,区别于绿色确认按钮 |
### 13.2 配置模块结构
src/config/ ├── index.ts # 统一导出 ├── ai.ts # AI 配置(provider/model/baseUrl/temperature/timeout/outputLanguage) ├── linter.ts # Linter 配置(linters.、PMD 路径) ├── fixer.ts # 修复器配置(contextLines) └── secret.ts # API Key SecretStorage(get/set/delete/isConfigured)
**配置读取方式**:每个模块导出 getter,运行时从 `vscode.workspace.getConfiguration` 读取,不做缓存。
**API Key 存储**:使用 VS Code `SecretStorage` 存储一个全局 Key(`vscode-code-reviewer.apiKey`),切换 provider 时由用户更新。
### 13.3 面板布局
侧边栏视图 `codeReviewer.setupView`:
┌──────────────────────────────────┐ │ ⚙ 代码审查 · 设置 │ ├──────────────────────────────────┤ │ 快速开始 │ │ ┌─① 配置 AI 模型及 API Key │ │ │ ② 启用自定义规则 │ │ │ ③ 保存并测试连接 Ctrl+R │ │ └─ │ │ │ │ 审核引擎 │ │ 🔵 共通规则 — Linter 静态分析 │ │ 🟡 自定义规则 — 团队编码规范 │ │ 🟢 AI 审核 — 深度代码审查 │ │ │ │ AI 模型配置 │ │ ┌──────────────────────────┐ │ │ │ 模型提供商 [已配置] │ │ │ │ [DeepSeek ▼] │ │ │ │ 模型名称 │ │ │ │ [deepseek-chat ▼] │ │ │ │ 建议使用结构化输出模型 │ │ │ └──────────────────────────┘ │ │ │ │ API Key │ │ ┌──────────────────────────┐ │ │ │ API Key [已配置] │ │ │ │ [•••••••••••••••••] │ │ │ │ Base URL │ │ │ │ [https://api.de...] │ │ │ │ Key 仅本地安全存储 │ │ │ └──────────────────────────┘ │ │ │ │ 输出语言 │ │ AI 审查结果输出语言 │ │ [中文(简体) ▼] │ │ │ │ 自定义规则 │ │ ┌──────────────────────────┐ │ │ │ 规则列表 [3 条启用] │ │ │ │ [开关] no-console-in... ×│ │ │ │ [开关] max-function-l... ×│ │ │ │ [开关] require-javad... ×│ │ │ │ [开关] no-any-type ×│ │ │ │ [输入规则名称...] [+ 添加]│ │ │ └──────────────────────────┘ │ │ │ │ [重置] [保存并测试连接] │ └──────────────────────────────────┘
**各区域说明**:
| 区域 | 元素 | 存储 |
|------|------|------|
| 快速开始 | 三步引导(圆形序号,完成变紫色) | — |
| 审核引擎 | 三个模块标签(紫/琥珀/绿圆点) | — |
| AI 模型配置 | 卡片:提供商下拉框 + 模型下拉框 + 状态标签 | `ai.provider` / `ai.model` |
| API Key | 卡片:密码输入框 + Base URL + 状态标签 | SecretStorage / `ai.baseUrl` |
| 输出语言 | 下拉框 | `ai.outputLanguage` |
| 自定义规则 | 卡片:开关列表 + 添加行 | `.code-review/config.yaml` |
**状态标签**:`已配置`(绿色药丸) / `未配置`(灰色药丸),实时反映配置完成状态。
### 13.4 快速引导流程
面板顶部提供三步渐进引导,步骤自动标记:
| 步骤 | 触发完成条件 | 行为 |
|------|------------|------|
| ① 配置 AI 模型及 API Key | API Key 输入框有值 | 圆形序号 `①` 变紫色 |
| ② 启用自定义规则 | 任意规则开关打开 | 圆形序号 `②` 变紫色 |
| ③ 保存并测试连接 | 前两步完成 + 测试成功 | 圆形序号 `③` 变紫色,底部显示快捷键 `Ctrl+Shift+R` |
### 13.5 保存与测试连接
| 阶段 | 按钮状态 | 显示 |
|------|---------|------|
| 待测试 | 正常 | `保存并测试连接`(紫色) |
| 测试中 | 禁用 + spinner | `<spinner> 测试中...` |
| 成功 | 可用 | `✓ 已连接`(绿色),Toast 绿色提示 5 秒 |
| 失败 | 可用 | `✗ 重试`(紫色),Toast 红色提示 5 秒 |
### 13.6 自定义规则文件管理
面板中规则列表展示所有已加载的规则,每条规则显示:
- 开关(紫色滑块)
- 规则名称(等宽字体)
- 删除按钮 `×`
**用户操作**:
| 操作 | 行为 |
|------|------|
| 点击开关 | 即时切换,在 `config.yaml` 写入 `rules.<id>.enabled` 覆盖项 |
| 点击添加 | 打开规则向导 → 保存到 `common-rules.yaml` |
| 点击删除 | 在 `config.yaml` 写入 `enabled: false` |
> 不修改规则 YAML 文件本身,仅通过 `config.yaml` 的覆盖项控制启用状态,避免破坏团队规则定义。
---
## 14. 结果合并与报告
### 14.1 合并逻辑
来自适配器、AI 引擎、自定义规则三路结果通过 `Merger` 合并为一个报告:
```typescript
interface MergedReport {
linterDiagnostics: LinterDiagnostic[];
customRuleDiagnostics: LinterDiagnostic[];
translatedDiagnostics: TranslatedDiagnostic[];
aiFindings: AIFinding[];
linterCount: number;
customRuleCount: number;
aiCount: number;
errors: string[];
degraded: boolean;
duration: number;
filePath: string;
language: string;
adapterNames: string[];
fixableLinterIndices: number[];
fixableCustomIndices: number[];
}
核心函数:
function mergeResults(
staticDiagnostics: LinterDiagnostic[],
aiResponse: AIResponse,
errors: string[],
degraded: boolean,
startTime: number,
filePath: string,
language: string,
adapterIds: string[]
): MergedReport;
14.2 Markdown 导出格式
# 代码审查报告
**文件:** `xxx.java`
**语言:** Java
**耗时:** 3.2s
## 🔧 PMD · 5 个问题
- 🔴 `pmd:AvoidDuplicateLiterals` L23
避免重复字符串常量...
## 📋 自定义规则 · 2 个问题
- 🔴 `no-sql-injection` L45
检测到 SQL 拼接...
## 🤖 AI 审查 · 3 条建议
- 🟡 [AI] [security] `hardcoded-secret` L12
发现硬编码密码...
建议: 使用环境变量...
```diff
- String password = "admin123";
+ String password = System.getenv("DB_PASSWORD");
---
## 15. 构建与打包
### 15.1 构建脚本
```javascript
// scripts/build.mjs
import * as esbuild from 'esbuild';
await esbuild.build({
entryPoints: ['src/extension.ts'],
bundle: true,
outfile: 'out/extension.js',
external: ['vscode'],
format: 'cjs',
platform: 'node',
target: 'node22',
minify: true,
sourcemap: false,
});
15.2 开发流程
# 安装依赖
npm install
# 编译
npm run compile
# 监听模式
npm run watch
# 运行测试
npm test
# 本地调试
# 在 VS Code 中按 F5 启动扩展开发宿主
15.3 生产打包
# 打包 vsix
vsce package
16. 测试策略
16.1 测试文件结构
src/test/
├── fixtures/ # 测试数据
│ ├── admin-system/ # Java 项目示例
│ │ ├── src/main/java/...
│ │ ├── src/main/webapp/...
│ │ └── pom.xml
│ └── UserService.java # 单文件测试
├── adapter.test.ts # 适配器测试
├── ai-engine.test.ts # AI 引擎测试
├── config.test.ts # 配置测试
├── merger.test.ts # 合并逻辑测试
├── pipeline.test.ts # 完整流程测试
└── extension.test.ts # 扩展入口测试
16.2 关键测试场景
| 场景 | 说明 |
|---|---|
| 适配器测试 | 验证各语言 linter 输出解析正确性 |
| AI 引擎测试 | 验证 Prompt 构建、JSON 解析、错误处理 |
| 合并测试 | 验证多源结果合并、统计计算 |
| 完整流程测试 | 模拟从代码输入到报告输出全链路 |
17. 依赖管理
17.1 运行时依赖
{
"dependencies": {
"eslint": "^9.39.3",
"stylelint": "^17.14.0",
"node-sql-parser": "^5.4.0"
}
}
17.2 开发依赖
{
"devDependencies": {
"@types/vscode": "^1.120.0",
"@types/node": "22.x",
"typescript": "^5.9.3",
"esbuild": "^0.28.1",
"eslint": "^9.39.3",
"typescript-eslint": "^8.56.1",
"@vscode/test-cli": "^0.0.12",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "^3.9.2"
}
}
17.3 外部工具
| 工具 | 版本 | 用途 | 获取方式 |
|---|---|---|---|
| PMD | 7.26.0 | Java/JSP 静态分析 | 自带 jars/pmd/ |
| Java | 11+ | PMD 运行环境 | 用户环境 |
18. 附录
A. 文件清单
| 文件 | 说明 |
|---|---|
src/extension.ts |
扩展入口,命令注册、流程编排 |
src/adapters/adapter.ts |
LinterAdapter 接口 + AdapterResult |
src/adapters/eslint.ts |
JS/TS 适配器 |
src/adapters/pmd.ts |
Java 适配器(支持虚拟文档) |
src/adapters/stylelint.ts |
CSS 适配器 |
src/adapters/sql-lint.ts |
SQL/PLSQL 适配器 |
src/adapters/jsp.ts |
JSP 组合适配器 |
src/jsp/jsp-extractor.ts |
JSP 内嵌代码块提取器 |
src/orchestrator/orchestrator.ts |
编排调度 |
src/ai/engine.ts |
AI 审查引擎 |
src/ai/schema.ts |
AI 响应结构定义 |
src/ai/providers/base.ts |
AIProvider 抽象基类 |
src/ai/providers/deepseek.ts |
DeepSeek 实现 |
src/ai/providers/openai.ts |
OpenAI 实现 |
src/ai/factory.ts |
Provider 工厂函数 |
src/fixer/fixer.ts |
自动修复逻辑 |
src/merger/merger.ts |
结果合并 |
src/rules/yaml-parser.ts |
YAML 规则解析 |
src/config/ai.ts |
AI 配置 |
src/config/linter.ts |
Linter 配置 |
src/config/fixer.ts |
修复器配置 |
src/config/secret.ts |
API Key SecretStorage |
src/config/index.ts |
配置统一导出 |
src/panel/webview.ts |
审查报告面板 |
src/**/setupView.ts |
侧边栏设置视图 |
jars/pmd/PmdRunner.java |
PMD Java 包装器(支持 stdin) |
jars/pmd-java-ruleset.xml |
Java 规则集 |
jars/pmd-jsp-ruleset.xml |
JSP 规则集 |
scripts/build.mjs |
esbuild 构建脚本 |
.code-review/rules/*.yaml |
自定义规则定义 |
.code-review/config.yaml |
规则启用状态 |
B. 关键接口签名
// 适配器
function getAdapters(): LinterAdapter[];
// 静态分析编排
function runStaticAnalysis(documents: vscode.TextDocument[], workingDir: string): Promise<StaticAnalysisResult>;
// AI 审查
function runAIReview(context: vscode.ExtensionContext, code: string, diagnostics: LinterDiagnostic[], customRules: CustomRule[]): Promise<AIEngineResult>;
// 结果合并
function mergeResults(staticDiagnostics: LinterDiagnostic[], aiResponse: AIResponse, errors: string[], degraded: boolean, startTime: number, filePath: string, language: string, adapterIds: string[]): MergedReport;
// 自动修复
function generateFix(context: vscode.ExtensionContext, diagnostic: FixableDiagnostic): Promise<CodeFix | null>;
function applyFix(fix: CodeFix): Promise<boolean>;
// 报告导出
function reportToMarkdown(report: MergedReport): string;
// 自定义规则
function loadActiveRules(workspaceRoot: string): CustomRule[];