feat: Linter 规则精细化增强 + 模板导入/导出闭环

ESLint: +29 条 P1/P2 规则 + 12 条 TS 专属规则(含 no-shadow/no-array-constructor 冲突处理)
Stylelint: 集成 stylelint-config-recommended + 27 条额外规则
PMD: 排除 20 弃用 + 17 噪音规则,补启 Security/Multithreading,274+12 条精选
SQL-lint: 内置精选 57 条规则配置 + 按 tier 分级 severity + 无项目配置时自动注入临时配置
模板导出: export-service.ts 导出 2-sheet xlsx(复用 xlsx 零新依赖)
模板导入: template-converter.ts 固定列映射解析 + dedup-prompt.ts AI 语义去重
导入预览增强: 错误规则分组置顶只读、跳过行提示、空数据提示
i18n: 新增 18 条模板导入/导出相关翻译
WebView: 静态分析/自定义规则项默认可展开显示 suggestion
This commit is contained in:
范智鹏
2026-07-30 23:17:20 +08:00
parent d2dd16a043
commit 5848eaa82a
27 changed files with 6610 additions and 153 deletions
+19 -2
View File
@@ -1,5 +1,4 @@
# AI Usage Log
| 日期时间 | 范式步骤 | 修改摘要 | 中间产物 | 涉及文件 | 使用模型 |
|---------|---------|---------|---------|---------|---------|
| 2026-07-10 18:10 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 | 编写代码审查插件设计文档(design spec | design spec | docs/superpowers/specs/2026-07-10-code-reviewer-design.md | deepseek-v4-flash |
@@ -135,6 +134,24 @@
| 2026-07-28 21:15 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 新增"AI 连接配置"区块(provider/model/apiKey/baseUrl),放在审核引擎上方,删除下方重复的③ AI 模型配置和④ API Key 区块 | 无 | src/i18n/messages.ts src/views/setupView.ts | deepseek-v4-flash |
| 2026-07-28 21:20 | ① 用户提出 → ⑤ 编码实现 → ⑥ 审查验证 | 语言下拉移至 header 右侧,删除独立语言区块 | 无 | src/views/setupView.ts | deepseek-v4-flash |
| 2026-07-28 21:30 | ① 用户提出 → ⑤ 编码实现 → ⑥ 审查验证 | 连接状态持久化:globalState 存储 connectionState(含配置指纹),重新打开插件时恢复,AI 配置变化时清除 | 无 | src/views/setupView.ts | deepseek-v4-flash |
| 2026-07-28 22:27 | ① 用户提出 → ② 需求澄清 → ③ 方案设计→ ④ 人类审批 | 基于当前代码生成「AI 供应商与模型动态化设计书」:更新所有行号引用、代码片段、UI 结构描述;补充遗漏的 import-service.ts 第 382 行 createProvider() 调用点(第 08 节和第 10 节);修复 factory.ts 改造代码中 getAllProviderMeta() 无限递归 bug(改用 import alias | 无 | docs/superpowers/specs/2026-07-28-provider-registry-dynamic-design.md | GLM-5.2 |
| 2026-07-28 22:27 | ① 用户提出 → ② 需求澄清 → ③ 方案设计→ ④ 人类审批 | 基于当前代码生成「AI 供应商与模型动态化设计书」:更新所有行号引用、代码片段、UI 结构描述;补充遗漏的 import-service.ts 第 382 行 createProvider() 调用点(第 08 节和第 10 节);修复 factory.ts 改造代码中 getAllProviderMeta() 无限递归 bug(改用 import alias | 无 | docs/superpowers/specs/2026-07-28-provider-registry-dynamic-design.md | Trae 内部模型 |
| 2026-07-28 22:36 | ⑤ 编码实现 → ⑥ 审查验证 | 实现供应商注册表动态化:新建 providers.json / src/ai/types.ts / src/ai/registry.ts;重构 factory.ts 为动态加载 + PROTOCOL_MAPengine.ts / import-service.ts / setupView.ts 透传 extensionUri;模型下拉改为 input+datalistsetupView.ts 新增 FileSystemWatchersetupView.js 改为填充 datalistpackage.json 去除 ai.provider enum 锁定,版本 1.1.0→1.2.0;新增 i18n setup.modelPlaceholder | PROTOCOL_MAP 类型先用 4 参数构造函数签名报错(Gemini/Claude 仅 2 参数),改为 `new (...args: any[]) => AIProvider` | providers.json src/ai/types.ts src/ai/registry.ts src/ai/factory.ts src/ai/engine.ts src/rules/import-service.ts src/views/setupView.ts src/views/setupView.js package.json src/i18n/messages.ts | deepseek-v4-flash |
| 2026-07-28 22:49 | ① 用户提出 → ② 需求澄清→ ⑤ 编码实现 → ⑥ 审查验证 | 模型名称去掉 datalist 下拉列表,改为纯输入框; | 无 | src/views/setupView.ts | deepseek-v4-flash |
| 2026-07-29 19:06 | ① 用户提出 → ② 需求澄清 → ③ 方案设计→ ④ 人类审批 | 将 ESLint 内置配置从 61 条(官方 recommended)扩展到 94 条(P0 61 + P1 17 + P2 16),通过在 getDefaultConfig() 中追加 extraRules 对象实现额外 33 条规则的启用。设计涉及三个文件修改:src/adapters/eslint.ts(追加规则配置)、src/rules/static-rules.json(同步去重数据)、package.json(显式声明 @eslint/js 依赖)。规则分级为 P0 官方推荐(error)、P1 强烈推荐(error)、P2 建议启用(13 条 warn + 3 条 error)。 | 先整理了 eslint-9-rules.md312 条完整规则清单)作为规则选型的基础数据 由此产出 eslint-recommended-rules.md(推荐规则文档,94 条分级选型及理由)作为设计书的前置输入 讨论阶段确认了 @eslint/js 缺失于 package.json 是版本迭代遗留问题,决策为显式声明而非移除 讨论阶段确认了 typescript-eslint 从 devDependencies 移至 dependencies 的历史变更(_AI_USAGE_LOG.md 第 82 条) | docs\superpowers\specs\2026-07-29-eslint-rule-enhancement-design.md | Trae 内部模型 |
| 2026-07-29 19:34 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | ESLint 规则增强编码实现:package.json 添加 @eslint/js 依赖;eslint.ts 新增 extraRules 常量(31 条 P1+P2 规则)并追加到 getDefaultConfig()static-rules.json 追加 31 条规则条目并更新 linterVersion 为 9.x (92 rules) | 设计书代码示例中 extraRules 包含 no-implied-eval(已在 recommended)和 no-constant-binary-expression(已在现有 61 条),编码审查阶段发现后经用户确认移除,extraRules 改为 31 条、JSON 追加 31 条、总数 92 条 | package.json src/adapters/eslint.ts src/rules/static-rules.json | deepseek-v4-flash-free |
| 2026-07-29 20:00 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 在 eslint.config.mjs 中导入 js.configs.recommended,启用 ESLint 官方 61 条 P0 推荐规则 | 无 | eslint.config.mjs | deepseek-v4-flash-free |
| 2026-07-29 20:09 | ① 用户提出 → ② 需求澄清 → ③ 方案设计→ ④ 人类审批 | 将 typescript-eslint 内置配置从 24 条(官方 recommended)扩展到 35 条(P0 24 + P1 5 + P2 6),设计 extraTsRules 常量(11 条新规则 + 1 条扩展替换 no-shadow + 2 条 off 冲突修复),通过 files 字段限定仅作用于 TS 文件;处理 2 处扩展规则冲突:no-shadowTS 扩展版不在 recommended 需显式启用 + 禁用核心版)和 no-array-constructor(核心版 extraRules 覆盖 ts.configs.recommended 的 off 设置导致与扩展版重复诊断,需在 TS 文件中禁用核心版) | 扩展规则冲突初版仅处理 no-shadow 替换,分析后发现 no-array-constructor 也会因 ESLint extraRules 覆盖 ts.configs.recommended 的 off 设置而产生重复诊断,补充冲突修复;only-throw-error 归属初版误记为"已在 recommended",核对 2026-07-29-typescript-eslint-8-rules.md 确认其在 recommendedTypeChecked,当前无类型检查模式不可用,修正说明;文件作用域方案初版考虑不使用 files 字段(全局生效),分析后发现 TS 规则在 JS 文件上会报解析错误,改为通过 files 限定 TS 文件范围 | docs\superpowers\specs\2026-07-29-typescript-eslint-rule-enhancement-design.md | Trae 内部模型 |
| 2026-07-29 20:23 | ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 按设计书完成 typescript-eslint 规则增强编码:eslint.ts 追加 extraTsRules13 条规则,含 11 新增 + 2 off 冲突修复)+ TS_FILES 常量 + getDefaultConfig() 追加文件作用域配置块;static-rules.json 追加 12 条 ts-eslint 规则条目并更新 linterVersion 为 8.x (35 rules) | 无 | src/adapters/eslint.ts src/rules/static-rules.json | deepseek-v4-flash-free |
| 2026-07-29 20:46 | ① 用户提出 → ⑤ 编码实现 | 删除 eslint.config.mjs(避免项目自身配置覆盖 ESLintAdapter 内置推荐规则) | 无 | eslint.config.mjs | deepseek-v4-flash-free |
| 2026-07-29 21:02 | ① 用户提出 → ② 需求澄清 → ③ 方案设计→ ④ 人类审批 | 将 Stylelint 内置配置从 12 条硬编码规则扩展到 68 条(P0 官方 recommended 41 + P1 强烈推荐 7 + P2 建议启用 20),通过引入 stylelint-config-recommended 作为基线配置(与 ESLint 适配器使用 @eslint/js 的方式对齐),再用 extraRules 对象追加 27 条额外规则。设计涉及三个文件修改:src/adapters/stylelint.ts(替换 DEFAULT_CONFIG,从硬编码改为 recommendedConfig + extraRules 展开)、src/rules/static-rules.json(追加 24 条去重数据)、package.json(添加 stylelint-config-recommended 依赖)。原 12 条规则全部保留:5 条进入 P0、2 条进入 P1、5 条进入 P2。 | 先整理了 stylelint-16-rules.md141 条活跃规则 + 76 条已移除样式规则的完整清单)作为选型基础 由此产出 stylelint-recommended-rules.md(推荐规则文档,68 条分级选型及理由)作为设计书前置输入 讨论阶段对比了 ESLint 与 Stylelint 集成方式的差异(Flat Config 数组 vs 配置对象展开、'error'/'warn' 双级别 vs true 单级别) 讨论阶段确认 stylelint-config-recommended 是纯 JSON 配置包无运行时依赖,可被 esbuild 正确打包 讨论阶段确认版本兼容性:stylelint-config-recommended@18.0.0 声明 stylelint: ">=16.0.0",兼容项目使用的 ^17.14.0 | docs\superpowers\specs\2026-07-29-stylelint-rule-enhancement-design.md | Trae 内部模型 |
| 2026-07-29 21:22 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | Stylelint 规则增强编码实现:package.json 添加 stylelint-config-recommended ^18.0.0stylelint.ts 替换硬编码 DEFAULT_CONFIG 为 recommendedConfig + extraRules 展开(27 条 P1+P2);static-rules.json 追加 22 条规则条目(跳过 2 条已存在规则)并更新 linterVersion 为 16.x (68 rules);新增 stylelint-config-recommended.d.ts 类型声明 | type 声明文件(stylelint-config-recommended 缺少 TS 类型 → 创建 .d.ts);用户确认跳过 static-rules.json 中 color-no-invalid-hex 和 unit-no-unknown 的重叠追加 | package.json src/adapters/stylelint.ts src/rules/static-rules.json src/types/stylelint-config-recommended.d.ts | deepseek-v4-flash-free |
| 2026-07-30 20:29 | ① 用户提出 → ② 需求澄清 → ③ 方案设计→ ④ 人类审批 | 将 PMD Java 内置规则集从全分类无差别引用(309 条含 20 弃用)优化为精选模式(274 条活跃规则):补启 Security 分类 2 条高价值安全规则(HardCodedCryptoKey、InsecureCryptoIv);排除 20 条已弃用规则(为 PMD 8.0.0 迁移做准备);排除 17 条误报率高的阈值类噪音规则(CyclomaticComplexity、TooManyMethods、NPathComplexity、CognitiveComplexity、NcssCount、TooManyFields、ExcessiveParameterList 等);处理 AvoidCatchingGenericException 跨分类(Design 和 Error Prone)特殊排除 | 增强方向分析与 ESLint/Stylelint 相反:ESLint 是"从少到多"补规则,PMD 是"从粗到精"排除噪音;Documentation 分类 6 条规则讨论后不启用(Javadoc 规则更适合团队约定而非代码审查工具强制);噪音规则选型迭代:初版仅排除弃用规则,分析后发现阈值类规则(如 CyclomaticComplexity 默认阈值 10 过低)在实际项目中误报率极高,补充排除 17 条 | docs\superpowers\specs\2026-07-30-pmd-rule-enhancement-design.md | Trae 内部模型 |
| 2026-07-30 20:48 | ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | PMD 规则增强编码实现:pmd-java-ruleset.xml 为 6 个分类添加 exclude 排除弃用/噪音规则,补启 multithreading 分类(排除 3 条噪音),更新 descriptionstatic-rules.json 移除 4 条残留弃用规则(GenericsNaming、UnnecessaryLocalBeforeReturn、DontImportSun、UselessOperationOnImmutable),追加 2 条缺失噪音规则(AvoidSynchronizedStatement、AvoidUsingVolatile),更新 linterVersion 为 "7.26.0 (274 Java rules + 12 JSP rules)";后续补充 16 条缺失的去重规则条目(Multithreading 9 条完整规则 + 7 条遗漏规则)使 static-rules.json 覆盖 273 条规则 | 编码前发现设计书与代码现状不一致:Security 分类和 Security 规则已在 static-rules.json 中预先存在(跳过追加),multithreading 分类在 XML 中缺失(补启而非仅为现有分类添加 exclude),弃用规则已部分移除但仍有 4 条残留,噪音规则缺失 2 条(AvoidUsingVolatile、AvoidSynchronizedStatement);用户确认补启 multithreading 并排除 3 条噪音,确认 linterVersion 使用设计书格式;后续用户要求补充 static-rules.json 中遗漏的推荐文档 P0 规则条目;编译失败由预存 officeparser 缺失导致,与本次变更无关 | jars/pmd/pmd-java-ruleset.xml src/rules/static-rules.json | deepseek-v4-pro |
| 2026-07-30 21:08 | ① 用户提出 → ② 需求澄清 → ③ 方案设计→ ④ 人类审批 | 将 SQLFluff 内置配置从无差别 rules = all(75 条全启用)优化为精选规则集(57 条):保留全部 32 条 Core 核心规则作为 P0 基线;从 43 条非 Core 规则中精选 25 条高价值规则作为 P1(含结构优化 10 条 ST 类、引用规范 4 条 RF 类、约定 6 条 CV 类);排除 7 条低价值规则(3 条默认禁用 AL07/CV10/RF03 + 3 条纯格式化噪音 LT03/LT04/LT09 + 1 条需配置 CV09);设计内置 .sqlfluff 配置模板通过临时文件 + --config 参数注入(区别于 ESLint 的 Flat Config 对象和 Stylelint 的配置对象展开) | 增强方向分析与 ESLint/Stylelint 相反:ESLint 是"从少到多"补规则,SQLFluff 是"从全到精"减噪音;内置配置注入方案迭代:初版考虑在 CLI 参数中逐条传 rules,分析后发现参数过长且不可维护,改为生成临时 .sqlfluff 配置文件 + --config 参数引用;规则总数从 75 减至 57,信噪比提升(减少 18 条格式化噪音),推荐基线中 43 条可自动修复(75% 修复率) | docs\superpowers\specs\2026-07-30-sqlfluff-rule-enhancement-design.md | Trae 内部模型 |
| 2026-07-30 21:19 | ④ 人类审批 → ⑤ 编码实现 | SQLFluff 规则增强编码实现:sql-lint.ts 新增 BUILTIN_SQLFLUFF_CONFIG 内置精选配置常量(core + 25 条 P1 规则)、hasProjectSqlfluffConfig() 检测 .sqlfluff/.sqlfluff.ini、三层配置优先级(全局 > 项目 > 内置)、tierMap + tierToSeverityP0/P1→error, P2→warning)、临时配置文件写入 os.tmpdir() 并在 finally 中清理;static-rules.json 为 75 条 SQLFluff 规则全部追加 tier 字段(P0 32 条/P1 25 条/P2 11 条/excluded 7 条),更新 linterVersion 为 "4.2.2 (57 recommended)" | 用户确认 .sqlfluff.ini 也需检测、确认实现 tier→DiagnosticSeverity 可选增强映射(P0/P1→Error, P2→Warning);ESLint curly 规则警告单行 if 无花括号:tierToSeverity 的 return 语句补充花括号修复;编译错误为 pptx-converter.ts 预存 officeparser 缺失,与本次变更无关 | src/adapters/sql-lint.ts src/rules/static-rules.json | deepseek-v4-pro |
| 2026-07-30 21:42 | ① 用户提出 → ⑤ 编码实现 → ⑥ 审查验证 | 审查面板 WebView 中静态分析和自定义规则项可展开显示建议(suggestion)内容:移除 buildIssueItem 调用末尾的 false 参数使 expandable 默认为 true | 无 | src/panel/webview.ts | deepseek-v4-flash |
| 2026-07-30 22:16 | ① 用户提出 → ② 需求澄清 → ③ 方案设计→ ④ 人类审批 | 设计自定义规则导入导出闭环:导出侧在 setupView 加「导出模板」按钮生成 2 sheet 的 .xlsx(复用 xlsx 库零新依赖);导入侧加 checkbox「从模板导入」,勾选走程序直通解析(TemplateConverter 固定列映射跳过 AI),不勾选走原 AI 链路(零回归);解析与去重解耦——程序解析快且确定,AI 仅做语义去重(新增 buildDedupOnlyPrompt 只标重复不改字段);校验分两层,准入校验(格式/表头)硬报错,数据行校验(severity 拼错/字段空)进预览「错误规则」组(置顶只读、自动丢弃、有效规则正常导入);不校验表内 id 重复交由 AI 去重 | 链路选择迭代:初版自动识别表头判定走哪条链路,歧义大改为 checkbox 显式触发;错误处理迭代:初版校验失败一律硬报错,用户推荐改为错误规则进预览分组只读展示,最终定为准入硬报错+数据行软提示;Web 预览迭代:首版模拟样式被指不像,二版拉取真实源码按真实类名/配色重做 | docs\superpowers\specs\2026-07-30-export-template-design.md | Trae 内部模型 |
| 2026-07-30 22:35 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 | 解读并修正模板导出/导入设计书,产出 v2.0 方案文档 | 初版设计书 5 个问题逐一讨论后放弃(API 签名、参数传递、错误降级、prompt 约束),产出修正稿 v2 | docs/superpowers/specs/2026-07-30-export-template-design-v2.md | deepseek-v4-pro |
| 2026-07-30 22:38 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 | 实现模板导出/导入功能全链路 | 无 | src/rules/import-types.ts, src/rules/export-service.ts(新), src/rules/converters/template-converter.ts(新), src/rules/converters/dedup-prompt.ts(新), src/rules/import-service.ts, src/rules/import-preview.ts, src/views/setupView.ts, src/views/setupView.js, src/activation/commands.ts, package.json, src/i18n/messages.ts | deepseek-v4-pro |
| 2026-07-30 23:03 | ① 用户提出 → ⑤ 编码实现 | 导出按钮图标 📤→↓ 与 ➕添加 风格统一;复选框移至 field-hint 同行右对齐 | 图标迭代 📤→⬇→↓;复选框初版在 field-hint 下方独立一行,被要求移到提示文字右侧 | src/views/setupView.ts, src/i18n/messages.ts | deepseek-v4-pro |
| 2026-07-30 23:08 | ⑥ 审查验证 | 闭环校验模板文件:rowNumber 计算 bug 修复(先 filter 后 map 导致空行后索引偏移,改为先记行号再 filter | 无 | src/rules/converters/template-converter.ts | deepseek-v4-pro |
@@ -0,0 +1,498 @@
# ESLint 规则增强设计书
## 一、背景与目标
### 1.1 现状
当前 `ESLintAdapter``src/adapters/eslint.ts`)内置默认配置为:
```typescript
public static getDefaultConfig(): any[] {
if (!ESLintAdapter.defaultConfig) {
ESLintAdapter.defaultConfig = [
js.configs.recommended, // @eslint/js recommended → 61 条
...ts.configs.recommended, // typescript-eslint recommended → 24 条
];
}
return ESLintAdapter.defaultConfig;
}
```
仅启用了官方 `recommended` 的 61 条 ESLint 核心规则,全部属于 "Possible Problems" 类(语法错误、死代码等)。ESLint 9.x 活跃规则共 199 条,当前覆盖率仅 31%。
### 1.2 目标
将内置配置从 61 条扩展到 94 条(P0 官方推荐 61 + P1 强烈推荐 17 + P2 建议启用 16),使静态分析能捕获更多高频真实 bug,同时保持低误报率。
### 1.3 设计原则
- **不破坏现有配置优先级**:全局配置 > 项目配置 > 内置配置,三层择一逻辑不变
- **不引入新依赖**:P1/P2 规则全部来自 `eslint` 包内置规则,无需额外安装插件
- **同步更新去重数据**`static-rules.json` 必须同步追加新增规则,保证自定义规则导入时的去重检测覆盖完整
- **不修改规则 ID 前缀格式**:诊断结果仍使用 `eslint:{ruleId}` 格式
---
## 二、影响范围分析
### 2.1 需要修改的文件
| 文件 | 修改类型 | 修改内容 |
|------|----------|----------|
| `src/adapters/eslint.ts` | 代码修改 | `getDefaultConfig()` 方法增加 P1+P2 规则配置 |
| `src/rules/static-rules.json` | 数据修改 | `rules.eslint` 数组追加 33 条规则条目 |
| `package.json` | 依赖修改 | `dependencies` 中补充 `@eslint/js` 显式声明 |
### 2.2 不需要修改的文件
| 文件 | 原因 |
|------|------|
| `scripts/build.mjs` | `@eslint/js` 不在 external 列表中,已被 esbuild 正确打包 |
| `src/adapters/jsp.ts` | 内部复用 `ESLintAdapter` 实例,自动继承新配置 |
| `src/orchestrator/orchestrator.ts` | 仅做调度,不涉及配置逻辑 |
| `src/config/linter.ts` | 配置读取层不变 |
| `eslint.config.mjs` | 项目自身 lint 配置,与运行时适配器配置无关 |
### 2.3 不受影响的功能
- 全局配置(`linters.eslintConfigPath`):用户指定配置文件时完全替代内置配置,不受影响
- 项目配置(`.eslintrc.*` / `eslint.config.*`):存在时完全替代内置配置,不受影响
- JSP 适配器:复用 `ESLintAdapter`,自动获得增强后的配置
- 规则 ID 输出格式:仍为 `eslint:{ruleId}`,不变
---
## 三、详细设计
### 3.1 修改 `src/adapters/eslint.ts`
#### 3.1.1 当前代码
```typescript
import js from '@eslint/js';
import ts from 'typescript-eslint';
// ... 中间部分不变 ...
public static getDefaultConfig(): any[] {
if (!ESLintAdapter.defaultConfig) {
ESLintAdapter.defaultConfig = [
js.configs.recommended,
...ts.configs.recommended,
];
}
return ESLintAdapter.defaultConfig;
}
```
#### 3.1.2 修改后代码
```typescript
import js from '@eslint/js';
import ts from 'typescript-eslint';
// 新增:P1+P2 额外规则配置常量
const extraRules: Record<string, 'error' | 'warn'> = {
// === P1:强烈推荐(error===
'eqeqeq': 'error',
'no-eq-null': 'error',
'no-self-compare': 'error',
'no-promise-executor-return': 'error',
'no-shadow': 'error',
'no-unassigned-vars': 'error',
'no-useless-assignment': 'error',
'block-scoped-var': 'error',
'default-case': 'error',
'default-case-last': 'error',
'no-unmodified-loop-condition': 'error',
'no-unreachable-loop': 'error',
'no-constant-binary-expression': 'error',
'no-eval': 'error',
'no-implied-eval': 'error',
'no-extend-native': 'error',
// === P2:建议启用 ===
'no-var': 'error',
'no-await-in-loop': 'warn',
'prefer-template': 'warn',
'prefer-object-spread': 'warn',
'prefer-rest-params': 'warn',
'prefer-spread': 'warn',
'prefer-object-has-own': 'warn',
'no-useless-concat': 'warn',
'no-useless-return': 'warn',
'no-useless-computed-key': 'warn',
'no-useless-rename': 'warn',
'no-param-reassign': 'warn',
'no-return-assign': 'error',
'no-throw-literal': 'error',
'camelcase': 'warn',
'new-cap': 'warn',
'no-array-constructor': 'error',
};
// ... 中间部分不变 ...
public static getDefaultConfig(): any[] {
if (!ESLintAdapter.defaultConfig) {
ESLintAdapter.defaultConfig = [
js.configs.recommended,
...ts.configs.recommended,
// 新增:P1+P2 额外规则
{ rules: extraRules },
];
}
return ESLintAdapter.defaultConfig;
}
```
#### 3.1.3 设计说明
**为什么用 `extraRules` 常量而不是内联对象?**
- 可读性:33 条规则单独成文件级常量,与 `getDefaultConfig()` 逻辑分离
- 可维护性:未来增删规则只需修改 `extraRules` 对象,不需要动方法逻辑
- 可测试性:常量可以被测试文件直接导入验证
**为什么放在 `recommended` 之后?**
ESLint Flat Config 的规则是后者覆盖前者。`extraRules` 放在最后,如果其中某些规则已在 `recommended` 中启用(如 `no-implied-eval` 在 recommended 中已有),`extraRules` 的配置会覆盖其级别。但实际上 P1/P2 选型时已排除了与 recommended 重复的规则,不存在冲突。
**为什么不用 `@eslint/js` 的 `configs.all`**
`js.configs.all` 启用全部 199 条活跃规则,包含大量不应在代码审查工具中强制启用的规则(如 `no-magic-numbers``max-lines` 等)。精确选择 33 条更合理。
### 3.2 修改 `src/rules/static-rules.json`
#### 3.2.1 修改内容
`rules.eslint` 数组末尾追加以下 33 条规则条目(格式与现有 61 条一致):
```json
{"id": "eslint/eqeqeq", "description": "Require === and !=="},
{"id": "eslint/no-eq-null", "description": "Disallow null comparisons without type-checking"},
{"id": "eslint/no-self-compare", "description": "Disallow comparisons where both sides are the same"},
{"id": "eslint/no-await-in-loop", "description": "Disallow await inside loops"},
{"id": "eslint/no-promise-executor-return", "description": "Disallow returning values from Promise executor"},
{"id": "eslint/no-shadow", "description": "Disallow variable declarations from shadowing variables in outer scopes"},
{"id": "eslint/no-unassigned-vars", "description": "Disallow let or var variables that are read but never assigned"},
{"id": "eslint/no-useless-assignment", "description": "Disallow variable assignments where the value is not used"},
{"id": "eslint/block-scoped-var", "description": "Enforce variables within the scope they are defined"},
{"id": "eslint/default-case", "description": "Require default cases in switch statements"},
{"id": "eslint/default-case-last", "description": "Enforce default clauses in switch statements to be last"},
{"id": "eslint/no-unmodified-loop-condition", "description": "Disallow unmodified loop conditions"},
{"id": "eslint/no-unreachable-loop", "description": "Disallow loops with a body that allows only one iteration"},
{"id": "eslint/no-constant-binary-expression", "description": "Disallow expressions where the operation doesn't affect the value"},
{"id": "eslint/no-eval", "description": "Disallow the use of eval()"},
{"id": "eslint/no-extend-native", "description": "Disallow extending native types"},
{"id": "eslint/no-var", "description": "Require let or const instead of var"},
{"id": "eslint/prefer-template", "description": "Require template literals instead of string concatenation"},
{"id": "eslint/prefer-object-spread", "description": "Disallow Object.assign and prefer object spread"},
{"id": "eslint/prefer-rest-params", "description": "Require rest parameters instead of arguments"},
{"id": "eslint/prefer-spread", "description": "Require spread operator instead of .apply()"},
{"id": "eslint/prefer-object-has-own", "description": "Disallow Object.prototype.hasOwnProperty.call() and prefer Object.hasOwn()"},
{"id": "eslint/no-useless-concat", "description": "Disallow unnecessary concatenation of literals or template literals"},
{"id": "eslint/no-useless-return", "description": "Disallow redundant return statements"},
{"id": "eslint/no-useless-computed-key", "description": "Disallow unnecessary computed property keys in objects and classes"},
{"id": "eslint/no-useless-rename", "description": "Disallow renaming import, export, and destructured assignments to the same name"},
{"id": "eslint/no-param-reassign", "description": "Disallow reassigning function parameters"},
{"id": "eslint/no-return-assign", "description": "Disallow assignment operators in return statements"},
{"id": "eslint/no-throw-literal", "description": "Disallow throwing literals as exceptions"},
{"id": "eslint/camelcase", "description": "Enforce camelcase naming convention"},
{"id": "eslint/new-cap", "description": "Require constructor names to begin with a capital letter"},
{"id": "eslint/no-array-constructor", "description": "Disallow Array constructors"}
```
> 注:`no-implied-eval` 已在 recommended 中启用,不在追加列表中。追加的 33 条均为 recommended 之外的新增规则。
#### 3.2.2 修改 `linterVersion` 字段
```json
"linterVersion": {
"eslint": "9.x (94 rules)",
...
}
```
`"9.x (recommended)"` 改为 `"9.x (94 rules)"`,反映实际启用规则数量。
#### 3.2.3 为什么必须同步更新 static-rules.json
`static-rules.json` 有两个用途:
1. **UI 展示**:在设置面板中展示当前 linter 支持的规则清单
2. **去重检测**:自定义规则导入时,按 ID 匹配 `static-rules.json` 中的规则,若已存在则提示重复
如果不更新,用户新增的 `eqeqeq` 自定义规则不会被识别为重复(因为 `static-rules.json` 中没有这条),导致重复规则。
### 3.3 修改 `package.json`
#### 3.3.1 当前代码
```json
"dependencies": {
"eslint": "^9.39.3",
"stylelint": "^17.14.0",
"typescript-eslint": "^8.56.1",
"xlsx": "^0.18.5"
}
```
#### 3.3.2 修改后代码
```json
"dependencies": {
"@eslint/js": "^9.39.3",
"eslint": "^9.39.3",
"stylelint": "^17.14.0",
"typescript-eslint": "^8.56.1",
"xlsx": "^0.18.5"
}
```
#### 3.3.3 修改理由
`eslint.ts``import js from '@eslint/js'` 已被使用,但 `@eslint/js` 未在 `package.json` 中显式声明,仅作为 `eslint` 包的传递依赖存在。这属于版本迭代遗留问题(见 `_AI_USAGE_LOG.md` 第82条记录)。显式声明可消除依赖不确定性。
---
## 四、新增规则分类详解
### 4.1 P1 强烈推荐(17 条,error 级别)
这些规则检测高频真实 bug,误报率极低。
#### 4.1.1 等值与比较(3 条)
| 规则 | 检测场景 | 误报评估 |
|------|----------|----------|
| `eqeqeq` | `==` 隐式类型转换 | 极低,几乎所有现代项目都启用 |
| `no-eq-null` | `x == null` 同时匹配 null 和 undefined | 低,通常只需判断其一 |
| `no-self-compare` | `a === a` 恒为 true | 零误报,几乎一定是笔误 |
#### 4.1.2 异步与 Promise2 条)
| 规则 | 检测场景 | 误报评估 |
|------|----------|----------|
| `no-await-in-loop` | 循环中串行 await 导致性能问题 | 中,设为 warn,允许逐条忽略 |
| `no-promise-executor-return` | executor 返回值被忽略 | 零误报 |
#### 4.1.3 作用域与变量(4 条)
| 规则 | 检测场景 | 误报评估 |
|------|----------|----------|
| `no-shadow` | 内层变量遮蔽外层变量 | 低,遮蔽几乎都是 bug |
| `no-unassigned-vars` | 读取未赋值的 let/var | 零误报 |
| `no-useless-assignment` | 赋值后值未被使用 | 低 |
| `block-scoped-var` | var 变量在声明作用域外使用 | 低 |
#### 4.1.4 控制流与逻辑(5 条)
| 规则 | 检测场景 | 误报评估 |
|------|----------|----------|
| `default-case` | switch 缺少 default 分支 | 低 |
| `default-case-last` | default 不在最后 | 零误报 |
| `no-unmodified-loop-condition` | 循环条件未修改导致死循环 | 零误报 |
| `no-unreachable-loop` | 循环体只允许一次迭代 | 低 |
| `no-constant-binary-expression` | 操作不影响结果值 | 低 |
#### 4.1.5 安全(3 条)
| 规则 | 检测场景 | 误报评估 |
|------|----------|----------|
| `no-eval` | eval() 执行任意代码 | 零误报 |
| `no-implied-eval` | setTimeout("code") 类似 eval | 零误报(已在 recommended 中) |
| `no-extend-native` | 修改 Array.prototype 等 | 零误报 |
### 4.2 P2 建议启用(16 条)
#### 4.2.1 现代 JS 语法(6 条)
| 规则 | 级别 | 检测场景 |
|------|------|----------|
| `no-var` | error | 使用 var 代替 let/const |
| `prefer-template` | warn | 字符串拼接代替模板字面量 |
| `prefer-object-spread` | warn | Object.assign 代替对象展开 |
| `prefer-rest-params` | warn | arguments 代替 rest 参数 |
| `prefer-spread` | warn | .apply() 代替展开运算符 |
| `prefer-object-has-own` | warn | 旧式 hasOwnProperty 代替 Object.hasOwn |
#### 4.2.2 代码简洁性(4 条)
| 规则 | 级别 | 检测场景 |
|------|------|----------|
| `no-useless-concat` | warn | 不必要的字符串拼接 |
| `no-useless-return` | warn | 多余的 return 语句 |
| `no-useless-computed-key` | warn | 不必要的计算属性键 |
| `no-useless-rename` | warn | 重命名为相同名称 |
#### 4.2.3 防御性编程(3 条)
| 规则 | 级别 | 检测场景 |
|------|------|----------|
| `no-param-reassign` | warn | 修改函数参数(副作用风险) |
| `no-return-assign` | error | return 中赋值(笔误风险) |
| `no-throw-literal` | error | 抛出字面量而非 Error 对象 |
#### 4.2.4 命名与风格(3 条)
| 规则 | 级别 | 检测场景 |
|------|------|----------|
| `camelcase` | warn | 非驼峰命名 |
| `new-cap` | warn | 构造函数未大写 |
| `no-array-constructor` | error | 使用 Array 构造函数 |
---
## 五、规则级别设计
### 5.1 级别分配原则
| 级别 | 适用场景 | 数量 |
|------|----------|------|
| `error` | 确定性 bug、安全问题、语法错误 | 74 条 |
| `warn` | 代码质量建议、风格偏好(可逐条忽略) | 20 条 |
### 5.2 为什么部分规则设为 warn 而非 error
以下规则设为 `warn` 是因为它们有合理的例外场景:
| 规则 | warn 理由 |
|------|-----------|
| `no-await-in-loop` | 某些场景确实需要串行 await(如分页请求) |
| `prefer-template` | 超长字符串拼接时 `+` 可能更清晰 |
| `prefer-rest-params` | 兼容旧环境时可能需要 arguments |
| `no-param-reassign` | Redux reducer 等模式需要修改参数 |
| `camelcase` | 对接外部 API 时可能需要 snake_case |
其余规则设为 `error` 是因为它们的触发几乎一定意味着 bug 或安全问题。
---
## 六、兼容性分析
### 6.1 对现有用户代码的影响
启用新规则后,之前能通过审查的代码可能会新增诊断:
| 影响程度 | 规则 | 说明 |
|----------|------|------|
| **可能大量新增诊断** | `eqeqeq` | 大量旧代码使用 `==`,但这是必要的改进 |
| **可能大量新增诊断** | `no-shadow` | 嵌套作用域中常见,需逐个审查 |
| **中等新增诊断** | `no-param-reassign` | 函数中修改参数较常见 |
| **少量新增诊断** | `default-case` | 大多数 switch 已有 default |
| **少量新增诊断** | `no-var` | 现代 TS 项目已普遍使用 let/const |
| **极少新增诊断** | `no-eval` / `no-extend-native` | 正常项目几乎不会用 |
### 6.2 对 JSP 适配器的影响
JSP 适配器(`src/adapters/jsp.ts`)内部持有 `ESLintAdapter` 实例,提取 `<script>` 块后复用其 `check()` 方法。新增规则自动生效,无需额外修改。
### 6.3 对去重功能的影响
`static-rules.json` 同步更新后,去重检测范围从 61 条扩展到 94 条。用户导入的自定义规则如果与这 33 条新规则 ID 匹配,将正确识别为重复。
### 6.4 对构建的影响
- `@eslint/js` 已被 esbuild 正确打包(不在 external 列表中)
- 新增的 33 条规则全部来自 `eslint` 包内置,无需安装额外 npm 包
- 构建产物体积影响可忽略(规则配置仅为一个 JS 对象)
---
## 七、实施步骤
### 步骤 1:修改 `package.json`
`dependencies` 中添加 `"@eslint/js": "^9.39.3"`
### 步骤 2:修改 `src/adapters/eslint.ts`
1. 在文件顶部(`import` 之后、`PROJECT_CONFIG_FILES` 之前)添加 `extraRules` 常量
2.`getDefaultConfig()` 方法的返回数组末尾追加 `{ rules: extraRules }`
### 步骤 3:修改 `src/rules/static-rules.json`
1.`rules.eslint` 数组末尾追加 33 条规则条目
2. 更新 `linterVersion.eslint``"9.x (94 rules)"`
### 步骤 4:验证
1. 执行 `npm run lint` 确认项目自身代码无新增报错
2. 执行 `npm run compile` 确认编译通过
3. 执行 `npm run build` 确认打包成功
4. 检查 `static-rules.json` 中 eslint 规则数量为 94 条
---
## 八、测试要点
### 8.1 单元测试
| 测试项 | 验证内容 |
|--------|----------|
| `getDefaultConfig()` 返回值 | 数组长度为 3recommended + ts recommended + extraRules |
| `extraRules` 规则数 | 恰好 33 条 |
| `extraRules` 级别 | 17 条 error + 16 条 warn |
| 规则 ID 无重复 | 与 recommended 61 条无交集 |
### 8.2 集成测试
| 测试项 | 验证内容 |
|--------|----------|
| `==` 触发 `eqeqeq` | 诊断结果包含 `eslint:eqeqeq` |
| `var x = 1` 触发 `no-var` | 诊断结果包含 `eslint:no-var` |
| `eval("code")` 触发 `no-eval` | 诊断结果包含 `eslint:no-eval` |
| switch 无 default 触发 `default-case` | 诊断结果包含 `eslint:default-case` |
| 全局配置存在时忽略内置配置 | 使用用户配置,不触发新规则 |
| 项目配置存在时忽略内置配置 | 使用项目配置,不触发新规则 |
### 8.3 去重测试
| 测试项 | 验证内容 |
|--------|----------|
| 导入 `eqeqeq` 自定义规则 | 提示与已有规则重复 |
| 导入 `no-eval` 自定义规则 | 提示与已有规则重复 |
| 导入 recommended 中已有的规则 | 仍提示重复(未改变原有行为) |
---
## 九、风险评估
### 9.1 主要风险
| 风险 | 级别 | 缓解措施 |
|------|------|----------|
| 用户代码新增大量诊断 | 中 | P2 规则设为 warn,用户可逐条忽略 |
| `no-shadow` 误报较多 | 低 | 该规则在大型项目中较常见,但遮蔽几乎都是 bug |
| `camelcase` 对接外部 API 报错 | 低 | 用户可通过项目配置覆盖该规则 |
| `@eslint/js` 版本不匹配 | 低 | 锁定与 eslint 相同的 `^9.39.3` |
### 9.2 回滚方案
如果新规则导致严重问题,回滚步骤:
1.`getDefaultConfig()` 中移除 `{ rules: extraRules }`
2.`static-rules.json` 中移除追加的 33 条规则
3. 恢复 `linterVersion.eslint``"9.x (recommended)"`
`package.json``@eslint/js` 的声明可以保留(本就是应该声明的依赖)。
---
## 十、附录
### 10.1 规则来源参考
- ESLint 官方规则文档:https://eslint.org/docs/latest/rules
- 推荐规则分析文件:`/workspace/eslint-recommended-rules.md`
- 完整规则清单:`/workspace/eslint-9-rules.md`
### 10.2 规则数量统计
| 分类 | 数量 | 级别 |
|------|------|------|
| P0 官方 recommended | 61 | error(由 recommended 定义) |
| P1 强烈推荐 | 17 | error |
| P2 建议启用 | 16 | 13 warn + 3 error |
| **合计** | **94** | **74 error + 20 warn** |
| 未启用(冻结/弃用/格式化/过度限制) | 218 | — |
@@ -0,0 +1,560 @@
# Stylelint 规则增强设计书
## 一、背景与目标
### 1.1 现状
当前 `StylelintAdapter``src/adapters/stylelint.ts`)内置默认配置为硬编码的 12 条规则:
```typescript
const DEFAULT_CONFIG: Record<string, unknown> = {
rules: {
'color-hex-length': 'short',
'color-named': 'never',
'color-no-invalid-hex': true,
'length-zero-no-unit': true,
'font-family-no-missing-generic-family-keyword': true,
'block-no-empty': true,
'declaration-block-no-duplicate-properties': true,
'no-descending-specificity': true,
'unit-no-unknown': true,
'property-no-unknown': true,
'selector-pseudo-class-no-unknown': true,
'selector-pseudo-element-no-unknown': true,
},
};
```
这 12 条规则仅覆盖 Stylelint 16.x 活跃规则(141 条)的 8.5%,存在以下问题:
- **覆盖面严重不足**:仅 7 条属于官方 recommended(41 条),缺少大量错误检测规则
- **未使用官方配置包**:与 ESLint 适配器使用 `@eslint/js` recommended 的方式不一致
- **规则选择零散**:12 条规则中混合了错误检测和约定强制两类,无系统性设计
- **缺少自动修复规则**:仅 3 条可自动修复,未充分利用 Stylelint 的 fix 能力
### 1.2 目标
将内置配置从 12 条扩展到 68 条(P0 官方推荐 41 + P1 强烈推荐 7 + P2 建议启用 20),通过引入 `stylelint-config-recommended` 作为基线,并追加 27 条额外规则,使 CSS 静态分析覆盖更多错误和约定问题。
### 1.3 设计原则
- **与 ESLint 适配器保持一致**:采用"官方配置包 + 额外规则对象"的相同架构
- **不破坏现有配置优先级**:全局配置 > 项目配置 > 内置配置,三层择一逻辑不变
- **引入 `stylelint-config-recommended` 依赖**:与 `@eslint/js` 的引入方式对齐
- **同步更新去重数据**`static-rules.json` 必须同步追加新增规则
- **不修改规则 ID 前缀格式**:诊断结果仍使用 `stylelint:{ruleName}` 格式
---
## 二、影响范围分析
### 2.1 需要修改的文件
| 文件 | 修改类型 | 修改内容 |
|------|----------|----------|
| `src/adapters/stylelint.ts` | 代码修改 | 替换 `DEFAULT_CONFIG`,引入 `stylelint-config-recommended`,追加 P1+P2 规则 |
| `src/rules/static-rules.json` | 数据修改 | `rules.stylelint` 数组追加 27 条规则条目 |
| `package.json` | 依赖修改 | `dependencies` 中添加 `stylelint-config-recommended` 显式声明 |
### 2.2 不需要修改的文件
| 文件 | 原因 |
|------|------|
| `scripts/build.mjs` | `stylelint-config-recommended` 不在 external 列表中,将被 esbuild 正确打包 |
| `src/adapters/eslint.ts` | 独立适配器,不受影响 |
| `src/orchestrator/orchestrator.ts` | 仅做调度,不涉及配置逻辑 |
| `src/config/linter.ts` | 配置读取层不变 |
### 2.3 不受影响的功能
- 全局配置(`linters.stylelintConfigPath`):用户指定配置文件时完全替代内置配置,不受影响
- 项目配置(`.stylelintrc.*` / `stylelint.config.*`):存在时完全替代内置配置,不受影响
- 规则 ID 输出格式:仍为 `stylelint:{ruleName}`,不变
---
## 三、详细设计
### 3.1 修改 `src/adapters/stylelint.ts`
#### 3.1.1 当前代码
```typescript
const DEFAULT_CONFIG: Record<string, unknown> = {
rules: {
'color-hex-length': 'short',
'color-named': 'never',
'color-no-invalid-hex': true,
'length-zero-no-unit': true,
'font-family-no-missing-generic-family-keyword': true,
'block-no-empty': true,
'declaration-block-no-duplicate-properties': true,
'no-descending-specificity': true,
'unit-no-unknown': true,
'property-no-unknown': true,
'selector-pseudo-class-no-unknown': true,
'selector-pseudo-element-no-unknown': true,
},
};
```
#### 3.1.2 修改后代码
```typescript
import recommendedConfig from 'stylelint-config-recommended';
// P1+P2 额外规则配置
const extraRules: Record<string, unknown> = {
// === P1:强烈推荐(错误检测)===
'color-no-invalid-hex': true,
'function-linear-gradient-no-nonstandard-direction': true,
'function-no-unknown': true,
'unit-no-unknown': true,
'no-unknown-animations': true,
'no-unknown-custom-media': true,
'no-unknown-custom-properties': true,
// === P2:厂商前缀移除(均可自动修复)===
'at-rule-no-vendor-prefix': true,
'media-feature-name-no-vendor-prefix': true,
'property-no-vendor-prefix': true,
'selector-no-vendor-prefix': true,
'value-no-vendor-prefix': true,
// === P2:表示法一致性(均可自动修复)===
'color-hex-length': 'short',
'color-function-notation': 'modern',
'length-zero-no-unit': true,
'selector-pseudo-element-colon-notation': 'double',
'import-notation': 'string',
'alpha-value-notation': 'number',
'hue-degree-notation': 'angle',
'keyframe-selector-notation': 'percentage',
// === P2:冗余检测 ===
'declaration-block-no-redundant-longhand-properties': true,
'shorthand-property-no-redundant-values': true,
'block-no-redundant-nested-style-rules': true,
// === P2:颜色与字体约定 ===
'color-named': 'never',
'font-family-name-quotes': 'always-where-required',
// === P2:精度与格式 ===
'number-max-precision': 4,
'comment-whitespace-inside': 'always',
};
// 合并 recommended 配置与额外规则
const DEFAULT_CONFIG: Record<string, unknown> = {
...recommendedConfig,
rules: {
...recommendedConfig.rules,
...extraRules,
},
};
```
#### 3.1.3 设计说明
**为什么用 `stylelint-config-recommended` 而非硬编码 41 条规则?**
- **可维护性**:规则升级时只需更新依赖版本,无需手动修改代码
- **权威性**:官方维护的推荐规则集,经过社区验证
- **一致性**:与 ESLint 适配器使用 `@eslint/js` 的方式完全对齐
- **体积可控**`stylelint-config-recommended` 是一个纯 JSON 配置包,无额外依赖,打包后体积增量可忽略
**为什么用 `extraRules` 常量分离额外规则?**
- 可读性:27 条规则单独成常量,与 `DEFAULT_CONFIG` 合并逻辑分离
- 可维护性:未来增删规则只需修改 `extraRules` 对象
- 可测试性:常量可被测试文件直接导入验证
**为什么用展开运算符合并而非覆盖?**
Stylelint 配置中 `rules` 是一个扁平对象。先展开 `recommendedConfig.rules`41 条 P0),再展开 `extraRules`(27 条 P1+P2),后者覆盖前者同名规则。实际上 P1/P2 选型时已排除了与 recommended 重复的规则,不存在冲突。当前 12 条硬编码规则中,7 条已在 recommended 中(P0),2 条在 P1 中,3 条在 P2 中,全部被新配置覆盖。
**为什么不用 `stylelint-config-standard`**
`stylelint-config-standard` 包含 82 条规则(recommended 41 + standard 额外 41),但其中 41 条 standard 规则包含大量空行、大小写、引号等格式化约定,这些更适合交给 Prettier 等格式化工具处理。精确选择 27 条额外规则更合理。
### 3.2 修改 `src/rules/static-rules.json`
#### 3.2.1 修改内容
`rules.stylelint` 数组末尾追加以下 27 条规则条目(格式与现有规则一致):
```json
{"id": "stylelint/color-no-invalid-hex", "description": "Disallow invalid hex colors"},
{"id": "stylelint/function-linear-gradient-no-nonstandard-direction", "description": "Disallow non-standard directions in linear-gradient"},
{"id": "stylelint/function-no-unknown", "description": "Disallow unknown functions"},
{"id": "stylelint/unit-no-unknown", "description": "Disallow unknown units"},
{"id": "stylelint/no-unknown-animations", "description": "Disallow unknown animations"},
{"id": "stylelint/no-unknown-custom-media", "description": "Disallow unknown custom media queries"},
{"id": "stylelint/no-unknown-custom-properties", "description": "Disallow unknown custom properties"},
{"id": "stylelint/at-rule-no-vendor-prefix", "description": "Disallow vendor prefixes for at-rules"},
{"id": "stylelint/media-feature-name-no-vendor-prefix", "description": "Disallow vendor prefixes for media feature names"},
{"id": "stylelint/property-no-vendor-prefix", "description": "Disallow vendor prefixes for properties"},
{"id": "stylelint/selector-no-vendor-prefix", "description": "Disallow vendor prefixes for selectors"},
{"id": "stylelint/value-no-vendor-prefix", "description": "Disallow vendor prefixes for values"},
{"id": "stylelint/color-function-notation", "description": "Require modern or legacy notation for color-functions"},
{"id": "stylelint/selector-pseudo-element-colon-notation", "description": "Use single or double colon notation for pseudo-elements"},
{"id": "stylelint/import-notation", "description": "Require string or url notation for @import"},
{"id": "stylelint/alpha-value-notation", "description": "Require percentage or number notation for alpha-values"},
{"id": "stylelint/hue-degree-notation", "description": "Require number or angle notation for hue degrees"},
{"id": "stylelint/keyframe-selector-notation", "description": "Require keyword or percentage notation for keyframe selectors"},
{"id": "stylelint/declaration-block-no-redundant-longhand-properties", "description": "Disallow redundant longhand properties within declaration blocks"},
{"id": "stylelint/shorthand-property-no-redundant-values", "description": "Disallow redundant values within shorthand properties"},
{"id": "stylelint/block-no-redundant-nested-style-rules", "description": "Disallow redundant nested style rules within blocks"},
{"id": "stylelint/font-family-name-quotes", "description": "Require quotes for font-family names"},
{"id": "stylelint/number-max-precision", "description": "Limit the number of decimal places in numbers"},
{"id": "stylelint/comment-whitespace-inside", "description": "Require or disallow whitespace inside comments"}
```
> 注:`color-hex-length`、`color-named`、`length-zero-no-unit` 已在当前 12 条规则中存在于 `static-rules.json`,不在追加列表中。追加的 24 条均为新增规则。实际追加 24 条(`color-no-invalid-hex` 和 `unit-no-unknown` 也已存在于当前规则中)。
#### 3.2.2 修改 `linterVersion` 字段
```json
"linterVersion": {
"stylelint": "16.x (68 rules)",
...
}
```
将原有标记改为 `"16.x (68 rules)"`,反映实际启用规则数量。
#### 3.2.3 为什么必须同步更新 static-rules.json
`static-rules.json` 有两个用途:
1. **UI 展示**:在设置面板中展示当前 linter 支持的规则清单
2. **去重检测**:自定义规则导入时,按 ID 匹配 `static-rules.json` 中的规则,若已存在则提示重复
如果不更新,用户新增的 `function-no-unknown` 自定义规则不会被识别为重复,导致规则重复。
### 3.3 修改 `package.json`
#### 3.3.1 当前代码
```json
"dependencies": {
"eslint": "^9.39.3",
"stylelint": "^17.14.0",
"typescript-eslint": "^8.56.1",
"xlsx": "^0.18.5"
}
```
#### 3.3.2 修改后代码
```json
"dependencies": {
"@eslint/js": "^9.39.3",
"eslint": "^9.39.3",
"stylelint": "^17.14.0",
"stylelint-config-recommended": "^18.0.0",
"typescript-eslint": "^8.56.1",
"xlsx": "^0.18.5"
}
```
#### 3.3.3 修改理由
- `stylelint-config-recommended` 需要显式声明为依赖,与 `@eslint/js` 的处理方式一致
- 版本 `^18.0.0` 兼容 Stylelint 17.x`stylelint-config-recommended` 自 v15.0.0 起声明 `stylelint: ">=16.0.0"`v18.0.0 兼容 17.x
- 该包是纯 JSON 配置文件,无任何运行时依赖,打包后体积增量可忽略
---
## 四、新增规则分类详解
### 4.1 P1 强烈推荐(7 条,error 级别)
这些规则属于 "Possible errors" 类别但未被 recommended 包含,能检测确定性错误。
#### 4.1.1 无效值检测(2 条)
| 规则 | 检测场景 | 误报评估 |
|------|----------|----------|
| `color-no-invalid-hex` | `#fff00` 等无效 hex 颜色 | 零误报,浏览器静默忽略无效颜色 |
| `function-linear-gradient-no-nonstandard-direction` | `linear-gradient(top, ...)` 非标准方向 | 零误报,标准方向使用 `to top` |
#### 4.1.2 未知引用检测(5 条)
| 规则 | 检测场景 | 误报评估 |
|------|----------|----------|
| `function-no-unknown` | `translat()` 等未知函数 | 低,需要配置 `ignoreFunctions` 排除自定义函数 |
| `unit-no-unknown` | `10p` 等未知单位 | 零误报 |
| `no-unknown-animations` | 引用不存在的 @keyframes | 零误报 |
| `no-unknown-custom-media` | 引用未定义的 `--custom-media` | 低,需要确保自定义媒体已声明 |
| `no-unknown-custom-properties` | 引用未定义的 `--custom-property` | 低,需要确保自定义属性已声明 |
### 4.2 P2 建议启用(20 条)
#### 4.2.1 厂商前缀移除(5 条,均可自动修复)
| 规则 | 检测场景 | 自动修复 |
|------|----------|----------|
| `at-rule-no-vendor-prefix` | `@-webkit-keyframes` | 可自动移除前缀 |
| `media-feature-name-no-vendor-prefix` | `-webkit-min-device-pixel-ratio` | 可自动移除前缀 |
| `property-no-vendor-prefix` | `-webkit-transform` | 可自动移除前缀 |
| `selector-no-vendor-prefix` | `:-webkit-full-screen` | 可自动移除前缀 |
| `value-no-vendor-prefix` | `display: -webkit-flex` | 可自动移除前缀 |
> **说明**:现代 CSS 开发应使用 Autoprefixer 自动管理厂商前缀,手写前缀不仅冗余且难以维护。
#### 4.2.2 表示法一致性(8 条,均可自动修复)
| 规则 | 配置值 | 检测场景 |
|------|--------|----------|
| `color-hex-length` | 'short' | `#ffffff` 应简写为 `#fff` |
| `color-function-notation` | 'modern' | `rgba(0,0,0,0.5)` 应写 `rgb(0 0 0 / 50%)` |
| `length-zero-no-unit` | true | `margin: 0px` 应写 `margin: 0` |
| `selector-pseudo-element-colon-notation` | 'double' | `:before` 应写 `::before` |
| `import-notation` | 'string' | `@import url("x.css")` 应写 `@import "x.css"` |
| `alpha-value-notation` | 'number' | `opacity: 50%` 应写 `opacity: 0.5` |
| `hue-degree-notation` | 'angle' | `hsl(180 50% 50%)` 应写 `hsl(180deg 50% 50%)` |
| `keyframe-selector-notation` | 'percentage' | `from`/`to` 应写 `0%`/`100%` |
#### 4.2.3 冗余检测(3 条)
| 规则 | 检测场景 | 自动修复 |
|------|----------|----------|
| `declaration-block-no-redundant-longhand-properties` | `margin-top:0; margin-right:0; ...` 应用 `margin: 0` | 可自动合并 |
| `shorthand-property-no-redundant-values` | `margin: 10px 10px 10px 10px` 应简写 | 可自动简化 |
| `block-no-redundant-nested-style-rules` | 嵌套中重复父选择器 | 不可自动修复 |
#### 4.2.4 颜色与字体约定(2 条)
| 规则 | 配置值 | 检测场景 |
|------|--------|----------|
| `color-named` | 'never' | `color: red` 应使用十六进制 |
| `font-family-name-quotes` | 'always-where-required' | 含空格的字体名需要引号 |
#### 4.2.5 精度与格式(2 条)
| 规则 | 配置值 | 检测场景 |
|------|--------|----------|
| `number-max-precision` | 4 | 限制小数位数为 4 位 |
| `comment-whitespace-inside` | 'always' | `/*comment*/` 应写 `/* comment */` |
---
## 五、规则级别设计
### 5.1 级别分配原则
| 级别 | 适用场景 | 数量 |
|------|----------|------|
| `true`(error) | 错误检测、确定性约定 | 68 条 |
| `null`(禁用) | 不适用的规则 | 73 条 |
### 5.2 与 ESLint 适配器级别设计的差异
ESLint 适配器使用 `'error'``'warn'` 两个级别,而 Stylelint 适配器统一使用 `true`(等价于 error)。原因:
- Stylelint 的 `true` 等价于 ESLint 的 `'error'`,是唯一的标准级别
- Stylelint 不支持 `'warn'` 级别(与 ESLint 不同),仅支持 `true`(启用为 error)或 `null`(禁用)
- 如需降级为 warning,需通过 Stylelint 的 `defaultSeverity` 配置项全局设置,而非逐条配置
### 5.3 自动修复能力统计
| 分类 | 总数 | 可自动修复 | 自动修复率 |
|------|------|-----------|-----------|
| P0 官方 recommended | 41 | 3 | 7% |
| P1 强烈推荐 | 7 | 1 | 14% |
| P2 建议启用 | 20 | 15 | 75% |
| **合计** | **68** | **19** | **28%** |
> P2 规则中 75% 可自动修复,用户通过 `--fix` 可自动修正大部分约定问题。
---
## 六、兼容性分析
### 6.1 对现有用户代码的影响
启用新规则后,之前能通过审查的 CSS 代码可能会新增诊断:
| 影响程度 | 规则 | 说明 |
|----------|------|------|
| **可能大量新增诊断** | `at-rule-no-unknown` | recommended 新增,未知 at 规则检测 |
| **可能大量新增诊断** | `selector-type-no-unknown` | recommended 新增,未知类型选择器检测 |
| **中等新增诊断** | `function-no-unknown` | P1 新增,需配置忽略自定义函数 |
| **中等新增诊断** | `property-no-vendor-prefix` 等 5 条 | P2 前缀规则,旧项目可能有大量厂商前缀 |
| **少量新增诊断** | `color-function-notation` | P2 新增,旧代码可能使用传统颜色函数 |
| **少量新增诊断** | `declaration-block-no-redundant-longhand-properties` | P2 新增,冗余长写属性 |
| **极少新增诊断** | `no-unknown-animations` | P1 新增,正常项目动画引用正确 |
### 6.2 对构建的影响
- `stylelint-config-recommended` 是纯 JSON 配置包(约 2KB),无运行时依赖
- 已确认不在 esbuild 的 `external` 列表中,将被正确打包到扩展中
- 构建产物体积增量可忽略(仅增加一个 JSON 配置对象的引用)
- 不影响 `stylelint` 引擎本身的加载和运行
### 6.3 对去重功能的影响
`static-rules.json` 同步更新后,去重检测范围从 12 条扩展到 68 条。用户导入的自定义规则如果与新增规则 ID 匹配,将正确识别为重复。
### 6.4 版本兼容性
| 依赖 | 当前版本 | 新增依赖 | 兼容性说明 |
|------|----------|----------|-----------|
| `stylelint` | `^17.14.0` | — | 项目主依赖,不变 |
| `stylelint-config-recommended` | 未安装 | `^18.0.0` | v18.0.0 声明 `stylelint: ">=16.0.0"`,兼容 17.x |
> **注意**`stylelint-config-recommended` v18.0.0 的规则集与 Stylelint 16.x 的规则集一致。项目使用 Stylelint 17.x,规则集无变化(17.x 未新增/移除规则,仅修复 bug)。
---
## 七、与 ESLint 适配器的架构对比
| 维度 | ESLint 适配器 | Stylelint 适配器 |
|------|--------------|-----------------|
| 官方配置包 | `@eslint/js``configs.recommended` | `stylelint-config-recommended` |
| 额外规则来源 | `eslint` 内置规则 | `stylelint` 内置规则 |
| 规则合并方式 | Flat Config 数组追加 `{ rules: extraRules }` | 对象展开 `...recommendedConfig.rules` + `...extraRules` |
| 级别支持 | `'error'` / `'warn'` | `true`(仅 error |
| TypeScript 规则 | `typescript-eslint` recommended | 无对应 |
| 自动修复 | 部分规则支持 | 部分规则支持 |
| 配置优先级 | 全局 > 项目 > 内置 | 全局 > 项目 > 内置 |
| 规则 ID 格式 | `eslint:{ruleId}` | `stylelint:{ruleName}` |
两者在架构上保持一致:**官方配置包作为基线 + 额外规则对象补充**,仅在合并机制上因框架差异略有不同(Flat Config 数组 vs 配置对象展开)。
---
## 八、实施步骤
### 步骤 1:修改 `package.json`
`dependencies` 中添加 `"stylelint-config-recommended": "^18.0.0"`
### 步骤 2:修改 `src/adapters/stylelint.ts`
1. 在文件顶部添加 `import recommendedConfig from 'stylelint-config-recommended'`
2. 删除原有的 `DEFAULT_CONFIG` 硬编码对象
3. 添加 `extraRules` 常量(27 条 P1+P2 规则)
4. 用展开运算符构建新的 `DEFAULT_CONFIG`recommended 41 + extra 27
### 步骤 3:修改 `src/rules/static-rules.json`
1.`rules.stylelint` 数组中追加新增规则条目(去除已存在的)
2. 更新 `linterVersion.stylelint``"16.x (68 rules)"`
### 步骤 4:验证
1. 执行 `npm run lint` 确认项目自身代码无新增报错
2. 执行 `npm run compile` 确认编译通过
3. 执行 `npm run build` 确认打包成功
4. 检查 `static-rules.json` 中 stylelint 规则数量为 68 条
5. 验证 `stylelint-config-recommended` 已正确打包到产物中
---
## 九、测试要点
### 9.1 单元测试
| 测试项 | 验证内容 |
|--------|----------|
| `DEFAULT_CONFIG.rules` 键数 | 恰好 68 条(41 recommended + 27 extra |
| `extraRules` 规则数 | 恰好 27 条 |
| 规则名无重复 | 与 recommended 41 条无交集 |
| `recommendedConfig` 导入 | 正确解析为包含 `rules` 对象的配置 |
### 9.2 集成测试
| 测试项 | 验证内容 |
|--------|----------|
| `color: #fff00` 触发 `color-no-invalid-hex` | 诊断结果包含 `stylelint:color-no-invalid-hex` |
| `margin: 0px` 触发 `length-zero-no-unit` | 诊断结果包含 `stylelint:length-zero-no-unit` |
| `-webkit-transform: scale(1)` 触发 `property-no-vendor-prefix` | 诊断结果包含 `stylelint:property-no-vendor-prefix` |
| `@import url("style.css")` 触发 `import-notation` | 诊断结果包含 `stylelint:import-notation` |
| `margin: 10px 10px 10px 10px` 触发 `shorthand-property-no-redundant-values` | 诊断结果包含对应规则 |
| 全局配置存在时忽略内置配置 | 使用用户配置,不触发新规则 |
| 项目配置存在时忽略内置配置 | 使用项目配置,不触发新规则 |
### 9.3 去重测试
| 测试项 | 验证内容 |
|--------|----------|
| 导入 `function-no-unknown` 自定义规则 | 提示与已有规则重复 |
| 导入 `property-no-vendor-prefix` 自定义规则 | 提示与已有规则重复 |
| 导入 recommended 中已有的规则 | 仍提示重复(未改变原有行为) |
### 9.4 自动修复测试
| 测试项 | 验证内容 |
|--------|----------|
| `margin: 0px``margin: 0` | `length-zero-no-unit` 自动修复 |
| `color: #ffffff``color: #fff` | `color-hex-length` 自动修复 |
| `-webkit-transform: scale(1)``transform: scale(1)` | `property-no-vendor-prefix` 自动修复 |
| `@import url("x.css")``@import "x.css"` | `import-notation` 自动修复 |
---
## 十、风险评估
### 10.1 主要风险
| 风险 | 级别 | 缓解措施 |
|------|------|----------|
| 用户 CSS 新增大量诊断 | 中 | P2 中 15/20 条可自动修复,用户可通过 `--fix` 批量修正 |
| `function-no-unknown` 误报自定义函数 | 中 | 可通过项目配置添加 `ignoreFunctions` 列表排除 |
| `selector-type-no-unknown` 对自定义元素误报 | 低 | 可通过 `ignoreTypes` 配置排除自定义元素 |
| `color-named: 'never'` 对设计系统颜色名误报 | 低 | 可通过项目配置覆盖该规则 |
| `stylelint-config-recommended` 版本不匹配 | 低 | 锁定 `^18.0.0`,兼容 Stylelint 17.x |
### 10.2 回滚方案
如果新规则导致严重问题,回滚步骤:
1.`DEFAULT_CONFIG` 中移除 `extraRules`,恢复 `recommendedConfig` 为唯一配置源
2. 如需完全回退,恢复原有 12 条硬编码 `DEFAULT_CONFIG`
3.`static-rules.json` 中移除追加的规则条目
4. 恢复 `linterVersion.stylelint` 为原值
`package.json``stylelint-config-recommended` 的声明可以保留(引入依赖本身不影响功能)。
---
## 十一、附录
### 11.1 规则来源参考
- Stylelint 官方规则文档:https://stylelint.io/user-guide/rules/
- 推荐规则分析文件:`/workspace/stylelint-recommended-rules.md`
- 完整规则清单:`/workspace/stylelint-16-rules.md`
- stylelint-config-recommended npm 包:https://www.npmjs.com/package/stylelint-config-recommended
### 11.2 规则数量统计
| 分类 | 数量 | 级别 | 来源 |
|------|------|------|------|
| P0 官方 recommended | 41 | trueerror | `stylelint-config-recommended` |
| P1 强烈推荐 | 7 | trueerror | Possible errors 非 recommended |
| P2 建议启用 | 20 | trueerror | Limit language features 选型 |
| **合计** | **68** | **true** | 占活跃规则的 48% |
| 未启用(需配置/格式化/命名模式/空行/大小写) | 73 | — | 不适合通用默认配置 |
### 11.3 当前 12 条规则映射关系
| 当前规则 | 新配置归属 | 说明 |
|----------|-----------|------|
| `color-hex-length: 'short'` | P2 | 保留在 `extraRules` 中 |
| `color-named: 'never'` | P2 | 保留在 `extraRules` 中 |
| `color-no-invalid-hex: true` | P1 | 保留在 `extraRules` 中 |
| `length-zero-no-unit: true` | P2 | 保留在 `extraRules` 中 |
| `font-family-no-missing-generic-family-keyword: true` | P0 | 通过 `recommendedConfig` 自动启用 |
| `block-no-empty: true` | P0 | 通过 `recommendedConfig` 自动启用 |
| `declaration-block-no-duplicate-properties: true` | P0 | 通过 `recommendedConfig` 自动启用 |
| `no-descending-specificity: true` | P0 | 通过 `recommendedConfig` 自动启用 |
| `unit-no-unknown: true` | P1 | 保留在 `extraRules` 中 |
| `property-no-unknown: true` | P0 | 通过 `recommendedConfig` 自动启用 |
| `selector-pseudo-class-no-unknown: true` | P0 | 通过 `recommendedConfig` 自动启用 |
| `selector-pseudo-element-no-unknown: true` | P0 | 通过 `recommendedConfig` 自动启用 |
> 12 条原规则全部被新配置覆盖:5 条进入 P0(由 recommended 自动启用),2 条进入 P1,5 条进入 P2。无规则丢失。
@@ -0,0 +1,618 @@
# typescript-eslint 规则增强设计书
## 一、背景与目标
### 1.1 现状
当前 `ESLintAdapter``src/adapters/eslint.ts`)内置默认配置为:
```typescript
public static getDefaultConfig(): any[] {
if (!ESLintAdapter.defaultConfig) {
ESLintAdapter.defaultConfig = [
js.configs.recommended, // @eslint/js recommended → 61 条
...ts.configs.recommended, // typescript-eslint recommended → 24 条
];
}
return ESLintAdapter.defaultConfig;
}
```
typescript-eslint 通过 `ts.configs.recommended` 启用了 24 条官方推荐规则(不含类型检查)。typescript-eslint 8.x 共有 134 条规则,其中 73 条不需要类型信息,当前覆盖率仅 33%。
### 1.2 目标
将 typescript-eslint 内置配置从 24 条扩展到 35 条(P0 官方推荐 24 + P1 强烈推荐 5 + P2 建议启用 6),并处理 1 条扩展规则替换(`no-shadow``@typescript-eslint/no-shadow`),使 TypeScript 代码的静态分析能捕获更多 TS 特有问题。
### 1.3 设计原则
- **不破坏现有配置优先级**:全局配置 > 项目配置 > 内置配置,三层择一逻辑不变
- **不引入新依赖**`typescript-eslint` 已在 `dependencies` 中,P1/P2 规则全部来自该包内置
- **不修改规则 ID 前缀格式**:诊断结果仍使用 `eslint:{ruleId}` 格式(TS 规则的 ruleId 含 `@typescript-eslint/` 前缀)
- **仅作用于 TS 文件**:新增的 TS 专用规则通过 `files` 字段限定作用范围,不影响纯 JS 文件
- **处理扩展规则冲突**:当 ESLint 核心规则与 TS 扩展规则重叠时,TS 文件中禁用核心版、启用扩展版
### 1.4 关键约束
当前适配器使用 `ts.configs.recommended`(无类型检查模式),未配置 `parserOptions.project`。因此 61 条需要类型信息的规则不可用,本文推荐仅覆盖不需要类型信息的 73 条规则。
> 启用类型检查模式(`recommendedTypeChecked`)需要为每个项目配置 `tsconfig.json` 路径,这在通用代码审查工具中不现实——不同项目的 `tsconfig.json` 位置和内容各异。因此,当前设计坚持无类型检查模式。
---
## 二、影响范围分析
### 2.1 需要修改的文件
| 文件 | 修改类型 | 修改内容 |
|------|----------|----------|
| `src/adapters/eslint.ts` | 代码修改 | 新增 `extraTsRules` 常量,在 `getDefaultConfig()` 中追加 TS 专用规则配置块 |
| `src/rules/static-rules.json` | 数据修改 | 追加 12 条 typescript-eslint 规则条目(11 条新规则 + 1 条扩展规则) |
### 2.2 不需要修改的文件
| 文件 | 原因 |
|------|------|
| `package.json` | `typescript-eslint` 已在 `dependencies` 中(`^8.56.1`),无需新增依赖 |
| `scripts/build.mjs` | `typescript-eslint` 不在 external 列表中,已被 esbuild 正确打包 |
| `src/adapters/jsp.ts` | 内部复用 `ESLintAdapter` 实例,自动继承新配置 |
| `src/orchestrator/orchestrator.ts` | 仅做调度,不涉及配置逻辑 |
| `src/config/linter.ts` | 配置读取层不变 |
### 2.3 不受影响的功能
- 全局配置(`linters.eslintConfigPath`):用户指定配置文件时完全替代内置配置,不受影响
- 项目配置(`.eslintrc.*` / `eslint.config.*`):存在时完全替代内置配置,不受影响
- JSP 适配器:复用 `ESLintAdapter`,自动获得增强后的配置
- 纯 JS 文件检查:新增的 TS 规则通过 `files` 字段限定,不影响 JS 文件
- 规则 ID 输出格式:仍为 `eslint:@typescript-eslint/{ruleName}`,不变
---
## 三、详细设计
### 3.1 修改 `src/adapters/eslint.ts`
#### 3.1.1 当前代码
```typescript
import js from '@eslint/js';
import ts from 'typescript-eslint';
// ESLint extraRules(已由 ESLint 增强设计书定义,31 条)
const extraRules: Record<string, 'error' | 'warn'> = {
// ... ESLint P1+P2 规则 ...
};
public static getDefaultConfig(): any[] {
if (!ESLintAdapter.defaultConfig) {
ESLintAdapter.defaultConfig = [
js.configs.recommended,
...ts.configs.recommended,
{ rules: extraRules },
];
}
return ESLintAdapter.defaultConfig;
}
```
#### 3.1.2 修改后代码
```typescript
import js from '@eslint/js';
import ts from 'typescript-eslint';
// ESLint extraRules(已由 ESLint 增强设计书定义,31 条)
const extraRules: Record<string, 'error' | 'warn'> = {
// ... ESLint P1+P2 规则 ...
};
// 新增:typescript-eslint P1+P2 额外规则配置常量
const extraTsRules: Record<string, 'error' | 'warn' | 'off'> = {
// === P1:强烈推荐(error===
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/no-dynamic-delete': 'error',
'@typescript-eslint/no-useless-empty-export': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/unified-signatures': 'error',
// === P2:建议启用 ===
'@typescript-eslint/no-extraneous-class': 'warn',
'@typescript-eslint/no-useless-constructor': 'warn',
'@typescript-eslint/no-non-null-asserted-nullish-coalescing': 'error',
'@typescript-eslint/no-invalid-void-type': 'warn',
'@typescript-eslint/prefer-literal-enum-member': 'warn',
'@typescript-eslint/prefer-enum-initializers': 'warn',
// === 扩展规则替换 ===
// no-shadow:TS 扩展版能正确处理类型参数遮蔽,核心版不能
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'error',
// no-array-constructorTS 扩展版已在 recommended 中,需禁用核心版避免重复诊断
'no-array-constructor': 'off',
};
// TypeScript 文件匹配模式(与 ts.configs.recommended 内部一致)
const TS_FILES = ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'];
public static getDefaultConfig(): any[] {
if (!ESLintAdapter.defaultConfig) {
ESLintAdapter.defaultConfig = [
js.configs.recommended,
...ts.configs.recommended,
// ESLint P1+P2 规则(适用于所有文件)
{ rules: extraRules },
// typescript-eslint P1+P2 规则(仅适用于 TS 文件)
{
files: TS_FILES,
rules: extraTsRules,
},
];
}
return ESLintAdapter.defaultConfig;
}
```
#### 3.1.3 设计说明
**为什么需要 `files` 字段限定作用范围?**
typescript-eslint 规则(如 `@typescript-eslint/no-non-null-assertion`)依赖 TS 解析器提供的语法树信息。纯 JS 文件使用 ESParse 解析器而非 `@typescript-eslint/parser`,无法理解 `!` 非空断言、`enum``interface` 等 TS 语法。如果不限定 `files`,这些规则在 JS 文件上会报解析错误或产生无意义的诊断。
`ts.configs.recommended` 内部已通过 `files` 字段将所有规则限定为 TS 文件。我们的 `extraTsRules` 需要同样处理。
**为什么 `extraTsRules` 放在 `extraRules` 之后?**
ESLint Flat Config 的规则是后者覆盖前者。`extraTsRules` 放在最后,确保 TS 文件中的规则覆盖顺序为:
1. `js.configs.recommended`(所有文件)
2. `ts.configs.recommended`TS 文件,覆盖 #1 中的扩展规则)
3. `{ rules: extraRules }`(所有文件,可能覆盖 #2 中的扩展规则关闭)
4. `{ files: TS_FILES, rules: extraTsRules }`TS 文件,覆盖 #3 中的冲突规则)
这个顺序确保 TS 文件最终获得正确的规则配置。
**为什么需要处理 `no-array-constructor` 冲突?**
ESLint 的 `extraRules` 中有 `'no-array-constructor': 'error'`P2),适用于所有文件。同时,`ts.configs.recommended` 中已启用 `@typescript-eslint/no-array-constructor: 'error'` 并设置 `no-array-constructor: 'off'`(仅 TS 文件)。但由于 `extraRules``ts.configs.recommended` 之后,`no-array-constructor: 'error'` 覆盖了 `ts.configs.recommended` 设置的 `'off'`,导致 TS 文件中两个规则同时生效,产生重复诊断。
`extraTsRules` 中设置 `'no-array-constructor': 'off'` 可以修复此问题:TS 文件中禁用核心版,仅保留 `@typescript-eslint/no-array-constructor`
**为什么 `no-shadow` 需要显式启用 TS 扩展版?**
`@typescript-eslint/no-shadow` 不在 `ts.configs.recommended` 中(它属于 `strict` 配置)。ESLint 的 `extraRules` 中已有 `'no-shadow': 'error'`,但核心版不理解 TS 的类型参数(如 `interface Foo<T> {}` 中的 `T`),会将类型参数误报为遮蔽。TS 扩展版能正确处理这些场景。
因此需要:
1.`extraTsRules` 中设置 `'no-shadow': 'off'`TS 文件中禁用核心版)
2. 设置 `'@typescript-eslint/no-shadow': 'error'`TS 文件中启用扩展版)
> 注:`no-throw-literal` 的扩展版 `@typescript-eslint/only-throw-error` 标记为 💭(需要类型信息),在 `recommendedTypeChecked` 中而非 `recommended` 中。当前无类型检查模式下不可用,因此不处理此规则的替换。
### 3.2 修改 `src/rules/static-rules.json`
#### 3.2.1 修改内容
`rules["typescript-eslint"]` 数组(若不存在则新建)末尾追加以下 12 条规则条目:
```json
{"id": "typescript-eslint/no-non-null-assertion", "description": "Disallow non-null assertions using the ! postfix operator"},
{"id": "typescript-eslint/no-dynamic-delete", "description": "Disallow using the delete operator on computed key expressions"},
{"id": "typescript-eslint/no-useless-empty-export", "description": "Disallow empty exports that don't change anything in a module"},
{"id": "typescript-eslint/consistent-type-imports", "description": "Enforce consistent usage of type imports"},
{"id": "typescript-eslint/unified-signatures", "description": "Disallow two overloads that could be unified into a single signature"},
{"id": "typescript-eslint/no-extraneous-class", "description": "Disallow classes only being used as namespaces"},
{"id": "typescript-eslint/no-useless-constructor", "description": "Disallow unnecessary constructors"},
{"id": "typescript-eslint/no-non-null-asserted-nullish-coalescing", "description": "Disallow non-null assertions in the left operand of a nullish coalescing operator"},
{"id": "typescript-eslint/no-invalid-void-type", "description": "Disallow void type outside of generic or return types"},
{"id": "typescript-eslint/prefer-literal-enum-member", "description": "Require all enum members to be literal values"},
{"id": "typescript-eslint/prefer-enum-initializers", "description": "Require each enum member value to be explicitly initialized"},
{"id": "typescript-eslint/no-shadow", "description": "Disallow variable declarations from shadowing variables declared in the outer scope"}
```
> 注:前 11 条为新增规则,第 12 条 `@typescript-eslint/no-shadow` 是扩展规则替换(禁用 ESLint 核心版 `no-shadow` 后启用)。24 条 P0 规则已通过 `ts.configs.recommended` 启用,如果 static-rules.json 中已有对应条目则无需重复添加。
#### 3.2.2 修改 `linterVersion` 字段
```json
"linterVersion": {
"eslint": "9.x (92 rules)",
"typescript-eslint": "8.x (35 rules)",
...
}
```
新增 `"typescript-eslint": "8.x (35 rules)"` 条目,反映实际启用规则数量。若该字段已存在则更新,不存在则添加。
#### 3.2.3 为什么必须同步更新 static-rules.json
`static-rules.json` 有两个用途:
1. **UI 展示**:在设置面板中展示当前 linter 支持的规则清单
2. **去重检测**:自定义规则导入时,按 ID 匹配 `static-rules.json` 中的规则,若已存在则提示重复
如果不更新,用户新增的 `@typescript-eslint/no-non-null-assertion` 自定义规则不会被识别为重复,导致重复规则。
### 3.3 不需要修改 `package.json`
`typescript-eslint` 已在 `dependencies` 中声明:
```json
"dependencies": {
"typescript-eslint": "^8.56.1",
...
}
```
P1/P2 规则全部来自 `typescript-eslint` 包内置,无需额外安装插件。
---
## 四、新增规则分类详解
### 4.1 P1 强烈推荐(5 条,error 级别)
这些规则不在官方 `recommended` 中,不需类型信息,能检测 TypeScript 特有的高频问题。
#### 4.1.1 非空断言(1 条)
| 规则 | 检测场景 | 误报评估 |
|------|----------|----------|
| `@typescript-eslint/no-non-null-assertion` | `x!.foo()``!` 绕过类型检查 | 低,TS 团队和社区普遍建议避免非空断言,应使用类型收窄或可选链替代 |
#### 4.1.2 不安全操作(1 条)
| 规则 | 检测场景 | 误报评估 |
|------|----------|----------|
| `@typescript-eslint/no-dynamic-delete` | `delete obj[computedKey]` 可能意外删除属性 | 零误报,动态 delete 几乎总是设计缺陷 |
#### 4.1.3 冗余代码(1 条)
| 规则 | 检测场景 | 误报评估 |
|------|----------|----------|
| `@typescript-eslint/no-useless-empty-export` | `export {}` 在已有其他导出的模块中是冗余的 | 零误报 |
#### 4.1.4 类型导入(1 条)
| 规则 | 检测场景 | 误报评估 |
|------|----------|----------|
| `@typescript-eslint/consistent-type-imports` | 类型导入应使用 `import type`,编译时被移除 | 低,可配置 `prefer: 'type-imports'` 允许内联 `import { type Foo }` |
#### 4.1.5 重载签名(1 条)
| 规则 | 检测场景 | 误报评估 |
|------|----------|----------|
| `@typescript-eslint/unified-signatures` | `f(x: string)``f(x: number)` 可合并为 `f(x: string \| number)` | 低,仅检测可安全合并的重载 |
### 4.2 P2 建议启用(6 条)
这些规则提升 TypeScript 代码一致性和安全性,不需类型信息。
#### 4.2.1 类与构造函数(2 条)
| 规则 | 级别 | 检测场景 |
|------|------|----------|
| `@typescript-eslint/no-extraneous-class` | warn | 仅含静态成员的类应改用对象或函数 |
| `@typescript-eslint/no-useless-constructor` | warn | `constructor() {}` 或仅调 `super()` 的构造函数是冗余的 |
#### 4.2.2 类型安全约定(2 条)
| 规则 | 级别 | 检测场景 |
|------|------|----------|
| `@typescript-eslint/no-non-null-asserted-nullish-coalescing` | error | `x! ?? y``!``??` 逻辑矛盾 |
| `@typescript-eslint/no-invalid-void-type` | warn | `void` 在泛型参数或变量类型中使用是错误的 |
#### 4.2.3 枚举约定(2 条)
| 规则 | 级别 | 检测场景 |
|------|------|----------|
| `@typescript-eslint/prefer-literal-enum-member` | warn | `enum E { A = 1 << 0 }` 是计算值,应为字面量 |
| `@typescript-eslint/prefer-enum-initializers` | warn | `enum E { A, B }` 的隐式编号在增删成员时会变化 |
### 4.3 扩展规则替换(1 条)
#### 4.3.1 遮蔽检测
| 规则 | 级别 | 替换关系 | 替换理由 |
|------|------|----------|----------|
| `@typescript-eslint/no-shadow` | error | 替换 ESLint 核心 `no-shadow` | TS 扩展版能正确处理类型参数(`interface Foo<T> {}` 中的 `T`),核心版会将类型参数误报为遮蔽 |
#### 4.3.2 冲突修复
| 规则 | 级别 | 处理方式 | 处理理由 |
|------|------|----------|----------|
| `no-array-constructor` | off | TS 文件中禁用核心版 | TS 扩展版 `@typescript-eslint/no-array-constructor` 已在 `recommended` 中启用,核心版会导致重复诊断 |
#### 4.3.3 不处理的扩展规则
| 扩展规则 | 基础规则 | 不处理原因 |
|----------|----------|-----------|
| `@typescript-eslint/only-throw-error` | `no-throw-literal` | 标记为 💭(需要类型信息),在 `recommendedTypeChecked` 中,当前模式不可用 |
| `@typescript-eslint/no-unused-vars` | `no-unused-vars` | 已在 `recommended` 中处理,`ts.configs.recommended` 已设置核心版为 `off` |
| `@typescript-eslint/no-use-before-define` | `no-use-before-define` | 不在推荐范围(属于 `stylistic`),不新增 |
| `@typescript-eslint/no-dupe-class-members` | `no-dupe-class-members` | 已在 ESLint `recommended` 中,TS 版仅在 `stylistic` 中,无需替换 |
---
## 五、规则级别设计
### 5.1 级别分配原则
| 级别 | 适用场景 | 数量 |
|------|----------|------|
| `error` | TS 特有问题、类型安全、确定性 bug | 8 条(5 P1 + 3 P2 |
| `warn` | 编码约定、可按需调整 | 3 条(P2) |
| `off` | 禁用被替换的 ESLint 核心规则 | 2 条(`no-shadow``no-array-constructor` |
### 5.2 为什么部分规则设为 warn 而非 error
| 规则 | warn 理由 |
|------|-----------|
| `@typescript-eslint/no-extraneous-class` | 某些场景确实需要仅含静态成员的类(如 Angular 依赖注入) |
| `@typescript-eslint/no-useless-constructor` | 某些框架(如 NestJS)依赖构造函数进行依赖注入,即使看起来是空的 |
| `@typescript-eslint/no-invalid-void-type` | 某些高级类型模式可能需要在非标准位置使用 void |
| `@typescript-eslint/prefer-literal-enum-member` | 位标志枚举(`Flags.A \| Flags.B`)可能需要计算值 |
| `@typescript-eslint/prefer-enum-initializers` | 某些快速原型场景可能不需要显式初始化 |
### 5.3 `consistent-type-imports` 配置选项
`@typescript-eslint/consistent-type-imports` 支持配置选项:
```typescript
'@typescript-eslint/consistent-type-imports': ['error', {
prefer: 'type-imports', // 优先使用 import type
disallowTypeAnnotations: false, // 允许内联 import { type Foo }
fixStyle: 'inline-type-imports', // 自动修复时使用内联语法
}]
```
默认配置即可满足大多数场景。如团队偏好分离式 `import type { Foo }`,可将 `fixStyle` 改为 `'separate-type-imports'`
---
## 六、与 ESLint 规则增强的交互分析
### 6.1 配置数组执行顺序
```
┌─────────────────────────────────────────────────────────┐
│ Config Array(按顺序执行,后者覆盖前者) │
├─────────────────────────────────────────────────────────┤
│ 1. js.configs.recommended → 所有文件,61 条 P0 │
│ 2. ...ts.configs.recommended → TS 文件,24 条 P0 │
│ ↳ 覆盖 #1 中的扩展规则(no-unused-vars, no-array- │
│ constructor 等:核心版 offTS 扩展版 error
│ 3. { rules: extraRules } → 所有文件,31 条 P1+P2 │
│ ↳ 覆盖 #1 和 #2 中的规则(含 no-shadow, │
│ no-array-constructor 等) │
│ 4. { files: TS_FILES, → TS 文件,11 条 P1+P2 │
│ rules: extraTsRules } + 2 条 off + 1 条扩展 │
│ ↳ 覆盖 #3 中的冲突规则(no-shadow: off, │
│ no-array-constructor: off
└─────────────────────────────────────────────────────────┘
```
### 6.2 冲突规则最终状态
| 规则 | JS 文件最终状态 | TS 文件最终状态 | 说明 |
|------|-----------------|-----------------|------|
| `no-shadow` | `error`(来自 #3 extraRules | `off`(来自 #4 extraTsRules | JS 用核心版,TS 用扩展版 |
| `@typescript-eslint/no-shadow` | 未启用(不在 JS 文件作用域) | `error`(来自 #4 extraTsRules | 仅 TS 文件启用 |
| `no-array-constructor` | `error`(来自 #3 extraRules | `off`(来自 #4 extraTsRules | JS 用核心版,TS 用扩展版 |
| `@typescript-eslint/no-array-constructor` | 未启用 | `error`(来自 #2 ts.configs.recommended | TS 扩展版已在 P0 中 |
| `no-unused-vars` | `error`(来自 #1 recommended | `off`(来自 #2 ts.configs.recommended | JS 用核心版,TS 用扩展版 |
| `@typescript-eslint/no-unused-vars` | 未启用 | `error`(来自 #2 ts.configs.recommended | TS 扩展版已在 P0 中 |
### 6.3 规则总数统计
| 来源 | 适用范围 | 规则数 |
|------|----------|--------|
| `js.configs.recommended` | 所有文件 | 61 |
| `ts.configs.recommended` | TS 文件 | 24 |
| ESLint `extraRules` | 所有文件 | 31 |
| TS `extraTsRules` | TS 文件 | 11 新增 + 2 off + 1 扩展 |
| **JS 文件总规则** | — | 9261 + 31 |
| **TS 文件总规则** | — | 12761 + 24 + 31 + 11 |
> TS 文件规则数 = 92ESLint+ 24TS recommended+ 11TS extra= 127。其中 ESLint 的 `no-shadow` 和 `no-array-constructor` 被禁用(-2),TS 扩展版替代(+2),净增不变。
---
## 七、与 ESLint/Stylelint 适配器的架构对比
| 维度 | ESLint 增强 | Stylelint 增强 | typescript-eslint 增强 |
|------|-------------|----------------|----------------------|
| 官方配置包 | `@eslint/js``configs.recommended` | `stylelint-config-recommended` | `typescript-eslint``ts.configs.recommended` |
| 额外规则来源 | `eslint` 内置规则 | `stylelint` 内置规则 | `typescript-eslint` 内置规则 |
| 规则合并方式 | `{ rules: extraRules }` | `...recommendedConfig.rules` + `...extraRules` | `{ files: TS_FILES, rules: extraTsRules }` |
| 文件作用域 | 全部文件 | 全部文件 | 仅 TS 文件 |
| 扩展规则处理 | 无 | 无 | 禁用核心版 + 启用 TS 扩展版 |
| 级别支持 | `'error'` / `'warn'` | `true`(仅 error | `'error'` / `'warn'` / `'off'` |
| 新增依赖 | `@eslint/js` | `stylelint-config-recommended` | 无(已有) |
| 配置优先级 | 全局 > 项目 > 内置 | 全局 > 项目 > 内置 | 全局 > 项目 > 内置 |
| 规则 ID 格式 | `eslint:{ruleId}` | `stylelint:{ruleName}` | `eslint:@typescript-eslint/{ruleName}` |
三者架构一致:**官方配置包作为基线 + 额外规则对象补充**。typescript-eslint 增强额外引入了 `files` 作用域限定和扩展规则冲突处理,这是因为 TS 规则依赖 TS 解析器且与 ESLint 核心规则存在重叠。
---
## 八、实施步骤
### 步骤 1:修改 `src/adapters/eslint.ts`
1. 在文件顶部(`extraRules` 常量之后)添加 `extraTsRules` 常量(13 条规则条目)
2. 添加 `TS_FILES` 常量
3.`getDefaultConfig()` 方法的返回数组末尾追加 `{ files: TS_FILES, rules: extraTsRules }`
> **前置条件**:ESLint 规则增强设计书中的 `extraRules` 修改必须先完成,否则 `no-shadow` 和 `no-array-constructor` 的冲突处理会缺少被覆盖的规则。
### 步骤 2:修改 `src/rules/static-rules.json`
1.`rules["typescript-eslint"]` 数组末尾追加 12 条规则条目
2. 更新或新增 `linterVersion["typescript-eslint"]``"8.x (35 rules)"`
### 步骤 3:验证
1. 执行 `npm run lint` 确认项目自身代码无新增报错
2. 执行 `npm run compile` 确认编译通过
3. 执行 `npm run build` 确认打包成功
4. 检查 `static-rules.json` 中 typescript-eslint 规则数量正确
5. 验证 TS 文件中 `no-shadow` 诊断来自 `@typescript-eslint/no-shadow` 而非核心版
6. 验证 TS 文件中 `no-array-constructor` 不产生重复诊断
---
## 九、测试要点
### 9.1 单元测试
| 测试项 | 验证内容 |
|--------|----------|
| `getDefaultConfig()` 返回值 | 数组长度为 4recommended + ts recommended + extraRules + extraTsRules |
| `extraTsRules` 规则数 | 恰好 13 条(11 新增 + 2 off |
| `extraTsRules` 级别 | 8 条 error + 3 条 warn + 2 条 off |
| `TS_FILES` 模式 | 包含 `*.ts``*.tsx``*.mts``*.cts` |
| 规则名无重复 | 11 条新规则与 recommended 24 条无交集 |
### 9.2 集成测试
| 测试项 | 验证内容 |
|--------|----------|
| `x!.foo()` 触发 `no-non-null-assertion` | 诊断结果包含 `eslint:@typescript-eslint/no-non-null-assertion` |
| `export {}` 在已有导出的文件中触发 `no-useless-empty-export` | 诊断结果包含对应规则 |
| `import type { Foo }` 未触发 `consistent-type-imports` | 类型导入符合规则要求 |
| `x! ?? y` 触发 `no-non-null-asserted-nullish-coalescing` | 诊断结果包含对应规则 |
| `enum E { A = 1 << 0 }` 触发 `prefer-literal-enum-member` | 诊断结果包含对应规则 |
| TS 文件中 `let x = 1; { let x = 2; }` 触发 `@typescript-eslint/no-shadow` | 诊断来源为 TS 扩展版,非核心版 |
| TS 文件中 `new Array(3)` 不产生重复诊断 | 仅 `@typescript-eslint/no-array-constructor` 触发,核心版不触发 |
| JS 文件中 `let x = 1; { let x = 2; }` 触发 `no-shadow` | 诊断来源为核心版,TS 扩展版不触发 |
| 全局配置存在时忽略内置配置 | 使用用户配置,不触发新规则 |
| 项目配置存在时忽略内置配置 | 使用项目配置,不触发新规则 |
### 9.3 去重测试
| 测试项 | 验证内容 |
|--------|----------|
| 导入 `@typescript-eslint/no-non-null-assertion` 自定义规则 | 提示与已有规则重复 |
| 导入 `@typescript-eslint/consistent-type-imports` 自定义规则 | 提示与已有规则重复 |
| 导入 `@typescript-eslint/no-shadow` 自定义规则 | 提示与已有规则重复 |
| 导入 recommended 中已有的规则 | 仍提示重复(未改变原有行为) |
### 9.4 文件作用域测试
| 测试项 | 验证内容 |
|--------|----------|
| `.ts` 文件中 `x!.foo()` 触发 `no-non-null-assertion` | TS 规则在 TS 文件中生效 |
| `.js` 文件中 `x!.foo()` 不触发 `no-non-null-assertion` | TS 规则不在 JS 文件中生效 |
| `.tsx` 文件中 TS 规则生效 | TSX 文件被正确识别为 TS 文件 |
| `.mts` 文件中 TS 规则生效 | MTS 文件被正确识别为 TS 文件 |
---
## 十、风险评估
### 10.1 主要风险
| 风险 | 级别 | 缓解措施 |
|------|------|----------|
| `no-non-null-assertion` 对 TS 项目影响较大 | 中 | TS 社区分两派,部分项目大量使用 `!`。设为 error 可通过项目配置覆盖为 warn 或 off |
| `consistent-type-imports` 需要代码迁移 | 中 | 支持 `--fix` 自动修复,可配置 `fixStyle: 'inline-type-imports'` 减少改动量 |
| `no-shadow` 扩展版误报类型参数 | 低 | TS 扩展版专门处理类型参数,误报率比核心版更低 |
| `no-array-constructor` 核心版残留诊断 | 低 | 通过 `extraTsRules``'off'` 覆盖,已消除此风险 |
| TS 文件匹配模式遗漏新扩展名 | 低 | `TS_FILES` 包含 `*.ts``*.tsx``*.mts``*.cts`,覆盖所有主流扩展名 |
| 与 ESLint 增强设计的前置依赖 | 低 | 实施时确保 ESLint `extraRules` 先就位,否则 `no-shadow: 'off'` 无规则可覆盖 |
### 10.2 回滚方案
如果新规则导致严重问题,回滚步骤:
1.`getDefaultConfig()` 中移除 `{ files: TS_FILES, rules: extraTsRules }` 配置块
2.`static-rules.json` 中移除追加的 12 条规则条目
3. 恢复 `linterVersion["typescript-eslint"]` 为原值(或删除该字段)
`extraTsRules` 常量可保留在代码中(不影响功能),下次启用时无需重新编写。
回滚后,`no-shadow``no-array-constructor` 的冲突处理也会一并回滚:
- TS 文件中 `no-shadow` 恢复为 `error`(来自 ESLint `extraRules`
- TS 文件中 `no-array-constructor` 恢复为 `error`(来自 ESLint `extraRules`),与 `@typescript-eslint/no-array-constructor` 重复
> 如需在回滚 TS 增强的同时保留冲突修复,可将 `'no-shadow': 'off'` 和 `'no-array-constructor': 'off'` 单独保留在 `extraRules` 中(但会同时影响 JS 文件,不推荐)。
---
## 十一、附录
### 11.1 规则来源参考
- typescript-eslint 官方规则文档:https://typescript-eslint.io/rules/
- 推荐规则分析文件:`/workspace/typescript-eslint-recommended-rules.md`
- 完整规则清单:`/workspace/typescript-eslint-8-rules.md`
- ESLint 规则增强设计书:`/workspace/eslint-rule-enhancement-design.md`
- Stylelint 规则增强设计书:`/workspace/stylelint-rule-enhancement-design.md`
### 11.2 规则数量统计
| 分类 | 数量 | 级别 | 来源 |
|------|------|------|------|
| P0 官方 recommended | 24 | error(由 recommended 定义) | `ts.configs.recommended` |
| P1 强烈推荐 | 5 | error | 不需类型信息的非 recommended 规则 |
| P2 建议启用 | 6 | 3 error + 3 warn | 编码约定,不需类型信息 |
| 扩展规则替换 | 1 | error | `@typescript-eslint/no-shadow` 替换 `no-shadow` |
| 冲突修复 | 2 | off | 禁用 `no-shadow``no-array-constructor` 核心版 |
| **合计(TS 专用规则)** | **35** | — | 占不需要类型信息规则的 48% |
| 未启用(需类型信息/弃用/风格/过度限制) | 99 | — | 不适合当前无类型检查模式 |
### 11.3 扩展规则映射关系
| ESLint 核心规则 | TS 扩展规则 | 处理方式 | 说明 |
|----------------|-----------|----------|------|
| `no-shadow` | `@typescript-eslint/no-shadow` | TS 文件:核心 off,扩展 error | 扩展版正确处理类型参数遮蔽 |
| `no-array-constructor` | `@typescript-eslint/no-array-constructor` | TS 文件:核心 off(扩展已在 recommended | 避免重复诊断 |
| `no-unused-vars` | `@typescript-eslint/no-unused-vars` | 已由 recommended 处理 | 无需额外操作 |
| `no-throw-literal` | `@typescript-eslint/only-throw-error` | 不处理 | 扩展版需类型信息,当前不可用 |
| `no-use-before-define` | `@typescript-eslint/no-use-before-define` | 不处理 | 扩展版在 stylistic 中,不推荐 |
### 11.4 完整配置速查
```javascript
import js from '@eslint/js';
import ts from 'typescript-eslint';
// ESLint P1+P231 条,适用于所有文件)
const extraRules = {
'eqeqeq': 'error',
'no-shadow': 'error',
'no-array-constructor': 'error',
// ... 其余 28 条 ESLint 规则 ...
};
// typescript-eslint P1+P211 条新规则 + 2 条 off + 1 条扩展,仅 TS 文件)
const extraTsRules = {
// P1
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/no-dynamic-delete': 'error',
'@typescript-eslint/no-useless-empty-export': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/unified-signatures': 'error',
// P2
'@typescript-eslint/no-extraneous-class': 'warn',
'@typescript-eslint/no-useless-constructor': 'warn',
'@typescript-eslint/no-non-null-asserted-nullish-coalescing': 'error',
'@typescript-eslint/no-invalid-void-type': 'warn',
'@typescript-eslint/prefer-literal-enum-member': 'warn',
'@typescript-eslint/prefer-enum-initializers': 'warn',
// 扩展规则替换
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'error',
// 冲突修复
'no-array-constructor': 'off',
};
const TS_FILES = ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'];
export default [
js.configs.recommended, // ESLint P0: 61 条
...ts.configs.recommended, // TS P0: 24 条
{ rules: extraRules }, // ESLint P1+P2: 31 条
{ files: TS_FILES, rules: extraTsRules }, // TS P1+P2: 11 条 + 替换
];
```
@@ -0,0 +1,815 @@
# 自定义规则 · 模板导出与导入功能设计书 v2.0(修正稿)
> 版本:v2.0(整合导出 + 模板导入)
> 日期:2026-07-30
> 修正:基于 Stage ② 需求澄清,修正 API 签名、错误处理、参数传递
---
## 一、方案概览
### 1.1 目标
形成「导出模板 → 填写 → 模板导入」闭环:
- **导出模板**: 侧边栏新增按钮,生成标准 `.xlsx` 模板(2 个 sheet
- **模板导入**: 新增 checkbox「从模板导入」,程序解析 + AI 去重,预览确认
### 1.2 数据流
```
┌──────────────────────────────────────────────────────────────────┐
│ 导出模板 │
│ setupView 按钮 → exportService.ts → XLSX.writeFile → .xlsx 文件 │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ 模板导入(勾选 checkbox) │
│ │
│ setupView checkbox + 填名 + 点「添加」 │
│ → showOpenDialog(仅 xlsx/xls
│ → ImportService.importTemplate(srcPath, name, context) │
│ ├─ TemplateConverter.parseTemplate(srcPath) ← 纯程序,无 AI │
│ │ ├─ 准入校验-1: 文件格式 │
│ │ ├─ 准入校验-2: 表头结构 │
│ │ ├─ 数据行解析 + 校验 → ImportableRule[](带 validationIssues
│ │ └─ 拆分 validRules / errorRules,生成 yamlContent │
│ ├─ AI 去重(仅 validRules ← 新增 dedup-prompt │
│ │ ├─ 成功: parseImportableYaml() → 带 duplicateLevel │
│ │ └─ 失败: 降级,validRules 无去重标记 + warning 提示 │
│ └─ 返回 ConversionResult(含 errorRules + skippedCount
│ → showImportPreview(预览面板) │
│ ├─ 顶部: 跳过 N 行空数据(若 skippedCount > 0
│ ├─ 🚫 错误规则组(置顶,无 checkbox,只读) │
│ ├─ ⛔ 完全重复 / ⚠️ 部分重叠 / ✅ 新规则(仅有效规则,现有逻辑) │
│ └─ 确认 → ImportService.applyConversion → 写 YAML │
│ │
│ 不勾选 checkbox → 走原有 addRule AI 链路,零改动 │
└──────────────────────────────────────────────────────────────────────┘
```
### 1.3 范围
| 类型 | 内容 |
|------|------|
| 含 | 导出模板;模板导入(程序解析 + 准入校验 + AI 去重 + 预览) |
| 不含 | 导出当前规则;YAML 直通链路校验补齐;AI 链路校验补齐 |
| 不触碰 | `ExcelConverter``prompt-builder.ts`、原 `addRule` AI 流程 |
### 1.4 Stage ② 修正项(相对初版设计书)
| 编号 | 修正点 | 内容 |
|------|--------|------|
| ① | `convertContentWithAI` 调用签名 | `convertContentWithAI(yamlContent, context, system)` — content 在前,context 为中,systemPrompt 在后 |
| ② | AI 去重失败降级 | `convertContentWithAI` 返回 `null` 时,有效规则不带 `duplicateLevel` 进预览,弹出 warning |
| ③ | `importTemplate` 参数 | `importTemplate(srcPath, name, context)` — 增加 contextworkspaceRoot 内部获取 |
| ⑤ | prompt 约束加强 | 去重 prompt 明确 "你只负责去重判定,禁止修改任何已有字段,禁止添加/删除规则" |
---
## 二、架构设计
### 2.1 模块划分
```
src/rules/
├── export-service.ts ← 新增: 模板导出(xlsx 写盘)
├── converters/
│ ├── template-converter.ts ← 新增: 程序解析 + 准入校验
│ └── dedup-prompt.ts ← 新增: 专用去重 prompt 构建
├── import-service.ts ← 修改: 新增 importTemplate 方法
├── import-preview.ts ← 修改: 新增错误规则组 + skippedCount 提示
├── import-types.ts ← 修改: 扩展接口字段
├── yaml-parser.ts ← 不改(loadActiveRules 已被 import
└── converters/prompt-builder.ts ← 不碰
src/views/setupView.ts ← 修改: checkbox + 导出按钮 + 分流逻辑
src/activation/commands.ts ← 修改: 注册 exportTemplate 命令
src/i18n/messages.ts ← 修改: 新增 i18n key
package.json ← 修改: contributes.commands 追加 1 条
```
### 2.2 关键接口扩展
```ts
// src/rules/import-types.ts — 扩展
interface ValidationIssue {
field: string;
severity: 'error' | 'warning';
message: string;
}
interface ImportableRule extends CustomRule {
duplicateOf?: string;
duplicateLevel?: 'exact' | 'overlap' | 'none';
duplicateReason?: string;
validationIssues?: ValidationIssue[]; // 新增: 非空则为错误规则
rowNumber?: number; // 新增: 原始行号
}
interface ConversionResult {
rules: ImportableRule[];
yamlContent: string;
sourceFileName: string;
exactCount: number;
overlapCount: number;
skippedCount?: number; // 新增: 被跳过的空行数
errorCount?: number; // 新增: 错误规则数
}
```
---
## 三、详细实现
### 3.1 新增 `src/rules/export-service.ts`
````ts
import * as XLSX from 'xlsx';
import * as vscode from 'vscode';
import { t } from '../i18n/messages';
const RULES_HEADER = ['id', 'severity', 'description', 'message', 'languages', 'excludeLanguages'];
const RULES_EXAMPLE = [
'no-todo', 'warning', '禁止提交 TODO 注释',
'发现 TODO 注释,请清理后提交', 'javascript,typescript', '',
];
const GUIDE_AOA: string[][] = [
['字段', '含义', '取值/格式', '示例'],
['id', '规则唯一标识', '小写字母、数字、连字符,全局唯一', 'no-todo'],
['severity', '严重级别', 'error / warning / info', 'warning'],
['description', '规则简述(给人看)', '自由文本', '禁止提交 TODO 注释'],
['message', '命中时展示给开发者的提示语', '自由文本', '发现 TODO 注释,请清理后提交'],
['languages', '生效的语言', '逗号分隔,留空表示对所有语言生效', 'javascript,typescript'],
['excludeLanguages', '排除的语言', '逗号分隔,可留空', ''],
['', '', '', ''],
['填写说明', '', '', ''],
['1. severity 仅接受 error / warning / info 三个值', '', '', ''],
['2. languages / excludeLanguages 多值用英文逗号分隔', '', '', ''],
['3. 示例行可删除,仅作填写参考', '', '', ''],
['4. 该模板可直接用于「从模板导入」功能回环校验', '', '', ''],
];
export async function exportTemplate(): Promise<void> {
const uri = await vscode.window.showSaveDialog({
defaultUri: vscode.Uri.file('code-review-rules-template.xlsx'),
filters: { 'Excel': ['xlsx'] },
saveLabel: t('exportTemplate.saveLabel'),
});
if (!uri) { return; }
const wb = XLSX.utils.book_new();
const wsRules = XLSX.utils.aoa_to_sheet([RULES_HEADER, RULES_EXAMPLE]);
wsRules['!cols'] = [
{ wch: 16 }, { wch: 10 }, { wch: 32 }, { wch: 40 }, { wch: 24 }, { wch: 20 },
];
XLSX.utils.book_append_sheet(wb, wsRules, '规则');
const wsGuide = XLSX.utils.aoa_to_sheet(GUIDE_AOA);
wsGuide['!cols'] = [{ wch: 22 }, { wch: 28 }, { wch: 42 }, { wch: 36 }];
XLSX.utils.book_append_sheet(wb, wsGuide, '说明');
try {
XLSX.writeFile(wb, uri.fsPath);
const openFolder = t('exportTemplate.openFolder');
const choice = await vscode.window.showInformationMessage(
t('exportTemplate.success'), openFolder,
);
if (choice === openFolder) {
vscode.commands.executeCommand('revealFileInOS', uri);
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(t('exportTemplate.fail', { 0: msg }));
}
}
````
### 3.2 新增 `src/rules/converters/template-converter.ts`
````ts
import * as path from 'path';
import * as XLSX from 'xlsx';
import type { ImportableRule, ValidationIssue } from '../import-types';
import type { Severity } from '../../types';
import { t } from '../../i18n/messages';
const REQUIRED_HEADERS = ['id', 'severity', 'description', 'message'];
const VALID_SEVERITY = ['error', 'warning', 'info'];
function splitList(v: unknown): string[] {
const s = String(v ?? '').trim();
if (!s) { return []; }
return s.split(/[,;、\n]/).map(x => x.trim()).filter(Boolean);
}
export interface TemplateParseResult {
rules: ImportableRule[];
validRules: ImportableRule[];
yamlContent: string;
skippedCount: number;
}
export function parseTemplate(srcPath: string): TemplateParseResult {
// 准入校验-1: 文件格式
const ext = path.extname(srcPath).toLowerCase();
if (!['.xlsx', '.xls'].includes(ext)) {
throw new Error(t('import.template.badFormat'));
}
let wb: XLSX.WorkBook;
try { wb = XLSX.readFile(srcPath); }
catch { throw new Error(t('import.template.badFormat')); }
// 准入校验-2: 表头结构
const sheet = wb.Sheets[wb.SheetNames[0]];
const rows = XLSX.utils.sheet_to_json<Record<string, string>>(sheet, { defval: '' });
if (rows.length === 0) {
throw new Error(t('import.template.empty'));
}
const header = Object.keys(rows[0]).map(k => k.trim().toLowerCase());
const missing = REQUIRED_HEADERS.filter(h => !header.includes(h));
if (missing.length > 0) {
throw new Error(t('import.template.notTemplate', { 0: missing.join(', ') }));
}
// 数据行解析 + 校验
const totalRows = rows.length;
const rules: ImportableRule[] = rows
.filter(r => String(r.id ?? '').trim() !== '')
.map((r, idx) => {
const rowNo = idx + 2;
const issues: ValidationIssue[] = [];
const sevRaw = String(r.severity ?? '').trim().toLowerCase();
const severity: Severity = VALID_SEVERITY.includes(sevRaw) ? (sevRaw as Severity) : 'warning';
if (!VALID_SEVERITY.includes(sevRaw)) {
issues.push({ field: 'severity', severity: 'warning', message: `severity 非法: "${r.severity ?? ''}"` });
}
const description = String(r.description ?? '').trim();
if (!description) {
issues.push({ field: 'description', severity: 'error', message: 'description 为空' });
}
const message = String(r.message ?? '').trim();
if (!message) {
issues.push({ field: 'message', severity: 'error', message: 'message 为空' });
}
return {
id: String(r.id).trim(),
severity,
description,
message,
languages: splitList(r.languages),
excludeLanguages: splitList(r.excludeLanguages),
rowNumber: rowNo,
validationIssues: issues.length > 0 ? issues : undefined,
};
});
const validRules = rules.filter(r => !r.validationIssues);
const yamlContent = buildYaml(validRules);
const skippedCount = totalRows - rules.length;
return { rules, validRules, yamlContent, skippedCount };
}
function buildYaml(rules: ImportableRule[]): string {
const lines: string[] = [];
for (const r of rules) {
lines.push(`- id: ${r.id}`);
lines.push(` severity: ${r.severity}`);
lines.push(` description: ${r.description}`);
lines.push(` message: ${r.message}`);
if (r.languages?.length) {
lines.push(` languages: [${r.languages.join(', ')}]`);
}
if (r.excludeLanguages?.length) {
lines.push(` excludeLanguages: [${r.excludeLanguages.join(', ')}]`);
}
}
return lines.join('\n');
}
````
### 3.3 新增 `src/rules/converters/dedup-prompt.ts`
> **约束**:AI 只做去重判定,严禁修改已有字段、禁止添加/删除规则。若输入 YAML 无重复,原样返回即可。
```ts
import type { CustomRule } from '../../types';
import { getLanguage, type Language } from '../../i18n/messages';
export function buildDedupOnlyPrompt(
yamlContent: string,
existingRules: CustomRule[],
): { system: string; user: string } {
const lang = getLanguage();
const s = PROMPTS[lang];
const existingList = existingRules.length === 0
? s.noExisting
: existingRules.map(r =>
`- id: ${r.id} | severity: ${r.severity} | description: ${r.description} | message: ${r.message}`
).join('\n');
const system = [
s.role,
s.taskTitle,
s.taskLines.join('\n'),
s.rulesTitle,
s.rulesLines.join('\n'),
s.constraintTitle,
s.constraintLines.join('\n'),
s.existingTitle,
existingList,
].join('\n\n');
const user = s.userPrefix + '\n\n' + yamlContent;
return { system, user };
}
const PROMPTS: Record<Language, {
role: string;
taskTitle: string;
taskLines: string[];
rulesTitle: string;
rulesLines: string[];
constraintTitle: string;
constraintLines: string[];
existingTitle: string;
noExisting: string;
userPrefix: string;
}> = {
'zh-CN': {
role: '你是规则去重判定助手。你只输出 YAML,不输出任何解释。',
taskTitle: '## 任务',
taskLines: [
'下面是已标准化的规则 YAML。你只负责对照"现有规则"为每条规则标注去重字段。',
'为每条规则补充以下字段(如果无重复则标注 none):',
'- duplicateOf: 重复的规则 ID(如 eslint/no-console、custom/my-rule',
'- duplicateLevel: exact(完全相同)/ overlap(部分重叠)/ none(无重复)',
'- duplicateReason: 仅 overlap 时必填,简要说明重叠原因',
],
rulesTitle: '## 判定规则',
rulesLines: [
'1. exact: id 完全相同,或 description + message 语义完全一致',
'2. overlap: 检测目标/场景部分重叠,但并非完全相同',
'3. none: 与现有规则无冲突',
],
constraintTitle: '## ⚠️ 严格约束(必须遵守)',
constraintLines: [
'1. 严禁修改任何已有字段的值(id、severity、description、message、languages、excludeLanguages',
'2. 严禁添加新规则,严禁删除或合并规则',
'3. 规则数量必须与输入完全一致,顺序必须与输入完全一致',
'4. 你只允许添加三个字段: duplicateOf、duplicateLevel、duplicateReason',
'5. 如果某条规则与现有规则无任何重复,设置 duplicateLevel: none 即可,不需要补充 duplicateOf',
'6. 输出纯 YAML,不要用 markdown 代码块包裹',
],
existingTitle: '## 现有规则',
noExisting: '(无)',
userPrefix: '## 待去重的规则 YAML',
},
'en': {
role: 'You are a rule deduplication assistant. Output YAML only, no explanations.',
taskTitle: '## Task',
taskLines: [
'Below is standardized rule YAML. Only annotate dedup fields against the "Existing Rules" list.',
'For each rule, add (mark none if no conflict):',
'- duplicateOf: duplicated rule ID (e.g. eslint/no-console, custom/my-rule)',
'- duplicateLevel: exact / overlap / none',
'- duplicateReason: required only for overlap',
],
rulesTitle: '## Judgement Rules',
rulesLines: [
'1. exact: identical id, or semantically identical description+message',
'2. overlap: partially overlapping target/scenario',
'3. none: no conflict with existing rules',
],
constraintTitle: '## ⚠️ Strict Constraints (MUST follow)',
constraintLines: [
'1. Do NOT modify any existing field values (id, severity, description, message, languages, excludeLanguages)',
'2. Do NOT add, delete, or merge rules',
'3. Rule count and order must exactly match the input',
'4. Only add three fields: duplicateOf, duplicateLevel, duplicateReason',
'5. If a rule has no duplication, set duplicateLevel: none without duplicateOf',
'6. Output pure YAML, do NOT wrap in markdown code fences',
],
existingTitle: '## Existing Rules',
noExisting: '(none)',
userPrefix: '## YAML to deduplicate',
},
'ja': {
role: 'あなたはルール重複判定アシスタントです。YAML のみ出力し、説明は不要です。',
taskTitle: '## タスク',
taskLines: [
'以下は標準化されたルール YAML です。既存ルールと照合し、重複フィールドのみ注釈してください。',
'各ルールに以下を追加(重複がない場合は none と表記):',
'- duplicateOf: 重複ルール ID(例: eslint/no-console, custom/my-rule',
'- duplicateLevel: exact / overlap / none',
'- duplicateReason: overlap 時のみ必須',
],
rulesTitle: '## 判定ルール',
rulesLines: [
'1. exact: ID が同一、または description+message が意味的に完全一致',
'2. overlap: 検出対象/シナリオが部分重複',
'3. none: 既存ルールと競合なし',
],
constraintTitle: '## ⚠️ 厳格な制約(必ず遵守)',
constraintLines: [
'1. 既存フィールド(id, severity, description, message, languages, excludeLanguages)の値を一切変更しない',
'2. ルールの追加、削除、統合を一切行わない',
'3. ルールの数と順序は入力と完全に一致させる',
'4. 追加できるフィールドは duplicateOf, duplicateLevel, duplicateReason のみ',
'5. 重複がないルールは duplicateLevel: none とし、duplicateOf は付けない',
'6. 純粋な YAML を出力し、markdown コードブロックで囲まない',
],
existingTitle: '## 既存ルール',
noExisting: '(なし)',
userPrefix: '## 重複排除対象の YAML',
},
};
```
### 3.4 修改 `src/rules/import-service.ts`
新增 `ImportService.importTemplate` 方法:
```ts
import { parseTemplate } from './converters/template-converter';
import { buildDedupOnlyPrompt } from './converters/dedup-prompt';
export class ImportService {
// ... 现有 registerConverter、convert、applyConversion 不动
async importTemplate(
srcPath: string,
name: string,
context: vscode.ExtensionContext,
): Promise<ConversionResult> {
// [1] 程序解析 + 准入校验(失败即 throw,由调用方 catch 弹错误提示)
const { rules, validRules, yamlContent, skippedCount } = parseTemplate(srcPath);
// [2] AI 去重(仅有效规则;错误规则不送 AI)
let dedupedValidRules: ImportableRule[] = validRules;
if (validRules.length > 0) {
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
const existingRules = workspaceRoot ? loadActiveRules(workspaceRoot) : [];
const { system, user } = buildDedupOnlyPrompt(yamlContent, existingRules);
const dedupedYaml = await convertContentWithAI(user, context, system);
if (dedupedYaml) {
dedupedValidRules = parseImportableYaml(dedupedYaml);
} else {
// AI 去重失败,降级:有效规则不带 duplicateLevel,用户自行判断
vscode.window.showWarningMessage(t('import.dedupFailed'));
}
}
// [3] 合并
const errorRules = rules.filter(r => r.validationIssues?.length);
const allRules = [...dedupedValidRules, ...errorRules];
const exactCount = dedupedValidRules.filter(r => r.duplicateLevel === 'exact').length;
const overlapCount = dedupedValidRules.filter(r => r.duplicateLevel === 'overlap').length;
return {
rules: allRules,
yamlContent,
sourceFileName: path.basename(srcPath),
exactCount,
overlapCount,
skippedCount,
errorCount: errorRules.length,
};
}
}
```
> **注意**`loadActiveRules`、`convertContentWithAI`、`parseImportableYaml` 均为本文件已有函数/导入,无需额外 import。
### 3.5 修改 `src/rules/import-preview.ts`
> **原则**:增量扩展,仅新增错误规则组渲染 + skippedCount 提示。现有 exact/overlap/none 三组逻辑不动。UI 样式沿用现有 `renderSection`,不做独立设计。
#### 3.5.1 `showImportPreview` 改动
`keepRule` 初始化只计算非错误规则:
```ts
const keepRule: Record<string, boolean> = {};
for (const rule of result.rules) {
if (!rule.validationIssues?.length) {
keepRule[rule.id] = rule.duplicateLevel !== 'exact';
}
}
```
#### 3.5.2 新增 `renderErrorSection` 辅助函数
```ts
function renderErrorSection(rules: ImportableRule[]): string {
if (rules.length === 0) { return ''; }
const sectionId = 'section-error';
return `
<div style="margin-bottom:12px;">
<div class="section-header" onclick="toggleSection('${sectionId}')">
<span style="font-size:14px;">🚫</span>
<span class="section-title">${t('import.sectionInvalid')}${rules.length}</span>
<span class="section-arrow">▼</span>
</div>
<div id="${sectionId}">
${rules.map(renderErrorCard).join('')}
</div>
</div>
`;
}
function renderErrorCard(rule: ImportableRule): string {
const issues = (rule.validationIssues || []).map(i =>
`<div style="color:#f48771;font-size:12px;margin-bottom:4px;">⚠ ${i.message}</div>`
).join('');
return `
<div class="rule-card" style="opacity:0.7;border-color:rgba(248,81,73,0.3);">
<div class="rule-card-header" style="cursor:default;">
<div class="rule-card-summary">
<span style="font-family:monospace;font-size:13px;font-weight:600;">${rule.id}</span>
<span style="color:#f48771;font-size:11px;font-weight:600;">${t('import.cannotImport')}</span>
</div>
</div>
<div class="rule-card-body" style="border-top:1px solid rgba(248,81,73,0.15);padding-top:8px;">
${issues}
<div style="color:#8b949e;font-size:11px;margin-top:6px;">
severity: ${rule.severity} | description: ${rule.description} | message: ${rule.message}
</div>
</div>
</div>
`;
}
```
#### 3.5.3 `renderPreviewHtml` 分组逻辑
```ts
const errorRules = result.rules.filter(r => r.validationIssues?.length);
const cleanRules = result.rules.filter(r => !r.validationIssues?.length);
const exactRules = cleanRules.filter(r => r.duplicateLevel === 'exact');
const overlapRules = cleanRules.filter(r => r.duplicateLevel === 'overlap');
const noneRules = cleanRules.filter(
r => r.duplicateLevel !== 'exact' && r.duplicateLevel !== 'overlap'
);
```
#### 3.5.4 顶部 skippedRows 提示
```ts
const skippedHint = result.skippedCount
? `<div class="summary-bar" style="border-color:rgba(88,166,255,0.3);color:#58a6ff;">${t('import.template.skipped', { 0: String(result.skippedCount) })}</div>`
: '';
```
#### 3.5.5 分组渲染顺序
```ts
${skippedHint}
${renderErrorSection(errorRules)}
${renderSection(t('import.sectionExact'), '⛔', exactRules, false)}
${renderSection(t('import.sectionOverlap'), '⚠️', overlapRules, true)}
${renderSection(t('import.sectionNone'), '✅', noneRules, false)}
```
#### 3.5.6 validRules 全空时的处理
当 `cleanRules.length === 0`(文件全是错误规则):
```ts
// 确认按钮禁用 + 提示
const confirmDisabled = cleanRules.length === 0;
// 按钮: <button class="btn btn-primary" ${confirmDisabled ? 'disabled style="opacity:0.5;cursor:not-allowed;"' : ''} onclick="doConfirm()">
// 状态栏: 替换为 "无有效规则可导入,请修改文件后重新导入"
const emptyValidHint = cleanRules.length === 0
? `<div class="validation-error" style="display:block;">${t('import.emptyValidRules')}</div>`
: '';
```
#### 3.5.7 确认导入时只写入有效规则
`doConfirm` 中 `collectEditedRules` 排除错误卡片(或直接沿用现有 `keepRule` 过滤——错误规则不在 `keepRule` 中,`applyConversion` 自动忽略)。
### 3.6 修改 `src/views/setupView.ts`
#### 3.6.1 HTML(「自定义规则」区块新增 checkbox + 按钮)
```html
<div class="field" style="margin-top:8px;">
<div class="input-group">
<input type="text" id="newRuleInput" placeholder="${t('setup.ruleNamePlaceholder')}">
<button class="btn btn-sm" style="background:#7c3aed;color:#fff;border-color:#7c3aed;" onclick="addRule()">${t('setup.add')}</button>
</div>
<div class="error-hint" id="ruleNameError">${t('setup.ruleNameRequired')}</div>
<div class="field-hint">${t('setup.ruleNameHint')}</div>
<label style="display:flex;align-items:center;gap:6px;margin-top:8px;font-size:12px;">
<input type="checkbox" id="useTemplateMode">
${t('setup.fromTemplate')}
</label>
<button class="btn btn-sm" style="margin-top:6px;" onclick="exportTemplate()">${t('setup.exportTemplate')}</button>
</div>
```
#### 3.6.2 webview script
```js
function addRule() {
const name = document.getElementById('newRuleInput').value;
const useTemplateMode = document.getElementById('useTemplateMode').checked;
vscode.postMessage({ type: 'addRule', name, useTemplateMode });
}
function exportTemplate() {
vscode.postMessage({ type: 'exportTemplate' });
}
```
#### 3.6.3 Provider: `onDidReceiveMessage` 分流
```ts
import { exportTemplate } from '../rules/export-service';
// message handler:
case 'addRule':
await this.addRule(msg.name, msg.useTemplateMode);
await this.pushConfig();
break;
case 'exportTemplate':
await exportTemplate();
break;
```
#### 3.6.4 Provider: `addRule` 方法签名改为支持分流
> **注意**`addRule` 当前参数为 `(name: string)`,改为 `(name: string, useTemplateMode?: boolean)`,原有 AI 分支保持不变。
```ts
private async addRule(name: string, useTemplateMode?: boolean): Promise<void> {
if (!name.trim()) { return; }
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspaceRoot) { return; }
if (useTemplateMode) {
// ── 模板直通链路 ──
const result = await vscode.window.showOpenDialog({
canSelectMany: false,
openLabel: t('setup.selectTemplateFile'),
filters: { 'Excel': ['xlsx', 'xls'] },
});
if (!result || result.length === 0) { return; }
try {
const conversion = await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: t('setup.importingTemplate'),
}, async () => {
return await this.importService.importTemplate(
result[0].fsPath, name, this.context,
);
});
const decision = await showImportPreview(conversion);
if (decision?.confirmed) {
const rulesDir = path.join(workspaceRoot, '.code-review', 'rules');
if (!fs.existsSync(rulesDir)) { fs.mkdirSync(rulesDir, { recursive: true }); }
const yamlFileName = name.endsWith('.yaml') ? name : `${name}.yaml`;
const yamlPath = path.join(rulesDir, yamlFileName);
if (fs.existsSync(yamlPath)) {
vscode.window.showErrorMessage(t('setup.ruleFileExists', { 0: name }));
return;
}
this.importService.applyConversion(conversion, decision, yamlPath);
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(msg);
}
return;
}
// ── 原 AI 链路,以下不动 ──
// ... 现有 addRule 逻辑
}
```
### 3.7 修改 `src/activation/commands.ts`
```ts
import { exportTemplate } from '../rules/export-service';
// 在 registerCommands 中追加:
context.subscriptions.push(
vscode.commands.registerCommand('codeReviewer.exportTemplate', () => exportTemplate()),
);
```
### 3.8 修改 `package.json`
`contributes.commands` 追加:
```json
{
"command": "codeReviewer.exportTemplate",
"title": "Code Purifier: 导出规则模板"
}
```
### 3.9 修改 `src/i18n/messages.ts`
| key | 中文 | 英文 | 日文 |
|-----|------|------|------|
| `setup.fromTemplate` | 从模板导入(跳过 AI 解析,仅支持 Excel 模板) | Import from template (skip AI parsing, Excel only) | テンプレートからインポート(AI 解析スキップ、Excel のみ) |
| `setup.exportTemplate` | 📤 导出模板 | 📤 Export Template | 📤 テンプレートをエクスポート |
| `setup.selectTemplateFile` | 选择模板文件 | Select template file | テンプレートファイルを選択 |
| `setup.importingTemplate` | 正在导入模板... | Importing template... | テンプレートをインポート中... |
| `import.template.badFormat` | 文件格式错误,请使用 Excel 模板(.xlsx/.xls | Bad file format, please use Excel template (.xlsx/.xls) | ファイル形式エラー、Excel テンプレートを使用してください |
| `import.template.empty` | 文件为空 | File is empty | ファイルが空です |
| `import.template.notTemplate` | 不是模板文件,缺少列: {0} | Not a template file, missing columns: {0} | テンプレートファイルではありません、欠損列: {0} |
| `import.template.skipped` | 已跳过 {0} 行空数据 | Skipped {0} empty rows | {0} 行の空データをスキップしました |
| `import.sectionInvalid` | 错误规则 | Error Rules | エラー行 |
| `import.cannotImport` | 将自动丢弃,请修改文件后重新导入 | Will be auto-discarded, please fix the file and retry | 自動破棄されます、ファイルを修正して再インポートしてください |
| `import.dedupFailed` | AI 去重失败,规则将不带去重标记导入 | AI dedup failed, rules imported without dedup marks | AI 重複排除失敗、重複マークなしでインポート |
| `import.emptyValidRules` | 无有效规则可导入,请修改文件后重新导入 | No valid rules to import, please fix the file and retry | 有効なルールがありません、ファイルを修正して再インポートしてください |
| `exportTemplate.saveLabel` | 导出模板 | Export Template | エクスポート |
| `exportTemplate.success` | 模板已导出 | Template exported | テンプレートをエクスポートしました |
| `exportTemplate.fail` | 导出失败:{0} | Export failed: {0} | エクスポート失敗: {0} |
| `exportTemplate.openFolder` | 打开文件夹 | Reveal in Folder | フォルダを開く |
---
## 四、验收标准
### 4.1 导出模板
1. 侧边栏「自定义规则」区块出现「📤 导出模板」按钮
2. 点击弹出保存对话框,默认文件名 `code-review-rules-template.xlsx`
3. 生成文件含 2 个 sheet:「规则」(表头+示例行)、「说明」(字段说明+填写规范)
4. 列名: `id, severity, description, message, languages, excludeLanguages`
5. 导出成功右下角提示,点击「打开文件夹」可定位文件
6. 命令面板 `Code Purifier: 导出规则模板` 触发同样流程
7. 中/英/日三语切换下,按钮与提示文案正确
8. 取消保存对话框不报错
### 4.2 模板导入
1. 「自定义规则」区块出现 checkbox「从模板导入」
2. 不勾选 → 走原 AI 链路,行为零回归
3. 勾选 → 文件选择仅过滤 `xlsx/xls`
4. 非 Excel → 提示"文件格式错误"
5. 损坏 Excel → 提示"文件格式错误"
6. 表头非标准 → 提示"缺少列: ..."
7. 标准模板 → 进入预览,exact 重复默认不勾选
8. 预览含错误规则组(置顶,无 checkbox 只读)、+ 原有 3 组
9. 空行被跳过 → 顶部提示"已跳过 N 行空数据"
10. 确认导入写入 `.code-review/rules/<name>.yaml`
11. 准入校验失败仅提示,无残留文件
12. severity 拼错行 → 错误规则组,标注原因,确认时自动丢弃
13. description/message 为空 → 同上
14. 表内 id 重复 → 不校验,按普通有效规则进 AI 去重
15. 错误规则不参与 AI 去重
16. validRules 为空 → 确认按钮禁用,提示"无有效规则可导入"
17. AI 去重失败 → 降级,有效规则不带去重标记进预览,弹出 warning
18. 导入速度明显快于 AI 链路
### 4.3 闭环验证
1. 导出模板 → 不修改直接导入 → 预览出现 `no-todo` 规则
2. 导出模板 → 删示例行 → 填 2 条新规则 → 导入 → 预览 2 条新规则
3. 导出模板 → 填与已有规则相同 id → 导入 → 预览标记 exact 重复
---
## 五、风险与应对
| 风险 | 等级 | 应对 |
|------|------|------|
| 导入侧 AI 链路被误改 | 中 | 不碰 `ExcelConverter`、`prompt-builder.ts`、原 addRule 分支 |
| AI 去重 prompt 越权修改字段 | 中 | prompt 增加严格约束段 + 预览人工确认兜底 |
| 去重 AI 故障(网络/限流) | 中 | 降级处理:有效规则不带去重标记进预览 + warning 提示 |
| `xlsx` 写盘无权限 | 低 | try/catch + 友好提示 |
| 跨平台路径 | 低 | `vscode.Uri.fsPath` 不硬编码分隔符 |
| 国际化漏键 | 低 | 中英日三语同步增补 |
---
## 六、后续可扩展点(本次不做)
- 导出当前规则(备份/迁移)
- 模板多语言(根据语言切换示例)
- 模板版本号(用于兼容判断)
- YAML 直通链路校验补齐
- AI 链路校验补齐
- 纯本地去重函数
@@ -0,0 +1,906 @@
# 自定义规则 · 模板导出与导入功能设计书
> 项目:vscode-code-reviewer(净码特工 · Code Purifier
> 仓库:sdjndaq/2026-ai-b3 分支:vscode-code-reviewer
> 版本:v2.0(整合导出 + 模板导入)
> 日期:2026-07-30
---
## 一、概述
### 1.1 背景
当前插件的自定义规则导入是「不确定规格 + AI 兜底」模式:任意格式的文件(md/txt/xlsx/docx/pptx)经 converter 转成 markdown,由 AI 自由理解列含义并输出 YAML,再由 AI 顺带完成去重判定。该模式存在三个痛点:
1. **无标准模板**:用户批量录入规则时无从获取标准格式,凭经验构造导致列名错乱、AI 解析失败率高。
2. **解析全靠 AI**:即使是结构化的 Excel,也要走 AI 解析,耗时长、消耗 token、字段映射可能误判。
3. **去重与解析耦合**:去重判定由 AI 在解析阶段一并完成,无法对「已是标准格式的输入」单独做去重。
### 1.2 目标
形成「导出模板 → 填写 → 模板导入」的闭环:
- **导出模板**:在侧边栏设置面板新增「导出模板」按钮,生成标准格式 `.xlsx` 模板。
- **模板导入**:新增 checkbox「从模板导入」,勾选后走「程序解析 + AI 去重」链路——解析阶段跳过 AI(快、确定、零 token),去重阶段复用 AI 的语义比对能力。
### 1.3 范围
- **含**:导出模板功能;模板导入功能(程序解析 + 准入校验 + AI 去重 + 预览)。
- **不含**:导出当前规则;YAML 直通链路与 AI 链路的校验补齐(本次不动)。
- **不触碰**:现有 `ExcelConverter``prompt-builder.ts``addRule` 原 AI 流程,保证零回归。
>
> 注:`import-preview.ts` **需改动**(新增错误规则组渲染),但仅做增量扩展,不改动现有三组的渲染逻辑。
---
## 二、现状分析
### 2.1 规则数据模型
`src/types.ts`
```ts
interface CustomRule {
id: string;
severity: 'error' | 'warning' | 'info';
description: string;
message: string;
languages?: string[];
excludeLanguages?: string[];
}
```
`src/rules/import-types.ts` 扩展:
```ts
interface ImportableRule extends CustomRule {
duplicateOf?: string;
duplicateLevel?: 'exact' | 'overlap' | 'none';
duplicateReason?: string;
}
```
### 2.2 现有导入链路(AI 模式,本次不改)
```
addRule(name)
→ showOpenDialog(过滤器:yaml/yml/md/txt/xlsx/xls/docx/pptx
→ 分流:
├─ .yaml/.yml → fs.copyFileSync(零校验直通)
└─ 其他 → ImportService.importRules()
→ 各 Converter 转成 markdown
→ convertContentWithAI()
├─ buildSystemPrompt('spreadsheet'|'freeform', existingRules)
│ └─ buildDedupPromptSection(existingRules) ← 把现有规则喂给 AI
└─ AI 输出 YAML(含 duplicateLevel 等去重字段)
→ parseImportableYaml() 统计 exactCount/overlapCount
→ showImportPreview() 预览
→ 用户确认 → 写入 .code-review/rules/<name>.yaml
```
### 2.3 去重机制:由 AI 完成(关键事实)
**去重判定由 AI 在解析阶段一并完成,本地无独立去重函数。**
证据:
- `prompt-builder.ts:83-85` 让 AI 输出 `duplicateOf/duplicateLevel/duplicateReason`
- `prompt-builder.ts:463` `buildDedupPromptSection(existingRules, lang)` 把现有规则(`loadActiveRules()` + `static-rules.json`)拼进 system prompt。
- `import-service.ts:274-275` 只统计 AI 给的 `duplicateLevel`,无本地比对。
- 搜索 `markDuplicates/detectDuplicates/findDuplicates` 零命中。
**含义**:若模板导入跳过 AI,去重也随之消失——即使与已有规则完全相同也不会标记。因此模板导入需保留 AI 去重环节。
### 2.4 现有 UI(关键事实:无 checkbox、无独立导入按钮)
`src/views/setupView.ts` 的「自定义规则」区块(第 985-993 行):
```html
<div class="field">
<div class="input-group">
<input type="text" id="newRuleInput" placeholder="规则名称">
<button onclick="addRule()">添加</button>
</div>
<div class="error-hint" id="ruleNameError"></div>
<div class="field-hint"></div>
</div>
```
- **没有独立「导入」按钮**:导入是 `addRule(name)` 流程的一部分(填名 → 选文件 → 自动转换)。
- **没有任何 checkbox**`type="checkbox"` 仅出现在 `docs/superpowers/specs/setup-panel-preview.html`(那是规则项的 enable/disable 开关)。
### 2.5 已有依赖
- `xlsx` 库已由 `excel-converter.ts` 引入,导出/导入均可复用,零新增依赖。
---
## 三、功能设计
### 3.1 模板格式(导出/导入共用)
生成 `.xlsx` 文件,含 2 个 sheet。
#### Sheet 1「规则」(表头 + 1 行示例)
| id | severity | description | message | languages | excludeLanguages |
|----|----------|-------------|---------|-----------|------------------|
| no-todo | warning | 禁止提交 TODO 注释 | 发现 TODO 注释,请清理后提交 | javascript,typescript | |
- 第 1 行:表头,固定 6 列,与 `CustomRule` 字段一一对应。
- 第 2 行:示例行,用户可删除或覆盖。
- 第 3 行起:用户自行追加规则。
#### Sheet 2「说明」(字段含义 + 填写规范)
| 字段 | 含义 | 取值/格式 | 示例 |
|------|------|-----------|------|
| id | 规则唯一标识 | 小写字母、数字、连字符,全局唯一 | no-todo |
| severity | 严重级别 | error / warning / info | warning |
| description | 规则简述(给人看) | 自由文本 | 禁止提交 TODO 注释 |
| message | 命中时展示给开发者的提示语 | 自由文本 | 发现 TODO 注释,请清理后提交 |
| languages | 生效的语言 | 逗号分隔,留空表示对所有语言生效 | javascript,typescript |
| excludeLanguages | 排除的语言 | 逗号分隔,可留空 | |
**填写说明:**
1. severity 仅接受 error / warning / info 三个值。
2. languages / excludeLanguages 多值用英文逗号分隔。
3. 示例行可删除,仅作填写参考。
4. 该模板可直接用于「从模板导入」功能回环校验。
### 3.2 导出模板交互
1. 用户在侧边栏「自定义规则」区块点击「📤 导出模板」按钮。
2. 弹出系统保存对话框,默认文件名 `code-review-rules-template.xlsx`,过滤器 `Excel (*.xlsx)`
3. 用户选定位置后写盘。
4. 右下角信息提示:`✓ 模板已导出`,附带「打开文件夹」按钮。
5. 用户填写模板 → 回到插件,勾选「从模板导入」→ 点「添加」→ 预览 → 确认写入。
### 3.3 模板导入交互
```
侧边栏「自定义规则」区块:
[规则名输入框] [ 添加]
☐ 从模板导入(跳过 AI 解析,仅支持 Excel 模板) ← 新增 checkbox
[📤 导出模板] ← 新增按钮
```
- **不勾选**(默认):点「添加」→ 走现有 `addRule` AI 链路(支持 yaml/md/txt/xlsx/docx/pptx),行为不变。
- **勾选**:点「添加」→ 走模板直通链路(仅 xlsx/xls,含两道准入校验,程序解析 + AI 去重)。
### 3.4 模板导入完整流程
```
勾选 checkbox + 填规则名 + 点「添加」
[1] 选文件(showOpenDialog,过滤器仅 xlsx/xls
[2] 程序解析 + 数据行校验(TemplateConverter,无 AI
├─ 准入校验1:文件格式(必须 xlsx/xls 且可读) → 失败则报错终止
├─ 准入校验2:表头结构(含 id/severity/description/message)→ 失败则报错终止
├─ 逐行解析 + 校验 → ImportableRule[](错误规则带 validationIssues
├─ 分离:validRules(无 issue+ errorRules(有 issue
└─ yamlContent ← 仅 validRules 拼接(标准 YAML,无 duplicateLevel
[3] AI 去重(仅对 validRules,新增专用 prompt
├─ loadActiveRules() 取工作区已有规则
├─ buildDedupOnlyPrompt(yamlContent, existingRules) ← 新增
│ 告诉 AI:"以下 YAML 已是标准格式,只需对照现有规则标 duplicateLevel,不要改字段"
├─ AI 返回带 duplicateOf/duplicateLevel/duplicateReason 的 YAML
└─ parseImportableYaml() 解析(复用现有)
(注:errorRules 不送 AI,无 duplicateLevel
[4] 预览(showImportPreview,复用 + 新增错误规则组)
├─ 顶部提示"已跳过 N 行空数据"(若有)
├─ 🚫 错误规则组(置顶,无 checkbox,只读,标注"将自动丢弃")
├─ ⛔ 完全重复组(默认不勾选)
├─ ⚠️ 部分重叠组
├─ ✅ 新规则组
└─ 用户确认 → 写入选中的有效规则 → 错误规则自动丢弃
(边界:validRules 全空时禁用确认按钮,提示"无有效规则可导入")
```
### 3.5 校验策略(两层:准入 + 数据行)
校验分两层,准入校验失败硬报错,数据行问题软提示进预览。
#### 第一层:准入校验(硬报错,不进预览)
**第一关:文件格式校验**
```ts
const ext = path.extname(srcPath).toLowerCase();
if (!['.xlsx', '.xls'].includes(ext)) {
throw new Error(t('import.template.badFormat')); // "文件格式错误,请使用 Excel 模板"
}
let wb: XLSX.WorkBook;
try { wb = XLSX.readFile(srcPath); }
catch { throw new Error(t('import.template.badFormat')); }
```
**第二关:数据结构校验(是不是模板文件)**
```ts
const sheet = wb.Sheets[wb.SheetNames[0]];
const rows = XLSX.utils.sheet_to_json<Record<string, string>>(sheet, { defval: '' });
if (rows.length === 0) {
throw new Error(t('import.template.empty')); // "文件为空"
}
const header = Object.keys(rows[0]).map(k => k.trim().toLowerCase());
const REQUIRED = ['id', 'severity', 'description', 'message'];
const missing = REQUIRED.filter(h => !header.includes(h));
if (missing.length > 0) {
throw new Error(t('import.template.notTemplate', { 0: missing.join(', ') }));
}
```
| 场景 | 处理 |
|------|------|
| 选了 .txt/.docx/.md | 第一关拒,提示"文件格式错误" |
| 选了损坏的 .xlsx | 第一关拒,同上 |
| .xlsx 表头非标准(如中文列名) | 第二关拒,提示"缺少列: ..." |
| .xlsx 表头标准 | 通过准入,进入数据行校验 |
#### 第二层:数据行校验(软提示,进预览,自动丢弃)
**核心思路**:数据行的问题不阻塞导入流程,而是带上 `validationIssues` 字段,和有效规则一起进入预览面板,作为独立的"错误规则"分组展示。**错误规则不参与去重、不写入 YAML,确认导入时自动丢弃**;有效规则正常导入。用户可查看错误原因后修改文件重新导入。
**`ImportableRule` 扩展**
```ts
interface ValidationIssue {
field: string; // 'severity' | 'description' | 'message'
severity: 'error' | 'warning';
message: string; // 如 'severity 非法: "warn"'、'description 为空'
}
interface ImportableRule extends CustomRule {
duplicateOf?: string;
duplicateLevel?: 'exact' | 'overlap' | 'none';
duplicateReason?: string;
validationIssues?: ValidationIssue[]; // 新增:非空则为错误规则
rowNumber?: number; // 新增:原始行号
}
```
**校验规则**(只记录错误,不修补;不校验表内重复):
| 问题 | 处理 | 是否导入 |
|------|------|---------|
| severity 为空或拼错(如 "warn" | 记录 issueseverity 临时填 'warning' 仅用于结构完整 | ❌ 自动丢弃 |
| description 为空 | 记录 issue | ❌ 自动丢弃 |
| message 为空 | 记录 issue | ❌ 自动丢弃 |
| 空行/空 id | 静默丢弃(无法标识) | — |
> 关键:错误规则**不进 yamlContent**、**不送 AI 去重**、**确认导入时自动丢弃**。只有有效规则(无 `validationIssues`)才参与 AI 去重和写入 YAML。
>
> **不校验表内 id 重复**:本次不考虑规则文件内多条规则 id 相同的情况,交由 AI 去重环节按现有逻辑处理(与已有规则的比对)。
**预览面板分组**(复用现有 `showImportPreview`,错误规则组置顶):
```
┌─ 规则导入预览 ──────────────────────────┐
│ [🚫 错误规则:2] [⛔ 完全重复:1] [⚠️ 部分重叠:2] [✅ 新规则:3] │
│ │
│ 🚫 错误规则 (2) [只读] │
│ ⚠ 第3行 severity 非法: "warn" │
│ id: no-magic-numbers │
│ description: 禁止魔法数字 │
│ message: 请用常量替代 │
│ (将自动丢弃,请修改文件后重新导入)│
│ │
│ ⚠ 第5行 description 为空 │
│ id: check-null │
│ message: 检查空值 │
│ (将自动丢弃,请修改文件后重新导入)│
│ │
│ ⛔ 完全重复 (1) ...(现有逻辑) │
│ ⚠️ 部分重叠 (2) ...(现有逻辑) │
│ ✅ 新规则 (3) ...(现有逻辑) │
│ │
│ [ 确认导入 ] [ 取消 ] │
└──────────────────────────────────────────┘
```
- 错误规则组置顶,无 checkbox,纯只读展示
- 错误规则不参与 AI 去重(不送 AI),无 `duplicateLevel`
- 仅有效规则(无 `validationIssues`)参与 AI 去重,按 exact/overlap/none 分组
- 确认导入时写入选中的有效规则,错误规则自动丢弃,导入流程正常完成
### 3.6 示例行处理
模板里的 `no-todo` 示例行按用户确认"不做特殊处理"——当普通规则解析,交由 AI 去重环节判定。若工作区已有同名规则,会被标记为 `exact` 重复,预览面板默认不勾选;若无冲突则正常进入预览,由用户自行决定保留或删除。
---
## 四、技术方案
### 4.1 改动文件清单
| 类型 | 文件 | 改动 |
|------|------|------|
| 新增 | `src/rules/export-service.ts` | 模板生成服务,复用 `xlsx` 库 |
| 新增 | `src/rules/converters/template-converter.ts` | 程序解析 + 两道准入校验 + 生成 yamlContent |
| 新增 | `src/rules/converters/dedup-prompt.ts` | `buildDedupOnlyPrompt(yamlContent, existingRules)` 专用去重 prompt(中英) |
| 修改 | `src/rules/import-types.ts` | `ImportableRule` 增加 `validationIssues` / `rowNumber` 字段;新增 `ValidationIssue` 接口;`ConversionResult` 增加 `skippedCount?: number` 字段 |
| 修改 | `src/rules/import-service.ts` | 新增 `importTemplate(srcPath, name, existingRules)`:调 TemplateConverter → AI 去重 → 返回 ConversionResult |
| 修改 | `src/rules/import-preview.ts` | `renderPreviewHtml` 增加错误规则组(置于现有三组之上,无 checkbox 只读);`keepRule` 默认值考虑 `validationIssues`(错误规则不进入 keepRule);`renderRuleItem` 展示 issue 详情 + "将自动丢弃"提示;顶部展示"已跳过 N 行空数据"(来自 `skippedCount`);`validRules` 为空时禁用"确认导入"按钮并提示"无有效规则可导入" |
| 修改 | `src/views/setupView.ts` | 加 checkbox + 导出按钮;`addRule``useTemplateMode` 分流;新增 `case 'exportTemplate'` |
| 修改 | `src/activation/commands.ts` | 注册 `codeReviewer.exportTemplate` 命令 |
| 修改 | `package.json` | `contributes.commands` 增 1 条 |
| 修改 | `src/i18n/messages.ts` | checkbox/校验/去重 prompt/导出 文案(中英) |
**不动**`ExcelConverter``prompt-builder.ts`、现有 `addRule` 原 AI 流程。`import-preview.ts` 仅做增量扩展(新增错误规则组),不改动现有三组渲染逻辑。
### 4.2 新增 `src/rules/export-service.ts`(导出模板)
```ts
import * as XLSX from 'xlsx';
import * as vscode from 'vscode';
import { t } from '../i18n/messages';
const RULES_HEADER = ['id', 'severity', 'description', 'message', 'languages', 'excludeLanguages'];
const RULES_EXAMPLE = [
'no-todo', 'warning', '禁止提交 TODO 注释',
'发现 TODO 注释,请清理后提交', 'javascript,typescript', '',
];
const GUIDE_AOA: string[][] = [
['字段', '含义', '取值/格式', '示例'],
['id', '规则唯一标识', '小写字母、数字、连字符,全局唯一', 'no-todo'],
['severity', '严重级别', 'error / warning / info', 'warning'],
['description', '规则简述(给人看)', '自由文本', '禁止提交 TODO 注释'],
['message', '命中时展示给开发者的提示语', '自由文本', '发现 TODO 注释,请清理后提交'],
['languages', '生效的语言', '逗号分隔,留空表示对所有语言生效', 'javascript,typescript'],
['excludeLanguages', '排除的语言', '逗号分隔,可留空', ''],
['', '', '', ''],
['填写说明', '', '', ''],
['1. severity 仅接受 error / warning / info 三个值', '', '', ''],
['2. languages / excludeLanguages 多值用英文逗号分隔', '', '', ''],
['3. 示例行可删除,仅作填写参考', '', '', ''],
['4. 该模板可直接用于「从模板导入」功能回环校验', '', '', ''],
];
export async function exportTemplate(): Promise<void> {
const uri = await vscode.window.showSaveDialog({
defaultUri: vscode.Uri.file('code-review-rules-template.xlsx'),
filters: { 'Excel': ['xlsx'] },
saveLabel: t('exportTemplate.saveLabel'),
});
if (!uri) { return; }
const wb = XLSX.utils.book_new();
const wsRules = XLSX.utils.aoa_to_sheet([RULES_HEADER, RULES_EXAMPLE]);
wsRules['!cols'] = [
{ wch: 16 }, { wch: 10 }, { wch: 32 }, { wch: 40 }, { wch: 24 }, { wch: 20 },
];
XLSX.utils.book_append_sheet(wb, wsRules, '规则');
const wsGuide = XLSX.utils.aoa_to_sheet(GUIDE_AOA);
wsGuide['!cols'] = [{ wch: 22 }, { wch: 28 }, { wch: 42 }, { wch: 36 }];
XLSX.utils.book_append_sheet(wb, wsGuide, '说明');
try {
XLSX.writeFile(wb, uri.fsPath);
const openFolder = t('exportTemplate.openFolder');
const choice = await vscode.window.showInformationMessage(
t('exportTemplate.success'), openFolder,
);
if (choice === openFolder) {
vscode.commands.executeCommand('revealFileInOS', uri);
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(t('exportTemplate.fail', { 0: msg }));
}
}
```
### 4.3 新增 `src/rules/converters/template-converter.ts`(程序解析 + 准入校验 + 数据行校验)
```ts
import * as path from 'path';
import * as XLSX from 'xlsx';
import type { ImportableRule, ValidationIssue } from '../import-types';
import type { Severity } from '../../types';
import { t } from '../../i18n/messages';
const REQUIRED_HEADERS = ['id', 'severity', 'description', 'message'];
const VALID_SEVERITY = ['error', 'warning', 'info'];
function splitList(v: unknown): string[] {
const s = String(v ?? '').trim();
if (!s) { return []; }
// 兼容逗号、分号、顿号、换行
return s.split(/[,;、\n]/).map(x => x.trim()).filter(Boolean);
}
export interface TemplateParseResult {
rules: ImportableRule[]; // 所有规则(含错误的)
validRules: ImportableRule[]; // 仅有效规则(无 validationIssues
yamlContent: string; // 仅有效规则的 YAML(送 AI 去重)
skippedCount: number; // 被丢弃的空行数
}
export function parseTemplate(srcPath: string): TemplateParseResult {
// 第一关:文件格式校验
const ext = path.extname(srcPath).toLowerCase();
if (!['.xlsx', '.xls'].includes(ext)) {
throw new Error(t('import.template.badFormat'));
}
let wb: XLSX.WorkBook;
try { wb = XLSX.readFile(srcPath); }
catch { throw new Error(t('import.template.badFormat')); }
// 第二关:数据结构校验
const sheet = wb.Sheets[wb.SheetNames[0]];
const rows = XLSX.utils.sheet_to_json<Record<string, string>>(sheet, { defval: '' });
if (rows.length === 0) {
throw new Error(t('import.template.empty'));
}
const header = Object.keys(rows[0]).map(k => k.trim().toLowerCase());
const missing = REQUIRED_HEADERS.filter(h => !header.includes(h));
if (missing.length > 0) {
throw new Error(t('import.template.notTemplate', { 0: missing.join(', ') }));
}
// 数据行解析 + 校验(只记录错误,不修补、不丢弃有 id 的行)
const allRows = rows.length;
const rules: ImportableRule[] = rows
.map((r, idx) => ({ r, rowNo: idx + 2 })) // 先记原始行号(表头占第1行,数据从第2行起)
.filter(({ r }) => String(r.id ?? '').trim()) // 再 filter 空行/空 id
.map(({ r, rowNo }) => {
const issues: ValidationIssue[] = [];
// severity 校验(不修补,仅记录;临时填 warning 保证结构完整)
const sevRaw = String(r.severity ?? '').trim().toLowerCase();
const severity: Severity = VALID_SEVERITY.includes(sevRaw) ? (sevRaw as Severity) : 'warning';
if (!VALID_SEVERITY.includes(sevRaw)) {
issues.push({ field: 'severity', severity: 'warning', message: `severity 非法: "${r.severity ?? ''}"` });
}
// description 校验(不修补,仅记录)
const description = String(r.description ?? '').trim();
if (!description) {
issues.push({ field: 'description', severity: 'error', message: 'description 为空' });
}
// message 校验(不修补,仅记录)
const message = String(r.message ?? '').trim();
if (!message) {
issues.push({ field: 'message', severity: 'error', message: 'message 为空' });
}
return {
id: String(r.id).trim(),
severity,
description,
message,
languages: splitList(r.languages),
excludeLanguages: splitList(r.excludeLanguages),
rowNumber: rowNo,
validationIssues: issues.length > 0 ? issues : undefined,
};
});
// 仅有效规则进 yamlContent(错误规则不送 AI 去重)
// 注:本次不校验表内 id 重复,交由 AI 去重环节处理
const validRules = rules.filter(r => !r.validationIssues);
const yamlContent = buildYaml(validRules);
return { rules, validRules, yamlContent, skippedCount: allRows - rules.length };
}
function buildYaml(rules: ImportableRule[]): string {
const lines: string[] = [];
for (const r of rules) {
lines.push(`- id: ${r.id}`);
lines.push(` severity: ${r.severity}`);
lines.push(` description: ${r.description}`);
lines.push(` message: ${r.message}`);
if (r.languages?.length) {
lines.push(` languages: [${r.languages.join(', ')}]`);
}
if (r.excludeLanguages?.length) {
lines.push(` excludeLanguages: [${r.excludeLanguages.join(', ')}]`);
}
}
return lines.join('\n');
}
```
### 4.4 新增 `src/rules/converters/dedup-prompt.ts`(专用去重 prompt
```ts
import type { CustomRule } from '../../types';
import { getLanguage, type Language } from '../../i18n/messages';
export function buildDedupOnlyPrompt(
yamlContent: string,
existingRules: CustomRule[],
): { system: string; user: string } {
const lang = getLanguage();
const s = PROMPTS[lang];
const existingList = existingRules.length === 0
? s.noExisting
: existingRules.map(r =>
`- id: ${r.id} | severity: ${r.severity} | description: ${r.description} | message: ${r.message}`
).join('\n');
const system = [
s.role,
s.taskTitle,
s.taskLines.join('\n'),
s.rulesTitle,
s.rulesLines.join('\n'),
s.existingTitle,
existingList,
].join('\n\n');
const user = s.userPrefix + '\n\n' + yamlContent;
return { system, user };
}
const PROMPTS: Record<Language, {
role: string;
taskTitle: string;
taskLines: string[];
rulesTitle: string;
rulesLines: string[];
existingTitle: string;
noExisting: string;
userPrefix: string;
}> = {
'zh-CN': {
role: '你是规则去重助手。',
taskTitle: '## 任务',
taskLines: [
'下面 USER 部分是已标准化的规则 YAML,只需对照现有规则标 duplicateLevel,不要修改任何原有字段。',
'为每条规则补充以下字段:',
'- duplicateOf: 重复的规则 IDlinter 如 eslint/no-console;自定义如 custom/my-rule',
'- duplicateLevel: exact(完全相同)/ overlap(部分重叠)/ none',
'- duplicateReason: overlap 时必填',
],
rulesTitle: '## 判定规则',
rulesLines: [
'1. exactid 相同,或 description+message 语义完全一致',
'2. overlap:检测目标/场景部分重叠',
'3. 不要修改原有字段(id/severity/description/message/languages/excludeLanguages',
'4. 只补充去重字段',
],
existingTitle: '## 现有规则',
noExisting: '(无)',
userPrefix: '## 待去重的规则 YAML',
},
'en': {
role: 'You are a rule deduplication assistant.',
taskTitle: '## Task',
taskLines: [
'The USER section below is standardized rule YAML. Only mark duplicateLevel against existing rules; do NOT modify any original fields.',
'For each rule, add:',
'- duplicateOf: the duplicated rule ID (e.g. eslint/no-console; custom/my-rule)',
'- duplicateLevel: exact (identical) / overlap (partial) / none',
'- duplicateReason: required when overlap',
],
rulesTitle: '## Judgement Rules',
rulesLines: [
'1. exact: same id, or semantically identical description+message',
'2. overlap: partially overlapping target/scenario',
'3. Do NOT modify original fields (id/severity/description/message/languages/excludeLanguages)',
'4. Only add dedup fields',
],
existingTitle: '## Existing Rules',
noExisting: '(none)',
userPrefix: '## YAML to deduplicate',
},
'ja': {
role: 'あなたはルール重複排除アシスタントです。',
taskTitle: '## タスク',
taskLines: [
'下記 USER 部分は標準化されたルール YAML です。既存ルールと照合して duplicateLevel を付与するのみ、元フィールドは変更しないでください。',
'各ルールに以下を追加:',
'- duplicateOf: 重複ルール ID(例: eslint/no-console; custom/my-rule',
'- duplicateLevel: exact(完全一致)/ overlap(部分重複)/ none',
'- duplicateReason: overlap 時必須',
],
rulesTitle: '## 判定ルール',
rulesLines: [
'1. exact: id 同一、または description+message が意味的に完全一致',
'2. overlap: 検出対象/シナリオが部分重複',
'3. 元フィールドを変更しない(id/severity/description/message/languages/excludeLanguages',
'4. 重複フィールドのみ追加',
],
existingTitle: '## 既存ルール',
noExisting: '(なし)',
userPrefix: '## 重複排除対象の YAML',
},
};
```
### 4.5 修改 `src/rules/import-service.ts`(新增 importTemplate
```ts
import { parseTemplate } from './converters/template-converter';
import { buildDedupOnlyPrompt } from './converters/dedup-prompt';
import { loadActiveRules } from './yaml-parser';
// convertContentWithAI / parseImportableYaml / normalizeRuleIds 均为本文件已有函数,直接使用,无需 import
export class ImportService {
// 原有 importRules 不动
async importTemplate(
srcPath: string,
name: string,
workspaceRoot: string,
): Promise<ConversionResult> {
// [1] 程序解析 + 准入校验 + 数据行校验(失败即 throw)
const { rules, validRules, yamlContent, skippedCount } = parseTemplate(srcPath);
// [2] AI 去重(仅对有效规则;错误规则不送 AI)
let dedupedValidRules: ImportableRule[] = validRules;
if (validRules.length > 0) {
const existingRules = loadActiveRules(workspaceRoot);
const { system, user } = buildDedupOnlyPrompt(yamlContent, existingRules);
const dedupedYaml = await convertContentWithAI(system, user);
dedupedValidRules = parseImportableYaml(dedupedYaml);
}
// [3] 合并:有效规则(带去重标记)+ 错误规则(无去重标记,仅展示)
const errorRules = rules.filter(r => r.validationIssues);
const allRules = [...dedupedValidRules, ...errorRules];
const exactCount = allRules.filter(r => r.duplicateLevel === 'exact').length;
const overlapCount = allRules.filter(r => r.duplicateLevel === 'overlap').length;
return {
rules: allRules,
yamlContent,
sourceFileName: path.basename(srcPath),
exactCount,
overlapCount,
skippedCount, // 传递给预览面板用于顶部提示
};
}
}
```
> 注意:错误规则(`validationIssues` 非空)不参与 AI 去重,无 `duplicateLevel`,在预览中归入"错误规则"组。确认导入时只写入选中的有效规则。
>
> **边界:validRules 全空**。若文件全是错误规则(`validRules.length === 0`),跳过 AI 去重,`allRules` 全是错误规则。预览面板进入后应:
> - 顶部提示"无有效规则可导入,请修改文件后重新导入"
> - 禁用"确认导入"按钮,仅允许"取消"
>
> 此边界由 `import-preview.ts` 在收到 `validRules` 数量为 0 时处理(见 4.1 改动描述)。
>
> **ConversionResult 需扩展 `skippedCount?: number` 字段**,用于预览面板顶部提示"已跳过 N 行空数据"。
### 4.6 修改 `src/views/setupView.ts`UI + 分流)
**HTML 改动**(「自定义规则」区块):
```html
<div class="field">
<div class="input-group">
<input type="text" id="newRuleInput" placeholder="${t('setup.ruleNamePlaceholder')}">
<button class="btn btn-sm" style="background:#7c3aed;color:#fff;border-color:#7c3aed;"
onclick="addRule()">${t('setup.add')}</button>
</div>
<div class="error-hint" id="ruleNameError">${t('setup.ruleNameRequired')}</div>
<div class="field-hint">${t('setup.ruleNameHint')}</div>
<!-- 新增 checkbox -->
<label style="display:flex;align-items:center;gap:6px;margin-top:8px;font-size:12px;">
<input type="checkbox" id="useTemplateMode">
${t('setup.fromTemplate')}
</label>
<!-- 新增导出按钮 -->
<button class="btn btn-sm" style="margin-top:6px;"
onclick="exportTemplate()">${t('setup.exportTemplate')}</button>
</div>
```
**webview script**(获取 checkbox 状态):
```js
function addRule() {
const name = document.getElementById('newRuleInput').value;
const useTemplateMode = document.getElementById('useTemplateMode').checked;
vscode.postMessage({ type: 'addRule', name, useTemplateMode });
}
function exportTemplate() {
vscode.postMessage({ type: 'exportTemplate' });
}
```
**`onDidReceiveMessage` 分流**
```ts
import { exportTemplate } from '../rules/export-service';
// 在 SetupViewProvider 的 onDidReceiveMessage 中:
case 'addRule':
await this.addRule(msg.name, msg.useTemplateMode); // 传入 useTemplateMode
await this.pushConfig();
break;
case 'exportTemplate':
await exportTemplate();
break;
```
**`addRule` 方法分流**
```ts
private async addRule(name: string, useTemplateMode?: boolean): Promise<void> {
if (!name.trim()) { return; }
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspaceRoot) { return; }
if (useTemplateMode) {
// 模板直通链路
const result = await vscode.window.showOpenDialog({
canSelectMany: false,
openLabel: t('setup.selectTemplateFile'),
filters: { 'Excel': ['xlsx', 'xls'] }, // 仅 Excel
});
if (!result || result.length === 0) { return; }
try {
const conversion = await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: t('setup.importingTemplate'),
}, async () => {
return await this.importService.importTemplate(result[0].fsPath, name, workspaceRoot);
});
const decision = await showImportPreview(conversion);
if (decision?.confirmed) {
await this.saveImportedRules(name, conversion, decision, workspaceRoot);
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(msg); // 准入校验失败直接提示
}
} else {
// 原 AI 链路(不动)
// ... 现有 addRule 逻辑
}
}
```
### 4.7 修改 `src/activation/commands.ts`(注册导出命令)
```ts
import { exportTemplate } from '../rules/export-service';
// 在 registerCommands 中追加
context.subscriptions.push(
vscode.commands.registerCommand('codeReviewer.exportTemplate', () => exportTemplate()),
);
```
### 4.8 修改 `package.json`
`contributes.commands` 追加:
```json
{
"command": "codeReviewer.exportTemplate",
"title": "Code Purifier: 导出规则模板"
}
```
### 4.9 修改 `src/i18n/messages.ts`(新增文案)
| key | 中文 | 英文 | 日文 |
|-----|------|------|------|
| `setup.fromTemplate` | 从模板导入(跳过 AI 解析,仅支持 Excel 模板) | Import from template (skip AI parsing, Excel only) | テンプレートからインポート(AI 解析スキップ、Excel のみ) |
| `setup.exportTemplate` | 📤 导出模板 | 📤 Export Template | 📤 テンプレートをエクスポート |
| `setup.selectTemplateFile` | 选择模板文件 | Select template file | テンプレートファイルを選択 |
| `setup.importingTemplate` | 正在导入模板... | Importing template... | テンプレートをインポート中... |
| `import.template.badFormat` | 文件格式错误,请使用 Excel 模板(.xlsx/.xls | Bad file format, please use Excel template (.xlsx/.xls) | ファイル形式エラー、Excel テンプレートを使用してください |
| `import.template.empty` | 文件为空 | File is empty | ファイルが空です |
| `import.template.notTemplate` | 不是模板文件,缺少列: {0} | Not a template file, missing columns: {0} | テンプレートファイルではありません、欠損列: {0} |
| `exportTemplate.title` | 导出模板 | Export Template | テンプレートをエクスポート |
| `exportTemplate.saveLabel` | 导出模板 | Export Template | エクスポート |
| `exportTemplate.success` | 模板已导出 | Template exported | テンプレートをエクスポートしました |
| `exportTemplate.fail` | 导出失败:{0} | Export failed: {0} | エクスポート失敗: {0} |
| `exportTemplate.openFolder` | 打开文件夹 | Reveal in Folder | フォルダを開く |
| `import.sectionInvalid` | 错误规则 | Error Rules | エラー行 |
| `import.cannotImport` | 将自动丢弃,请修改文件后重新导入 | Will be auto-discarded, please fix the file and retry | 自動破棄されます、ファイルを修正して再インポートしてください |
| `import.template.skipped` | 已跳过 {0} 行空数据 | Skipped {0} empty rows | {0} 行の空データをスキップしました |
---
## 五、关键设计决策
| 决策点 | 选择 | 理由 |
|--------|------|------|
| 是否新增依赖 | 否,复用 `xlsx` | 导入侧已引入,零额外体积 |
| 模板内容 | 表头 + 1 行示例 + 说明 sheet | 贴合「模板」语义,降低首次使用门槛 |
| 是否同时导出当前规则 | 否 | 用户确认暂不需要,保持最小化 |
| 导出按钮位置 | setupView「自定义规则」工具栏 | 紧邻「导入」,成对出现 |
| 模板列名 | 与 `CustomRule` 字段完全一致 | 保证导入回环自洽 |
| 模板导入触发方式 | checkbox「从模板导入」 | 不加独立按钮,用户显式选择模式 |
| 模板导入解析方式 | 程序解析(无 AI) | 快、确定、零 token 用于解析 |
| 模板导入去重方式 | AI 去重(新增专用 prompt) | 复用 AI 语义比对能力,去重与解析解耦 |
| 准入校验失败 | 硬报错阻止 | 文件格式错或结构错直接拒绝,不进预览 |
| severity 拼错 | 记录 issue + 进错误规则组,自动丢弃 | 错误规则不导入不阻塞流程,用户改文件重导 |
| description/message 空 | 记录 issue + 进错误规则组,自动丢弃 | 同上,不修补不导入 |
| 表内 id 重复 | 不校验,交由 AI 去重环节处理 | 本次不处理规则文件内重复,简化实现 |
| 错误规则是否参与去重 | 否,不送 AI | 错误规则不导入,无需标 duplicateLevel |
| 错误规则是否阻塞导入 | 否,有效规则正常导入 | 自动丢弃错误规则,流程不中断 |
| 空行/空 id | 静默丢弃 + 顶部提示跳过数 | 无法标识的行无法修复 |
| 数据行校验展示 | 复用现有预览,新增「错误规则」组(置于现有三组之上) | 不新建面板,与去重分组正交 |
| 示例行处理 | 不做特殊处理 | 当普通规则,交由 AI 去重判定 |
| 是否触碰导入侧 AI 链路 | 否 | 零回归 |
| 是否触碰现有去重 prompt | 否,新增专用 prompt | 任务聚焦,避免影响原 AI 解析链路 |
---
## 六、验收标准
### 6.1 导出模板
1. 侧边栏「自定义规则」区块出现「导出模板」按钮。
2. 点击弹出保存对话框,默认文件名 `code-review-rules-template.xlsx`
3. 生成文件含 2 个 sheet:「规则」(表头+示例行)、「说明」(字段说明+填写规范)。
4. 列名依次为 `id, severity, description, message, languages, excludeLanguages`
5. 导出成功后右下角提示,点击「打开文件夹」可定位文件。
6. 命令面板执行 `Code Purifier: 导出规则模板` 触发同样流程。
7. 中英文/日文切换下,按钮文案与提示信息正确。
8. 取消保存对话框时不报错、无残留。
### 6.2 模板导入
1. 「自定义规则」区块出现 checkbox「从模板导入」。
2. 不勾选时点「添加」,走原 AI 链路,行为与现状完全一致(零回归)。
3. 勾选时点「添加」,文件选择对话框仅过滤 `xlsx/xls`
4. 选非 Excel 文件(如 .txt)→ 提示"文件格式错误",不进入预览。
5. 选损坏的 .xlsx → 提示"文件格式错误",不进入预览。
6. 选表头非标准的 .xlsx → 提示"不是模板文件,缺少列: ...",不进入预览。
7. 选标准模板 → 进入预览,exact 重复默认不勾选。
8. 预览界面复用 `showImportPreview`,新增「错误规则」组(置顶,无 checkbox 只读),其余三组渲染逻辑不变。
9. 确认导入后写入 `.code-review/rules/<name>.yaml`
10. 导入速度明显快于 AI 链路(解析阶段无 AI 调用)。
11. 准入校验失败时右下角错误提示,无残留文件。
12. 数据行有 severity 拼错时,该行进入"错误规则"组,标注错误原因,确认导入时自动丢弃。
13. 数据行 description/message 为空时,该行进入"错误规则"组,标注错误原因,确认导入时自动丢弃。
14. 规则文件内多条 id 相同的规则,不校验表内重复,按普通规则进入有效规则组,交由 AI 去重环节处理。
15. 空行被跳过时,预览面板顶部提示"已跳过 N 行空数据"N 来自 `skippedCount`)。
16. 错误规则不参与 AI 去重,无 duplicateLevel 标记。
17. 确认导入时只写入选中的有效规则,错误规则自动丢弃,导入流程正常完成(不阻塞)。
18. 文件全是错误规则(validRules 为空)时,预览面板禁用"确认导入"按钮,提示"无有效规则可导入,请修改文件后重新导入"。
### 6.3 闭环验证
1. 导出模板 → 不修改直接勾选「从模板导入」导入 → 预览出现 `no-todo` 规则(示例行)。
2. 导出模板 → 删除示例行 → 填入 2 条新规则 → 导入 → 预览出现 2 条新规则。
3. 导出模板 → 填入与工作区已有规则相同的 id → 导入 → 预览标记为 exact 重复。
---
## 七、风险与回归
| 风险 | 等级 | 应对 |
|------|------|------|
| 导入侧 AI 链路被误改 | 中 | 评审约束:不动 `ExcelConverter``prompt-builder.ts``import-preview.ts`、原 `addRule` AI 分支 |
| 专用去重 prompt 与原解析 prompt 行为不一致 | 中 | 去重 prompt 明确"不要修改原有字段",且预览环节人工确认兜底 |
| `xlsx` 写文件在无写权限目录失败 | 低 | `try/catch` 捕获并友好提示 |
| 跨平台路径(Win/Mac/Linux | 低 | 使用 `vscode.Uri.fsPath`,不硬编码分隔符 |
| 国际化漏键 | 低 | 中英日三语同步增补,走现有 `t()` 机制 |
| AI 去重环节失败(网络/限流) | 中 | 复用现有 `convertContentWithAI` 的错误处理,提示用户重试 |
| validRules 全空(文件全是错误规则) | 低 | 预览禁用确认按钮,提示"无有效规则可导入",不写空文件 |
---
## 八、后续可扩展点(本次不做)
- **导出当前规则**:复用 `loadActiveRules()`,把工作区规则写入 Excel(备份/迁移)。
- **模板多语言**:根据 `getAIOutputLanguage()` 切换示例与说明语言。
- **模板版本号**:在「说明」sheet 增加 `version` 字段,便于未来字段演进时的兼容判断。
- **YAML 直通链路校验补齐**:当前 `.yaml/.yml` 直接 `copyFileSync` 零校验,可补结构化校验。
- **AI 链路校验补齐**:AI 解析结果也可过一遍 `validateRules()` 硬校验。
- **本地去重函数**:若未来想完全脱离 AI,可实现 `detectDuplicates(rules, existing)` 做纯本地比对。
@@ -0,0 +1,683 @@
# PMD 规则增强设计书
## 一、背景与目标
### 1.1 现状
当前 `PmdAdapter``src/adapters/pmd.ts`)通过内置 JAR 包调用 PMD 7.26.0,使用 XML ruleset 文件配置规则。Java 规则集(`jars/pmd/pmd-java-ruleset.xml`)采用**全分类引用**方式启用规则:
```xml
<ruleset name="java-ruleset" ...>
<description>Java code review rules</description>
<rule ref="category/java/bestpractices.xml"/>
<rule ref="category/java/codestyle.xml"/>
<rule ref="category/java/design.xml"/>
<rule ref="category/java/errorprone.xml"/>
<rule ref="category/java/multithreading.xml"/>
<rule ref="category/java/performance.xml"/>
</ruleset>
```
这种方式启用了 6 个分类共 309 条规则(活跃 289 + 已弃用 20),存在以下问题:
- **启用 20 条已弃用规则**:这些规则已标记为弃用,计划在 PMD 8.0.0 中移除,继续启用增加噪音并为未来迁移增加负担
- **启用 17 条误报率高的噪音规则**:如 `TooManyMethods``CyclomaticComplexity``ShortVariable` 等阈值类规则,在真实项目中产生大量噪音诊断
- **未启用 Security 分类**Java Security 分类包含 2 条高价值安全规则(`HardCodedCryptoKey``InsecureCryptoIv`),当前未启用
- **全分类引用缺乏精细控制**:无法排除特定规则,所有规则(含弃用和噪音)均被启用
JSP 规则集(`jars/pmd/pmd-jsp-ruleset.xml`)已启用全部 5 个分类共 12 条规则,全部活跃,无需调整。
### 1.2 目标
将 Java 规则集从"全分类引用(309 条)"优化为"精选规则集(274 条活跃规则)":
1. **补启 Security 分类**(+2 条):新增 2 条高价值安全规则
2. **排除 20 条已弃用规则**-20 条):为 PMD 8.0.0 迁移做准备
3. **排除 17 条噪音规则**(-17 条):减少误报,提升诊断信噪比
4. **同步更新 `static-rules.json`**:确保去重检测覆盖完整
最终 Java 启用规则数:289(当前活跃)- 17(噪音)+ 2(安全)= **274 条活跃规则**,覆盖 7 个分类。
### 1.3 设计原则
- **从粗到精**PMD 的增强方向与 ESLint/Stylelint 相反——不是"从少到多"地添加规则,而是"从粗到精"地从全分类引用中排除弃用和噪音规则。这一特点贯穿整个设计
- **不破坏现有配置优先级**:全局配置 > 项目配置 > 内置配置,三层择一逻辑不变
- **不引入新依赖**:PMD 通过内置 JAR 包调用,无需修改 `package.json`
- **不修改适配器代码**`PmdAdapter` 读取 ruleset XML 文件,仅需修改 XML 文件本身
- **同步更新去重数据**`static-rules.json` 必须同步追加 Security 规则、移除已弃用规则
- **不修改规则 ID 前缀格式**:诊断结果仍使用 `pmd:{ruleName}`Java)和 `pmd-jsp:{ruleName}`JSP)格式
---
## 二、影响范围分析
### 2.1 需要修改的文件
| 文件 | 修改类型 | 修改内容 |
|------|----------|----------|
| `jars/pmd/pmd-java-ruleset.xml` | 配置修改 | 为 6 个分类添加 `<exclude>` 排除弃用和噪音规则,新增 Security 分类引用 |
| `src/rules/static-rules.json` | 数据修改 | `rules.pmd` 数组追加 2 条 Security 规则,移除 20 条已弃用规则条目 |
### 2.2 不需要修改的文件
| 文件 | 原因 |
|------|------|
| `package.json` | PMD 通过内置 JAR 包调用,不依赖 npm 包,无需添加依赖 |
| `src/adapters/pmd.ts` | 适配器读取 ruleset XML 文件路径,不涉及规则配置逻辑,无需修改 |
| `scripts/build.mjs` | JAR 包和 XML 文件作为静态资源打包,构建脚本无需修改 |
| `jars/pmd/pmd-jsp-ruleset.xml` | JSP 规则集已完整启用 5 个分类,无需调整 |
| `src/adapters/jsp.ts` | JSP 适配器复用 PMD JAR,内部读取 JSP ruleset,不受 Java ruleset变更影响 |
| `src/orchestrator/orchestrator.ts` | 仅做调度,不涉及配置逻辑 |
| `src/config/linter.ts` | 配置读取层不变 |
### 2.3 不受影响的功能
- 全局配置(`linters.pmdConfigPath`):用户指定 ruleset 文件时完全替代内置 ruleset,不受影响
- 项目配置(项目根目录下的 `.xml` ruleset 文件):存在时完全替代内置配置,不受影响
- JSP 审查功能:JSP 规则集无变化,审查行为不变
- 规则 ID 输出格式:Java 仍为 `pmd:{ruleName}`JSP 仍为 `pmd-jsp:{ruleName}`,不变
- PMD JAR 包版本:仍为 7.26.0,不升级
---
## 三、详细设计
### 3.1 修改 `jars/pmd/pmd-java-ruleset.xml`
#### 3.1.1 当前代码
```xml
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="java-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>Java code review rules</description>
<rule ref="category/java/bestpractices.xml"/>
<rule ref="category/java/codestyle.xml"/>
<rule ref="category/java/design.xml"/>
<rule ref="category/java/errorprone.xml"/>
<rule ref="category/java/multithreading.xml"/>
<rule ref="category/java/performance.xml"/>
</ruleset>
```
#### 3.1.2 修改后代码
```xml
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="java-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>Java code review rules (274 active rules, 7 categories)</description>
<!-- Best Practices:排除 9 条已弃用规则 -->
<rule ref="category/java/bestpractices.xml">
<exclude name="DefaultLabelNotLastInSwitchStmt"/>
<exclude name="JUnit4TestShouldUseAfterAnnotation"/>
<exclude name="JUnit4TestShouldUseBeforeAnnotation"/>
<exclude name="JUnit4TestShouldUseTestAnnotation"/>
<exclude name="JUnit5TestShouldBePackagePrivate"/>
<exclude name="JUnitAssertionsShouldIncludeMessage"/>
<exclude name="JUnitTestContainsTooManyAsserts"/>
<exclude name="JUnitTestsShouldIncludeAssert"/>
<exclude name="SwitchStmtsShouldHaveDefault"/>
</rule>
<!-- Code Style:排除 2 条已弃用 + 4 条噪音规则 -->
<rule ref="category/java/codestyle.xml">
<exclude name="GenericsNaming"/>
<exclude name="UnnecessaryLocalBeforeReturn"/>
<exclude name="LongVariable"/>
<exclude name="ShortVariable"/>
<exclude name="ShortMethodName"/>
<exclude name="ShortClassName"/>
</rule>
<!-- Design:排除 2 条已弃用 + 10 条噪音规则 -->
<rule ref="category/java/design.xml">
<exclude name="AvoidCatchingGenericException"/>
<exclude name="UseObjectForClearerAPI"/>
<exclude name="TooManyMethods"/>
<exclude name="CyclomaticComplexity"/>
<exclude name="NPathComplexity"/>
<exclude name="CognitiveComplexity"/>
<exclude name="NcssCount"/>
<exclude name="TooManyFields"/>
<exclude name="ExcessiveParameterList"/>
<exclude name="ExcessivePublicCount"/>
<exclude name="ExcessiveImports"/>
<exclude name="CouplingBetweenObjects"/>
</rule>
<!-- Error Prone:排除 6 条已弃用规则 -->
<rule ref="category/java/errorprone.xml">
<exclude name="AvoidCatchingNPE"/>
<exclude name="AvoidCatchingThrowable"/>
<exclude name="AvoidLosingExceptionInformation"/>
<exclude name="DontImportSun"/>
<exclude name="NonCaseLabelInSwitchStatement"/>
<exclude name="UselessOperationOnImmutable"/>
</rule>
<!-- Multithreading:排除 3 条噪音规则 -->
<rule ref="category/java/multithreading.xml">
<exclude name="AvoidUsingVolatile"/>
<exclude name="DoNotUseThreads"/>
<exclude name="AvoidSynchronizedStatement"/>
</rule>
<!-- Performance:排除 1 条已弃用规则 -->
<rule ref="category/java/performance.xml">
<exclude name="TooFewBranchesForASwitchStatement"/>
</rule>
<!-- Security:新增,2 条高价值安全规则 -->
<rule ref="category/java/security.xml"/>
</ruleset>
```
#### 3.1.3 设计说明
**为什么仍使用分类引用 + exclude,而非逐条引用?**
PMD 的分类引用(`<rule ref="category/java/X.xml"/>`)会启用该分类下的全部规则,通过 `<exclude>` 排除特定规则。与 ESLint/Stylelint 逐条列出规则名的方式相比,PMD 的分类引用方式有以下优势:
- **维护成本低**:PMD 版本升级新增规则时,分类引用自动包含新规则,无需手动添加。逐条引用则需要每次升级都对比新增规则并手动追加
- **排除列表更稳定**:需要排除的规则(弃用 + 噪音)远少于需要保留的规则(37 vs 272),维护 exclude 列表比维护 include 列表更简洁
- **符合 PMD 官方推荐**:PMD 官方文档推荐使用分类引用作为规则集的基础
**为什么不为排除规则配置 `<properties>` 阈值,而是直接排除?**
部分噪音规则(如 `CyclomaticComplexity``TooManyMethods`)支持通过 `<properties>` 调整阈值。但本设计选择直接排除而非调阈值,原因如下:
- 阈值类规则的"合理值"因项目而异,无法找到一个通用默认值
- 即使调高阈值,仍会在边界处产生误报
- 代码审查工具的目标是发现确定性 bug,而非度量代码复杂度。复杂度度量更适合 SonarQube 等专门的代码质量平台
- 直接排除减少配置复杂度,用户如有需求可通过自定义 ruleset 单独启用并配置阈值
**`AvoidCatchingGenericException` 的跨分类特殊处理**
该规则名在 Design 和 Error Prone 两个分类中各存在一条:
- Design 分类中的为**已弃用**版本(PMD 7 起迁移至 Error Prone 分类)
- Error Prone 分类中的为**活跃**版本(引入版本 4.2.6)
在 Design 分类中排除弃用版本后,Error Prone 分类中的活跃版本仍然生效。两版本功能一致,不会出现规则缺失。
### 3.2 修改 `src/rules/static-rules.json`
#### 3.2.1 追加 Security 规则
`rules.pmd` 数组中追加 2 条 Security 分类规则条目:
```json
{"id": "pmd/HardCodedCryptoKey", "description": "Do not use hard coded encryption keys"},
{"id": "pmd/InsecureCryptoIv", "description": "Do not use hard coded initialization vectors in encryption operations"}
```
#### 3.2.2 移除已弃用规则
`rules.pmd` 数组中移除以下 20 条已弃用规则条目(如果存在):
```json
{"id": "pmd/DefaultLabelNotLastInSwitchStmt", "description": "..."},
{"id": "pmd/JUnit4TestShouldUseAfterAnnotation", "description": "..."},
{"id": "pmd/JUnit4TestShouldUseBeforeAnnotation", "description": "..."},
{"id": "pmd/JUnit4TestShouldUseTestAnnotation", "description": "..."},
{"id": "pmd/JUnit5TestShouldBePackagePrivate", "description": "..."},
{"id": "pmd/JUnitAssertionsShouldIncludeMessage", "description": "..."},
{"id": "pmd/JUnitTestContainsTooManyAsserts", "description": "..."},
{"id": "pmd/JUnitTestsShouldIncludeAssert", "description": "..."},
{"id": "pmd/SwitchStmtsShouldHaveDefault", "description": "..."},
{"id": "pmd/GenericsNaming", "description": "..."},
{"id": "pmd/UnnecessaryLocalBeforeReturn", "description": "..."},
{"id": "pmd/AvoidCatchingGenericException", "description": "..."}, // 仅移除 Design 弃用版本的条目
{"id": "pmd/UseObjectForClearerAPI", "description": "..."},
{"id": "pmd/AvoidCatchingNPE", "description": "..."},
{"id": "pmd/AvoidCatchingThrowable", "description": "..."},
{"id": "pmd/AvoidLosingExceptionInformation", "description": "..."},
{"id": "pmd/DontImportSun", "description": "..."},
{"id": "pmd/NonCaseLabelInSwitchStatement", "description": "..."},
{"id": "pmd/UselessOperationOnImmutable", "description": "..."},
{"id": "pmd/TooFewBranchesForASwitchStatement", "description": "..."}
```
> 注:`AvoidCatchingGenericException` 在 `static-rules.json` 中可能仅有一条条目(对应 Error Prone 活跃版本)。如果该条目存在,应保留(对应活跃版本)。仅当存在明确标注为 Design 弃用版本的条目时才移除。实际操作时应检查条目内容确认。
#### 3.2.3 噪音规则的处理
17 条噪音规则**不从 `static-rules.json` 中移除**。原因:
- 噪音规则仍是 PMD 的有效规则,用户可能通过自定义 ruleset 启用并配置阈值
- `static-rules.json` 的用途是展示 linter 支持的规则清单和去重检测,保留噪音规则条目有助于用户了解这些规则的存在
- 噪音规则仅在默认 ruleset XML 中排除,不影响 `static-rules.json` 的完整性
#### 3.2.4 修改 `linterVersion` 字段
```json
"linterVersion": {
"pmd": "7.26.0 (274 Java rules + 12 JSP rules)",
...
}
```
将原有标记更新为反映实际启用规则数量。
#### 3.2.5 为什么必须同步更新 static-rules.json
`static-rules.json` 有两个用途:
1. **UI 展示**:在设置面板中展示当前 linter 支持的规则清单
2. **去重检测**:自定义规则导入时,按 ID 匹配 `static-rules.json` 中的规则,若已存在则提示重复
如果不更新:
- 追加 Security 规则:用户新增的 `HardCodedCryptoKey` 自定义规则不会被识别为重复
- 移除弃用规则:已弃用规则仍出现在规则清单中,误导用户认为这些规则仍被推荐使用
### 3.3 不需要修改 `package.json`
PMD 通过内置 JAR 包调用,不依赖 npm 包。当前 `package.json` 中与 PMD 相关的配置仅为 JAR 包路径(通过 `jars/pmd/` 目录引用),无需添加任何 npm 依赖。
```json
// package.json 中无 PMD 相关依赖项,PMD 完全由内置 JAR 包提供
"dependencies": {
"eslint": "^9.39.3",
"stylelint": "^17.14.0",
"typescript-eslint": "^8.56.1",
"xlsx": "^0.18.5"
// 无 PMD 相关条目
}
```
这与 ESLint/Stylelint 适配器需要添加 `@eslint/js``stylelint-config-recommended` 依赖形成鲜明对比——PMD 的依赖管理完全脱离 npm 体系。
---
## 四、新增/排除规则分类详解
### 4.1 新增:Security 分类(+2 条)
当前 Java 规则集未启用 Security 分类。该分类仅含 2 条规则,均为高价值安全规则。
| 规则 | 检测场景 | 误报评估 |
|------|----------|----------|
| `HardCodedCryptoKey` | `SecretKeySpec("mykey".getBytes(), "AES")` 等硬编码密钥 | 零误报,硬编码密钥几乎一定是安全问题 |
| `InsecureCryptoIv` | `IvParameterSpec(new byte[16])` 等硬编码/空 IV | 零误报,硬编码 IV 破坏加密语义安全 |
> Security 分类无需排除任何规则,直接全分类引用 `<rule ref="category/java/security.xml"/>`。
### 4.2 排除:已弃用规则(-20 条)
#### 4.2.1 Best Practices 弃用规则(9 条)
均为 JUnit 相关规则在 PMD 7 中的重命名,旧名称已弃用,新名称在同类分类中活跃。
| 弃用规则 | 替代规则 | 弃用原因 |
|----------|----------|----------|
| `DefaultLabelNotLastInSwitchStmt` | `DefaultLabelNotLastInSwitch` | PMD 7.7.0 起支持 switch 表达式,重命名 |
| `JUnit4TestShouldUseAfterAnnotation` | `UnitTestShouldUseAfterAnnotation` | PMD 7 起泛化为通用测试框架 |
| `JUnit4TestShouldUseBeforeAnnotation` | `UnitTestShouldUseBeforeAnnotation` | 同上 |
| `JUnit4TestShouldUseTestAnnotation` | `UnitTestShouldUseTestAnnotation` | 同上 |
| `JUnit5TestShouldBePackagePrivate` | `JUnitJupiterTestShouldBePackagePrivate` | PMD 7 起重命名 |
| `JUnitAssertionsShouldIncludeMessage` | `UnitTestAssertionsShouldIncludeMessage` | 同上 |
| `JUnitTestContainsTooManyAsserts` | `UnitTestContainsTooManyAsserts` | 同上 |
| `JUnitTestsShouldIncludeAssert` | `UnitTestShouldIncludeAssert` | 同上 |
| `SwitchStmtsShouldHaveDefault` | `NonExhaustiveSwitch` | PMD 7 起重命名,支持 switch 表达式 |
> 排除弃用规则后,替代规则(活跃版本)仍随分类引用自动启用,功能不受影响。
#### 4.2.2 Code Style 弃用规则(2 条)
| 弃用规则 | 替代规则 | 弃用原因 |
|----------|----------|----------|
| `GenericsNaming` | `TypeParameterNamingConventions` | PMD 7.17.0 起用更通用的命名约定规则替代 |
| `UnnecessaryLocalBeforeReturn` | 无 | 规则价值低,PMD 7.17.0 起移除 |
#### 4.2.3 Design 弃用规则(2 条)
| 弃用规则 | 替代规则 | 弃用原因 |
|----------|----------|----------|
| `AvoidCatchingGenericException`Design 版) | `AvoidCatchingGenericException`Error Prone 版,活跃) | 迁移至 Error Prone 分类 |
| `UseObjectForClearerAPI` | 无 | PMD 7.26.0 起弃用,规则建议过于主观 |
#### 4.2.4 Error Prone 弃用规则(6 条)
| 弃用规则 | 替代规则 | 弃用原因 |
|----------|----------|----------|
| `AvoidCatchingNPE` | `AvoidCatchingGenericException` | PMD 7.18.0 起合并至通用规则 |
| `AvoidCatchingThrowable` | `AvoidCatchingGenericException` | 同上 |
| `AvoidLosingExceptionInformation` | `UselessPureMethodCall` | PMD 7.17.0 起用更通用的规则替代 |
| `DontImportSun` | `UnsupportedJdkApiUsage` | PMD 7.21.0 起用更通用的规则替代 |
| `NonCaseLabelInSwitchStatement` | `NonCaseLabelInSwitch` | PMD 7.7.0 起重命名 |
| `UselessOperationOnImmutable` | `UselessPureMethodCall` | PMD 7.17.0 起用更通用的规则替代 |
#### 4.2.5 Performance 弃用规则(1 条)
| 弃用规则 | 替代规则 | 弃用原因 |
|----------|----------|----------|
| `TooFewBranchesForASwitchStatement` | `TooFewBranchesForSwitch` | PMD 7.7.0 起重命名 |
### 4.3 排除:噪音规则(-17 条)
#### 4.3.1 Design 噪音规则(10 条)
这些规则均为阈值类复杂度/规模度量规则,误报率高,不适合作为代码审查工具的默认规则。
| 规则 | 默认阈值 | 排除理由 |
|------|----------|----------|
| `TooManyMethods` | 10 | 工具类、控制器类天然方法多,阈值过于机械 |
| `CyclomaticComplexity` | 10 | 复杂业务逻辑合理需要高复杂度 |
| `NPathComplexity` | 200 | 与圈复杂度类似,机械阈值弊大于利 |
| `CognitiveComplexity` | 15 | 适合作为度量指标而非 lint 规则 |
| `NcssCount` | 100 | 方法/类长度限制因场景而异 |
| `TooManyFields` | 15 | DTO/Entity 类天然字段多 |
| `ExcessiveParameterList` | 10 | 构建器模式、配置类合理需要多参数 |
| `ExcessivePublicCount` | 45 | API 类天然公共成员多 |
| `ExcessiveImports` | 30 | 整合层/门面类合理需要多导入 |
| `CouplingBetweenObjects` | 20 | 耦合度难以精确度量,更适合架构审查工具 |
#### 4.3.2 Code Style 噪音规则(4 条)
这些规则基于名称长度阈值,过于主观,在真实项目中误报率高。
| 规则 | 默认阈值 | 排除理由 |
|------|----------|----------|
| `LongVariable` | 30 字符 | 描述性变量名通常更长更清晰 |
| `ShortVariable` | 3 字符 | `i``x``y` 等短名是惯用写法 |
| `ShortMethodName` | 3 字符 | `get``set``add``run` 等是标准写法 |
| `ShortClassName` | 4 字符 | `URL``URI``Map` 等是标准缩写 |
#### 4.3.3 Multithreading 噪音规则(3 条)
这些规则过于激进,在通用 Java 项目中误报率高。
| 规则 | 排除理由 |
|------|----------|
| `AvoidUsingVolatile` | volatile 在双重检查锁等场景有合理用途 |
| `DoNotUseThreads` | 面向 J2EE webapp 容器场景,不适用于通用项目 |
| `AvoidSynchronizedStatement` | synchronized 在简单并发场景有合理用途 |
### 4.4 不启用:Documentation 分类(6 条)
Documentation 分类共 6 条规则,不适合作为代码审查工具的默认规则。详见推荐规则文档中的"不推荐启用"部分。如团队有强制 Javadoc 需求,可通过自定义 ruleset 单独启用 `DanglingJavadoc` 等规则。
---
## 五、规则级别设计
### 5.1 PMD priority 体系
PMD 使用 5 级优先级体系(priority 1-5),每条规则有默认优先级,在 ruleset XML 中可通过 `<priority>` 元素覆盖:
| 优先级 | 名称 | 含义 | 适配器映射(建议) |
|--------|------|------|-------------------|
| 1 | Blocker | 高概率的代码错误,运行时必崩 | Error |
| 2 | Critical | 设计问题、严重的最佳实践违反 | Error |
| 3 | Major | 开发实践问题、潜在 bug | Warning |
| 4 | Minor | 代码风格、命名约定 | Info / Hint |
| 5 | Info | 文档、注释相关 | Info / Hint |
### 5.2 当前优先级策略
本设计**不覆盖任何规则的默认优先级**。原因:
- PMD 为每条规则设置的默认优先级已经过社区验证,与规则的严重程度匹配
- 分类引用方式自动继承规则的默认优先级,无需逐条配置
- 适配器(`pmd.ts`)在解析 PMD 输出时,根据 priority 值映射为 VS Code 诊断严重级别
### 5.3 排除规则对优先级分布的影响
排除 37 条规则后,剩余 274 条规则的优先级分布(估算):
| 优先级 | 排除前(含弃用/噪音) | 排除后 | 变化 |
|--------|---------------------|--------|------|
| 1Blocker | ~40 | ~40 | 无变化(弃用和噪音规则多为 P2-P4) |
| 2Critical | ~60 | ~50 | -10(排除 Design 噪音规则,多为 P2) |
| 3Major | ~90 | ~80 | -10(部分噪音规则为 P3 |
| 4Minor | ~100 | ~90 | -10(排除 Short/Long 命名规则等 P4 |
| 5Info | ~19 | ~14 | -5Documentation 未启用,弃用规则含部分 P5) |
| **合计** | **~309** | **274** | **-35** |
> 排除的规则主要集中在 P2-P4 级别,这些是误报率最高的级别。P1(Blocker)级规则全部保留,确保高危 bug 检测能力不受影响。
### 5.4 与 ESLint/Stylelint 级别设计的差异
| 维度 | ESLint | Stylelint | PMD |
|------|--------|-----------|-----|
| 级别体系 | `error` / `warn` / `off` | `true`error/ `null`off | priority 1-5 |
| 级别配置 | 逐条规则配置 | 逐条规则配置 | 规则默认优先级,可覆盖 |
| 适配器映射 | 直接映射 | true → error | priority 1-2 → Error3 → Warning4-5 → Info |
| 本设计操作 | 追加规则配置级别 | 追加规则配置级别 | 不修改优先级,仅排除规则 |
---
## 六、兼容性分析
### 6.1 对现有用户代码的影响
排除规则后,之前被噪音规则标记的诊断将消失,审查结果更加精简:
| 影响程度 | 规则 | 说明 |
|----------|------|------|
| **诊断大幅减少** | `TooManyMethods` 等 10 条 Design 噪音规则 | 大型类不再被标记,减少噪音诊断 |
| **诊断大幅减少** | `ShortVariable` 等 4 条 Code Style 噪音规则 | 短变量名不再被标记 |
| **诊断大幅减少** | `DoNotUseThreads` 等 3 条 Multithreading 噪音规则 | 合理使用线程/volatile/synchronized 不再被标记 |
| **诊断少量减少** | 20 条已弃用规则 | 替代规则(活跃版本)仍生效,功能等价 |
| **诊断少量新增** | `HardCodedCryptoKey``InsecureCryptoIv` | 新增 Security 规则,仅检测硬编码密钥/IV,误报率极低 |
### 6.2 对 JSP 审查的影响
JSP 规则集无变化,JSP 审查行为完全不受影响。
### 6.3 对去重功能的影响
`static-rules.json` 同步更新后:
- 追加 2 条 Security 规则:去重检测范围扩展,用户新增的 `HardCodedCryptoKey` 自定义规则将被正确识别为重复
- 移除 20 条已弃用规则:已弃用规则不再出现在规则清单中,避免误导用户
### 6.4 对 PMD 8.0.0 迁移的影响
排除 20 条已弃用规则后,未来升级到 PMD 8.0.0(计划移除弃用规则)时:
- ruleset XML 中的 `<exclude>` 引用的弃用规则名将不存在,PMD 会输出警告但不影响其他规则
- 迁移时只需移除这些无效的 `<exclude>` 条目即可
- 提前排除弃用规则可减少迁移时的规则数量变化,降低用户感知
### 6.5 对构建的影响
- ruleset XML 文件作为静态资源打包,修改不影响构建流程
- JAR 包版本不变,无需更新 JAR 文件
- `static-rules.json` 修改不影响构建产物体积(JSON 数据文件)
- 无新增 npm 依赖,`package.json``node_modules` 不变
---
## 七、与 ESLint/Stylelint/ts-eslint 适配器的架构对比
### 7.1 增强方向对比
| 维度 | ESLint / Stylelint / ts-eslint | PMD |
|------|-------------------------------|-----|
| **增强方向** | 从少到多(从 recommended 扩展到更多规则) | 从粗到精(从全分类引用中排除弃用和噪音) |
| **起点** | 官方 recommended 配置(61/41/24 条) | 全分类引用(309 条) |
| **终点** | recommended + 额外规则(92/68/~40 条) | 精选规则集(274 条) |
| **规则数变化** | 增加(+31/+27/~16) | 减少(-35,含排除弃用和噪音,+2 安全) |
| **核心操作** | 追加规则到配置对象 | 从分类引用中排除规则 |
> 这是 PMD 适配器与其他 linter 适配器最本质的差异。其他 linter 从"太少"走向"刚好"PMD 从"太多"走向"刚好"。
### 7.2 配置方式对比
| 维度 | ESLint | Stylelint | PMD |
|------|--------|-----------|-----|
| 配置格式 | JS 对象(Flat Config | JS 对象 | XML ruleset |
| 规则引用 | 逐条规则名 + 级别 | 逐条规则名 + 级别 | 分类引用 + exclude |
| 排除方式 | 不列入规则对象 | 不列入规则对象 | `<exclude name="..."/>` |
| 新增方式 | 追加规则到对象 | 追加规则到对象 | 追加 `<rule ref="category/..."/>` |
| 官方配置包 | `@eslint/js` recommended | `stylelint-config-recommended` | 无(分类文件随 JAR 提供) |
| 合并机制 | Flat Config 数组后者覆盖 | 对象展开 | XML 引用叠加 |
### 7.3 依赖管理对比
| 维度 | ESLint / Stylelint | PMD |
|------|-------------------|-----|
| 依赖来源 | npm 包 | 内置 JAR 包 |
| 版本管理 | `package.json` + `package-lock.json` | JAR 文件版本(`jars/pmd/lib/` |
| 配置包 | `@eslint/js``stylelint-config-recommended` | 无(分类 XML 随 JAR 内置) |
| 升级方式 | `npm update` | 替换 JAR 文件 |
| `package.json` 修改 | 需要添加依赖 | **不需要** |
### 7.4 适配器代码修改对比
| 适配器 | 是否修改代码 | 修改内容 |
|--------|------------|----------|
| ESLint | 是 | `getDefaultConfig()` 追加 `extraRules` 对象 |
| Stylelint | 是 | `DEFAULT_CONFIG` 替换为 recommended + extraRules |
| ts-eslint | 是 | `getDefaultConfig()` 追加 ts-eslint 额外规则 |
| **PMD** | **否** | **仅修改 XML 文件和 JSON 数据,适配器代码不变** |
> PMD 适配器的设计将规则配置完全外部化到 XML 文件,使得规则增强无需修改任何 TypeScript 代码。这是 PMD 适配器的架构优势。
---
## 八、实施步骤
### 步骤 1:修改 `jars/pmd/pmd-java-ruleset.xml`
1.`bestpractices.xml` 添加 9 条 `<exclude>`(弃用规则)
2.`codestyle.xml` 添加 6 条 `<exclude>`2 弃用 + 4 噪音)
3.`design.xml` 添加 12 条 `<exclude>`2 弃用 + 10 噪音)
4.`errorprone.xml` 添加 6 条 `<exclude>`(弃用规则)
5.`multithreading.xml` 添加 3 条 `<exclude>`(噪音规则)
6.`performance.xml` 添加 1 条 `<exclude>`(弃用规则)
7. 在文件末尾追加 `<rule ref="category/java/security.xml"/>`
8. 更新 `<description>` 内容
### 步骤 2:修改 `src/rules/static-rules.json`
1.`rules.pmd` 数组中追加 2 条 Security 规则条目
2.`rules.pmd` 数组中移除 20 条已弃用规则条目(逐条检查是否存在)
3. 更新 `linterVersion.pmd``"7.26.0 (274 Java rules + 12 JSP rules)"`
### 步骤 3:验证
1. 执行 `npm run lint` 确认项目自身代码无新增报错
2. 执行 `npm run compile` 确认编译通过
3. 执行 `npm run build` 确认打包成功
4. 检查 `static-rules.json` 中 PMD Java 规则数量变化(+2 Security-20 弃用)
5. 用测试 Java 文件验证:
- 硬编码密钥触发 `pmd:HardCodedCryptoKey`
- 硬编码 IV 触发 `pmd:InsecureCryptoIv`
- 短变量名不再触发 `pmd:ShortVariable`
- 方法过多不再触发 `pmd:TooManyMethods`
---
## 九、测试要点
### 9.1 单元测试
| 测试项 | 验证内容 |
|--------|----------|
| ruleset XML 格式校验 | XML 格式合法,所有 `<exclude>``name` 属性值正确 |
| Security 规则启用 | ruleset 中包含 `<rule ref="category/java/security.xml"/>` |
| 弃用规则排除 | 20 条弃用规则均在对应分类的 `<exclude>` 列表中 |
| 噪音规则排除 | 17 条噪音规则均在对应分类的 `<exclude>` 列表中 |
| `static-rules.json` Security 规则 | 包含 `pmd/HardCodedCryptoKey``pmd/InsecureCryptoIv` |
| `static-rules.json` 弃用规则移除 | 不包含 `pmd/JUnit4TestShouldUseTestAnnotation` 等已移除的弃用规则 |
### 9.2 集成测试
| 测试项 | 验证内容 |
|--------|----------|
| 硬编码密钥触发 Security 规则 | 诊断结果包含 `pmd:HardCodedCryptoKey` |
| 硬编码 IV 触发 Security 规则 | 诊断结果包含 `pmd:InsecureCryptoIv` |
| 短变量名不触发 `ShortVariable` | `int i = 0;` 不产生 `pmd:ShortVariable` 诊断 |
| 方法过多不触发 `TooManyMethods` | 含 15 个方法的类不产生 `pmd:TooManyMethods` 诊断 |
| 弃用规则不触发 | 代码不产生 `pmd:JUnit4TestShouldUseTestAnnotation` 等弃用规则诊断 |
| 替代规则仍生效 | `switch` 缺少 `default` 触发 `pmd:NonExhaustiveSwitch`(替代 `SwitchStmtsShouldHaveDefault` |
| `AvoidCatchingGenericException` 仍生效 | `catch (Exception e)` 触发 `pmd:AvoidCatchingGenericException`Error Prone 活跃版本) |
| 全局配置存在时忽略内置配置 | 使用用户 ruleset,不触发新规则 |
| JSP 审查不受影响 | JSP 文件审查行为与修改前一致 |
### 9.3 去重测试
| 测试项 | 验证内容 |
|--------|----------|
| 导入 `HardCodedCryptoKey` 自定义规则 | 提示与已有规则重复 |
| 导入 `InsecureCryptoIv` 自定义规则 | 提示与已有规则重复 |
| 导入已移除的弃用规则(如 `JUnit4TestShouldUseTestAnnotation`) | 不提示重复(已从 static-rules.json 移除) |
---
## 十、风险评估
### 10.1 主要风险
| 风险 | 级别 | 缓解措施 |
|------|------|----------|
| 用户依赖被排除的噪音规则 | 低 | 噪音规则仍在 `static-rules.json` 中,用户可通过自定义 ruleset 启用并配置阈值 |
| 用户依赖被排除的弃用规则 | 低 | 替代规则(活跃版本)自动生效,功能等价。如确需旧名称,可通过自定义 ruleset 引用 |
| PMD 8.0.0 移除弃用规则后 `<exclude>` 失效 | 低 | PMD 8.0.0 升级时移除无效的 `<exclude>` 条目即可,不影响其他规则 |
| Security 规则误报 | 极低 | `HardCodedCryptoKey``InsecureCryptoIv` 检测的是确定性的硬编码模式,误报率极低 |
| 用户未更新 `static-rules.json` 导致去重失效 | 低 | 实施步骤中明确要求同步更新,且有测试用例覆盖 |
### 10.2 回滚方案
如果增强后导致严重问题,回滚步骤:
1.`jars/pmd/pmd-java-ruleset.xml` 恢复为修改前的全分类引用版本(移除所有 `<exclude>` 和 Security 引用)
2.`src/rules/static-rules.json` 恢复为修改前的状态(移除 Security 规则,恢复弃用规则)
3. 恢复 `linterVersion.pmd` 为原值
回滚仅需还原 2 个文件,不涉及代码修改和依赖变更,操作简单安全。
---
## 十一、附录
### 11.1 规则来源参考
- PMD 7.26.0 官方规则文档:https://docs.pmd-code.org/pmd-doc-7.26.0/pmd_rules_java.html
- 推荐规则分析文件:`/workspace/pmd-recommended-rules.md`
- 完整规则清单:`/workspace/pmd-7-rules.md`
- 适配器配置分析:`/workspace/custom-rule-analysis.md`
### 11.2 规则数量统计
| 分类 | 数量 | 操作 | 来源 |
|------|------|------|------|
| P0 当前已启用(保留) | 272 | 保留 | 6 个分类的活跃规则,排除噪音后 |
| P1 新增推荐 | 2 | 新增 | Security 分类 |
| P2 建议排除(弃用) | 20 | 排除 | 5 个分类的已弃用规则 |
| P2 建议排除(噪音) | 17 | 排除 | 3 个分类的误报率高的阈值规则 |
| 不推荐 | 6 | 不启用 | Documentation 分类 |
| **推荐启用合计** | **274** | — | P0 + P1 |
### 11.3 排除规则分类汇总
| 分类 | 弃用排除 | 噪音排除 | 合计排除 | 保留 |
|------|----------|----------|----------|------|
| Best Practices | 9 | 0 | 9 | 60 |
| Code Style | 2 | 4 | 6 | 57 |
| Design | 2 | 10 | 12 | 30 |
| Error Prone | 6 | 0 | 6 | 92 |
| Multithreading | 0 | 3 | 3 | 9 |
| Performance | 1 | 0 | 1 | 24 |
| Security | 0 | 0 | 0 | 2(新增) |
| **合计** | **20** | **17** | **37** | **274** |
### 11.4 JSP 规则集(无变化)
JSP 规则集当前已完整启用 5 个分类共 12 条规则,全部活跃,无弃用规则,无需任何修改。
### 11.5 与其他适配器增强方案对比
| 适配器 | 增强方向 | 修改文件数 | 代码修改 | 依赖修改 | 最终规则数 |
|--------|----------|-----------|----------|----------|-----------|
| ESLint | 从少到多 | 3 | 是(`eslint.ts` | 是(`@eslint/js` | 92 |
| Stylelint | 从少到多 | 3 | 是(`stylelint.ts` | 是(`stylelint-config-recommended` | 68 |
| ts-eslint | 从少到多 | 2 | 是(`eslint.ts` | 否 | ~40 |
| **PMD** | **从粗到精** | **2** | **否** | **否** | **274** |
> PMD 适配器的增强方案修改文件最少(仅 XML 和 JSON),无需修改 TypeScript 代码和 npm 依赖,实施成本最低。这得益于 PMD 将规则配置完全外部化到 XML ruleset 文件的架构设计。
@@ -0,0 +1,705 @@
# SQLFluff 规则增强设计书
## 一、背景与目标
### 1.1 现状
当前 `SqlLintAdapter``src/adapters/sql-lint.ts`)作为外部 CLI 工具适配器,通过 `spawn('sqlfluff', ...)` 调用系统安装的 SQLFluff 4.2.2。其内置默认行为为:
```typescript
// 当前行为:无内置配置文件,无配置时使用 CLI 默认(rules = all
// 方言映射:sql → ansiplsql → postgres
// 配置优先级:全局配置 > 项目 .sqlfluff > CLI 默认(rules = all
```
SQLFluff 的 CLI 默认 `rules = all` 会无差别启用全部 75 条规则,存在以下问题:
- **无差别全启用**`rules = all` 启用 75 条规则,其中包含 7 条不应在代码审查中强制的规则(默认禁用、纯格式化噪音、需项目配置)
- **格式化噪音泛滥**:LT03(操作符换行)、LT04(逗号风格)、LT09(SELECT 目标换行)等纯格式化规则在审查中产生大量低价值诊断,淹没了真正的问题
- **争议性规则干扰**:AL07(禁止表别名)、CV10(引号风格)、RF03(引用一致性)等规则因 `force_enable = False` 默认不生效,且规则本身存在争议,全启用后行为不一致
- **与 ESLint/Stylelint 架构不对齐**ESLint 适配器使用 `@eslint/js` recommended + 精选规则,Stylelint 适配器使用 `stylelint-config-recommended` + 精选规则,而 SQLFluff 适配器无内置精选配置,仅依赖 CLI 默认全启用
### 1.2 目标
将内置配置从"无差别 `rules = all`75 条全启用)"改为"精选规则集(57 条)"
- **P0**:保留全部 32 条 Core 核心规则(通过 `rules = core` 启用)
- **P1**:从 43 条非 Core 规则中精选 25 条高价值规则显式启用
- **P2**:保留 11 条可选规则(方言专用 + 团队偏好),供按需启用
- **排除**:明确排除 7 条不推荐规则(3 条默认禁用 + 3 条格式化噪音 + 1 条需项目配置)
### 1.3 设计原则
> **核心特点:从全到精。** 与 ESLint/Stylelint 适配器"从少到多"recommended 61→92、12→68)的增强方向相反,SQLFluff 适配器是"从全到精"——从无差别 `rules = all`(75 条)精简为精选规则集(57 条),排除噪音规则,提升审查信噪比。
- **不破坏现有配置优先级**:全局配置 > 项目 `.sqlfluff` > 内置配置,三层择一逻辑不变
- **不引入新依赖**SQLFluff 是外部 CLI 工具,无 npm 依赖变更,不需要修改 `package.json`
- **设计内置 .sqlfluff 配置模板**:新增内置配置常量,与 ESLint/Stylelint 三层配置架构对齐
- **同步更新 static-rules.json**:为 75 条规则追加 `tier` 分级标记,保证 UI 展示和去重检测覆盖完整
- **不修改规则 ID 前缀格式**:诊断结果仍使用 `sql-lint:{规则代码}` 格式
---
## 二、影响范围分析
### 2.1 需要修改的文件
| 文件 | 修改类型 | 修改内容 |
|------|----------|----------|
| `src/adapters/sql-lint.ts` | 代码修改 | 新增 `BUILTIN_SQLFLUFF_CONFIG` 内置配置常量,无项目配置时注入精选规则集 |
| `src/rules/static-rules.json` | 数据修改 | 为 75 条 SQLFluff 规则追加 `tier` 分级字段(P0/P1/P2/excluded |
### 2.2 不需要修改的文件
| 文件 | 原因 |
|------|------|
| `package.json` | SQLFluff 是外部 CLI 工具,非 npm 依赖,无依赖变更 |
| `scripts/build.mjs` | 不涉及构建配置变更,内置配置为纯字符串常量 |
| `src/orchestrator/orchestrator.ts` | 仅做调度,不涉及配置逻辑 |
| `src/config/linter.ts` | 配置读取层不变,三层优先级逻辑不变 |
| `src/adapters/eslint.ts` / `stylelint.ts` | 独立适配器,互不影响 |
| `src/adapters/jsp.ts` | 复用 ESLint 适配器,与 SQLFluff 无关 |
### 2.3 不受影响的功能
- 全局配置(用户指定的 `sqlfluff` 配置路径):存在时完全替代内置配置,不受影响
- 项目配置(`.sqlfluff`):存在时完全替代内置配置,不受影响
- 方言映射(`sql → ansi``plsql → postgres`):不变
- 规则 ID 输出格式:仍为 `sql-lint:{规则代码}`,不变
- `sqlfluff fix` 自动修复能力:不变,精选规则集中 43 条(75%)仍支持自动修复
---
## 三、详细设计
### 3.1 修改 `src/adapters/sql-lint.ts`:新增内置配置
#### 3.1.1 当前行为
当前适配器在无项目 `.sqlfluff` 配置文件时,不传递任何 `--rules``--config` 参数,完全依赖 SQLFluff CLI 的默认行为(`rules = all`75 条全启用):
```typescript
// 伪代码:当前 spawn 调用逻辑
function buildSqlfluffArgs(filePath: string, dialect: string): string[] {
const args: string[] = ['lint', '--format', 'json'];
// 方言映射
args.push('--dialect', dialect === 'plsql' ? 'postgres' : 'ansi');
// 无内置 rules 配置 —— 依赖 CLI 默认(rules = all
// 若存在项目 .sqlfluffSQLFluff 自动发现并使用
args.push(filePath);
return args;
}
```
#### 3.1.2 修改后:新增内置配置常量
```typescript
// 新增:内置精选 .sqlfluff 配置模板(从全到精:rules = all → 57 条精选)
// P032 条 Core+ P125 条精选非 Core= 57 条
const BUILTIN_SQLFLUFF_CONFIG = `[sqlfluff]
rules = core,AM03,AM05,AM08,CV01,CV02,CV06,CV08,CV12,LT13,LT14,LT15,ST01,ST02,ST04,ST05,ST06,ST07,ST09,ST10,ST11,ST12,RF02,RF04,RF05,RF06
dialect = ansi
max_line_length = 80
indent_unit = space
tab_space_size = 4
`;
// 新增:判断是否存在项目 .sqlfluff 配置
function hasProjectSqlfluffConfig(workspaceRoot: string): boolean {
// 检查 workspaceRoot 下是否存在 .sqlfluff 或 .sqlfluff.ini
// ...
}
// 修改后的 spawn 调用逻辑
function buildSqlfluffArgs(
filePath: string,
dialect: string,
workspaceRoot: string,
globalConfigPath?: string
): { args: string[]; tempConfigPath?: string } {
const args: string[] = ['lint', '--format', 'json'];
args.push('--dialect', dialect === 'plsql' ? 'postgres' : 'ansi');
// 三层配置优先级:全局 > 项目 > 内置
if (globalConfigPath) {
// 1. 全局配置:用户指定路径,完全替代内置配置
args.push('--config', globalConfigPath);
} else if (hasProjectSqlfluffConfig(workspaceRoot)) {
// 2. 项目配置:.sqlfluff 存在,让 SQLFluff 自动发现(不传 --config
// 不需要额外参数
} else {
// 3. 内置配置(新增):写入临时 .sqlfluff 文件,传递精选规则集
const tempConfigPath = path.join(
os.tmpdir(),
`sqlfluff-builtin-${Date.now()}.cfg`
);
fs.writeFileSync(tempConfigPath, BUILTIN_SQLFLUFF_CONFIG);
args.push('--config', tempConfigPath);
return { args, tempConfigPath };
}
args.push(filePath);
return { args };
}
```
#### 3.1.3 设计说明
**为什么用临时配置文件而非 `--rules` 参数?**
SQLFluff CLI 支持 `--rules "core,AM03,..."` 参数直接指定规则列表。但使用临时 `.sqlfluff` 配置文件更优:
- **支持 per-rule 配置**`--rules` 仅能指定规则列表,无法配置 `[sqlfluff:rules:CP01]` 等规则参数(如大小写偏好)。临时配置文件支持完整 INI 配置
- **与三层配置架构对齐**ESLint/Stylelint 的内置配置也是完整配置对象,临时配置文件使 SQLFluff 的内置层与之对称
- **可扩展性**:未来需调整规则参数(如行长度、缩进)时,只需修改 `BUILTIN_SQLFLUFF_CONFIG` 字符串
**为什么用 `dialect = ansi` 而非动态方言?**
`BUILTIN_SQLFLUFF_CONFIG` 中的 `dialect = ansi` 是默认值。实际执行时,`buildSqlfluffArgs` 已通过 `--dialect` 参数覆盖方言(`plsql → postgres`),`--dialect` 命令行参数优先于配置文件中的 `dialect` 设置。配置文件中保留 `dialect = ansi` 仅作为文档性默认值。
**为什么内置配置不包含方言专用规则(OR01/PG01/TQ01-03)?**
方言专用规则仅对特定方言生效。当前适配器方言映射为 `sql → ansi``plsql → postgres`,故内置配置仅覆盖通用规则。方言专用规则作为 P2 可选项,由用户在项目 `.sqlfluff` 中按实际方言追加。
### 3.2 修改 `src/rules/static-rules.json`:追加分级标记
#### 3.2.1 修改内容
SQLFluff 的 75 条规则已全部收录在 `static-rules.json` 中。本次修改不新增规则条目(与 ESLint/Stylelint 不同——那两者是追加新规则),而是为每条规则追加 `tier` 分级字段,标记其在推荐体系中的归属。
```json
// 修改前
{"id": "sql-lint/AL02", "description": "Column aliasing using explicit AS"}
// 修改后:追加 tier 字段
{"id": "sql-lint/AL02", "description": "Column aliasing using explicit AS", "tier": "P0"}
{"id": "sql-lint/AM03", "description": "ORDER BY clause direction ambiguity", "tier": "P1"}
{"id": "sql-lint/AL01", "description": "Implicit/explicit table aliasing", "tier": "P2"}
{"id": "sql-lint/AL07", "description": "Forbid table aliases in FROM/JOIN", "tier": "excluded"}
```
#### 3.2.2 分级标记分配
| tier 值 | 含义 | 规则数 | 启用状态 |
|---------|------|--------|----------|
| `P0` | Core 核心规则 | 32 | 内置配置启用(`rules = core` |
| `P1` | 精选非 Core 高价值规则 | 25 | 内置配置启用(显式追加) |
| `P2` | 可选规则(方言专用/团队偏好) | 11 | 内置配置不启用,按需追加 |
| `excluded` | 不推荐规则(禁用/噪音/需配置) | 7 | 内置配置排除 |
| **合计** | — | **75** | — |
#### 3.2.3 修改 `linterVersion` 字段
```json
"linterVersion": {
"sql-lint": "4.2.2 (57 recommended)",
...
}
```
将原标记改为 `"4.2.2 (57 recommended)"`,反映精选规则集数量。`static-rules.json` 中规则总数仍为 75 条(全部收录,仅分级不同)。
#### 3.2.4 为什么必须同步更新 static-rules.json
`static-rules.json` 有两个用途:
1. **UI 展示**:在设置面板中展示当前 linter 支持的规则清单,追加 `tier` 后可展示分级标识
2. **去重检测**:自定义规则导入时,按 ID 匹配 `static-rules.json` 中的规则。75 条规则已全部收录,去重检测范围不变,但分级标记使 UI 能区分"推荐/可选/排除"
与 ESLint/Stylelint 不同(那两者是因新增规则而必须更新去重数据),SQLFluff 的更新是**分级标记追加**而非规则新增,去重数据本身已完整。
### 3.3 不需要修改 `package.json`
SQLFluff 是通过 `spawn('sqlfluff', ...)` 调用的外部 CLI 工具,不是 npm 依赖。内置配置是纯字符串常量,不引入任何新依赖。这是 SQLFluff 适配器与 ESLint/Stylelint 适配器在依赖管理上的本质区别:
| 适配器 | 依赖管理 | 配置注入方式 |
|--------|----------|-------------|
| ESLint | npm 依赖(`@eslint/js` | Flat Config 数组对象 |
| Stylelint | npm 依赖(`stylelint-config-recommended` | 配置对象展开 |
| SQLFluff | 外部 CLI(无 npm 依赖) | 临时 `.sqlfluff` 配置文件 / `--config` 参数 |
---
## 四、新增/排除规则分类详解
### 4.1 设计方向:从全到精
SQLFluff 增强与其他 linter 的根本差异:
| 维度 | ESLint/Stylelint | SQLFluff |
|------|------------------|----------|
| 增强方向 | 从少到多(recommended → 更多规则) | **从全到精**all → 精选规则集) |
| 当前状态 | 启用少量规则(61/12 条) | 无差别全启用(75 条) |
| 增强动作 | 追加新规则 | 精选保留 + 排除噪音 |
| 规则数变化 | 61→92+31)、12→68+56 | 75→57-18,排除噪音) |
| static-rules.json | 追加新规则条目 | 规则不变,追加分级标记 |
### 4.2 P0 Core 核心规则(32 条,全部保留)
Core 规则是 SQLFluff 官方标记的稳定、通用、非争议性规则。全部保留,通过 `rules = core` 关键字启用。
| 分组 | 规则代码 | 数量 |
|------|----------|------|
| Aliasing | AL02, AL03, AL04, AL05, AL06, AL08, AL09, AL10 | 8 |
| Ambiguous | AM01, AM02, AM06 | 3 |
| Capitalisation | CP01, CP02, CP03, CP04, CP05 | 5 |
| Convention | CV03, CV04, CV05 | 3 |
| Jinja | JJ01 | 1 |
| Layout | LT01, LT02, LT05, LT06, LT07, LT08, LT10, LT11, LT12 | 9 |
| Structure | ST03, ST08 | 2 |
| References | RF01 | 1 |
| **合计** | — | **32** |
> **说明**RF01 虽标记 `force_enable = False`(对 BigQuery 等方言默认禁用),但它是 Core 规则,对 ansi/postgres 方言默认生效,故保留在 P0。
### 4.3 P1 精选非 Core 规则(25 条,从 43 条中精选)
从 43 条非 Core 规则中精选 25 条高价值规则,按问题类型分组:
#### 4.3.1 歧义与连接(3 条)
| 规则 | 检测场景 | 误报评估 |
|------|----------|----------|
| `AM03` | ORDER BY 混合 ASC/DESC 时方向不可预期 | 低,显式指定方向是最佳实践 |
| `AM05` | `JOIN` 未明确连接类型(应为 INNER/LEFT 等) | 低,完全限定连接类型消除歧义 |
| `AM08` | 隐式 CROSS JOIN | 低,隐式交叉连接几乎都是意外 |
#### 4.3.2 约定一致性(5 条)
| 规则 | 检测场景 | 误报评估 |
|------|----------|----------|
| `CV01` | `!=``<>` 混用 | 零误报,统一运算符 |
| `CV02` | 使用 IFNULL/NVL 代替 COALESCE | 零误报,COALESCE 是标准 SQL |
| `CV06` | 语句缺少分号结尾 | 低,分号是语句终止符 |
| `CV08` | 使用 RIGHT JOIN | 低,LEFT JOIN 更易读 |
| `CV12` | 连接条件放在 WHERE 而非 ON | 低,ON 子句语义更清晰 |
#### 4.3.3 结构优化(10 条,核心价值)
| 规则 | 检测场景 | 误报评估 |
|------|----------|----------|
| `ST01` | CASE 中冗余的 ELSE NULL | 零误报 |
| `ST02` | 可简化的不必要 CASE | 低 |
| `ST04` | ELSE 中嵌套 CASE 可展平 | 低 |
| `ST05` | JOIN/FROM 含子查询(应提为 CTE) | 中,但 CTE 显著提升可维护性 |
| `ST06` | SELECT 列顺序不规范 | 低 |
| `ST07` | 使用 USING 而非显式连接键 | 低 |
| `ST09` | JOIN 列顺序不规范 | 低 |
| `ST10` | WHERE 1=1 等冗余常量条件 | 零误报 |
| `ST11` | JOIN 的表未被引用(死连接) | 零误报 |
| `ST12` | 连续分号 | 零误报 |
#### 4.3.4 引用规范(4 条)
| 规则 | 检测场景 | 误报评估 |
|------|----------|----------|
| `RF02` | 多表查询时引用未限定表名 | 低,限定表名消除歧义 |
| `RF04` | 将关键字用作标识符 | 零误报 |
| `RF05` | 标识符含特殊字符 | 低 |
| `RF06` | 不必要的引号标识符 | 低 |
#### 4.3.5 布局整洁(3 条)
| 规则 | 检测场景 | 误报评估 |
|------|----------|----------|
| `LT13` | 文件以空白开头 | 零误报 |
| `LT14` | 关键字换行位置不统一 | 低 |
| `LT15` | 连续空行过多 | 低 |
> **与排除的格式化规则的区别**LT13/LT14/LT15 关注文件结构和空行整洁,具有实际可读性价值;而被排除的 LT03/LT04/LT09 是纯风格偏好(操作符位置、逗号风格、SELECT 换行),属格式化范畴。
### 4.4 P2 可选规则(11 条,不纳入推荐基线)
#### 4.4.1 方言专用规则(5 条)
| 规则 | 适用方言 | 说明 |
|------|----------|------|
| `OR01` | Oracle | 移除空批次 |
| `PG01` | PostgreSQL | 避免过度锁(对 plsql→postgres 映射有意义) |
| `TQ01` | T-SQL | 存储过程不用 SP_ 前缀 |
| `TQ02` | T-SQL | 过程体用 BEGIN/END |
| `TQ03` | T-SQL | 移除空批次 |
> 当前适配器仅映射 `ansi`/`postgres` 两种方言,PG01 可按需启用,其余需用户切换方言。
#### 4.4.2 团队偏好规则(6 条)
| 规则 | 检测场景 | 可选理由 |
|------|----------|----------|
| `AL01` | 表别名要求显式 AS | 与 AL02 对称,但表别名风格因团队而异 |
| `AM04` | SELECT * 与其他列混合 | 检测结果列数不可预测,但部分场景 SELECT * 合理 |
| `AM07` | 集合查询子查询列数不同 | 检测集合查询错误,但场景较少 |
| `AM09` | 无 ORDER BY 时用 LIMIT/OFFSET | 检测非确定性结果,但部分场景可接受 |
| `CV07` | 顶层语句被括号包裹 | 多余括号,但部分方言需要 |
| `CV11` | 类型转换风格不一致 | CAST/::/CONVERT 风格统一,但方言偏好不同 |
### 4.5 排除规则(7 条,明确不推荐)
| 规则 | 排除分类 | 排除理由 |
|------|----------|----------|
| `AL07` | 默认禁用 | 禁止所有表别名过于激进,force_enable = False |
| `CV10` | 默认禁用 | 引号风格因方言而异,force_enable = False |
| `RF03` | 默认禁用 | 列引用限定一致性争议大,force_enable = False |
| `LT03` | 格式化噪音 | 操作符换行位置是纯风格偏好 |
| `LT04` | 格式化噪音 | 前导/尾随逗号是团队风格选择 |
| `LT09` | 格式化噪音 | SELECT 目标换行是格式偏好 |
| `CV09` | 需项目配置 | 需自定义禁止词列表,无通用默认 |
---
## 五、规则级别设计
### 5.1 级别设计原则:精选而非分级
SQLFluff 与 ESLint/Stylelint 的级别设计有本质区别:
| 维度 | ESLint/Stylelint | SQLFluff |
|------|------------------|----------|
| 级别机制 | per-rule `error`/`warn`ESLint)或 `true`Stylelint | 无 per-rule error/warn,仅启用/排除 |
| 级别设计核心 | 区分 error 与 warn | **精选规则集**(从全到精) |
| 噪音控制方式 | 低价值规则设为 warn | 低价值规则直接排除 |
SQLFluff 不支持像 ESLint 那样的 per-rule `error`/`warn` 级别配置。因此,SQLFluff 适配器的"级别设计"通过**规则集精选**实现:将高价值规则纳入推荐基线,将噪音规则排除,从源头控制审查信噪比。
### 5.2 P0/P1/P2 分级与启用策略
| 分级 | 启用策略 | 规则数 | 严重程度定位 |
|------|----------|--------|-------------|
| P0 | `rules = core` 自动启用 | 32 | 高(核心稳定性问题) |
| P1 | 显式追加规则代码 | 25 | 高(歧义/结构/引用问题) |
| P2 | 不纳入推荐基线,按需追加 | 11 | 中(方言/偏好) |
| excluded | 不启用 | 7 | —(噪音/争议/需配置) |
### 5.3 适配器层面的严重级别映射
虽然 SQLFluff 无 per-rule severity,但适配器在解析 CLI 输出后,可基于 `static-rules.json` 中的 `tier` 字段映射到 VS Code 的 `DiagnosticSeverity`
| tier | VS Code DiagnosticSeverity | 说明 |
|------|---------------------------|------|
| P0 | Error | 核心规则触发,几乎一定是问题 |
| P1 | Error | 高价值规则触发,强烈建议修复 |
| P2 | Warning(若启用) | 可选规则触发,建议但不强制 |
| excluded | — | 不触发(规则未启用) |
> 此映射为可选增强。当前适配器将所有 SQLFluff 诊断映射为统一严重级别,未来可按 tier 细化。
### 5.4 自动修复能力统计
| 分级 | 规则数 | 可自动修复 | 自动修复率 |
|------|--------|-----------|-----------|
| P0 Core | 32 | 24 | 75% |
| P1 精选 | 25 | 19 | 76% |
| **推荐基线合计** | **57** | **43** | **75%** |
| P2 可选 | 11 | 7 | 64% |
| excluded | 7 | 6 | 86% |
> 推荐基线 57 条规则中 43 条(75%)支持 `sqlfluff fix` 自动修复。排除的 7 条规则中虽有 6 条可修复,但因属噪音/争议规则,修复后仍会引入风格争议,故排除。
---
## 六、兼容性分析
### 6.1 对现有用户代码的影响
`rules = all`(75 条)精简为 57 条精选规则集后,**审查诊断数量将减少**(而非增加,与 ESLint/Stylelint 相反):
| 影响程度 | 规则 | 说明 |
|----------|------|------|
| **诊断减少** | LT03, LT04, LT09 | 排除格式化噪音,减少大量低价值诊断 |
| **诊断减少** | AL07, CV10, RF03 | 排除默认禁用规则(原本因 force_enable 不生效) |
| **诊断减少** | CV09 | 排除需项目配置的规则 |
| **诊断不变** | P0 + P157 条) | 推荐基线规则与 all 模式下行为一致 |
| **可能新增(可选)** | P2 方言规则 | 用户按需启用后,对应方言新增诊断 |
> **核心收益**:排除格式化噪音规则后,审查结果信噪比显著提升,真正的问题不再被格式化诊断淹没。
### 6.2 对配置优先级的影响
三层配置优先级不变:
```
全局配置(linters.sqlfluffConfigPath
↓ 不存在
项目配置(.sqlfluff / .sqlfluff.ini
↓ 不存在
内置配置(BUILTIN_SQLFLUFF_CONFIG57 条精选) ← 新增
↓ (此前为 CLI 默认 rules = all
```
新增内置配置层替代了原先的"CLI 默认"层,三层择一逻辑不变。
### 6.3 对去重功能的影响
`static-rules.json` 中 SQLFluff 规则仍为 75 条(全部收录),去重检测范围不变。新增的 `tier` 字段不影响去重匹配逻辑,仅用于 UI 分级展示。
### 6.4 对自动修复的影响
- 推荐基线 57 条规则中 43 条支持 `sqlfluff fix`
- 适配器的自动修复调用不受影响(仍通过 `sqlfluff fix` 命令)
- 排除的格式化规则原本可修复,但修复属风格偏好,排除后用户可自行用格式化工具处理
### 6.5 方言兼容性
| 方言映射 | 内置配置适用性 | 方言专用规则 |
|----------|---------------|-------------|
| `sql → ansi` | 完全适用(57 条均为通用规则) | 无 |
| `plsql → postgres` | 完全适用 | PG01 可选启用 |
---
## 七、与 ESLint/Stylelint/ts-eslint 适配器的架构对比
### 7.1 架构对比总览
| 维度 | ESLint 适配器 | Stylelint 适配器 | SQLFluff 适配器 |
|------|--------------|------------------|-----------------|
| Linter 类型 | npm 库(进程内调用) | npm 库(进程内调用) | 外部 CLI(spawn 调用) |
| 增强方向 | 从少到多(61→92) | 从少到多(12→68) | **从全到精(75→57** |
| 官方配置基线 | `@eslint/js` recommended | `stylelint-config-recommended` | `rules = core` 关键字 |
| 额外规则来源 | `eslint` 内置规则 | `stylelint` 内置规则 | SQLFluff 内置规则(CLI |
| 配置注入方式 | Flat Config 数组追加 | 配置对象展开 | **临时 .sqlfluff 文件 + --config** |
| 级别支持 | `error`/`warn` | `true`(仅 error | 无 per-rule 级别(启用/排除) |
| 噪音控制 | 低价值规则设为 warn | 全部 true | **低价值规则直接排除** |
| npm 依赖变更 | 新增 `@eslint/js` | 新增 `stylelint-config-recommended` | **无(外部 CLI** |
| static-rules.json 变更 | 追加 31 条新规则 | 追加 27 条新规则 | **规则不变,追加 tier 标记** |
| 配置优先级 | 全局 > 项目 > 内置 | 全局 > 项目 > 内置 | 全局 > 项目 > 内置 |
| 规则 ID 格式 | `eslint:{ruleId}` | `stylelint:{ruleName}` | `sql-lint:{规则代码}` |
### 7.2 关键差异分析
#### 差异一:增强方向相反
- ESLint/Stylelint:当前启用规则过少(31%/8.5%),增强方向是**追加规则**提升覆盖率
- SQLFluff:当前无差别全启用(100%),增强方向是**精选规则**提升信噪比
这决定了 SQLFluff 的增强不是"做加法"而是"做减法"——从 75 条精简为 57 条,排除 18 条低价值/噪音规则。
#### 差异二:配置注入方式不同
- ESLint/Stylelintnpm 库,配置为 JS 对象/数组,进程内直接传入
- SQLFluff:外部 CLI,配置需写入临时文件并通过 `--config` 参数传递
SQLFluff 适配器需额外处理临时配置文件的生命周期(创建、传递、清理)。
#### 差异三:无 per-rule 严重级别
- ESLint 支持 `error`/`warn` 两级,Stylelint 支持 `true`/`null`
- SQLFluff 仅支持启用/排除,无 per-rule 严重级别
因此 SQLFluff 的"级别设计"通过**规则集精选**实现,而非 error/warn 分配。低价值规则直接排除,而非降级为 warning。
#### 差异四:static-rules.json 变更性质不同
- ESLint/Stylelint:新增规则条目(去重数据扩展)
- SQLFluff:规则条目不变(75 条已全收录),追加 `tier` 分级标记
### 7.3 架构一致性
尽管实现方式不同,三个适配器在**设计理念**上保持一致:
| 设计理念 | ESLint | Stylelint | SQLFluff |
|----------|--------|-----------|----------|
| 官方配置作为基线 | recommended | recommended | core |
| 精选额外规则 | extraRules 对象 | extraRules 对象 | rules 显式追加 |
| 三层配置优先级 | 全局 > 项目 > 内置 | 全局 > 项目 > 内置 | 全局 > 项目 > 内置 |
| 同步更新 static-rules.json | 是(追加规则) | 是(追加规则) | 是(追加 tier) |
| 规则 ID 前缀不变 | `eslint:` | `stylelint:` | `sql-lint:` |
---
## 八、实施步骤
### 步骤 1:修改 `src/adapters/sql-lint.ts`
1. 在文件顶部添加 `BUILTIN_SQLFLUFF_CONFIG` 常量(精选 57 条规则的 INI 配置字符串)
2. 添加 `hasProjectSqlfluffConfig()` 辅助函数,检测项目 `.sqlfluff` 配置文件
3. 修改 `buildSqlfluffArgs()`(或等效的参数构建逻辑),实现三层配置优先级:
- 全局配置存在 → 传递 `--config <全局路径>`
- 项目配置存在 → 不传 `--config`,让 SQLFluff 自动发现
- 均不存在 → 写入临时配置文件,传递 `--config <临时路径>`
4. 在 spawn 完成后(或 finally 块中)清理临时配置文件
### 步骤 2:修改 `src/rules/static-rules.json`
1. 为 75 条 SQLFluff 规则条目追加 `tier` 字段(P0/P1/P2/excluded
2. 更新 `linterVersion.sql-lint``"4.2.2 (57 recommended)"`
### 步骤 3:验证
1. 确认无项目 `.sqlfluff` 时,临时配置文件正确生成并被 SQLFluff 使用
2. 确认有项目 `.sqlfluff` 时,内置配置不生效(项目配置优先)
3. 确认全局配置路径存在时,内置配置不生效(全局配置优先)
4. 确认临时配置文件在 spawn 完成后被清理
5. 检查 `static-rules.json` 中 75 条规则均有 `tier` 字段
---
## 九、测试要点
### 9.1 单元测试
| 测试项 | 验证内容 |
|--------|----------|
| `BUILTIN_SQLFLUFF_CONFIG` 内容 | 包含 `rules = core` 及 25 条 P1 规则代码 |
| `BUILTIN_SQLFLUFF_CONFIG` 规则数 | core32+ 显式追加(25= 57 条 |
| 临时配置文件生成 | 无项目配置时正确写入临时文件 |
| 临时配置文件清理 | spawn 完成后临时文件被删除 |
| 三层优先级 | 全局 > 项目 > 内置,择一逻辑正确 |
### 9.2 集成测试
| 测试项 | 验证内容 |
|--------|----------|
| 无配置时使用精选规则集 | 诊断结果仅包含 57 条推荐规则,不包含 LT03/LT04/LT09 |
| 项目 `.sqlfluff` 存在时使用项目配置 | 诊断结果遵循项目配置,内置配置不生效 |
| 全局配置存在时使用全局配置 | 诊断结果遵循全局配置,内置配置不生效 |
| `WHERE 1=1` 触发 `ST10` | 诊断结果包含 `sql-lint:ST10` |
| `CASE ... ELSE NULL END` 触发 `ST01` | 诊断结果包含 `sql-lint:ST01` |
| 未使用的 CTE 触发 `ST03` | 诊断结果包含 `sql-lint:ST03` |
| `= NULL` 触发 `CV05` | 诊断结果包含 `sql-lint:CV05` |
| 格式化噪音规则不触发 | LT03/LT04/LT09 不产生诊断 |
### 9.3 去重测试
| 测试项 | 验证内容 |
|--------|----------|
| 导入 `ST05` 自定义规则 | 提示与已有规则重复(75 条已收录) |
| 导入 `CV05` 自定义规则 | 提示与已有规则重复 |
| `tier` 字段不影响去重 | 去重匹配逻辑仅按 ID,与 tier 无关 |
### 9.4 自动修复测试
| 测试项 | 验证内容 |
|--------|----------|
| `sqlfluff fix` 修复 ST01 | `ELSE NULL` 被移除 |
| `sqlfluff fix` 修复 CP01 | 关键字大小写统一 |
| `sqlfluff fix` 修复 CV05 | `= NULL` 改为 `IS NULL` |
| 排除规则不被修复 | LT03/LT04/LT09 不触发修复 |
---
## 十、风险评估
### 10.1 主要风险
| 风险 | 级别 | 缓解措施 |
|------|------|----------|
| 临时配置文件未清理导致残留 | 中 | 在 finally 块中确保清理,使用 `os.tmpdir()` 避免污染工作区 |
| 用户依赖 `rules = all` 的全量诊断 | 中 | 用户可通过项目 `.sqlfluff` 设置 `rules = all` 恢复原行为 |
| 精选规则集遗漏用户需要的高价值规则 | 低 | P2 可选规则可按需追加,用户可自行在项目配置中启用 |
| `--config` 参数与 `--dialect` 参数冲突 | 低 | `--dialect` 命令行参数优先级高于配置文件,已验证不冲突 |
| SQLFluff 版本升级导致规则代码变化 | 低 | 锁定 SQLFluff 4.2.2,规则代码在 4.x 内稳定 |
### 10.2 与 ESLint/Stylelint 的风险差异
| 风险类型 | ESLint/Stylelint | SQLFluff |
|----------|------------------|----------|
| 诊断数量变化 | **增加**(新增规则) | **减少**(排除噪音) |
| 用户感知 | 可能新增大量诊断 | 诊断减少,信噪比提升 |
| 回滚复杂度 | 需移除新增规则和依赖 | 仅需移除内置配置注入,恢复 CLI 默认 |
SQLFluff 的增强风险显著低于 ESLint/Stylelint:诊断数量减少而非增加,不会引入用户未曾见过的诊断,仅是减少噪音。
### 10.3 回滚方案
如果精选规则集导致问题,回滚步骤:
1.`buildSqlfluffArgs()` 中移除内置配置注入逻辑,恢复"无配置时使用 CLI 默认(rules = all"
2. 删除 `BUILTIN_SQLFLUFF_CONFIG` 常量
3. `static-rules.json` 中的 `tier` 字段可保留(不影响功能,仅用于 UI 展示)
4. 恢复 `linterVersion.sql-lint``"4.2.2 (75 rules, all)"`
回滚仅需修改适配器代码,无需修改依赖(因本就无 npm 依赖变更)。
---
## 十一、附录
### 11.1 规则来源参考
- SQLFluff 官方规则文档:https://docs.sqlfluff.com/en/stable/reference/rules.html
- SQLFluff GitHub 源码:https://github.com/sqlfluff/sqlfluff/tree/4.2.2/src/sqlfluff/rules
- 推荐规则分析文件:`/workspace/sqlfluff-recommended-rules.md`
- 完整规则清单:`/workspace/sqlfluff-rules.md`
- 适配器配置分析:`/workspace/custom-rule-analysis.md`
### 11.2 规则数量统计
| 分类 | 数量 | tier | 启用状态 |
|------|------|------|----------|
| P0 Core 核心规则 | 32 | P0 | 内置配置启用 |
| P1 精选非 Core | 25 | P1 | 内置配置启用 |
| P2 方言专用 | 5 | P2 | 按需启用 |
| P2 团队偏好 | 6 | P2 | 按需启用 |
| excluded 默认禁用 | 3 | excluded | 排除 |
| excluded 格式化噪音 | 3 | excluded | 排除 |
| excluded 需项目配置 | 1 | excluded | 排除 |
| **合计** | **75** | — | 推荐基线 57 条 |
### 11.3 内置配置模板(完整)
```ini
[sqlfluff]
# 从全到精:rules = all(75 条)→ 精选规则集(57 条)
# P032 条 Core+ P125 条精选非 Core
rules = core,AM03,AM05,AM08,CV01,CV02,CV06,CV08,CV12,LT13,LT14,LT15,ST01,ST02,ST04,ST05,ST06,ST07,ST09,ST10,ST11,ST12,RF02,RF04,RF05,RF06
dialect = ansi
max_line_length = 80
indent_unit = space
tab_space_size = 4
```
### 11.4 P1 精选规则完整列表(25 条)
| # | 规则代码 | 规则名 | 分组 | 可修复 |
|---|----------|--------|------|--------|
| 1 | `AM03` | `ambiguous.order_by` | Ambiguous | Fix |
| 2 | `AM05` | `ambiguous.join` | Ambiguous | Fix |
| 3 | `AM08` | `ambiguous.join_condition` | Ambiguous | Fix |
| 4 | `CV01` | `convention.not_equal` | Convention | Fix |
| 5 | `CV02` | `convention.coalesce` | Convention | Fix |
| 6 | `CV06` | `convention.terminator` | Convention | Fix |
| 7 | `CV08` | `convention.left_join` | Convention | — |
| 8 | `CV12` | `convention.join_condition` | Convention | Fix |
| 9 | `LT13` | `layout.start_of_file` | Layout | Fix |
| 10 | `LT14` | `layout.keyword_newline` | Layout | Fix |
| 11 | `LT15` | `layout.newlines` | Layout | Fix |
| 12 | `ST01` | `structure.else_null` | Structure | Fix |
| 13 | `ST02` | `structure.simple_case` | Structure | Fix |
| 14 | `ST04` | `structure.nested_case` | Structure | Fix |
| 15 | `ST05` | `structure.subquery` | Structure | Fix |
| 16 | `ST06` | `structure.column_order` | Structure | Fix |
| 17 | `ST07` | `structure.using` | Structure | Fix |
| 18 | `ST09` | `structure.join_condition_order` | Structure | Fix |
| 19 | `ST10` | `structure.constant_expression` | Structure | — |
| 20 | `ST11` | `structure.unused_join` | Structure | — |
| 21 | `ST12` | `structure.consecutive_semicolons` | Structure | Fix |
| 22 | `RF02` | `references.qualification` | References | — |
| 23 | `RF04` | `references.keywords` | References | — |
| 24 | `RF05` | `references.special_chars` | References | — |
| 25 | `RF06` | `references.quoting` | References | Fix |
### 11.5 排除规则完整列表(7 条)
| # | 规则代码 | 规则名 | 排除分类 | 原标记 |
|---|----------|--------|----------|--------|
| 1 | `AL07` | `aliasing.forbid` | 默认禁用 | Fix / 禁用 |
| 2 | `CV10` | `convention.quoted_literals` | 默认禁用 | Fix / 禁用 |
| 3 | `RF03` | `references.consistent` | 默认禁用 | Fix / 禁用 |
| 4 | `LT03` | `layout.operators` | 格式化噪音 | Fix |
| 5 | `LT04` | `layout.commas` | 格式化噪音 | Fix |
| 6 | `LT09` | `layout.select_targets` | 格式化噪音 | Fix |
| 7 | `CV09` | `convention.blocked_words` | 需项目配置 | — |
### 11.6 与其他适配器增强对比总结
| 对比项 | ESLint | Stylelint | SQLFluff |
|--------|--------|-----------|----------|
| 增强方向 | 从少到多 | 从少到多 | **从全到精** |
| 规则数变化 | 61 → 92+31 | 12 → 68+56 | 75 → 57-18 |
| 增强动作 | 追加规则 | 追加规则 + 引入配置包 | **精选保留 + 排除噪音** |
| 诊断影响 | 增加 | 增加 | **减少(信噪比提升)** |
| npm 依赖 | 新增 `@eslint/js` | 新增 `stylelint-config-recommended` | 无 |
| 配置注入 | Flat Config 对象 | 配置对象展开 | 临时配置文件 |
| static-rules.json | 追加新规则 | 追加新规则 | 追加 tier 标记 |
| 风险等级 | 中(诊断增加) | 中(诊断增加) | **低(诊断减少)** |
+58 -7
View File
@@ -3,11 +3,62 @@
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>Java Code Review Rules</description>
<rule ref="category/java/bestpractices.xml"/>
<rule ref="category/java/codestyle.xml"/>
<rule ref="category/java/design.xml"/>
<rule ref="category/java/errorprone.xml"/>
<rule ref="category/java/performance.xml"/>
<description>Java Code Review Rules (274 active rules, 7 categories)</description>
<rule ref="category/java/bestpractices.xml">
<exclude name="DefaultLabelNotLastInSwitchStmt"/>
<exclude name="JUnit4TestShouldUseAfterAnnotation"/>
<exclude name="JUnit4TestShouldUseBeforeAnnotation"/>
<exclude name="JUnit4TestShouldUseTestAnnotation"/>
<exclude name="JUnit5TestShouldBePackagePrivate"/>
<exclude name="JUnitAssertionsShouldIncludeMessage"/>
<exclude name="JUnitTestContainsTooManyAsserts"/>
<exclude name="JUnitTestsShouldIncludeAssert"/>
<exclude name="SwitchStmtsShouldHaveDefault"/>
</rule>
<rule ref="category/java/codestyle.xml">
<exclude name="GenericsNaming"/>
<exclude name="UnnecessaryLocalBeforeReturn"/>
<exclude name="LongVariable"/>
<exclude name="ShortVariable"/>
<exclude name="ShortMethodName"/>
<exclude name="ShortClassName"/>
</rule>
<rule ref="category/java/design.xml">
<exclude name="AvoidCatchingGenericException"/>
<exclude name="UseObjectForClearerAPI"/>
<exclude name="TooManyMethods"/>
<exclude name="CyclomaticComplexity"/>
<exclude name="NPathComplexity"/>
<exclude name="CognitiveComplexity"/>
<exclude name="NcssCount"/>
<exclude name="TooManyFields"/>
<exclude name="ExcessiveParameterList"/>
<exclude name="ExcessivePublicCount"/>
<exclude name="ExcessiveImports"/>
<exclude name="CouplingBetweenObjects"/>
</rule>
<rule ref="category/java/errorprone.xml">
<exclude name="AvoidCatchingNPE"/>
<exclude name="AvoidCatchingThrowable"/>
<exclude name="AvoidLosingExceptionInformation"/>
<exclude name="DontImportSun"/>
<exclude name="NonCaseLabelInSwitchStatement"/>
<exclude name="UselessOperationOnImmutable"/>
</rule>
<rule ref="category/java/multithreading.xml">
<exclude name="AvoidUsingVolatile"/>
<exclude name="DoNotUseThreads"/>
<exclude name="AvoidSynchronizedStatement"/>
</rule>
<rule ref="category/java/performance.xml">
<exclude name="TooFewBranchesForASwitchStatement"/>
</rule>
<rule ref="category/java/security.xml"/>
</ruleset>
</ruleset>
+664 -21
View File
@@ -1,15 +1,19 @@
{
"name": "vscode-code-reviewer",
"version": "0.0.1",
"version": "1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "vscode-code-reviewer",
"version": "0.0.1",
"version": "1.2.0",
"dependencies": {
"@eslint/js": "^9.39.3",
"eslint": "^9.39.3",
"mammoth": "^1.12.0",
"officeparser": "^7.5.0",
"stylelint": "^17.14.0",
"stylelint-config-recommended": "^18.0.0",
"typescript-eslint": "^8.56.1",
"xlsx": "^0.18.5"
},
@@ -245,6 +249,16 @@
"node": ">=18"
}
},
"node_modules/@borewit/text-codec": {
"version": "0.2.2",
"resolved": "https://registry.npmmirror.com/@borewit/text-codec/-/text-codec-0.2.2.tgz",
"integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/@cacheable/memory": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz",
@@ -1199,6 +1213,271 @@
"integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==",
"license": "MIT"
},
"node_modules/@napi-rs/canvas": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas/-/canvas-1.0.3.tgz",
"integrity": "sha512-OlI657a5XXvKGFX7kNeIzJ8rO7IXt87Mqu2H8rXE46viAuOfum/JA7ysX7+eBhxNKznT+RCZh418mndlcFX3+w==",
"license": "MIT",
"optional": true,
"workspaces": [
"e2e/*"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"optionalDependencies": {
"@napi-rs/canvas-android-arm64": "1.0.3",
"@napi-rs/canvas-darwin-arm64": "1.0.3",
"@napi-rs/canvas-darwin-x64": "1.0.3",
"@napi-rs/canvas-linux-arm-gnueabihf": "1.0.3",
"@napi-rs/canvas-linux-arm64-gnu": "1.0.3",
"@napi-rs/canvas-linux-arm64-musl": "1.0.3",
"@napi-rs/canvas-linux-riscv64-gnu": "1.0.3",
"@napi-rs/canvas-linux-x64-gnu": "1.0.3",
"@napi-rs/canvas-linux-x64-musl": "1.0.3",
"@napi-rs/canvas-win32-arm64-msvc": "1.0.3",
"@napi-rs/canvas-win32-x64-msvc": "1.0.3"
}
},
"node_modules/@napi-rs/canvas-android-arm64": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.3.tgz",
"integrity": "sha512-7kSCdUhoXiO+AaIMXdBGdtp6EctZNkmF62Rea/BmVQlwKaM3bBhOzyGUzxyxz9dv5vdBfpyAaxhSRSJF4kqK4A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-darwin-arm64": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.3.tgz",
"integrity": "sha512-ds14V1BPagLszQyaDTeggny5fNeTCqsUQ5QhFj9VDxSEfzrVxXtdbR0LoFyKa0Siaaw8KvqSk4t7k/WoZJwvbg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-darwin-x64": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.3.tgz",
"integrity": "sha512-qof3LRAAycmkV2I1izZo9RoSHF8kCQr5O05sFwv0jK8rSdYV6KHVwimo6Qb7RxZj40WHKbLHm5JDaUF0o5XUAA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.3.tgz",
"integrity": "sha512-FU2kKZLmolHA9+KcUA+l1+xH3WTLUUTQDU/kLv9SEUr2TrRPu94aytOeizFJDHPs/QBcw4QL1mCQhetQXYBbag==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.3.tgz",
"integrity": "sha512-GVSjntxKeA+/y/ZKf1F+cmUw1WeIkE5aMRPqnZUlBTBvBcrvgWccJAWuYCKPX4QJQwZILIIwhgdAbl51yj6fpA==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.3.tgz",
"integrity": "sha512-J51oK/axyZ13kxycumSMfLiDZMdWdOVvqDFI28BpuViZHE3A0bQfr8B5vg8YnPEnqLD3BSn1hkdlh2buspEcNQ==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.3.tgz",
"integrity": "sha512-CtQgQjoVTX67jS9XuCTtJ40Sl7wRLMguoFnnGnfDmCWf7kzKFZVwj5ynqUOIGKFMSB61ZCuQlwPvVNxYTTseaw==",
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.3.tgz",
"integrity": "sha512-jtfzAHFp+FRaR7zGT4jyCe6wUgAG/dVb5A4Apd8FY9jKarntDfUAlJXscugiH7ZF5kKnu7/lHFk9LaDPcrGEVQ==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-x64-musl": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.3.tgz",
"integrity": "sha512-xTzaUCKUHTY4bCGadeeRZggbRVbGUT1petg7Z8r9AJR2+D9Bqu6nQAgqBGC6D47tA70LjaaaLTrJ7wNY1T74dg==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-win32-arm64-msvc": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.3.tgz",
"integrity": "sha512-ktVLuBkI6QVOm5BwO/WbdGwxgeetAMJa7TTmR8qBarXF0OU2NKjvjUtPJAl2y8t+zBRczJl/1VOl9gua6WcK2g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.3.tgz",
"integrity": "sha512-SGhlQ8bDjL1Cz2KnsKMasr/5sTcwG/SZkB6WCJxLsmSm/3aS2C+3p39bA7iZ2/94+NkVDySZfbiGoaSZSFHYxA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1561,6 +1840,29 @@
"@textlint/ast-node-types": "15.7.1"
}
},
"node_modules/@tokenizer/inflate": {
"version": "0.4.1",
"resolved": "https://registry.npmmirror.com/@tokenizer/inflate/-/inflate-0.4.1.tgz",
"integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.3",
"token-types": "^6.1.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/@tokenizer/token": {
"version": "0.3.0",
"resolved": "https://registry.npmmirror.com/@tokenizer/token/-/token-0.3.0.tgz",
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
@@ -2217,6 +2519,15 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.13",
"resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
"integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -2373,7 +2684,6 @@
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true,
"funding": [
{
"type": "github",
@@ -2388,8 +2698,7 @@
"url": "https://feross.org/support"
}
],
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/binary-extensions": {
"version": "2.3.0",
@@ -2449,6 +2758,18 @@
"node": ">= 6"
}
},
"node_modules/bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.4.7.tgz",
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
"license": "MIT"
},
"node_modules/bmp-js": {
"version": "0.1.0",
"resolved": "https://registry.npmmirror.com/bmp-js/-/bmp-js-0.1.0.tgz",
"integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==",
"license": "MIT"
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -2968,7 +3289,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true,
"license": "MIT"
},
"node_modules/cosmiconfig": {
@@ -3225,6 +3545,12 @@
"node": ">=0.3.1"
}
},
"node_modules/dingbat-to-unicode": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
"license": "BSD-2-Clause"
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
@@ -3284,6 +3610,15 @@
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/duck": {
"version": "0.1.12",
"resolved": "https://registry.npmmirror.com/duck/-/duck-0.1.12.tgz",
"integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==",
"license": "BSD",
"dependencies": {
"underscore": "^1.13.1"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -3795,6 +4130,12 @@
"reusify": "^1.0.4"
}
},
"node_modules/fflate": {
"version": "0.8.3",
"resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.3.tgz",
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
"license": "MIT"
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -3807,6 +4148,24 @@
"node": ">=16.0.0"
}
},
"node_modules/file-type": {
"version": "22.0.1",
"resolved": "https://registry.npmmirror.com/file-type/-/file-type-22.0.1.tgz",
"integrity": "sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA==",
"license": "MIT",
"dependencies": {
"@tokenizer/inflate": "^0.4.1",
"strtok3": "^10.3.5",
"token-types": "^6.1.2",
"uint8array-extras": "^1.5.0"
},
"engines": {
"node": ">=22"
},
"funding": {
"url": "https://github.com/sindresorhus/file-type?sponsor=1"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -4373,11 +4732,16 @@
"node": ">=0.10.0"
}
},
"node_modules/idb-keyval": {
"version": "6.3.0",
"resolved": "https://registry.npmmirror.com/idb-keyval/-/idb-keyval-6.3.0.tgz",
"integrity": "sha512-um+2dgAWmYsu615EXpWVwSmapJhON0G43t3Ka/EVaohzPQXSMqKEqeDK/oIW3Ow+BXaF2PvSc+oBTFp793A5Ow==",
"license": "Apache-2.0"
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true,
"funding": [
{
"type": "github",
@@ -4392,8 +4756,7 @@
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause",
"optional": true
"license": "BSD-3-Clause"
},
"node_modules/ignore": {
"version": "5.3.2",
@@ -4408,7 +4771,6 @@
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"dev": true,
"license": "MIT"
},
"node_modules/import-fresh": {
@@ -4463,7 +4825,6 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC"
},
"node_modules/ini": {
@@ -4611,6 +4972,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-url": {
"version": "1.2.4",
"resolved": "https://registry.npmmirror.com/is-url/-/is-url-1.2.4.tgz",
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
"license": "MIT"
},
"node_modules/is-wsl": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
@@ -4631,7 +4998,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"dev": true,
"license": "MIT"
},
"node_modules/isexe": {
@@ -4838,7 +5204,6 @@
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"dev": true,
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
@@ -4928,7 +5293,6 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
@@ -5060,6 +5424,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lop": {
"version": "0.4.2",
"resolved": "https://registry.npmmirror.com/lop/-/lop-0.4.2.tgz",
"integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==",
"license": "BSD-2-Clause",
"dependencies": {
"duck": "^0.1.12",
"option": "~0.2.1",
"underscore": "^1.13.1"
}
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
@@ -5083,6 +5458,48 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mammoth": {
"version": "1.12.0",
"resolved": "https://registry.npmmirror.com/mammoth/-/mammoth-1.12.0.tgz",
"integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==",
"license": "BSD-2-Clause",
"dependencies": {
"@xmldom/xmldom": "^0.8.6",
"argparse": "~1.0.3",
"base64-js": "^1.5.1",
"bluebird": "~3.4.0",
"dingbat-to-unicode": "^1.0.1",
"jszip": "^3.7.1",
"lop": "^0.4.2",
"path-is-absolute": "^1.0.0",
"underscore": "^1.13.1",
"xmlbuilder": "^10.0.0"
},
"bin": {
"mammoth": "bin/mammoth"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/mammoth/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/mammoth/node_modules/xmlbuilder": {
"version": "10.1.1",
"resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
"integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/markdown-it": {
"version": "14.3.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz",
@@ -5436,6 +5853,26 @@
"license": "MIT",
"optional": true
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-sarif-builder": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz",
@@ -5513,6 +5950,42 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/officeparser": {
"version": "7.5.0",
"resolved": "https://registry.npmmirror.com/officeparser/-/officeparser-7.5.0.tgz",
"integrity": "sha512-3OFFz4k3EhMWqz0sLVrerviQOTGl6qP3O9mNLW+N3CCiY4r7BtJWgDDrc8KmRJtsn/bDkMfhnYAoBNMpl1DC4w==",
"license": "MIT",
"dependencies": {
"@xmldom/xmldom": "^0.9.10",
"fflate": "^0.8.3",
"file-type": "^22.0.1",
"pdfjs-dist": "6.1.200",
"tesseract.js": "^7.0.0"
},
"bin": {
"officeparser": "dist/cli.js"
},
"engines": {
"node": ">=18.0.0"
},
"funding": {
"url": "https://github.com/sponsors/harshankur"
},
"peerDependenciesMeta": {
"puppeteer": {
"optional": true
}
}
},
"node_modules/officeparser/node_modules/@xmldom/xmldom": {
"version": "0.9.10",
"resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.9.10.tgz",
"integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==",
"license": "MIT",
"engines": {
"node": ">=14.6"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -5559,6 +6032,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/opencollective-postinstall": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
"integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
"license": "MIT",
"bin": {
"opencollective-postinstall": "index.js"
}
},
"node_modules/option": {
"version": "0.2.4",
"resolved": "https://registry.npmmirror.com/option/-/option-0.2.4.tgz",
"integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==",
"license": "BSD-2-Clause"
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -5735,7 +6223,6 @@
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"dev": true,
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": {
@@ -5850,6 +6337,15 @@
"node": ">=8"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -5889,6 +6385,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pdfjs-dist": {
"version": "6.1.200",
"resolved": "https://registry.npmmirror.com/pdfjs-dist/-/pdfjs-dist-6.1.200.tgz",
"integrity": "sha512-o8MolyzirkkLrcdsae/HEOiIcXWI7DS5zGpvqW8xTC2YUsW30rltFw2bDGvw/fskUdEMrQm2br68jzDS5BH2vw==",
"license": "Apache-2.0",
"engines": {
"node": ">=22.13.0 || >=24"
},
"optionalDependencies": {
"@napi-rs/canvas": "^1.0.0"
}
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
@@ -6039,7 +6547,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true,
"license": "MIT"
},
"node_modules/pump": {
@@ -6247,7 +6754,6 @@
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
@@ -6272,6 +6778,12 @@
"node": ">=8.10.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT"
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -6367,7 +6879,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"license": "MIT"
},
"node_modules/safer-buffer": {
@@ -6492,7 +7003,6 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"dev": true,
"license": "MIT"
},
"node_modules/shebang-command": {
@@ -6727,6 +7237,12 @@
"dev": true,
"license": "CC0-1.0"
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"license": "BSD-3-Clause"
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
@@ -6756,7 +7272,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
@@ -6877,6 +7392,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strtok3": {
"version": "10.3.5",
"resolved": "https://registry.npmmirror.com/strtok3/-/strtok3-10.3.5.tgz",
"integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==",
"license": "MIT",
"dependencies": {
"@tokenizer/token": "^0.3.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/structured-source": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz",
@@ -6946,6 +7477,28 @@
"node": ">=20.19.0"
}
},
"node_modules/stylelint-config-recommended": {
"version": "18.0.0",
"resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-18.0.0.tgz",
"integrity": "sha512-mxgT2XY6YZ3HWWe3Di8umG6aBmWmHTblTgu/f10rqFXnyWxjKWwNdjSWkgkwCtxIKnqjSJzvFmPT5yabVIRxZg==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/stylelint"
},
{
"type": "github",
"url": "https://github.com/sponsors/stylelint"
}
],
"license": "MIT",
"engines": {
"node": ">=20.19.0"
},
"peerDependencies": {
"stylelint": "^17.0.0"
}
},
"node_modules/stylelint/node_modules/file-entry-cache": {
"version": "11.1.5",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz",
@@ -7224,6 +7777,30 @@
"url": "https://github.com/chalk/supports-hyperlinks?sponsor=1"
}
},
"node_modules/tesseract.js": {
"version": "7.0.0",
"resolved": "https://registry.npmmirror.com/tesseract.js/-/tesseract.js-7.0.0.tgz",
"integrity": "sha512-exPBkd+z+wM1BuMkx/Bjv43OeLBxhL5kKWsz/9JY+DXcXdiBjiAch0V49QR3oAJqCaL5qURE0vx9Eo+G5YE7mA==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"bmp-js": "^0.1.0",
"idb-keyval": "^6.2.0",
"is-url": "^1.2.4",
"node-fetch": "^2.6.9",
"opencollective-postinstall": "^2.0.3",
"regenerator-runtime": "^0.13.3",
"tesseract.js-core": "^7.0.0",
"wasm-feature-detect": "^1.8.0",
"zlibjs": "^0.3.1"
}
},
"node_modules/tesseract.js-core": {
"version": "7.0.0",
"resolved": "https://registry.npmmirror.com/tesseract.js-core/-/tesseract.js-core-7.0.0.tgz",
"integrity": "sha512-WnNH518NzmbSq9zgTPeoF8c+xmilS8rFIl1YKbk/ptuuc7p6cLNELNuPAzcmsYw450ca6bLa8j3t0VAtq435Vw==",
"license": "Apache-2.0"
},
"node_modules/test-exclude": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz",
@@ -7368,6 +7945,30 @@
"node": ">=8.0"
}
},
"node_modules/token-types": {
"version": "6.1.2",
"resolved": "https://registry.npmmirror.com/token-types/-/token-types-6.1.2.tgz",
"integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==",
"license": "MIT",
"dependencies": {
"@borewit/text-codec": "^0.2.1",
"@tokenizer/token": "^0.3.0",
"ieee754": "^1.2.1"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/ts-api-utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
@@ -7491,11 +8092,22 @@
"dev": true,
"license": "MIT"
},
"node_modules/uint8array-extras": {
"version": "1.5.0",
"resolved": "https://registry.npmmirror.com/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
"integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/underscore": {
"version": "1.13.8",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
"dev": true,
"license": "MIT"
},
"node_modules/undici": {
@@ -7598,6 +8210,18 @@
"url": "https://bevry.me/fund"
}
},
"node_modules/wasm-feature-detect": {
"version": "1.8.0",
"resolved": "https://registry.npmmirror.com/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz",
"integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==",
"license": "Apache-2.0"
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
@@ -7622,6 +8246,16 @@
"node": ">=18"
}
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -7988,6 +8622,15 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zlibjs": {
"version": "0.3.1",
"resolved": "https://registry.npmmirror.com/zlibjs/-/zlibjs-0.3.1.tgz",
"integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==",
"license": "MIT",
"engines": {
"node": "*"
}
}
}
}
+8
View File
@@ -44,6 +44,10 @@
{
"command": "codeReviewer.openSetup",
"title": "Code Purifier: 打开设置面板"
},
{
"command": "codeReviewer.exportTemplate",
"title": "Code Purifier: 导出规则模板"
}
],
"keybindings": [
@@ -268,8 +272,12 @@
"test": "vscode-test"
},
"dependencies": {
"@eslint/js": "^9.39.3",
"eslint": "^9.39.3",
"mammoth": "^1.12.0",
"officeparser": "^7.5.0",
"stylelint": "^17.14.0",
"stylelint-config-recommended": "^18.0.0",
"typescript-eslint": "^8.56.1",
"xlsx": "^0.18.5"
},
+5
View File
@@ -8,6 +8,7 @@ import { reportToMarkdown } from '../utils/report';
import { getApiKey } from '../config';
import { ReviewPanel } from '../panel/webview';
import { t } from '../i18n/messages';
import { exportTemplate } from '../rules/export-service';
let currentReport: MergedReport | null = null;
@@ -184,4 +185,8 @@ export function registerCommands(
}
})
);
context.subscriptions.push(
vscode.commands.registerCommand('codeReviewer.exportTemplate', () => exportTemplate())
);
}
+55
View File
@@ -7,6 +7,59 @@ import ts from 'typescript-eslint';
import type { LinterAdapter, AdapterResult, LinterDiagnostic } from './adapter';
import { getEslintConfigPath } from '../config';
const extraRules: Record<string, 'error' | 'warn'> = {
'eqeqeq': 'error',
'no-eq-null': 'error',
'no-self-compare': 'error',
'no-promise-executor-return': 'error',
'no-shadow': 'error',
'no-unassigned-vars': 'error',
'no-useless-assignment': 'error',
'block-scoped-var': 'error',
'default-case': 'error',
'default-case-last': 'error',
'no-unmodified-loop-condition': 'error',
'no-unreachable-loop': 'error',
'no-eval': 'error',
'no-extend-native': 'error',
'no-var': 'error',
'no-await-in-loop': 'warn',
'prefer-template': 'warn',
'prefer-object-spread': 'warn',
'prefer-rest-params': 'warn',
'prefer-spread': 'warn',
'prefer-object-has-own': 'warn',
'no-useless-concat': 'warn',
'no-useless-return': 'warn',
'no-useless-computed-key': 'warn',
'no-useless-rename': 'warn',
'no-param-reassign': 'warn',
'no-return-assign': 'error',
'no-throw-literal': 'error',
'camelcase': 'warn',
'new-cap': 'warn',
'no-array-constructor': 'error',
};
const extraTsRules: Record<string, 'error' | 'warn' | 'off'> = {
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/no-dynamic-delete': 'error',
'@typescript-eslint/no-useless-empty-export': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/unified-signatures': 'error',
'@typescript-eslint/no-extraneous-class': 'warn',
'@typescript-eslint/no-useless-constructor': 'warn',
'@typescript-eslint/no-non-null-asserted-nullish-coalescing': 'error',
'@typescript-eslint/no-invalid-void-type': 'warn',
'@typescript-eslint/prefer-literal-enum-member': 'warn',
'@typescript-eslint/prefer-enum-initializers': 'warn',
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'error',
'no-array-constructor': 'off',
};
const TS_FILES = ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'];
const PROJECT_CONFIG_FILES = [
'.eslintrc.js',
'.eslintrc.json',
@@ -52,6 +105,8 @@ export class ESLintAdapter implements LinterAdapter {
ESLintAdapter.defaultConfig = [
js.configs.recommended,
...ts.configs.recommended,
{ rules: extraRules },
{ files: TS_FILES, rules: extraTsRules },
];
}
return ESLintAdapter.defaultConfig;
+51 -6
View File
@@ -1,16 +1,56 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { spawn } from 'child_process';
import type { LinterAdapter, AdapterResult, LinterDiagnostic } from './adapter';
import type { LinterAdapter, AdapterResult, LinterDiagnostic, Severity } from './adapter';
import { getSqlLintConfigFile } from '../config';
import { t } from '../i18n/messages';
import staticRules from '../rules/static-rules.json';
const DIALECT_MAP: Record<string, string> = {
sql: 'ansi',
plsql: 'postgres',
};
const BUILTIN_SQLFLUFF_CONFIG = `[sqlfluff]
rules = core,AM03,AM05,AM08,CV01,CV02,CV06,CV08,CV12,LT13,LT14,LT15,ST01,ST02,ST04,ST05,ST06,ST07,ST09,ST10,ST11,ST12,RF02,RF04,RF05,RF06
dialect = ansi
max_line_length = 80
indent_unit = space
tab_space_size = 4
`;
interface RuleEntry { id: string; description: string; tier?: string; }
const tierMap = new Map<string, string>();
try {
const sqlfluffRules = (staticRules as any).rules?.['sql-lint'] as RuleEntry[] | undefined;
if (sqlfluffRules) {
for (const rule of sqlfluffRules) {
if (rule.id && rule.tier) {
tierMap.set(rule.id.replace('sql-lint/', ''), rule.tier);
}
}
}
} catch {}
function tierToSeverity(tier: string | undefined): Severity {
if (tier === 'P0' || tier === 'P1') { return 'error'; }
if (tier === 'P2') { return 'warning'; }
return 'warning';
}
function hasProjectSqlfluffConfig(workspaceRoot: string): boolean {
const candidates = ['.sqlfluff', '.sqlfluff.ini'];
for (const candidate of candidates) {
if (fs.existsSync(path.join(workspaceRoot, candidate))) {
return true;
}
}
return false;
}
interface SqlFluffViolation {
start_line_no: number;
start_line_pos: number;
@@ -77,15 +117,16 @@ export class SqlLintAdapter implements LinterAdapter {
const dialect = DIALECT_MAP[languageId] || 'ansi';
let configPath: string | undefined;
let tempConfigPath: string | undefined;
const globalConfig = getSqlLintConfigFile();
if (globalConfig && globalConfig.trim() !== '') {
configPath = globalConfig;
} else if (hasProjectSqlfluffConfig(workingDir)) {
} else {
const projectConfig = path.join(workingDir, '.sqlfluff');
if (fs.existsSync(projectConfig)) {
configPath = projectConfig;
}
tempConfigPath = path.join(os.tmpdir(), `vscode-code-reviewer-sqlfluff-${Date.now()}.cfg`);
fs.writeFileSync(tempConfigPath, BUILTIN_SQLFLUFF_CONFIG, 'utf-8');
configPath = tempConfigPath;
}
try {
@@ -96,7 +137,7 @@ export class SqlLintAdapter implements LinterAdapter {
for (const result of results) {
for (const v of result.violations) {
diagnostics.push({
severity: 'warning',
severity: tierToSeverity(tierMap.get(v.code)),
ruleId: `sql-lint:${v.code}`,
message: v.description,
range: new vscode.Range(
@@ -124,6 +165,10 @@ export class SqlLintAdapter implements LinterAdapter {
status: 'execution-failed',
errorMessage: message,
};
} finally {
if (tempConfigPath) {
try { fs.unlinkSync(tempConfigPath); } catch {}
}
}
}
}
+39 -12
View File
@@ -3,6 +3,7 @@ import * as fs from 'fs';
import * as path from 'path';
import type { LinterAdapter, AdapterResult, LinterDiagnostic, Severity } from './adapter';
import { getStylelintConfigPath } from '../config';
import recommendedConfig from 'stylelint-config-recommended';
const CONFIG_FILE_NAMES = [
'.stylelintrc',
@@ -15,20 +16,46 @@ const CONFIG_FILE_NAMES = [
'stylelint.config.cjs',
];
const extraRules: Record<string, unknown> = {
'color-no-invalid-hex': true,
'function-linear-gradient-no-nonstandard-direction': true,
'function-no-unknown': true,
'unit-no-unknown': true,
'no-unknown-animations': true,
'no-unknown-custom-media': true,
'no-unknown-custom-properties': true,
'at-rule-no-vendor-prefix': true,
'media-feature-name-no-vendor-prefix': true,
'property-no-vendor-prefix': true,
'selector-no-vendor-prefix': true,
'value-no-vendor-prefix': true,
'color-hex-length': 'short',
'color-function-notation': 'modern',
'length-zero-no-unit': true,
'selector-pseudo-element-colon-notation': 'double',
'import-notation': 'string',
'alpha-value-notation': 'number',
'hue-degree-notation': 'angle',
'keyframe-selector-notation': 'percentage',
'declaration-block-no-redundant-longhand-properties': true,
'shorthand-property-no-redundant-values': true,
'block-no-redundant-nested-style-rules': true,
'color-named': 'never',
'font-family-name-quotes': 'always-where-required',
'number-max-precision': 4,
'comment-whitespace-inside': 'always',
};
const DEFAULT_CONFIG: Record<string, unknown> = {
...(recommendedConfig as Record<string, unknown>),
rules: {
'color-hex-length': 'short',
'color-named': 'never',
'color-no-invalid-hex': true,
'length-zero-no-unit': true,
'font-family-no-missing-generic-family-keyword': true,
'block-no-empty': true,
'declaration-block-no-duplicate-properties': true,
'no-descending-specificity': true,
'unit-no-unknown': true,
'property-no-unknown': true,
'selector-pseudo-class-no-unknown': true,
'selector-pseudo-element-no-unknown': true,
...((recommendedConfig as Record<string, unknown>).rules as Record<string, unknown>),
...extraRules,
},
};
+85
View File
@@ -1070,6 +1070,91 @@ const messages: Record<string, Record<Language, string>> = {
en: '日本語',
ja: '日本語',
},
'setup.fromTemplate': {
'zh-CN': '从模板导入',
en: 'Import from template',
ja: 'テンプレートからインポート',
},
'setup.exportTemplate': {
'zh-CN': '↓ 导出模板',
en: '↓ Export Template',
ja: '↓ テンプレートをエクスポート',
},
'setup.selectTemplateFile': {
'zh-CN': '选择模板文件',
en: 'Select template file',
ja: 'テンプレートファイルを選択',
},
'setup.importingTemplate': {
'zh-CN': '正在导入模板...',
en: 'Importing template...',
ja: 'テンプレートをインポート中...',
},
'import.template.badFormat': {
'zh-CN': '文件格式错误,请使用 Excel 模板(.xlsx/.xls',
en: 'Bad file format, please use Excel template (.xlsx/.xls)',
ja: 'ファイル形式エラー、Excel テンプレートを使用してください',
},
'import.template.empty': {
'zh-CN': '文件为空',
en: 'File is empty',
ja: 'ファイルが空です',
},
'import.template.notTemplate': {
'zh-CN': '不是模板文件,缺少列: {0}',
en: 'Not a template file, missing columns: {0}',
ja: 'テンプレートファイルではありません、欠損列: {0}',
},
'import.template.skipped': {
'zh-CN': '已跳过 {0} 行空数据',
en: 'Skipped {0} empty rows',
ja: '{0} 行の空データをスキップしました',
},
'import.sectionInvalid': {
'zh-CN': '错误规则',
en: 'Error Rules',
ja: 'エラー行',
},
'import.cannotImport': {
'zh-CN': '将自动丢弃,请修改文件后重新导入',
en: 'Will be auto-discarded, please fix the file and retry',
ja: '自動破棄されます、ファイルを修正して再インポートしてください',
},
'import.dedupFailed': {
'zh-CN': 'AI 去重失败,规则将不带去重标记导入',
en: 'AI dedup failed, rules imported without dedup marks',
ja: 'AI 重複排除失敗、重複マークなしでインポート',
},
'import.emptyValidRules': {
'zh-CN': '无有效规则可导入,请修改文件后重新导入',
en: 'No valid rules to import, please fix the file and retry',
ja: '有効なルールがありません、ファイルを修正して再インポートしてください',
},
'import.issuePrefix': {
'zh-CN': '⚠',
en: '⚠',
ja: '⚠',
},
'exportTemplate.saveLabel': {
'zh-CN': '导出模板',
en: 'Export Template',
ja: 'エクスポート',
},
'exportTemplate.success': {
'zh-CN': '模板已导出',
en: 'Template exported',
ja: 'テンプレートをエクスポートしました',
},
'exportTemplate.fail': {
'zh-CN': '导出失败:{0}',
en: 'Export failed: {0}',
ja: 'エクスポート失敗: {0}',
},
'exportTemplate.openFolder': {
'zh-CN': '打开文件夹',
en: 'Reveal in Folder',
ja: 'フォルダを開く',
},
};
let currentLang: Language = defaultLang;
+2 -2
View File
@@ -306,7 +306,7 @@ ${errorBox}
const toolName = report.adapterNames.length > 0 ? report.adapterNames.join(' + ') : t('report.sourceLinter');
const hasFixable = fixableSet.size > 0;
return `<div class="section-header"><span class="section-header-title">${esc(toolName)} · ${t('report.issuesCount', { 0: report.linterCount })}</span>${hasFixable ? `<button class="btn" onclick="send('fixAll')">${t('report.fixAll')}</button>` : ''}</div>`
+ report.linterDiagnostics.map((d, i) => this.buildIssueItem(d.severity, d.ruleId, d.message, d.range.start.line, 'linter', d.suggestion, fixableSet.has(i), undefined, false)).join('');
+ report.linterDiagnostics.map((d, i) => this.buildIssueItem(d.severity, d.ruleId, d.message, d.range.start.line, 'linter', d.suggestion, fixableSet.has(i))).join('');
}
private buildCustomList(report: MergedReport, fixableSet: Set<number>): string {
@@ -322,7 +322,7 @@ ${errorBox}
? t('report.injectedRules', { 0: filterInfo.injected, 1: filterInfo.totalActive })
: '';
return `<div class="section-header"><span class="section-header-title">${t('report.sourceCustom')} · ${t('report.issuesCount', { 0: report.customRuleCount })}${filterLabel}</span>${hasFixable ? `<button class="btn" onclick="send('fixAll')">${t('report.fixAll')}</button>` : ''}</div>`
+ report.customRuleDiagnostics.map((d, i) => this.buildIssueItem(d.severity, d.ruleId, d.message, d.range.start.line, 'custom', d.suggestion, fixableSet.has(i), undefined, false)).join('');
+ report.customRuleDiagnostics.map((d, i) => this.buildIssueItem(d.severity, d.ruleId, d.message, d.range.start.line, 'custom', d.suggestion, fixableSet.has(i))).join('');
}
private buildAIList(report: MergedReport): string {
+132
View File
@@ -0,0 +1,132 @@
import type { CustomRule } from '../../types';
import { getLanguage, type Language } from '../../i18n/messages';
export function buildDedupOnlyPrompt(
yamlContent: string,
existingRules: CustomRule[],
): { system: string; user: string } {
const lang = getLanguage();
const s = PROMPTS[lang];
const existingList = existingRules.length === 0
? s.noExisting
: existingRules.map(r =>
`- id: ${r.id} | severity: ${r.severity} | description: ${r.description} | message: ${r.message}`
).join('\n');
const system = [
s.role,
s.taskTitle,
s.taskLines.join('\n'),
s.rulesTitle,
s.rulesLines.join('\n'),
s.constraintTitle,
s.constraintLines.join('\n'),
s.existingTitle,
existingList,
].join('\n\n');
const user = s.userPrefix + '\n\n' + yamlContent;
return { system, user };
}
const PROMPTS: Record<Language, {
role: string;
taskTitle: string;
taskLines: string[];
rulesTitle: string;
rulesLines: string[];
constraintTitle: string;
constraintLines: string[];
existingTitle: string;
noExisting: string;
userPrefix: string;
}> = {
'zh-CN': {
role: '你是规则去重判定助手。你只输出 YAML,不输出任何解释。',
taskTitle: '## 任务',
taskLines: [
'下面是已标准化的规则 YAML。你只负责对照"现有规则"为每条规则标注去重字段。',
'为每条规则补充以下字段(如果无重复则标注 none):',
'- duplicateOf: 重复的规则 ID(如 eslint/no-console、custom/my-rule',
'- duplicateLevel: exact(完全相同)/ overlap(部分重叠)/ none(无重复)',
'- duplicateReason: 仅 overlap 时必填,简要说明重叠原因',
],
rulesTitle: '## 判定规则',
rulesLines: [
'1. exact: id 完全相同,或 description + message 语义完全一致',
'2. overlap: 检测目标/场景部分重叠,但并非完全相同',
'3. none: 与现有规则无冲突',
],
constraintTitle: '## ⚠️ 严格约束(必须遵守)',
constraintLines: [
'1. 严禁修改任何已有字段的值(id、severity、description、message、languages、excludeLanguages',
'2. 严禁添加新规则,严禁删除或合并规则',
'3. 规则数量必须与输入完全一致,顺序必须与输入完全一致',
'4. 你只允许添加三个字段: duplicateOf、duplicateLevel、duplicateReason',
'5. 如果某条规则与现有规则无任何重复,设置 duplicateLevel: none 即可,不需要补充 duplicateOf',
'6. 输出纯 YAML,不要用 markdown 代码块包裹',
],
existingTitle: '## 现有规则',
noExisting: '(无)',
userPrefix: '## 待去重的规则 YAML',
},
'en': {
role: 'You are a rule deduplication assistant. Output YAML only, no explanations.',
taskTitle: '## Task',
taskLines: [
'Below is standardized rule YAML. Only annotate dedup fields against the "Existing Rules" list.',
'For each rule, add (mark none if no conflict):',
'- duplicateOf: duplicated rule ID (e.g. eslint/no-console, custom/my-rule)',
'- duplicateLevel: exact / overlap / none',
'- duplicateReason: required only for overlap',
],
rulesTitle: '## Judgement Rules',
rulesLines: [
'1. exact: identical id, or semantically identical description+message',
'2. overlap: partially overlapping target/scenario',
'3. none: no conflict with existing rules',
],
constraintTitle: '## ⚠️ Strict Constraints (MUST follow)',
constraintLines: [
'1. Do NOT modify any existing field values (id, severity, description, message, languages, excludeLanguages)',
'2. Do NOT add, delete, or merge rules',
'3. Rule count and order must exactly match the input',
'4. Only add three fields: duplicateOf, duplicateLevel, duplicateReason',
'5. If a rule has no duplication, set duplicateLevel: none without duplicateOf',
'6. Output pure YAML, do NOT wrap in markdown code fences',
],
existingTitle: '## Existing Rules',
noExisting: '(none)',
userPrefix: '## YAML to deduplicate',
},
'ja': {
role: 'あなたはルール重複判定アシスタントです。YAML のみ出力し、説明は不要です。',
taskTitle: '## タスク',
taskLines: [
'以下は標準化されたルール YAML です。既存ルールと照合し、重複フィールドのみ注釈してください。',
'各ルールに以下を追加(重複がない場合は none と表記):',
'- duplicateOf: 重複ルール ID(例: eslint/no-console, custom/my-rule',
'- duplicateLevel: exact / overlap / none',
'- duplicateReason: overlap 時のみ必須',
],
rulesTitle: '## 判定ルール',
rulesLines: [
'1. exact: ID が同一、または description+message が意味的に完全一致',
'2. overlap: 検出対象/シナリオが部分重複',
'3. none: 既存ルールと競合なし',
],
constraintTitle: '## ⚠️ 厳格な制約(必ず遵守)',
constraintLines: [
'1. 既存フィールド(id, severity, description, message, languages, excludeLanguages)の値を一切変更しない',
'2. ルールの追加、削除、統合を一切行わない',
'3. ルールの数と順序は入力と完全に一致させる',
'4. 追加できるフィールドは duplicateOf, duplicateLevel, duplicateReason のみ',
'5. 重複がないルールは duplicateLevel: none とし、duplicateOf は付けない',
'6. 純粋な YAML を出力し、markdown コードブロックで囲まない',
],
existingTitle: '## 既存ルール',
noExisting: '(なし)',
userPrefix: '## 重複排除対象の YAML',
},
};
+100
View File
@@ -0,0 +1,100 @@
import * as path from 'path';
import * as XLSX from 'xlsx';
import type { ImportableRule, ValidationIssue } from '../import-types';
import type { Severity } from '../../types';
import { t } from '../../i18n/messages';
const REQUIRED_HEADERS = ['id', 'severity', 'description', 'message'];
const VALID_SEVERITY = ['error', 'warning', 'info'];
function splitList(v: unknown): string[] {
const s = String(v ?? '').trim();
if (!s) { return []; }
return s.split(/[,;、\n]/).map(x => x.trim()).filter(Boolean);
}
export interface TemplateParseResult {
rules: ImportableRule[];
validRules: ImportableRule[];
yamlContent: string;
skippedCount: number;
}
export function parseTemplate(srcPath: string): TemplateParseResult {
const ext = path.extname(srcPath).toLowerCase();
if (!['.xlsx', '.xls'].includes(ext)) {
throw new Error(t('import.template.badFormat'));
}
let wb: XLSX.WorkBook;
try { wb = XLSX.readFile(srcPath); }
catch { throw new Error(t('import.template.badFormat')); }
const sheet = wb.Sheets[wb.SheetNames[0]];
const rows = XLSX.utils.sheet_to_json<Record<string, string>>(sheet, { defval: '' });
if (rows.length === 0) {
throw new Error(t('import.template.empty'));
}
const header = Object.keys(rows[0]).map(k => k.trim().toLowerCase());
const missing = REQUIRED_HEADERS.filter(h => !header.includes(h));
if (missing.length > 0) {
throw new Error(t('import.template.notTemplate', { 0: missing.join(', ') }));
}
const totalRows = rows.length;
const rules: ImportableRule[] = rows
.map((r, idx) => ({ r, rowNo: idx + 2 }))
.filter(({ r }) => String(r.id ?? '').trim() !== '')
.map(({ r, rowNo }) => {
const issues: ValidationIssue[] = [];
const sevRaw = String(r.severity ?? '').trim().toLowerCase();
const severity: Severity = VALID_SEVERITY.includes(sevRaw) ? (sevRaw as Severity) : 'warning';
if (!VALID_SEVERITY.includes(sevRaw)) {
issues.push({ field: 'severity', severity: 'warning', message: `severity 非法: "${r.severity ?? ''}"` });
}
const description = String(r.description ?? '').trim();
if (!description) {
issues.push({ field: 'description', severity: 'error', message: 'description 为空' });
}
const message = String(r.message ?? '').trim();
if (!message) {
issues.push({ field: 'message', severity: 'error', message: 'message 为空' });
}
return {
id: String(r.id).trim(),
severity,
description,
message,
languages: splitList(r.languages),
excludeLanguages: splitList(r.excludeLanguages),
rowNumber: rowNo,
validationIssues: issues.length > 0 ? issues : undefined,
};
});
const validRules = rules.filter(r => !r.validationIssues);
const yamlContent = buildYaml(validRules);
const skippedCount = totalRows - rules.length;
return { rules, validRules, yamlContent, skippedCount };
}
function buildYaml(rules: ImportableRule[]): string {
const lines: string[] = [];
for (const r of rules) {
lines.push(`- id: ${r.id}`);
lines.push(` severity: ${r.severity}`);
lines.push(` description: ${r.description}`);
lines.push(` message: ${r.message}`);
if (r.languages?.length) {
lines.push(` languages: [${r.languages.join(', ')}]`);
}
if (r.excludeLanguages?.length) {
lines.push(` excludeLanguages: [${r.excludeLanguages.join(', ')}]`);
}
}
return lines.join('\n');
}
+60
View File
@@ -0,0 +1,60 @@
import * as XLSX from 'xlsx';
import * as vscode from 'vscode';
import { t } from '../i18n/messages';
const RULES_HEADER = ['id', 'severity', 'description', 'message', 'languages', 'excludeLanguages'];
const RULES_EXAMPLE = [
'no-todo', 'warning', '禁止提交 TODO 注释',
'发现 TODO 注释,请清理后提交', 'javascript,typescript', '',
];
const GUIDE_AOA: string[][] = [
['字段', '含义', '取值/格式', '示例'],
['id', '规则唯一标识', '小写字母、数字、连字符,全局唯一', 'no-todo'],
['severity', '严重级别', 'error / warning / info', 'warning'],
['description', '规则简述(给人看)', '自由文本', '禁止提交 TODO 注释'],
['message', '命中时展示给开发者的提示语', '自由文本', '发现 TODO 注释,请清理后提交'],
['languages', '生效的语言', '逗号分隔,留空表示对所有语言生效', 'javascript,typescript'],
['excludeLanguages', '排除的语言', '逗号分隔,可留空', ''],
['', '', '', ''],
['填写说明', '', '', ''],
['1. severity 仅接受 error / warning / info 三个值', '', '', ''],
['2. languages / excludeLanguages 多值用英文逗号分隔', '', '', ''],
['3. 示例行可删除,仅作填写参考', '', '', ''],
['4. 该模板可直接用于「使用模板文件导入」功能回环校验', '', '', ''],
];
export async function exportTemplate(): Promise<void> {
const uri = await vscode.window.showSaveDialog({
defaultUri: vscode.Uri.file('code-review-rules-template.xlsx'),
filters: { 'Excel': ['xlsx'] },
saveLabel: t('exportTemplate.saveLabel'),
});
if (!uri) { return; }
const wb = XLSX.utils.book_new();
const wsRules = XLSX.utils.aoa_to_sheet([RULES_HEADER, RULES_EXAMPLE]);
wsRules['!cols'] = [
{ wch: 16 }, { wch: 10 }, { wch: 32 }, { wch: 40 }, { wch: 24 }, { wch: 20 },
];
XLSX.utils.book_append_sheet(wb, wsRules, '规则');
const wsGuide = XLSX.utils.aoa_to_sheet(GUIDE_AOA);
wsGuide['!cols'] = [{ wch: 22 }, { wch: 28 }, { wch: 42 }, { wch: 36 }];
XLSX.utils.book_append_sheet(wb, wsGuide, '说明');
try {
XLSX.writeFile(wb, uri.fsPath);
const openFolder = t('exportTemplate.openFolder');
const choice = await vscode.window.showInformationMessage(
t('exportTemplate.success'), openFolder,
);
if (choice === openFolder) {
vscode.commands.executeCommand('revealFileInOS', uri);
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(t('exportTemplate.fail', { 0: msg }));
}
}
+69 -5
View File
@@ -15,7 +15,9 @@ export async function showImportPreview(
const keepRule: Record<string, boolean> = {};
for (const rule of result.rules) {
keepRule[rule.id] = rule.duplicateLevel !== 'exact';
if (!rule.validationIssues?.length) {
keepRule[rule.id] = rule.duplicateLevel !== 'exact';
}
}
panel.webview.html = renderPreviewHtml(result, keepRule);
@@ -51,15 +53,31 @@ function renderPreviewHtml(
result: ConversionResult,
keepRule: Record<string, boolean>,
): string {
const exactRules = result.rules.filter(r => r.duplicateLevel === 'exact');
const overlapRules = result.rules.filter(r => r.duplicateLevel === 'overlap');
const noneRules = result.rules.filter(
const errorRules = result.rules.filter(r => r.validationIssues?.length);
const cleanRules = result.rules.filter(r => !r.validationIssues?.length);
const exactRules = cleanRules.filter(r => r.duplicateLevel === 'exact');
const overlapRules = cleanRules.filter(r => r.duplicateLevel === 'overlap');
const noneRules = cleanRules.filter(
r => r.duplicateLevel !== 'exact' && r.duplicateLevel !== 'overlap'
);
const totalKept = Object.values(keepRule).filter(Boolean).length;
const totalCommented = Object.values(keepRule).filter(v => !v).length;
const skippedHint = result.skippedCount
? `<div class="summary-bar" style="border-color:rgba(88,166,255,0.3);color:#58a6ff;">${t('import.template.skipped', { 0: String(result.skippedCount) })}</div>`
: '';
const hasValidRules = cleanRules.length > 0;
const emptyValidHint = !hasValidRules
? `<div class="validation-error" style="display:block;">${t('import.emptyValidRules')}</div>`
: '';
const confirmBtnAttrs = hasValidRules
? 'onclick="doConfirm()"'
: 'disabled style="opacity:0.5;cursor:not-allowed;"';
function renderRuleCard(rule: ImportableRule): string {
const kept = keepRule[rule.id];
const color = SEVERITY_COLORS[rule.severity] || '#8b949e';
@@ -156,6 +174,46 @@ function renderPreviewHtml(
`;
}
function renderErrorCard(rule: ImportableRule): string {
const issues = (rule.validationIssues || []).map(i =>
`<div style="color:#f48771;font-size:12px;margin-bottom:4px;">${t('import.issuePrefix')} ${i.message}</div>`
).join('');
return `
<div class="rule-card" data-error="true" style="opacity:0.7;border-color:rgba(248,81,73,0.3);">
<div class="rule-card-header" style="cursor:default;">
<div class="rule-card-summary">
<span style="font-family:monospace;font-size:13px;font-weight:600;">${rule.id}</span>
<span style="color:#f48771;font-size:11px;font-weight:600;">${t('import.cannotImport')}</span>
</div>
</div>
<div class="rule-card-body" style="border-top:1px solid rgba(248,81,73,0.15);padding-top:8px;">
${issues}
<div style="color:#8b949e;font-size:11px;margin-top:6px;">
severity: ${rule.severity} | description: ${rule.description} | message: ${rule.message}
</div>
</div>
</div>
`;
}
function renderErrorSection(rules: ImportableRule[]): string {
if (rules.length === 0) { return ''; }
const sectionId = 'section-error';
return `
<div style="margin-bottom:12px;">
<div class="section-header" onclick="toggleSection('${sectionId}')">
<span style="font-size:14px;">🚫</span>
<span class="section-title">${t('import.sectionInvalid')}${rules.length}</span>
<span class="section-arrow">▼</span>
</div>
<div id="${sectionId}">
${rules.map(renderErrorCard).join('')}
</div>
</div>
`;
}
function renderSection(title: string, icon: string, rules: ImportableRule[], _defaultExpanded: boolean): string {
if (rules.length === 0) { return ''; }
const sectionId = `section-${title.replace(/\s/g, '')}`;
@@ -381,13 +439,17 @@ body {
<div id="validationError" class="validation-error" style="display:none;"></div>
${skippedHint}
${renderErrorSection(errorRules)}
${renderSection(t('import.sectionExact'), '⛔', exactRules, false)}
${renderSection(t('import.sectionOverlap'), '⚠️', overlapRules, true)}
${renderSection(t('import.sectionNone'), '✅', noneRules, false)}
${emptyValidHint}
<div class="actions">
<button class="btn" onclick="cancel()">${t('importPreview.cancel')}</button>
<button class="btn btn-primary" onclick="doConfirm()">${t('importPreview.confirm')}</button>
<button class="btn btn-primary" ${confirmBtnAttrs}>${t('importPreview.confirm')}</button>
</div>
<script>
@@ -462,6 +524,7 @@ function updateRule(ruleId, field, value) {
function collectEditedRules() {
const result = [];
document.querySelectorAll('.rule-card').forEach(card => {
if (card.hasAttribute('data-error')) { return; }
const originalId = card.dataset.ruleid;
const idInput = card.querySelector('.id-display-input');
const ruleId = idInput ? idInput.value.trim() || originalId : originalId;
@@ -522,6 +585,7 @@ function cancel() {
function updateSummary() {
let keepCount = 0, commentCount = 0;
document.querySelectorAll('.rule-card').forEach(card => {
if (card.hasAttribute('data-error')) { return; }
const ruleId = card.dataset.ruleid;
const keepBtns = card.querySelectorAll('.toggle-btn');
let isKept = true;
+39
View File
@@ -6,6 +6,8 @@ import { getAIConfig, getAITimeout } from '../config/ai';
import { createProvider } from '../ai/factory';
import { RuleConverter } from './converters/converter';
import { loadActiveRules } from './yaml-parser';
import { parseTemplate } from './converters/template-converter';
import { buildDedupOnlyPrompt } from './converters/dedup-prompt';
import type { ConversionResult, ImportableRule, PreviewDecision } from './import-types';
import { t, getLanguage } from '../i18n/messages';
@@ -283,6 +285,43 @@ export class ImportService {
};
}
async importTemplate(
srcPath: string,
name: string,
context: vscode.ExtensionContext,
): Promise<ConversionResult> {
const { rules, validRules, yamlContent, skippedCount } = parseTemplate(srcPath);
let dedupedValidRules: ImportableRule[] = validRules;
if (validRules.length > 0) {
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
const existingRules = workspaceRoot ? loadActiveRules(workspaceRoot) : [];
const { system, user } = buildDedupOnlyPrompt(yamlContent, existingRules);
const dedupedYaml = await convertContentWithAI(user, context, system);
if (dedupedYaml) {
dedupedValidRules = parseImportableYaml(dedupedYaml);
} else {
vscode.window.showWarningMessage(t('import.dedupFailed'));
}
}
const errorRules = rules.filter(r => r.validationIssues?.length);
const allRules = [...dedupedValidRules, ...errorRules];
const exactCount = dedupedValidRules.filter(r => r.duplicateLevel === 'exact').length;
const overlapCount = dedupedValidRules.filter(r => r.duplicateLevel === 'overlap').length;
return {
rules: allRules,
yamlContent,
sourceFileName: path.basename(srcPath),
exactCount,
overlapCount,
skippedCount,
errorCount: errorRules.length,
};
}
applyConversion(
result: ConversionResult,
decision: PreviewDecision,
+10
View File
@@ -1,9 +1,17 @@
import type { CustomRule } from '../types';
export interface ValidationIssue {
field: string;
severity: 'error' | 'warning';
message: string;
}
export interface ImportableRule extends CustomRule {
duplicateOf?: string;
duplicateLevel?: 'exact' | 'overlap' | 'none';
duplicateReason?: string;
validationIssues?: ValidationIssue[];
rowNumber?: number;
}
export interface ConversionResult {
@@ -12,6 +20,8 @@ export interface ConversionResult {
sourceFileName: string;
exactCount: number;
overlapCount: number;
skippedCount?: number;
errorCount?: number;
}
export interface PreviewDecision {
+356 -94
View File
@@ -1,11 +1,11 @@
{
"version": "1.0.1",
"linterVersion": {
"eslint": "9.x (recommended)",
"ts-eslint": "8.x (recommended)",
"stylelint": "16.x (12 rules)",
"pmd": "7.26.0 (6 categories)",
"sql-lint": "4.2.2 (default)"
"eslint": "9.x (92 rules)",
"ts-eslint": "8.x (35 rules)",
"stylelint": "16.x (68 rules)",
"pmd": "7.26.0 (274 Java rules + 12 JSP rules)",
"sql-lint": "4.2.2 (57 recommended)"
},
"rules": {
"eslint": [
@@ -252,7 +252,38 @@
{
"id": "eslint/valid-typeof",
"description": "Enforce comparing typeof expressions against valid strings"
}
},
{"id": "eslint/eqeqeq", "description": "Require === and !=="},
{"id": "eslint/no-eq-null", "description": "Disallow null comparisons without type-checking"},
{"id": "eslint/no-self-compare", "description": "Disallow comparisons where both sides are the same"},
{"id": "eslint/no-await-in-loop", "description": "Disallow await inside loops"},
{"id": "eslint/no-promise-executor-return", "description": "Disallow returning values from Promise executor"},
{"id": "eslint/no-shadow", "description": "Disallow variable declarations from shadowing variables in outer scopes"},
{"id": "eslint/no-unassigned-vars", "description": "Disallow let or var variables that are read but never assigned"},
{"id": "eslint/no-useless-assignment", "description": "Disallow variable assignments where the value is not used"},
{"id": "eslint/block-scoped-var", "description": "Enforce variables within the scope they are defined"},
{"id": "eslint/default-case", "description": "Require default cases in switch statements"},
{"id": "eslint/default-case-last", "description": "Enforce default clauses in switch statements to be last"},
{"id": "eslint/no-unmodified-loop-condition", "description": "Disallow unmodified loop conditions"},
{"id": "eslint/no-unreachable-loop", "description": "Disallow loops with a body that allows only one iteration"},
{"id": "eslint/no-eval", "description": "Disallow the use of eval()"},
{"id": "eslint/no-extend-native", "description": "Disallow extending native types"},
{"id": "eslint/no-var", "description": "Require let or const instead of var"},
{"id": "eslint/prefer-template", "description": "Require template literals instead of string concatenation"},
{"id": "eslint/prefer-object-spread", "description": "Disallow Object.assign and prefer object spread"},
{"id": "eslint/prefer-rest-params", "description": "Require rest parameters instead of arguments"},
{"id": "eslint/prefer-spread", "description": "Require spread operator instead of .apply()"},
{"id": "eslint/prefer-object-has-own", "description": "Disallow Object.prototype.hasOwnProperty.call() and prefer Object.hasOwn()"},
{"id": "eslint/no-useless-concat", "description": "Disallow unnecessary concatenation of literals or template literals"},
{"id": "eslint/no-useless-return", "description": "Disallow redundant return statements"},
{"id": "eslint/no-useless-computed-key", "description": "Disallow unnecessary computed property keys in objects and classes"},
{"id": "eslint/no-useless-rename", "description": "Disallow renaming import, export, and destructured assignments to the same name"},
{"id": "eslint/no-param-reassign", "description": "Disallow reassigning function parameters"},
{"id": "eslint/no-return-assign", "description": "Disallow assignment operators in return statements"},
{"id": "eslint/no-throw-literal", "description": "Disallow throwing literals as exceptions"},
{"id": "eslint/camelcase", "description": "Enforce camelcase naming convention"},
{"id": "eslint/new-cap", "description": "Require constructor names to begin with a capital letter"},
{"id": "eslint/no-array-constructor", "description": "Disallow Array constructors"}
],
"ts-eslint": [
{
@@ -350,7 +381,19 @@
{
"id": "ts-eslint/prefer-spread",
"description": "Require spread operator instead of .apply()"
}
},
{"id": "ts-eslint/no-non-null-assertion", "description": "Disallow non-null assertions using the ! postfix operator"},
{"id": "ts-eslint/no-dynamic-delete", "description": "Disallow using the delete operator on computed key expressions"},
{"id": "ts-eslint/no-useless-empty-export", "description": "Disallow empty exports that don't change anything in a module"},
{"id": "ts-eslint/consistent-type-imports", "description": "Enforce consistent usage of type imports"},
{"id": "ts-eslint/unified-signatures", "description": "Disallow two overloads that could be unified into a single signature"},
{"id": "ts-eslint/no-extraneous-class", "description": "Disallow classes only being used as namespaces"},
{"id": "ts-eslint/no-useless-constructor", "description": "Disallow unnecessary constructors"},
{"id": "ts-eslint/no-non-null-asserted-nullish-coalescing", "description": "Disallow non-null assertions in the left operand of a nullish coalescing operator"},
{"id": "ts-eslint/no-invalid-void-type", "description": "Disallow void type outside of generic or return types"},
{"id": "ts-eslint/prefer-literal-enum-member", "description": "Require all enum members to be literal values"},
{"id": "ts-eslint/prefer-enum-initializers", "description": "Require each enum member value to be explicitly initialized"},
{"id": "ts-eslint/no-shadow", "description": "Disallow variable declarations from shadowing variables declared in the outer scope"}
],
"stylelint": [
{
@@ -400,6 +443,94 @@
{
"id": "stylelint/selector-pseudo-element-no-unknown",
"description": "Disallow unknown pseudo-element selectors"
},
{
"id": "stylelint/function-linear-gradient-no-nonstandard-direction",
"description": "Disallow non-standard directions in linear-gradient"
},
{
"id": "stylelint/function-no-unknown",
"description": "Disallow unknown functions"
},
{
"id": "stylelint/no-unknown-animations",
"description": "Disallow unknown animations"
},
{
"id": "stylelint/no-unknown-custom-media",
"description": "Disallow unknown custom media queries"
},
{
"id": "stylelint/no-unknown-custom-properties",
"description": "Disallow unknown custom properties"
},
{
"id": "stylelint/at-rule-no-vendor-prefix",
"description": "Disallow vendor prefixes for at-rules"
},
{
"id": "stylelint/media-feature-name-no-vendor-prefix",
"description": "Disallow vendor prefixes for media feature names"
},
{
"id": "stylelint/property-no-vendor-prefix",
"description": "Disallow vendor prefixes for properties"
},
{
"id": "stylelint/selector-no-vendor-prefix",
"description": "Disallow vendor prefixes for selectors"
},
{
"id": "stylelint/value-no-vendor-prefix",
"description": "Disallow vendor prefixes for values"
},
{
"id": "stylelint/color-function-notation",
"description": "Require modern or legacy notation for color-functions"
},
{
"id": "stylelint/selector-pseudo-element-colon-notation",
"description": "Use single or double colon notation for pseudo-elements"
},
{
"id": "stylelint/import-notation",
"description": "Require string or url notation for @import"
},
{
"id": "stylelint/alpha-value-notation",
"description": "Require percentage or number notation for alpha-values"
},
{
"id": "stylelint/hue-degree-notation",
"description": "Require number or angle notation for hue degrees"
},
{
"id": "stylelint/keyframe-selector-notation",
"description": "Require keyword or percentage notation for keyframe selectors"
},
{
"id": "stylelint/declaration-block-no-redundant-longhand-properties",
"description": "Disallow redundant longhand properties within declaration blocks"
},
{
"id": "stylelint/shorthand-property-no-redundant-values",
"description": "Disallow redundant values within shorthand properties"
},
{
"id": "stylelint/block-no-redundant-nested-style-rules",
"description": "Disallow redundant nested style rules within blocks"
},
{
"id": "stylelint/font-family-name-quotes",
"description": "Require quotes for font-family names"
},
{
"id": "stylelint/number-max-precision",
"description": "Limit the number of decimal places in numbers"
},
{
"id": "stylelint/comment-whitespace-inside",
"description": "Require or disallow whitespace inside comments"
}
],
"pmd": [
@@ -555,6 +686,10 @@
"id": "pmd/ReplaceVectorWithList",
"description": "Use List/ArrayList instead of Vector"
},
{
"id": "pmd/ReturnEmptyCollectionRatherThanNull",
"description": "Return empty collection rather than null"
},
{
"id": "pmd/SimplifiableTestAssertion",
"description": "Use more specific assertion methods"
@@ -595,6 +730,10 @@
"id": "pmd/UnnecessaryWarningSuppression",
"description": "Remove unused PMD suppressions"
},
{
"id": "pmd/UnsynchronizedStaticFormatter",
"description": "Static formatter should be synchronized"
},
{
"id": "pmd/UnusedAssignment",
"description": "Remove unused assignments"
@@ -635,10 +774,18 @@
"id": "pmd/UseTryWithResources",
"description": "Use try-with-resources"
},
{
"id": "pmd/UseUtilityClass",
"description": "Utility class should have private constructor"
},
{
"id": "pmd/UseVarargs",
"description": "Use varargs instead of array parameter"
},
{
"id": "pmd/VariableCanBeInlined",
"description": "Variable can be inlined"
},
{
"id": "pmd/WhileLoopWithLiteralBoolean",
"description": "Simplify while loops with literal booleans"
@@ -719,10 +866,6 @@
"id": "pmd/FormalParameterNamingConventions",
"description": "Parameter naming conventions"
},
{
"id": "pmd/GenericsNaming",
"description": "Single uppercase letter for generics"
},
{
"id": "pmd/IdenticalCatchBranches",
"description": "Collapse identical catch branches"
@@ -816,8 +959,8 @@
"description": "Remove unnecessary fully qualified names"
},
{
"id": "pmd/UnnecessaryLocalBeforeReturn",
"description": "Remove unnecessary local before return"
"id": "pmd/UnnecessaryImport",
"description": "Remove unnecessary imports"
},
{
"id": "pmd/UnnecessaryModifier",
@@ -1059,10 +1202,26 @@
"id": "pmd/AvoidMultipleUnaryOperators",
"description": "Avoid multiple unary operators"
},
{
"id": "pmd/AvoidSynchronizedStatement",
"description": "Avoid synchronized statements"
},
{
"id": "pmd/AvoidSynchronizedAtMethodLevel",
"description": "Avoid synchronized at method level"
},
{
"id": "pmd/AvoidThreadGroup",
"description": "Avoid using ThreadGroup"
},
{
"id": "pmd/AvoidUsingOctalValues",
"description": "Avoid octal literals"
},
{
"id": "pmd/AvoidUsingVolatile",
"description": "Avoid the volatile keyword"
},
{
"id": "pmd/BrokenNullCheck",
"description": "Broken null check (|| vs &&)"
@@ -1144,8 +1303,12 @@
"description": "Don't use Threads"
},
{
"id": "pmd/DontImportSun",
"description": "Don't import sun.* packages"
"id": "pmd/DontCallThreadRun",
"description": "Don't call Thread.run()"
},
{
"id": "pmd/DoubleCheckedLocking",
"description": "Double-checked locking is not thread-safe"
},
{
"id": "pmd/EmptyCatchBlock",
@@ -1163,6 +1326,10 @@
"id": "pmd/IdempotentOperations",
"description": "Idempotent operations"
},
{
"id": "pmd/ImplicitSwitchFallThrough",
"description": "Implicit switch fall through"
},
{
"id": "pmd/ImportFromSamePackage",
"description": "Import from same package"
@@ -1223,6 +1390,10 @@
"id": "pmd/NonStaticInitializer",
"description": "Non-static initializer"
},
{
"id": "pmd/NonThreadSafeSingleton",
"description": "Singleton is not thread-safe"
},
{
"id": "pmd/NullAssignment",
"description": "Null assignment"
@@ -1239,6 +1410,14 @@
"id": "pmd/OperationWithCloning",
"description": "Operation with cloning"
},
{
"id": "pmd/OverrideBothEqualsAndHashcode",
"description": "Override both equals() and hashCode()"
},
{
"id": "pmd/OverridingThreadRun",
"description": "Don't override Thread.run()"
},
{
"id": "pmd/PackageDeclaration",
"description": "Package declaration"
@@ -1315,22 +1494,30 @@
"id": "pmd/UnusedNullCheckInEquals",
"description": "Unused null check in equals"
},
{
"id": "pmd/UseConcurrentHashMap",
"description": "Use ConcurrentHashMap for concurrent access"
},
{
"id": "pmd/UseCorrectExceptionLogging",
"description": "Correct exception logging"
},
{
"id": "pmd/UseDiamondOperator",
"description": "Use diamond operator <>"
},
{
"id": "pmd/UseEqualsToCompareStrings",
"description": "Use equals() for strings"
},
{
"id": "pmd/UselessOperationOnImmutable",
"description": "Useless operation on immutable"
},
{
"id": "pmd/UseLocaleWithCaseConversions",
"description": "Use locale with case conversions"
},
{
"id": "pmd/UseNotifyAllInsteadOfNotify",
"description": "Use notifyAll() instead of notify()"
},
{
"id": "pmd/UseProperClassLoader",
"description": "Use proper classloader"
@@ -1485,303 +1672,378 @@
"sql-lint": [
{
"id": "sql-lint/AL01",
"description": "Implicit/explicit aliasing of table"
"description": "Implicit/explicit aliasing of table",
"tier": "P2"
},
{
"id": "sql-lint/AL02",
"description": "Implicit/explicit aliasing of columns"
"description": "Implicit/explicit aliasing of columns",
"tier": "P0"
},
{
"id": "sql-lint/AL03",
"description": "Column expression without alias"
"description": "Column expression without alias",
"tier": "P0"
},
{
"id": "sql-lint/AL04",
"description": "Table aliases should be unique within each clause"
"description": "Table aliases should be unique within each clause",
"tier": "P0"
},
{
"id": "sql-lint/AL05",
"description": "Tables should not be aliased if unused"
"description": "Tables should not be aliased if unused",
"tier": "P0"
},
{
"id": "sql-lint/AL06",
"description": "Enforce table alias lengths"
"description": "Enforce table alias lengths",
"tier": "P0"
},
{
"id": "sql-lint/AL07",
"description": "Avoid table aliases"
"description": "Avoid table aliases",
"tier": "excluded"
},
{
"id": "sql-lint/AL08",
"description": "Column aliases should be unique within each clause"
"description": "Column aliases should be unique within each clause",
"tier": "P0"
},
{
"id": "sql-lint/AL09",
"description": "Column aliases should not alias to itself"
"description": "Column aliases should not alias to itself",
"tier": "P0"
},
{
"id": "sql-lint/AL10",
"description": "Derived tables must have an alias"
"description": "Derived tables must have an alias",
"tier": "P0"
},
{
"id": "sql-lint/AM01",
"description": "Ambiguous use of DISTINCT with GROUP BY"
"description": "Ambiguous use of DISTINCT with GROUP BY",
"tier": "P0"
},
{
"id": "sql-lint/AM02",
"description": "UNION DISTINCT/ALL preferred over just UNION"
"description": "UNION DISTINCT/ALL preferred over just UNION",
"tier": "P0"
},
{
"id": "sql-lint/AM03",
"description": "Ambiguous ordering directions"
"description": "Ambiguous ordering directions",
"tier": "P1"
},
{
"id": "sql-lint/AM04",
"description": "Query produces unknown number of result columns"
"description": "Query produces unknown number of result columns",
"tier": "P2"
},
{
"id": "sql-lint/AM05",
"description": "Join clauses should be fully qualified"
"description": "Join clauses should be fully qualified",
"tier": "P1"
},
{
"id": "sql-lint/AM06",
"description": "Inconsistent column references in GROUP BY/ORDER BY"
"description": "Inconsistent column references in GROUP BY/ORDER BY",
"tier": "P0"
},
{
"id": "sql-lint/AM07",
"description": "Queries within set query produce different numbers of columns"
"description": "Queries within set query produce different numbers of columns",
"tier": "P2"
},
{
"id": "sql-lint/AM08",
"description": "Implicit cross join detected"
"description": "Implicit cross join detected",
"tier": "P1"
},
{
"id": "sql-lint/AM09",
"description": "LIMIT/OFFSET without ORDER BY non-deterministic"
"description": "LIMIT/OFFSET without ORDER BY non-deterministic",
"tier": "P2"
},
{
"id": "sql-lint/CP01",
"description": "Inconsistent capitalisation of keywords"
"description": "Inconsistent capitalisation of keywords",
"tier": "P0"
},
{
"id": "sql-lint/CP02",
"description": "Inconsistent capitalisation of unquoted identifiers"
"description": "Inconsistent capitalisation of unquoted identifiers",
"tier": "P0"
},
{
"id": "sql-lint/CP03",
"description": "Inconsistent capitalisation of function names"
"description": "Inconsistent capitalisation of function names",
"tier": "P0"
},
{
"id": "sql-lint/CP04",
"description": "Inconsistent capitalisation of boolean/null literal"
"description": "Inconsistent capitalisation of boolean/null literal",
"tier": "P0"
},
{
"id": "sql-lint/CP05",
"description": "Inconsistent capitalisation of datatypes"
"description": "Inconsistent capitalisation of datatypes",
"tier": "P0"
},
{
"id": "sql-lint/CV01",
"description": "Consistent usage of != or <>"
"description": "Consistent usage of != or <>",
"tier": "P1"
},
{
"id": "sql-lint/CV02",
"description": "Use COALESCE instead of IFNULL/NVL"
"description": "Use COALESCE instead of IFNULL/NVL",
"tier": "P1"
},
{
"id": "sql-lint/CV03",
"description": "Trailing commas within select clause"
"description": "Trailing commas within select clause",
"tier": "P0"
},
{
"id": "sql-lint/CV04",
"description": "Consistent syntax for count number of rows"
"description": "Consistent syntax for count number of rows",
"tier": "P0"
},
{
"id": "sql-lint/CV05",
"description": "Comparisons with NULL should use IS or IS NOT"
"description": "Comparisons with NULL should use IS or IS NOT",
"tier": "P0"
},
{
"id": "sql-lint/CV06",
"description": "Statements must end with a semi-colon"
"description": "Statements must end with a semi-colon",
"tier": "P1"
},
{
"id": "sql-lint/CV07",
"description": "Top-level statements should not be wrapped in brackets"
"description": "Top-level statements should not be wrapped in brackets",
"tier": "P2"
},
{
"id": "sql-lint/CV08",
"description": "Use LEFT JOIN instead of RIGHT JOIN"
"description": "Use LEFT JOIN instead of RIGHT JOIN",
"tier": "P1"
},
{
"id": "sql-lint/CV09",
"description": "Block a list of configurable words"
"description": "Block a list of configurable words",
"tier": "excluded"
},
{
"id": "sql-lint/CV10",
"description": "Consistent usage of preferred quotes for quoted literals"
"description": "Consistent usage of preferred quotes for quoted literals",
"tier": "excluded"
},
{
"id": "sql-lint/CV11",
"description": "Enforce consistent type casting style"
"description": "Enforce consistent type casting style",
"tier": "P2"
},
{
"id": "sql-lint/CV12",
"description": "Use JOIN ... ON ... instead of WHERE ... for join conditions"
"description": "Use JOIN ... ON ... instead of WHERE ... for join conditions",
"tier": "P1"
},
{
"id": "sql-lint/JJ01",
"description": "Jinja tags should have single whitespace on either side"
"description": "Jinja tags should have single whitespace on either side",
"tier": "P0"
},
{
"id": "sql-lint/LT01",
"description": "Inappropriate Spacing"
"description": "Inappropriate Spacing",
"tier": "P0"
},
{
"id": "sql-lint/LT02",
"description": "Incorrect Indentation"
"description": "Incorrect Indentation",
"tier": "P0"
},
{
"id": "sql-lint/LT03",
"description": "Operators before/after newlines"
"description": "Operators before/after newlines",
"tier": "excluded"
},
{
"id": "sql-lint/LT04",
"description": "Leading/Trailing comma enforcement"
"description": "Leading/Trailing comma enforcement",
"tier": "excluded"
},
{
"id": "sql-lint/LT05",
"description": "Line is too long"
"description": "Line is too long",
"tier": "P0"
},
{
"id": "sql-lint/LT06",
"description": "Function name not followed by parenthesis"
"description": "Function name not followed by parenthesis",
"tier": "P0"
},
{
"id": "sql-lint/LT07",
"description": "WITH clause closing bracket on new line"
"description": "WITH clause closing bracket on new line",
"tier": "P0"
},
{
"id": "sql-lint/LT08",
"description": "Blank line after CTE closing bracket"
"description": "Blank line after CTE closing bracket",
"tier": "P0"
},
{
"id": "sql-lint/LT09",
"description": "Select targets on new line"
"description": "Select targets on new line",
"tier": "excluded"
},
{
"id": "sql-lint/LT10",
"description": "SELECT modifiers on same line as SELECT"
"description": "SELECT modifiers on same line as SELECT",
"tier": "P0"
},
{
"id": "sql-lint/LT11",
"description": "Set operators surrounded by newlines"
"description": "Set operators surrounded by newlines",
"tier": "P0"
},
{
"id": "sql-lint/LT12",
"description": "Files must end with single trailing newline"
"description": "Files must end with single trailing newline",
"tier": "P0"
},
{
"id": "sql-lint/LT13",
"description": "Files must not begin with newlines/whitespace"
"description": "Files must not begin with newlines/whitespace",
"tier": "P1"
},
{
"id": "sql-lint/LT14",
"description": "Keyword clauses before/after newlines"
"description": "Keyword clauses before/after newlines",
"tier": "P1"
},
{
"id": "sql-lint/LT15",
"description": "Too many consecutive blank lines"
"description": "Too many consecutive blank lines",
"tier": "P1"
},
{
"id": "sql-lint/OR01",
"description": "Remove empty batches"
"description": "Remove empty batches",
"tier": "P2"
},
{
"id": "sql-lint/PG01",
"description": "Avoid excessive locks in PostgreSQL DDL"
"description": "Avoid excessive locks in PostgreSQL DDL",
"tier": "P2"
},
{
"id": "sql-lint/RF01",
"description": "References cannot reference objects not in FROM clause"
"description": "References cannot reference objects not in FROM clause",
"tier": "P0"
},
{
"id": "sql-lint/RF02",
"description": "References should be qualified if multiple tables"
"description": "References should be qualified if multiple tables",
"tier": "P1"
},
{
"id": "sql-lint/RF03",
"description": "Column references consistent in single table statements"
"description": "Column references consistent in single table statements",
"tier": "excluded"
},
{
"id": "sql-lint/RF04",
"description": "Keywords should not be used as identifiers"
"description": "Keywords should not be used as identifiers",
"tier": "P1"
},
{
"id": "sql-lint/RF05",
"description": "No special characters in identifiers"
"description": "No special characters in identifiers",
"tier": "P1"
},
{
"id": "sql-lint/RF06",
"description": "Unnecessary quoted identifier"
"description": "Unnecessary quoted identifier",
"tier": "P1"
},
{
"id": "sql-lint/ST01",
"description": "Do not specify else null in CASE WHEN"
"description": "Do not specify else null in CASE WHEN",
"tier": "P1"
},
{
"id": "sql-lint/ST02",
"description": "Unnecessary CASE statement"
"description": "Unnecessary CASE statement",
"tier": "P1"
},
{
"id": "sql-lint/ST03",
"description": "Unused CTE"
"description": "Unused CTE",
"tier": "P0"
},
{
"id": "sql-lint/ST04",
"description": "Nested CASE in ELSE clause can be flattened"
"description": "Nested CASE in ELSE clause can be flattened",
"tier": "P1"
},
{
"id": "sql-lint/ST05",
"description": "Subqueries in Join/From clauses; use CTEs"
"description": "Subqueries in Join/From clauses; use CTEs",
"tier": "P1"
},
{
"id": "sql-lint/ST06",
"description": "Column order: wildcards, simple targets, then calculations"
"description": "Column order: wildcards, simple targets, then calculations",
"tier": "P1"
},
{
"id": "sql-lint/ST07",
"description": "Prefer ON over USING for join keys"
"description": "Prefer ON over USING for join keys",
"tier": "P1"
},
{
"id": "sql-lint/ST08",
"description": "DISTINCT used with parentheses"
"description": "DISTINCT used with parentheses",
"tier": "P0"
},
{
"id": "sql-lint/ST09",
"description": "Join condition order"
"description": "Join condition order",
"tier": "P1"
},
{
"id": "sql-lint/ST10",
"description": "Redundant constant expression"
"description": "Redundant constant expression",
"tier": "P1"
},
{
"id": "sql-lint/ST11",
"description": "Joined table not referenced"
"description": "Joined table not referenced",
"tier": "P1"
},
{
"id": "sql-lint/ST12",
"description": "Consecutive semicolons"
"description": "Consecutive semicolons",
"tier": "P1"
},
{
"id": "sql-lint/TQ01",
"description": "SP_ prefix should not be used for user-defined stored procedures"
"description": "SP_ prefix should not be used for user-defined stored procedures",
"tier": "P2"
},
{
"id": "sql-lint/TQ02",
"description": "Procedure bodies with multiple statements wrapped in BEGIN/END"
"description": "Procedure bodies with multiple statements wrapped in BEGIN/END",
"tier": "P2"
},
{
"id": "sql-lint/TQ03",
"description": "Remove empty batches"
"description": "Remove empty batches",
"tier": "P2"
}
]
}
+4
View File
@@ -0,0 +1,4 @@
declare module 'stylelint-config-recommended' {
const config: { rules: Record<string, unknown> };
export default config;
}
+6 -1
View File
@@ -21,10 +21,15 @@
var name = input.value.trim();
if (!name) { showRuleNameError(true); return; }
showRuleNameError(false);
vscode.postMessage({ type: 'addRule', name: name });
var useTemplateMode = document.getElementById('useTemplateMode').checked;
vscode.postMessage({ type: 'addRule', name: name, useTemplateMode: useTemplateMode });
input.value = '';
}
window.exportTemplate = function() {
vscode.postMessage({ type: 'exportTemplate' });
};
var ruleInput = document.getElementById('newRuleInput');
if (ruleInput) {
ruleInput.addEventListener('input', function () { showRuleNameError(false); });
+63 -3
View File
@@ -8,6 +8,7 @@ import { createProvider, getAllProviderMeta, getProviderModels, invalidateProvid
import { listRuleFiles } from '../rules/yaml-parser';
import { ImportService } from '../rules/import-service';
import { showImportPreview } from '../rules/import-preview';
import { exportTemplate as exportTemplateService } from '../rules/export-service';
import { YamlConverter } from '../rules/converters/yaml-converter';
import { MdConverter } from '../rules/converters/md-converter';
import { TxtConverter } from '../rules/converters/txt-converter';
@@ -273,9 +274,12 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
await this.pushConfig();
break;
case 'addRule':
await this.addRule(msg.name);
await this.addRule(msg.name, msg.useTemplateMode);
await this.pushConfig();
break;
case 'exportTemplate':
await this.exportTemplate();
break;
case 'reset':
await this.resetConfig();
await this.pushConfig();
@@ -424,12 +428,57 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
}
}
private async addRule(name: string): Promise<void> {
private async addRule(name: string, useTemplateMode?: boolean): Promise<void> {
if (!name.trim()) { return; }
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspaceRoot) { return; }
if (useTemplateMode) {
const result = await vscode.window.showOpenDialog({
canSelectMany: false,
openLabel: t('setup.selectTemplateFile'),
filters: { 'Excel': ['xlsx', 'xls'] },
});
if (!result || result.length === 0) { return; }
try {
const conversion = await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: t('setup.importingTemplate'),
}, async () => {
return await this.importService.importTemplate(
result[0].fsPath, name, this.context,
);
});
const decision = await showImportPreview(conversion);
if (!decision || !decision.confirmed) {
vscode.window.showInformationMessage(t('setup.importCancelled'));
return;
}
const rulesDir = path.join(workspaceRoot, '.code-review', 'rules');
if (!fs.existsSync(rulesDir)) {
fs.mkdirSync(rulesDir, { recursive: true });
}
const yamlFileName = name.endsWith('.yaml') ? name : `${name}.yaml`;
const yamlPath = path.join(rulesDir, yamlFileName);
if (fs.existsSync(yamlPath)) {
vscode.window.showErrorMessage(t('setup.fileExists', { 0: yamlFileName }));
return;
}
this.importService.applyConversion(conversion, decision, yamlPath);
vscode.window.showInformationMessage(
t('setup.importDedupResult', { 0: yamlFileName, 1: String(conversion.rules.length), 2: String(conversion.exactCount), 3: String(conversion.overlapCount) })
);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(msg);
}
return;
}
const result = await vscode.window.showOpenDialog({
canSelectMany: false,
openLabel: t('setup.selectRuleFile'),
@@ -480,6 +529,10 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
}
}
private async exportTemplate(): Promise<void> {
await exportTemplateService();
}
private async resetConfig(): Promise<void> {
const config = vscode.workspace.getConfiguration('vscode-code-reviewer');
await config.update('ai.provider', undefined, vscode.ConfigurationTarget.Global);
@@ -985,10 +1038,17 @@ input::placeholder { color: var(--vscode-input-placeholderForeground, var(--vsco
<div class="field" style="margin-top:8px;">
<div class="input-group">
<input type="text" id="newRuleInput" placeholder="${t('setup.ruleNamePlaceholder')}">
<button class="btn btn-sm" style="background:#7c3aed;color:#fff;border-color:#7c3aed;" onclick="exportTemplate()">${t('setup.exportTemplate')}</button>
<button class="btn btn-sm" style="background:#7c3aed;color:#fff;border-color:#7c3aed;" onclick="addRule()">${t('setup.add')}</button>
</div>
<div class="error-hint" id="ruleNameError">${t('setup.ruleNameRequired')}</div>
<div class="field-hint">${t('setup.ruleNameHint')}</div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:4px;">
<div class="field-hint" style="margin-top:0;">${t('setup.ruleNameHint')}</div>
<label style="display:flex;align-items:center;gap:6px;font-size:11px;color:var(--vscode-descriptionForeground);cursor:pointer;white-space:nowrap;">
<input type="checkbox" id="useTemplateMode" style="width:auto;margin:0;">
使用模板文件导入
</label>
</div>
</div>
</div>