# 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; } function parseYamlSimple(content: string): object[] { const items: Array> = []; let current: Record | 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)[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..enabled 覆盖 5. 返回激活的规则列表 ``` - `enabled`: 文件级启用列表,为空时默认全部启用 - `rules..enabled: false`: 单独禁用特定规则 - 规则 ID 不含 `custom:` 前缀,运行时由 AI 引擎自动拼接 - 规则各字段与设计 §12.2 一致 --- ## 验收 - [ ] 文件创建完成 - [ ] `npm run compile` 通过 - [ ] `npm run lint` 通过