224 lines
6.2 KiB
Markdown
224 lines
6.2 KiB
Markdown
# Step 11 — Phase 4.3: 自定义规则系统
|
||
|
||
**依赖**: Step 01(配置模块)
|
||
**参考设计**: §12
|
||
|
||
## 目标
|
||
|
||
实现 YAML 规则加载器,从 `.code-review/rules/*.yaml` 读取规则定义,按 `config.yaml` 过滤启用状态。
|
||
|
||
## 新建文件
|
||
|
||
| # | 文件 | 说明 |
|
||
|---|------|------|
|
||
| 1 | `src/rules/yaml-parser.ts` | `loadActiveRules()` + `parseConfigYaml()` |
|
||
|
||
## 现有参考文件(已存在,不修改)
|
||
|
||
```
|
||
.code-review/
|
||
├── config.yaml # 规则启用配置
|
||
└── rules/
|
||
├── security-rules.yaml # 安全规则
|
||
└── coding-conventions.yaml # 编码规范
|
||
```
|
||
|
||
---
|
||
|
||
## `src/rules/yaml-parser.ts`
|
||
|
||
```typescript
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import * as vscode from 'vscode';
|
||
|
||
export interface CustomRule {
|
||
id: string;
|
||
severity: 'error' | 'warning' | 'info';
|
||
description: string;
|
||
message: string;
|
||
languages?: string[];
|
||
}
|
||
|
||
interface RuleYamlItem {
|
||
id: string;
|
||
severity: string;
|
||
description: string;
|
||
message: string;
|
||
languages?: string[];
|
||
}
|
||
|
||
interface RuleConfig {
|
||
enabled?: string[];
|
||
rules?: Record<string, { enabled: boolean }>;
|
||
}
|
||
|
||
function parseYamlSimple(content: string): object[] {
|
||
const items: Array<Record<string, unknown>> = [];
|
||
let current: Record<string, unknown> | null = null;
|
||
let currentKey = '';
|
||
|
||
for (const line of content.split('\n')) {
|
||
const trimmed = line.trim();
|
||
if (!trimmed || trimmed.startsWith('#')) { continue; }
|
||
|
||
if (trimmed.startsWith('- ')) {
|
||
if (current) { items.push(current); }
|
||
current = {};
|
||
const indentMatch = trimmed.match(/^- (\w[\w-]*)\s*:\s*(.*)$/);
|
||
if (indentMatch) {
|
||
const key = indentMatch[1];
|
||
let value: unknown = indentMatch[2].trim();
|
||
if (value.startsWith('[') && value.endsWith(']')) {
|
||
value = value.slice(1, -1).split(',').map(s =>
|
||
s.trim().replace(/^['"]|['"]$/g, '')
|
||
);
|
||
}
|
||
current[key] = value;
|
||
}
|
||
} else if (current) {
|
||
const propMatch = trimmed.match(/^(\w[\w-]*)\s*:\s*(.*)$/);
|
||
if (propMatch) {
|
||
const key = propMatch[1];
|
||
let value: unknown = propMatch[2].trim();
|
||
if (!value || value === '[]') {
|
||
value = [];
|
||
} else if (value.startsWith('[') && value.endsWith(']')) {
|
||
value = value.slice(1, -1).split(',').map(s =>
|
||
s.trim().replace(/^['"]|['"]$/g, '')
|
||
);
|
||
}
|
||
current[key] = value;
|
||
}
|
||
}
|
||
}
|
||
if (current) { items.push(current); }
|
||
|
||
return items;
|
||
}
|
||
|
||
function parseConfigYaml(content: string): RuleConfig {
|
||
const config: RuleConfig = { enabled: [], rules: {} };
|
||
let section: string | null = null;
|
||
|
||
for (const line of content.split('\n')) {
|
||
const trimmed = line.trim();
|
||
if (!trimmed || trimmed.startsWith('#')) { continue; }
|
||
|
||
if (trimmed === 'enabled:') {
|
||
section = 'enabled';
|
||
continue;
|
||
}
|
||
if (trimmed === 'rules:') {
|
||
section = 'rules';
|
||
continue;
|
||
}
|
||
|
||
if (section === 'enabled' && trimmed.startsWith('- ')) {
|
||
const name = trimmed.substring(2).trim();
|
||
if (!config.enabled) { config.enabled = []; }
|
||
config.enabled!.push(name);
|
||
}
|
||
|
||
if (section === 'rules') {
|
||
const ruleMatch = trimmed.match(/^(\w[\w-]*)\s*:\s*$/);
|
||
if (ruleMatch) {
|
||
currentKey = ruleMatch[1];
|
||
if (!config.rules) { config.rules = {}; }
|
||
config.rules[currentKey] = { enabled: true };
|
||
} else if (currentKey) {
|
||
const propMatch = trimmed.match(/^(\w+)\s*:\s*(.*)$/);
|
||
if (propMatch) {
|
||
const key = propMatch[1];
|
||
const value = propMatch[2].trim();
|
||
if (!config.rules) { config.rules = {}; }
|
||
if (!config.rules[currentKey]) { config.rules[currentKey] = { enabled: true }; }
|
||
(config.rules[currentKey] as Record<string, unknown>)[key] =
|
||
value === 'false' ? false : value === 'true' ? true : value;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return config;
|
||
}
|
||
|
||
export function loadActiveRules(workspaceRoot: string): CustomRule[] {
|
||
const rulesDir = path.join(workspaceRoot, '.code-review', 'rules');
|
||
const configPath = path.join(workspaceRoot, '.code-review', 'config.yaml');
|
||
|
||
if (!fs.existsSync(rulesDir)) { return []; }
|
||
|
||
let ruleConfig: RuleConfig = {};
|
||
if (fs.existsSync(configPath)) {
|
||
const configContent = fs.readFileSync(configPath, 'utf-8');
|
||
ruleConfig = parseConfigYaml(configContent);
|
||
}
|
||
|
||
const enabledFiles = new Set(ruleConfig.enabled ?? []);
|
||
const disabledRules = new Set(
|
||
Object.entries(ruleConfig.rules ?? {})
|
||
.filter(([, v]) => v.enabled === false)
|
||
.map(([k]) => k)
|
||
);
|
||
|
||
const allRules: CustomRule[] = [];
|
||
|
||
const files = fs.readdirSync(rulesDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
|
||
for (const file of files) {
|
||
if (enabledFiles.size > 0 && !enabledFiles.has(file)) { continue; }
|
||
|
||
const content = fs.readFileSync(path.join(rulesDir, file), 'utf-8');
|
||
const items = parseYamlSimple(content) as RuleYamlItem[];
|
||
|
||
for (const item of items) {
|
||
if (disabledRules.has(item.id)) { continue; }
|
||
if (!item.id || !item.severity || !item.description || !item.message) { continue; }
|
||
|
||
const severity = ['error', 'warning', 'info'].includes(item.severity)
|
||
? (item.severity as 'error' | 'warning' | 'info')
|
||
: 'warning';
|
||
|
||
allRules.push({
|
||
id: item.id,
|
||
severity,
|
||
description: item.description,
|
||
message: item.message,
|
||
languages: item.languages,
|
||
});
|
||
}
|
||
}
|
||
|
||
return allRules;
|
||
}
|
||
```
|
||
|
||
> **注意**: 如果项目已安装 `js-yaml` 依赖,可替换 `parseYamlSimple` 为 `yaml.load()`。此处用简易解析器避免额外依赖。
|
||
|
||
---
|
||
|
||
## 关键逻辑
|
||
|
||
**加载流程**(按设计 §12.3):
|
||
|
||
```
|
||
1. 扫描 .code-review/rules/*.yaml → 加载所有规则定义
|
||
2. 读取 .code-review/config.yaml
|
||
3. 按文件级 enabled 列表过滤
|
||
4. 按规则级 rules.<id>.enabled 覆盖
|
||
5. 返回激活的规则列表
|
||
```
|
||
|
||
- `enabled`: 文件级启用列表,为空时默认全部启用
|
||
- `rules.<id>.enabled: false`: 单独禁用特定规则
|
||
- 规则 ID 不含 `custom:` 前缀,运行时由 AI 引擎自动拼接
|
||
- 规则各字段与设计 §12.2 一致
|
||
|
||
---
|
||
|
||
## 验收
|
||
|
||
- [ ] 文件创建完成
|
||
- [ ] `npm run compile` 通过
|
||
- [ ] `npm run lint` 通过
|