- 适配器 i18n 接入(eslint/pmd/sql-lint/stylelint) - Provider 动态注册机制(registry.ts + providers.json + factory 重构) - SetupView 全面重构(setupView.ts 新增 600+ 行) - i18n 消息扩展(messages.ts +210 行) - 规则导入流程优化(import-service / prompt-builder) - 新增 PMD jars 依赖及测试用例
984 lines
36 KiB
Markdown
984 lines
36 KiB
Markdown
# 静态分析适配器优化设计书(AI 编码用)
|
||
|
||
> 面向 AI 编码助手的技术实现规格。覆盖三种配置模式优先级机制、侧边栏适配器配置面板、外部依赖检测、配置文件模板生成等全部优化项。包含 TypeScript 接口定义、方法签名、文件级变更规格和实现伪代码。
|
||
|
||
- **项目**: vscode-code-reviewer (Code Purifier)
|
||
- **分支**: vscode-code-reviewer
|
||
- **日期**: 2026-07-27
|
||
- **版本**: 1.0.1 → 1.1.0
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
- [01 设计目标与范围](#01-设计目标与范围)
|
||
- [02 现有架构分析](#02-现有架构分析)
|
||
- [03 配置模式优先级机制](#03-配置模式优先级机制)
|
||
- [04 数据结构设计](#04-数据结构设计)
|
||
- [05 配置项 Schema 变更](#05-配置项-schema-变更)
|
||
- [06 状态检测逻辑](#06-状态检测逻辑)
|
||
- [07 侧边栏面板实现](#07-侧边栏面板实现)
|
||
- [08 文件级变更规格](#08-文件级变更规格)
|
||
- [09 默认配置模板](#09-默认配置模板)
|
||
- [10 测试要点](#10-测试要点)
|
||
|
||
---
|
||
|
||
## 01 设计目标与范围
|
||
|
||
本次优化在不破坏现有代码审查流程的前提下,为四个静态分析适配器(ESLint、Stylelint、PMD、SQL-Lint)增加统一的三层配置模式机制,并在侧边栏设置面板中新增"静态分析适配器"可视化配置区域。用户无需查阅外部文档即可完成全部适配器配置。
|
||
|
||
### 优化项清单
|
||
|
||
| # | 优化项 | 涉及文件 | 变更类型 |
|
||
|---|--------|----------|----------|
|
||
| 1 | 三层配置模式优先级(全局 > 项目级 > 内置) | `config/linter.ts`、各适配器文件 | 新增逻辑 |
|
||
| 2 | 适配器启用/禁用开关 | `package.json`、`config/linter.ts`、`orchestrator/*` | 新增配置 + 逻辑 |
|
||
| 3 | 侧边栏适配器配置面板(4 张卡片) | `views/setupView.ts`、`views/setupView.js` | 新增 UI + 逻辑 |
|
||
| 4 | 配置模式自动检测 | `views/setupView.ts` | 新增方法 |
|
||
| 5 | 外部依赖检测(Java / Python+sqlfluff) | `views/setupView.ts` | 新增方法 |
|
||
| 6 | 配置文件模板创建与打开 | `views/setupView.ts` | 新增方法 |
|
||
| 7 | 配置变更实时刷新 | `views/setupView.ts` | 新增监听器 |
|
||
| 8 | ESLint / Stylelint 自定义配置路径支持 | `package.json`、`config/linter.ts`、`adapters/eslint.ts`、`adapters/stylelint.ts` | 新增配置 + 逻辑 |
|
||
|
||
> **零破坏性原则**:所有变更均为新增或追加,不修改任何现有逻辑分支。现有的 AI 配置、规则文件导入、连接测试、代码审查命令等功能完全不受影响。适配器卡片列表由独立的 `<div id="adapter-list">` 容器渲染,即使渲染函数出错也不影响其他区域。
|
||
|
||
---
|
||
|
||
## 02 现有架构分析
|
||
|
||
### 源码目录结构
|
||
|
||
```
|
||
src/
|
||
├── activation/ # 插件激活逻辑
|
||
├── adapters/ # 静态分析适配器
|
||
│ ├── adapter.ts # 类型 re-export
|
||
│ ├── eslint.ts # ESLint 适配器
|
||
│ ├── stylelint.ts # Stylelint 适配器
|
||
│ ├── pmd.ts # PMD 适配器
|
||
│ ├── sql-lint.ts # SQL-Lint 适配器
|
||
│ └── jsp.ts # JSP 适配器
|
||
├── ai/ # AI 审查引擎
|
||
├── config/ # 配置读取层
|
||
│ ├── index.ts
|
||
│ ├── ai.ts
|
||
│ ├── linter.ts # ← 本次重点变更
|
||
│ ├── fixer.ts
|
||
│ └── secret.ts
|
||
├── orchestrator/ # 审查编排器(调度适配器)
|
||
├── views/
|
||
│ ├── setupView.ts # ← 本次重点变更
|
||
│ └── setupView.js # ← 本次重点变更
|
||
├── panel/ # 审查结果面板
|
||
├── types.ts # 核心类型定义
|
||
├── extension.ts # 插件入口
|
||
└── ...
|
||
```
|
||
|
||
### 现有核心类型(src/types.ts)
|
||
|
||
```typescript
|
||
import * as vscode from 'vscode';
|
||
|
||
export type Severity = 'error' | 'warning' | 'info';
|
||
|
||
export type AdapterStatus = 'ok' | 'tool-unavailable' | 'execution-failed';
|
||
|
||
export interface LinterDiagnostic {
|
||
severity: Severity;
|
||
ruleId: string;
|
||
message: string;
|
||
range: vscode.Range;
|
||
suggestion?: string;
|
||
}
|
||
|
||
export interface AdapterResult {
|
||
diagnostics: LinterDiagnostic[];
|
||
status: AdapterStatus;
|
||
errorMessage?: string;
|
||
}
|
||
|
||
export interface LinterAdapter {
|
||
id: string;
|
||
supportedLanguages: string[];
|
||
check(document: vscode.TextDocument, workingDir: string): Promise<AdapterResult>;
|
||
isAvailable(): boolean;
|
||
}
|
||
```
|
||
|
||
### 现有配置读取层(src/config/linter.ts)
|
||
|
||
```typescript
|
||
import * as vscode from 'vscode';
|
||
|
||
const ROOT = 'vscode-code-reviewer';
|
||
|
||
export function getLinterForLanguage(language: string): string {
|
||
return vscode.workspace.getConfiguration(ROOT).get<string>(`linters.${language}`, '');
|
||
}
|
||
|
||
export function getPmdJarPath(): string {
|
||
return vscode.workspace.getConfiguration(ROOT).get<string>('pmd.jarPath', '');
|
||
}
|
||
|
||
export function getPmdRulesetPath(): string {
|
||
return vscode.workspace.getConfiguration(ROOT).get<string>('pmd.rulesetPath', '');
|
||
}
|
||
|
||
export function getPmdJspRulesetPath(): string {
|
||
return vscode.workspace.getConfiguration(ROOT).get<string>('pmd.jspRulesetPath', '');
|
||
}
|
||
|
||
export function getSqlLintConfigFile(): string {
|
||
return vscode.workspace.getConfiguration(ROOT).get<string>('sql-lint.configFile', '');
|
||
}
|
||
```
|
||
|
||
### 现有 package.json 配置项(相关部分)
|
||
|
||
| 配置键 | 类型 | 默认值 | 说明 |
|
||
|--------|------|--------|------|
|
||
| `linters.javascript` | string (enum) | `"eslint"` | JS 语言的 linter 选择 |
|
||
| `linters.typescript` | string (enum) | `"eslint"` | TS 语言的 linter 选择 |
|
||
| `linters.java` | string (enum) | `"pmd"` | Java 语言的 linter 选择 |
|
||
| `linters.css` | string (enum) | `"stylelint"` | CSS 语言的 linter 选择 |
|
||
| `linters.sql` | string (enum) | `"sql-lint"` | SQL 语言的 linter 选择 |
|
||
| `pmd.jarPath` | string | `""` | PMD jar 目录路径 |
|
||
| `pmd.rulesetPath` | string | `""` | PMD 规则集 XML 路径 |
|
||
| `pmd.jspRulesetPath` | string | `""` | JSP 规则集 XML 路径 |
|
||
| `sql-lint.configFile` | string | `""` | sqlfluff 配置文件路径 |
|
||
|
||
> **现有缺口**:ESLint 和 Stylelint 适配器目前没有自定义配置路径的设置项(只有 `linters.javascript` 等语言级 linter 选择开关),无法通过 VS Code Settings 指定自定义 ESLint/Stylelint 配置文件路径。也没有适配器级别的启用/禁用开关。
|
||
|
||
---
|
||
|
||
## 03 配置模式优先级机制
|
||
|
||
每个适配器支持三种配置模式,优先级从高到低。高优先级配置存在时,低优先级配置自动忽略。
|
||
|
||
| 优先级 | 模式名称 | 配置来源 | 徽章标识 |
|
||
|--------|----------|----------|----------|
|
||
| **最高** | 全局配置(VS Code Settings) | `linters.eslintConfigPath` / `linters.stylelintConfigPath` / `pmd.rulesetPath` / `sql-lint.configFile` 等设置项 | 全局配置 |
|
||
| **中** | 项目级配置 | 项目根目录下的配置文件(`.eslintrc.*` / `.stylelintrc.*` / `ruleset.xml` / `.sqlfluff`) | 项目配置 |
|
||
| **最低** | 内置规则(零配置) | 插件打包的内置规则集 | 内置规则 |
|
||
|
||
### 各适配器的配置文件探测列表
|
||
|
||
`detectConfigMode()` 方法需要按以下列表在项目根目录探测文件是否存在:
|
||
|
||
| 适配器 | 项目级配置文件名(按探测顺序) | VS Code Settings 键 |
|
||
|--------|-------------------------------|---------------------|
|
||
| **ESLint** | `.eslintrc.js` → `.eslintrc.json` → `.eslintrc.yaml` → `.eslintrc.yml` → `.eslintrc` → `eslint.config.js` → `eslint.config.mjs` | `linters.eslintConfigPath` |
|
||
| **Stylelint** | `.stylelintrc.js` → `.stylelintrc.json` → `.stylelintrc.yaml` → `.stylelintrc.yml` → `.stylelintrc` → `stylelint.config.js` | `linters.stylelintConfigPath` |
|
||
| **PMD** | `ruleset.xml` | `pmd.rulesetPath` |
|
||
| **SQL-Lint** | `.sqlfluff` | `sql-lint.configFile` |
|
||
|
||
### PMD 特殊处理:双配置项
|
||
|
||
PMD 适配器有两类独立配置项:**规则集(rulesetPath)**和**运行时(jarPath)**。两者独立配置,组合关系如下:
|
||
|
||
| jarPath | rulesetPath | 结果 |
|
||
|---------|-------------|------|
|
||
| 空 | 空 | 内置引擎 + 内置规则集 |
|
||
| 空 | 有值 | 内置引擎 + 自定义规则集 |
|
||
| 有值 | 空 | 自定义引擎 + 内置规则集 |
|
||
| 有值 | 有值 | 自定义引擎 + 自定义规则集 |
|
||
|
||
> **jarPath 回退规则**:`jarPath` 指向的目录中必须存在 `PmdRunner.class` 文件,否则插件回退到内置 PMD。如果只设置了 `jarPath` 而没设置 `rulesetPath`,规则集仍使用内置的 `pmd-java-ruleset.xml`。
|
||
|
||
---
|
||
|
||
## 04 数据结构设计
|
||
|
||
### 新增类型定义
|
||
|
||
以下接口需添加到 `src/types.ts` 或 `src/views/setupView.ts` 顶部(推荐放在 setupView.ts 内部,因为仅该文件使用):
|
||
|
||
```typescript
|
||
/** 配置模式枚举 */
|
||
export type ConfigMode = 'builtin' | 'project' | 'global';
|
||
|
||
/** 外部依赖就绪状态 */
|
||
export type DependencyStatus = 'ready' | 'missing' | 'none';
|
||
|
||
/** 单个适配器的配置面板状态 */
|
||
export interface AdapterConfigStatus {
|
||
/** 适配器唯一标识 */
|
||
id: string;
|
||
/** 显示名称(PMD / SQL-Lint / ESLint / Stylelint) */
|
||
name: string;
|
||
/** 是否已启用 */
|
||
enabled: boolean;
|
||
/** 当前生效的配置模式 */
|
||
configMode: ConfigMode;
|
||
/** 外部依赖状态(ESLint/Stylelint 为 'none') */
|
||
dependencyStatus: DependencyStatus;
|
||
/** 外部依赖显示文本(如 "Java"、"Python + sqlfluff") */
|
||
dependencyLabel?: string;
|
||
/** 配置是否已完成(全局或项目级配置存在时为 true) */
|
||
configured: boolean;
|
||
/** 面板显示的操作指南文本 */
|
||
guideText: string;
|
||
/** 项目级配置文件名(用于"配置文件"按钮创建/打开) */
|
||
projectConfigFileName: string;
|
||
/** VS Code Settings 跳转目标键 */
|
||
settingsTarget: string;
|
||
}
|
||
|
||
/** 侧边栏消息:适配器操作类型 */
|
||
export type AdapterMessageAction =
|
||
| 'openAdapterConfig'
|
||
| 'openSettings'
|
||
| 'toggleAdapter';
|
||
|
||
/** 侧边栏消息:适配器操作载荷 */
|
||
export interface AdapterMessage {
|
||
action: AdapterMessageAction;
|
||
adapterId: string;
|
||
}
|
||
```
|
||
|
||
### 适配器元数据常量表
|
||
|
||
在 `setupView.ts` 中定义一个静态常量表,描述四个适配器的元信息,供 `collectAdapterStatus()` 使用:
|
||
|
||
```typescript
|
||
const ADAPTER_METADATA: Record<string, {
|
||
name: string;
|
||
projectConfigFileName: string;
|
||
settingsTarget: string;
|
||
guideText: string;
|
||
hasExternalDependency: boolean;
|
||
dependencyLabel?: string;
|
||
configFileTemplate: string;
|
||
}> = {
|
||
pmd: {
|
||
name: 'PMD',
|
||
projectConfigFileName: 'ruleset.xml',
|
||
settingsTarget: 'vscode-code-reviewer.pmd',
|
||
guideText: '需要 Java 运行环境;项目根目录创建 ruleset.xml 或在设置中配置 pmd.rulesetPath',
|
||
hasExternalDependency: true,
|
||
dependencyLabel: 'Java',
|
||
configFileTemplate: PMD_RULESET_TEMPLATE,
|
||
},
|
||
'sql-lint': {
|
||
name: 'SQL-Lint',
|
||
projectConfigFileName: '.sqlfluff',
|
||
settingsTarget: 'vscode-code-reviewer.sql-lint',
|
||
guideText: '需要 Python 环境和 sqlfluff;运行 pip install sqlfluff,项目根目录创建 .sqlfluff',
|
||
hasExternalDependency: true,
|
||
dependencyLabel: 'Python + sqlfluff',
|
||
configFileTemplate: SQLFLUFF_TEMPLATE,
|
||
},
|
||
eslint: {
|
||
name: 'ESLint',
|
||
projectConfigFileName: '.eslintrc.js',
|
||
settingsTarget: 'vscode-code-reviewer.linters',
|
||
guideText: '项目根目录创建 .eslintrc.js 或在 VS Code 设置中配置 eslintConfigPath',
|
||
hasExternalDependency: false,
|
||
configFileTemplate: ESLINT_TEMPLATE,
|
||
},
|
||
stylelint: {
|
||
name: 'Stylelint',
|
||
projectConfigFileName: '.stylelintrc.js',
|
||
settingsTarget: 'vscode-code-reviewer.linters',
|
||
guideText: '项目根目录创建 .stylelintrc 或在 VS Code 设置中配置 stylelintConfigPath',
|
||
hasExternalDependency: false,
|
||
configFileTemplate: STYLELINT_TEMPLATE,
|
||
},
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 05 配置项 Schema 变更
|
||
|
||
### package.json — 新增配置项
|
||
|
||
在 `contributes.configuration.properties` 中追加以下配置项:
|
||
|
||
```jsonc
|
||
// ESLint 自定义配置路径
|
||
"vscode-code-reviewer.linters.eslintConfigPath": {
|
||
"type": "string",
|
||
"default": "",
|
||
"description": "ESLint 自定义配置文件路径(绝对路径)。留空则使用项目 .eslintrc 或内置规则"
|
||
}
|
||
|
||
// Stylelint 自定义配置路径
|
||
"vscode-code-reviewer.linters.stylelintConfigPath": {
|
||
"type": "string",
|
||
"default": "",
|
||
"description": "Stylelint 自定义配置文件路径(绝对路径)。留空则使用项目 .stylelintrc 或内置规则"
|
||
}
|
||
|
||
// 适配器启用/禁用开关(4 项)
|
||
"vscode-code-reviewer.linter.pmd.enabled": {
|
||
"type": "boolean",
|
||
"default": true,
|
||
"description": "启用/禁用 PMD 适配器"
|
||
}
|
||
"vscode-code-reviewer.linter.sql-lint.enabled": {
|
||
"type": "boolean",
|
||
"default": true,
|
||
"description": "启用/禁用 SQL-Lint 适配器"
|
||
}
|
||
"vscode-code-reviewer.linter.eslint.enabled": {
|
||
"type": "boolean",
|
||
"default": true,
|
||
"description": "启用/禁用 ESLint 适配器"
|
||
}
|
||
"vscode-code-reviewer.linter.stylelint.enabled": {
|
||
"type": "boolean",
|
||
"default": true,
|
||
"description": "启用/禁用 Stylelint 适配器"
|
||
}
|
||
```
|
||
|
||
> **命名空间注意**:现有配置使用 `linters.*`(复数)作为语言级 linter 选择,`pmd.*` / `sql-lint.*` 作为各适配器的独立配置。新增的启用/禁用开关统一放在 `linter.*`(单数)命名空间下,避免与现有 `linters.*` 冲突。ESLint 和 Stylelint 的自定义配置路径放在 `linters.*` 下,与现有 `linters.javascript` 等保持同级。
|
||
|
||
### config/linter.ts — 新增读取函数
|
||
|
||
```typescript
|
||
/** 获取 ESLint 自定义配置路径 */
|
||
export function getEslintConfigPath(): string {
|
||
return vscode.workspace.getConfiguration(ROOT).get<string>('linters.eslintConfigPath', '');
|
||
}
|
||
|
||
/** 获取 Stylelint 自定义配置路径 */
|
||
export function getStylelintConfigPath(): string {
|
||
return vscode.workspace.getConfiguration(ROOT).get<string>('linters.stylelintConfigPath', '');
|
||
}
|
||
|
||
/** 获取适配器启用状态 */
|
||
export function isAdapterEnabled(adapterId: string): boolean {
|
||
return vscode.workspace.getConfiguration(ROOT).get<boolean>(`linter.${adapterId}.enabled`, true);
|
||
}
|
||
|
||
/** 设置适配器启用状态(写入 Global Settings) */
|
||
export async function setAdapterEnabled(adapterId: string, enabled: boolean): Promise<void> {
|
||
await vscode.workspace.getConfiguration(ROOT).update(
|
||
`linter.${adapterId}.enabled`,
|
||
enabled,
|
||
vscode.ConfigurationTarget.Global
|
||
);
|
||
}
|
||
```
|
||
|
||
### 适配器集成:启用/禁用检查
|
||
|
||
在 `orchestrator/` 中调度适配器前,需检查该适配器是否已启用。在调用 `adapter.check()` 之前添加守卫:
|
||
|
||
```typescript
|
||
import { isAdapterEnabled } from '../config/linter';
|
||
|
||
// 在 orchestrator 的适配器调度循环中
|
||
for (const adapter of adapters) {
|
||
if (!isAdapterEnabled(adapter.id)) {
|
||
continue; // 跳过已禁用的适配器
|
||
}
|
||
if (!adapter.isAvailable()) {
|
||
continue; // 跳过不可用的适配器
|
||
}
|
||
const result = await adapter.check(document, workingDir);
|
||
// ... 处理结果
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 06 状态检测逻辑
|
||
|
||
### detectConfigMode() — 配置模式检测
|
||
|
||
该方法检测指定适配器当前生效的配置模式。优先检查全局配置,其次项目级配置,最后回退到内置。
|
||
|
||
```typescript
|
||
import * as path from 'path';
|
||
import * as fs from 'fs';
|
||
import * as vscode from 'vscode';
|
||
import { getEslintConfigPath, getStylelintConfigPath, getPmdRulesetPath, getSqlLintConfigFile }
|
||
from '../config/linter';
|
||
|
||
/** 各适配器项目级配置文件探测列表 */
|
||
const PROJECT_CONFIG_FILES: Record<string, string[]> = {
|
||
eslint: ['.eslintrc.js', '.eslintrc.json', '.eslintrc.yaml', '.eslintrc.yml', '.eslintrc', 'eslint.config.js', 'eslint.config.mjs'],
|
||
stylelint: ['.stylelintrc.js', '.stylelintrc.json', '.stylelintrc.yaml', '.stylelintrc.yml', '.stylelintrc', 'stylelint.config.js'],
|
||
pmd: ['ruleset.xml'],
|
||
'sql-lint': ['.sqlfluff'],
|
||
};
|
||
|
||
/** 各适配器全局配置路径读取函数 */
|
||
const GLOBAL_CONFIG_GETTERS: Record<string, () => string> = {
|
||
eslint: getEslintConfigPath,
|
||
stylelint: getStylelintConfigPath,
|
||
pmd: getPmdRulesetPath,
|
||
'sql-lint': getSqlLintConfigFile,
|
||
};
|
||
|
||
function detectConfigMode(adapterId: string): ConfigMode {
|
||
// 1. 检查全局配置(VS Code Settings)
|
||
const globalPath = GLOBAL_CONFIG_GETTERS[adapterId]?.();
|
||
if (globalPath && globalPath.trim() !== '') {
|
||
return 'global';
|
||
}
|
||
|
||
// 2. 检查项目级配置文件
|
||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||
const rootPath = workspaceFolders[0].uri.fsPath;
|
||
const configFiles = PROJECT_CONFIG_FILES[adapterId] ?? [];
|
||
for (const fileName of configFiles) {
|
||
const filePath = path.join(rootPath, fileName);
|
||
if (fs.existsSync(filePath)) {
|
||
return 'project';
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. 回退到内置
|
||
return 'builtin';
|
||
}
|
||
```
|
||
|
||
### checkJavaReady() — Java 环境检测
|
||
|
||
通过执行 `java -version` 检测 Java 运行环境是否可用。使用 `child_process.execSync` 同步执行,捕获 stderr 输出(Java 版本信息输出到 stderr)。
|
||
|
||
```typescript
|
||
import { execSync } from 'child_process';
|
||
|
||
function checkJavaReady(): boolean {
|
||
try {
|
||
const output = execSync('java -version', {
|
||
encoding: 'utf-8',
|
||
timeout: 5000,
|
||
stdio: ['pipe', 'pipe', 'pipe'],
|
||
});
|
||
return true;
|
||
} catch {
|
||
// java -version 输出到 stderr,execSync 会因非零退出码抛错
|
||
// 但即使版本信息在 stderr 中,只要命令存在就算就绪
|
||
try {
|
||
const result = execSync('java -version 2>&1', {
|
||
encoding: 'utf-8',
|
||
timeout: 5000,
|
||
});
|
||
return result.includes('version');
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### checkPythonReady() — Python + sqlfluff 检测
|
||
|
||
先检测 Python(尝试 `python3` 和 `python`),再检测 `sqlfluff` 命令是否可用。
|
||
|
||
```typescript
|
||
function checkPythonReady(): boolean {
|
||
// 1. 检测 Python
|
||
let pythonCmd = '';
|
||
for (const cmd of ['python3', 'python']) {
|
||
try {
|
||
execSync(`${cmd} --version`, { encoding: 'utf-8', timeout: 5000, stdio: 'pipe' });
|
||
pythonCmd = cmd;
|
||
break;
|
||
} catch { continue; }
|
||
}
|
||
if (!pythonCmd) return false;
|
||
|
||
// 2. 检测 sqlfluff
|
||
try {
|
||
execSync('sqlfluff --version', { encoding: 'utf-8', timeout: 5000, stdio: 'pipe' });
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
```
|
||
|
||
> **依赖检测注意事项**:依赖检测仅验证运行环境是否存在,不验证具体版本。`java -version` 能正常输出即判定为就绪,但不检查是否满足 Java 8+ 要求。检测操作使用 `execSync` 同步执行,需设置 5 秒超时防止卡死。检测结果需缓存,避免每次刷新面板都执行命令行检测(建议在 `collectAdapterStatus()` 中缓存,配置变更时重新检测)。
|
||
|
||
### collectAdapterStatus() — 汇总适配器状态
|
||
|
||
遍历 `ADAPTER_METADATA`,调用上述检测方法,组装 `AdapterConfigStatus[]` 数组。
|
||
|
||
```typescript
|
||
function collectAdapterStatus(): AdapterConfigStatus[] {
|
||
const statuses: AdapterConfigStatus[] = [];
|
||
|
||
for (const [id, meta] of Object.entries(ADAPTER_METADATA)) {
|
||
const configMode = detectConfigMode(id);
|
||
const enabled = isAdapterEnabled(id);
|
||
|
||
let dependencyStatus: DependencyStatus = 'none';
|
||
if (meta.hasExternalDependency) {
|
||
if (id === 'pmd') {
|
||
dependencyStatus = checkJavaReady() ? 'ready' : 'missing';
|
||
} else if (id === 'sql-lint') {
|
||
dependencyStatus = checkPythonReady() ? 'ready' : 'missing';
|
||
}
|
||
}
|
||
|
||
const configured = configMode !== 'builtin' || !meta.hasExternalDependency;
|
||
|
||
statuses.push({
|
||
id,
|
||
name: meta.name,
|
||
enabled,
|
||
configMode,
|
||
dependencyStatus,
|
||
dependencyLabel: meta.dependencyLabel,
|
||
configured,
|
||
guideText: meta.guideText,
|
||
projectConfigFileName: meta.projectConfigFileName,
|
||
settingsTarget: meta.settingsTarget,
|
||
});
|
||
}
|
||
|
||
return statuses;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 07 侧边栏面板实现
|
||
|
||
### 面板位置
|
||
|
||
新增区域位于侧边栏中**"审核引擎"区域之后、"AI 模型配置"区域之前**。视线流程为:快速开始引导 → 引擎状态总览 → **适配器配置引导(本节)** → AI 模型配置 → 规则文件管理。
|
||
|
||
### setupView.ts — pushConfig() 变更
|
||
|
||
在 `pushConfig()` 方法中追加 `adapterStatus` 字段,将采集的适配器状态推送到前端:
|
||
|
||
```typescript
|
||
private pushConfig() {
|
||
// ... 现有配置推送逻辑保持不变 ...
|
||
|
||
// 追加适配器状态
|
||
const adapterStatus = this.collectAdapterStatus();
|
||
this.view?.webview.postMessage({
|
||
type: 'initConfig',
|
||
// ... 现有字段 ...
|
||
adapterStatus,
|
||
});
|
||
}
|
||
```
|
||
|
||
### setupView.ts — onDidReceiveMessage 新增消息处理
|
||
|
||
在 `resolveWebviewView` 的 `onDidReceiveMessage` 回调中,新增三个 case:
|
||
|
||
```typescript
|
||
this.view.webview.onDidReceiveMessage(async (message) => {
|
||
switch (message.type) {
|
||
// ... 现有 case 保持不变 ...
|
||
|
||
case 'openAdapterConfig': {
|
||
await this.handleAdapterConfig(message.adapterId);
|
||
break;
|
||
}
|
||
|
||
case 'openSettings': {
|
||
await vscode.commands.executeCommand(
|
||
'workbench.action.openSettings',
|
||
message.settingsTarget
|
||
);
|
||
break;
|
||
}
|
||
|
||
case 'toggleAdapter': {
|
||
await setAdapterEnabled(message.adapterId, message.enabled);
|
||
// 配置变更后 pushConfig 会由 onDidChangeConfiguration 触发
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
```
|
||
|
||
### handleAdapterConfig() — 配置文件创建/打开
|
||
|
||
```typescript
|
||
private async handleAdapterConfig(adapterId: string) {
|
||
const meta = ADAPTER_METADATA[adapterId];
|
||
if (!meta) return;
|
||
|
||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||
vscode.window.showWarningMessage('请先打开一个工作区文件夹');
|
||
return;
|
||
}
|
||
|
||
const rootPath = workspaceFolders[0].uri.fsPath;
|
||
const filePath = path.join(rootPath, meta.projectConfigFileName);
|
||
|
||
if (!fs.existsSync(filePath)) {
|
||
// 文件不存在 → 创建默认模板
|
||
fs.writeFileSync(filePath, meta.configFileTemplate, 'utf-8');
|
||
vscode.window.showInformationMessage(`配置文件已创建: ${meta.projectConfigFileName}`);
|
||
}
|
||
|
||
// 打开文件
|
||
const doc = await vscode.workspace.openTextDocument(filePath);
|
||
await vscode.window.showTextDocument(doc);
|
||
}
|
||
```
|
||
|
||
### resolveWebviewView — 配置变更监听
|
||
|
||
在 `resolveWebviewView` 方法中注册 `onDidChangeConfiguration` 监听器,当 `vscode-code-reviewer.linter` 或 `vscode-code-reviewer.linters` 或 `vscode-code-reviewer.pmd` 或 `vscode-code-reviewer.sql-lint` 命名空间下的配置变更时,触发 `pushConfig()` 刷新:
|
||
|
||
```typescript
|
||
resolveWebviewView(view: vscode.WebviewView) {
|
||
this.view = view;
|
||
// ... 现有初始化逻辑 ...
|
||
|
||
// 新增:配置变更监听
|
||
const configChangeDisposable = vscode.workspace.onDidChangeConfiguration((e) => {
|
||
if (
|
||
e.affectsConfiguration('vscode-code-reviewer.linter') ||
|
||
e.affectsConfiguration('vscode-code-reviewer.linters') ||
|
||
e.affectsConfiguration('vscode-code-reviewer.pmd') ||
|
||
e.affectsConfiguration('vscode-code-reviewer.sql-lint')
|
||
) {
|
||
this.pushConfig();
|
||
}
|
||
});
|
||
|
||
// 随 webview 销毁自动清理
|
||
view.onDidDispose(() => {
|
||
configChangeDisposable.dispose();
|
||
});
|
||
}
|
||
```
|
||
|
||
### setupView.ts — getHtml() 模板变更
|
||
|
||
在 `getHtml()` 方法返回的 HTML 模板中,在"审核引擎"区域和"AI 模型配置"区域之间插入以下 HTML:
|
||
|
||
```html
|
||
<!-- 适配器配置面板 -->
|
||
<div class="section adapter-section-panel">
|
||
<h3 class="section-title">静态分析适配器</h3>
|
||
<div id="adapter-list"></div>
|
||
</div>
|
||
```
|
||
|
||
同时在 `<style>` 块中追加适配器卡片样式(约 55 行 CSS):
|
||
|
||
```css
|
||
/* 适配器卡片 */
|
||
.adapter-card {
|
||
background: var(--bg2);
|
||
border: 1px solid var(--rule);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
margin-bottom: 8px;
|
||
transition: opacity 0.2s;
|
||
}
|
||
.adapter-card.disabled { opacity: 0.45; }
|
||
.adapter-card-header {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
margin-bottom: 6px;
|
||
}
|
||
.adapter-card-name { font-size: 12px; font-weight: 600; color: #ccc; }
|
||
.adapter-toggle {
|
||
width: 30px; height: 16px; border-radius: 8px;
|
||
background: #3fb950; position: relative; cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
.adapter-toggle.off { background: #3c3c3c; }
|
||
.adapter-toggle::after {
|
||
content: ''; position: absolute; top: 2px; left: 2px;
|
||
width: 12px; height: 12px; border-radius: 50%; background: #fff;
|
||
transition: transform 0.2s;
|
||
transform: translateX(14px);
|
||
}
|
||
.adapter-toggle.off::after { transform: translateX(0); background: #ccc; }
|
||
.adapter-badges { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 5px; }
|
||
.adapter-badge {
|
||
display: inline-flex; align-items: center; padding: 1px 5px;
|
||
border-radius: 3px; font-size: 9px; font-weight: 600;
|
||
font-family: var(--font-mono);
|
||
}
|
||
.adapter-badge-info { background: rgba(139,92,246,0.15); color: #8b5cf6; }
|
||
.adapter-badge-ok { background: rgba(63,185,80,0.15); color: #3fb950; }
|
||
.adapter-badge-warn { background: rgba(210,153,34,0.15); color: #d29922; }
|
||
.adapter-badge-error { background: rgba(248,81,73,0.15); color: #f48771; }
|
||
.adapter-guide { font-size: 10px; color: #9d9d9d; line-height: 1.4; margin-bottom: 7px; }
|
||
.adapter-actions { display: flex; gap: 5px; }
|
||
.adapter-btn {
|
||
padding: 2px 8px; border-radius: 3px; font-size: 10px;
|
||
border: 1px solid #3c3c3c; background: transparent; color: #ccc;
|
||
cursor: pointer; font-family: var(--font);
|
||
}
|
||
.adapter-btn:hover { background: var(--bg3); }
|
||
```
|
||
|
||
### setupView.js — renderAdapters() 前端渲染函数
|
||
|
||
在 `setupView.js` 中新增 `renderAdapters()` 函数,接收 `adapterStatus` 数组并渲染卡片 HTML:
|
||
|
||
```javascript
|
||
function renderAdapters(adapterStatus) {
|
||
const container = document.getElementById('adapter-list');
|
||
if (!container || !adapterStatus) return;
|
||
|
||
const modeBadgeMap = {
|
||
builtin: { class: 'adapter-badge-info', text: '内置规则' },
|
||
project: { class: 'adapter-badge-ok', text: '项目配置' },
|
||
global: { class: 'adapter-badge-warn', text: '全局配置' },
|
||
};
|
||
|
||
const html = adapterStatus.map(a => {
|
||
const modeBadge = modeBadgeMap[a.configMode];
|
||
const depBadge = a.dependencyStatus === 'none' ? '' :
|
||
a.dependencyStatus === 'ready'
|
||
? `<span class="adapter-badge adapter-badge-ok">${a.dependencyLabel} ✓</span>`
|
||
: `<span class="adapter-badge adapter-badge-error">${a.dependencyLabel} ✗</span>`;
|
||
const configBadge = a.configured
|
||
? '<span class="adapter-badge adapter-badge-ok">已配置</span>'
|
||
: '<span class="adapter-badge adapter-badge-warn">未配置</span>';
|
||
|
||
return `
|
||
<div class="adapter-card ${a.enabled ? '' : 'disabled'}">
|
||
<div class="adapter-card-header">
|
||
<span class="adapter-card-name">${a.name}</span>
|
||
<div class="adapter-toggle ${a.enabled ? '' : 'off'}"
|
||
data-adapter-id="${a.id}"></div>
|
||
</div>
|
||
<div class="adapter-badges">
|
||
<span class="adapter-badge ${modeBadge.class}">${modeBadge.text}</span>
|
||
${depBadge}
|
||
${configBadge}
|
||
</div>
|
||
<div class="adapter-guide">${a.guideText}</div>
|
||
<div class="adapter-actions">
|
||
<button class="adapter-btn" data-action="openAdapterConfig"
|
||
data-adapter-id="${a.id}">配置文件</button>
|
||
<button class="adapter-btn" data-action="openSettings"
|
||
data-settings-target="${a.settingsTarget}">VS Code 设置</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
container.innerHTML = html;
|
||
|
||
// 绑定事件
|
||
container.querySelectorAll('.adapter-toggle').forEach(el => {
|
||
el.addEventListener('click', () => {
|
||
const adapterId = el.dataset.adapterId;
|
||
const isEnabled = !el.classList.contains('off');
|
||
vscode.postMessage({
|
||
type: 'toggleAdapter',
|
||
adapterId,
|
||
enabled: !isEnabled,
|
||
});
|
||
});
|
||
});
|
||
|
||
container.querySelectorAll('.adapter-btn').forEach(el => {
|
||
el.addEventListener('click', () => {
|
||
const action = el.dataset.action;
|
||
const adapterId = el.dataset.adapterId;
|
||
const settingsTarget = el.dataset.settingsTarget;
|
||
if (action === 'openAdapterConfig') {
|
||
vscode.postMessage({ type: 'openAdapterConfig', adapterId });
|
||
} else if (action === 'openSettings') {
|
||
vscode.postMessage({ type: 'openSettings', settingsTarget });
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 在 initConfig 消息处理中调用
|
||
window.addEventListener('message', event => {
|
||
const message = event.data;
|
||
if (message.type === 'initConfig') {
|
||
// ... 现有初始化逻辑 ...
|
||
renderAdapters(message.adapterStatus);
|
||
}
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 08 文件级变更规格
|
||
|
||
### [MODIFY] package.json
|
||
|
||
**范围**: `configuration.properties`
|
||
|
||
在 `contributes.configuration.properties` 对象中追加 6 个新配置项:
|
||
|
||
- `linters.eslintConfigPath` — ESLint 自定义配置路径(string, default: "")
|
||
- `linters.stylelintConfigPath` — Stylelint 自定义配置路径(string, default: "")
|
||
- `linter.pmd.enabled` — PMD 启用开关(boolean, default: true)
|
||
- `linter.sql-lint.enabled` — SQL-Lint 启用开关(boolean, default: true)
|
||
- `linter.eslint.enabled` — ESLint 启用开关(boolean, default: true)
|
||
- `linter.stylelint.enabled` — Stylelint 启用开关(boolean, default: true)
|
||
|
||
不修改任何现有配置项。版本号从 `1.0.1` 升至 `1.1.0`。
|
||
|
||
### [MODIFY] src/config/linter.ts
|
||
|
||
**范围**: 新增 4 个导出函数
|
||
|
||
- `getEslintConfigPath(): string` — 读取 `linters.eslintConfigPath`
|
||
- `getStylelintConfigPath(): string` — 读取 `linters.stylelintConfigPath`
|
||
- `isAdapterEnabled(adapterId: string): boolean` — 读取 `linter.{adapterId}.enabled`
|
||
- `setAdapterEnabled(adapterId: string, enabled: boolean): Promise<void>` — 写入 Global Settings
|
||
|
||
不修改任何现有函数。新增函数追加在文件末尾。
|
||
|
||
### [MODIFY] src/views/setupView.ts
|
||
|
||
**范围**: `SetupViewProvider` 类
|
||
|
||
变更内容(全部为新增或追加):
|
||
|
||
| 变更位置 | 变更内容 |
|
||
|----------|----------|
|
||
| 文件顶部 | 新增 `import` 语句(path、fs、execSync、linter 配置函数) |
|
||
| 类外部 | 新增 `ConfigMode`、`DependencyStatus`、`AdapterConfigStatus` 类型定义 |
|
||
| 类外部 | 新增 `ADAPTER_METADATA` 常量表 |
|
||
| 类外部 | 新增 `PROJECT_CONFIG_FILES` 和 `GLOBAL_CONFIG_GETTERS` 常量 |
|
||
| 类内部 — 新增方法 | `detectConfigMode(adapterId): ConfigMode` |
|
||
| 类内部 — 新增方法 | `checkJavaReady(): boolean` |
|
||
| 类内部 — 新增方法 | `checkPythonReady(): boolean` |
|
||
| 类内部 — 新增方法 | `collectAdapterStatus(): AdapterConfigStatus[]` |
|
||
| 类内部 — 新增方法 | `handleAdapterConfig(adapterId: string): Promise<void>` |
|
||
| 类内部 — 修改方法 | `pushConfig()` 追加 `adapterStatus` 字段到 postMessage |
|
||
| 类内部 — 修改方法 | `resolveWebviewView()` 的 `onDidReceiveMessage` 新增 `openAdapterConfig` / `openSettings` / `toggleAdapter` 三个 case |
|
||
| 类内部 — 修改方法 | `resolveWebviewView()` 新增 `onDidChangeConfiguration` 监听器 |
|
||
| 类内部 — 修改方法 | `getHtml()` 模板新增 HTML(`<div id="adapter-list">`)和 CSS(约 55 行样式) |
|
||
|
||
### [MODIFY] src/views/setupView.js
|
||
|
||
**范围**: 前端脚本
|
||
|
||
- 新增 `renderAdapters(adapterStatus)` 函数
|
||
- 在 `initConfig` 消息处理回调中追加 `renderAdapters(message.adapterStatus)` 调用
|
||
|
||
不修改任何现有函数的逻辑分支。
|
||
|
||
### [MODIFY] src/adapters/eslint.ts
|
||
|
||
**范围**: `ESLintAdapter` 类
|
||
|
||
在 `check()` 方法中,优先使用 `getEslintConfigPath()` 返回的自定义配置路径。如果为空,再检测项目根目录的 `.eslintrc.*` 文件。如果两者都不存在,使用内置 `eslint:recommended` 规则集。
|
||
|
||
### [MODIFY] src/adapters/stylelint.ts
|
||
|
||
**范围**: `StylelintAdapter` 类
|
||
|
||
同 ESLint,在 `check()` 方法中优先使用 `getStylelintConfigPath()` 返回的自定义配置路径。如果为空,再检测项目根目录的 `.stylelintrc.*` 文件。如果两者都不存在,使用内置 11 条默认规则。
|
||
|
||
### [MODIFY] src/orchestrator/*
|
||
|
||
**范围**: 审查编排器
|
||
|
||
在适配器调度循环中,调用 `adapter.check()` 之前新增 `isAdapterEnabled(adapter.id)` 守卫,跳过已禁用的适配器。
|
||
|
||
---
|
||
|
||
## 09 默认配置模板
|
||
|
||
点击"配置文件"按钮时,如果项目根目录尚不存在对应配置文件,插件自动创建以下默认模板并打开。模板内容作为字符串常量定义在 `setupView.ts` 中。
|
||
|
||
### PMD 默认模板(ruleset.xml)
|
||
|
||
```typescript
|
||
const PMD_RULESET_TEMPLATE = `<?xml version="1.0" encoding="UTF-8"?>
|
||
<ruleset xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
|
||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
|
||
<description>Custom PMD Ruleset</description>
|
||
<rule ref="category/java/bestpractices.xml" />
|
||
<rule ref="category/java/codestyle.xml" />
|
||
</ruleset>`;
|
||
```
|
||
|
||
### SQL-Lint 默认模板(.sqlfluff)
|
||
|
||
```typescript
|
||
const SQLFLUFF_TEMPLATE = `[sqlfluff]
|
||
dialect = postgres
|
||
rules = all`;
|
||
```
|
||
|
||
### ESLint 默认模板(.eslintrc.js)
|
||
|
||
```typescript
|
||
const ESLINT_TEMPLATE = `module.exports = {
|
||
root: true,
|
||
env: { node: true, es2022: true },
|
||
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
|
||
rules: {
|
||
'no-unused-vars': 'warn',
|
||
'no-console': 'off',
|
||
'semi': ['error', 'always'],
|
||
},
|
||
};`;
|
||
```
|
||
|
||
### Stylelint 默认模板(.stylelintrc.js)
|
||
|
||
```typescript
|
||
const STYLELINT_TEMPLATE = `module.exports = {
|
||
extends: 'stylelint-config-standard',
|
||
rules: {
|
||
'indentation': 2,
|
||
'no-empty': true,
|
||
},
|
||
};`;
|
||
```
|
||
|
||
> **文件存在时不覆盖**:如果项目根目录已存在对应配置文件,点击"配置文件"按钮会直接打开该文件,不会覆盖已有内容。创建新文件时会弹出 `showInformationMessage` 通知提示"配置文件已创建"。
|
||
|
||
---
|
||
|
||
## 10 测试要点
|
||
|
||
### 配置模式检测测试
|
||
|
||
| 测试场景 | 前置条件 | 期望结果 |
|
||
|----------|----------|----------|
|
||
| 全局配置优先 | Settings 中 `eslintConfigPath` 有值 + 项目根有 `.eslintrc.js` | `detectConfigMode('eslint')` 返回 `'global'` |
|
||
| 项目配置次之 | Settings 中 `eslintConfigPath` 为空 + 项目根有 `.eslintrc.json` | 返回 `'project'` |
|
||
| 内置回退 | Settings 为空 + 项目根无配置文件 | 返回 `'builtin'` |
|
||
| PMD 双配置组合 | `jarPath` 有值但目录无 `PmdRunner.class` | 回退到内置引擎 |
|
||
|
||
### 外部依赖检测测试
|
||
|
||
| 测试场景 | 期望结果 |
|
||
|----------|----------|
|
||
| Java 已安装(`java -version` 正常输出) | `checkJavaReady()` 返回 `true` |
|
||
| Java 未安装(命令不存在) | 返回 `false` |
|
||
| Python3 + sqlfluff 均已安装 | `checkPythonReady()` 返回 `true` |
|
||
| Python 已安装但 sqlfluff 未安装 | 返回 `false` |
|
||
| 仅 python(无 python3)+ sqlfluff 已安装 | 返回 `true` |
|
||
|
||
### 侧边栏面板交互测试
|
||
|
||
| 测试场景 | 操作步骤 | 期望结果 |
|
||
|----------|----------|----------|
|
||
| 切换适配器开关 | 点击 PMD 卡片的开关 | 卡片变半透明 + `linter.pmd.enabled` 写入 `false` + 代码审查时 PMD 被跳过 |
|
||
| 创建配置文件 | 点击 ESLint 的"配置文件"按钮(项目根无 .eslintrc.js) | 项目根创建 `.eslintrc.js` + 文件在编辑器中打开 + 弹出通知 |
|
||
| 打开已有配置文件 | 点击 Stylelint 的"配置文件"按钮(项目根已有 .stylelintrc.js) | 直接打开文件,不覆盖内容 |
|
||
| 跳转 VS Code 设置 | 点击 SQL-Lint 的"VS Code 设置"按钮 | VS Code 设置面板打开,定位到 `vscode-code-reviewer.sql-lint` |
|
||
| 配置变更实时刷新 | 在 VS Code 设置面板修改 `pmd.rulesetPath` | 侧边栏 PMD 卡片徽章自动更新为"全局配置" |
|
||
|
||
### 零破坏性回归测试
|
||
|
||
> **回归验证清单**:以下功能在本次变更后必须仍然正常工作:AI 代码审查命令(`Ctrl+Shift+R`)、选中代码审查、审查结果面板导出、自定义规则管理、AI 模型配置与连接测试、规则文件导入。适配器卡片渲染失败时不应影响侧边栏其他区域的正常显示。
|
||
|
||
---
|
||
|
||
*本设计书基于 vscode-code-reviewer 插件 vscode-code-reviewer 分支(v1.0.1)编写,覆盖静态分析适配器三层配置模式机制和侧边栏适配器配置面板的全部技术实现规格。AI 编码助手应按第 08 节文件级变更规格逐文件实施,以第 04-06 节的数据结构和检测逻辑为实现依据。*
|