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:
+19
-2
@@ -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_MAP;engine.ts / import-service.ts / setupView.ts 透传 extensionUri;模型下拉改为 input+datalist;setupView.ts 新增 FileSystemWatcher;setupView.js 改为填充 datalist;package.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.md(312 条完整规则清单)作为规则选型的基础数据 由此产出 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-shadow(TS 扩展版不在 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 追加 extraTsRules(13 条规则,含 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.md(141 条活跃规则 + 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.0;stylelint.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 条噪音),更新 description;static-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 + tierToSeverity(P0/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 异步与 Promise(2 条)
|
||||
|
||||
| 规则 | 检测场景 | 误报评估 |
|
||||
|------|----------|----------|
|
||||
| `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()` 返回值 | 数组长度为 3(recommended + 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 | true(error) | `stylelint-config-recommended` |
|
||||
| P1 强烈推荐 | 7 | true(error) | Possible errors 非 recommended |
|
||||
| P2 建议启用 | 20 | true(error) | 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-constructor:TS 扩展版已在 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 等:核心版 off,TS 扩展版 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 文件总规则** | — | 92(61 + 31) |
|
||||
| **TS 文件总规则** | — | 127(61 + 24 + 31 + 11) |
|
||||
|
||||
> TS 文件规则数 = 92(ESLint)+ 24(TS recommended)+ 11(TS 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()` 返回值 | 数组长度为 4(recommended + 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+P2(31 条,适用于所有文件)
|
||||
const extraRules = {
|
||||
'eqeqeq': 'error',
|
||||
'no-shadow': 'error',
|
||||
'no-array-constructor': 'error',
|
||||
// ... 其余 28 条 ESLint 规则 ...
|
||||
};
|
||||
|
||||
// typescript-eslint P1+P2(11 条新规则 + 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)` — 增加 context,workspaceRoot 内部获取 |
|
||||
| ⑤ | 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") | 记录 issue,severity 临时填 '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: 重复的规则 ID(linter 如 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',
|
||||
},
|
||||
'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 条规则的优先级分布(估算):
|
||||
|
||||
| 优先级 | 排除前(含弃用/噪音) | 排除后 | 变化 |
|
||||
|--------|---------------------|--------|------|
|
||||
| 1(Blocker) | ~40 | ~40 | 无变化(弃用和噪音规则多为 P2-P4) |
|
||||
| 2(Critical) | ~60 | ~50 | -10(排除 Design 噪音规则,多为 P2) |
|
||||
| 3(Major) | ~90 | ~80 | -10(部分噪音规则为 P3) |
|
||||
| 4(Minor) | ~100 | ~90 | -10(排除 Short/Long 命名规则等 P4) |
|
||||
| 5(Info) | ~19 | ~14 | -5(Documentation 未启用,弃用规则含部分 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 → Error,3 → Warning,4-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 → ansi,plsql → 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)
|
||||
// 若存在项目 .sqlfluff,SQLFluff 自动发现并使用
|
||||
args.push(filePath);
|
||||
return args;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.1.2 修改后:新增内置配置常量
|
||||
|
||||
```typescript
|
||||
// 新增:内置精选 .sqlfluff 配置模板(从全到精:rules = all → 57 条精选)
|
||||
// P0(32 条 Core)+ P1(25 条精选非 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 + P1(57 条) | 推荐基线规则与 all 模式下行为一致 |
|
||||
| **可能新增(可选)** | P2 方言规则 | 用户按需启用后,对应方言新增诊断 |
|
||||
|
||||
> **核心收益**:排除格式化噪音规则后,审查结果信噪比显著提升,真正的问题不再被格式化诊断淹没。
|
||||
|
||||
### 6.2 对配置优先级的影响
|
||||
|
||||
三层配置优先级不变:
|
||||
|
||||
```
|
||||
全局配置(linters.sqlfluffConfigPath)
|
||||
↓ 不存在
|
||||
项目配置(.sqlfluff / .sqlfluff.ini)
|
||||
↓ 不存在
|
||||
内置配置(BUILTIN_SQLFLUFF_CONFIG,57 条精选) ← 新增
|
||||
↓ (此前为 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/Stylelint:npm 库,配置为 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` 规则数 | core(32)+ 显式追加(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 条)
|
||||
# P0(32 条 Core)+ P1(25 条精选非 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 标记 |
|
||||
| 风险等级 | 中(诊断增加) | 中(诊断增加) | **低(诊断减少)** |
|
||||
@@ -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>
|
||||
Generated
+664
-21
@@ -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": "*"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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 }));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -0,0 +1,4 @@
|
||||
declare module 'stylelint-config-recommended' {
|
||||
const config: { rules: Record<string, unknown> };
|
||||
export default config;
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user