feat: 规则转换器/导入/过滤/i18n 基础设施

- converters: 8 个转换器(markdown/txt/excel/docx/pptx/yaml + prompt-builder + converter 接口)
- import: import-preview/import-service/import-types
- rule-filter: 规则预过滤功能
- static-rules.json: 静态规则数据
- i18n/messages.ts: 国际化消息
- types/mammoth.d.ts: mammoth 类型声明
This commit is contained in:
范智鹏
2026-07-26 23:04:13 +08:00
parent 6d29f304e3
commit 83712aa260
16 changed files with 4396 additions and 0 deletions
+5
View File
@@ -115,3 +115,8 @@
| 2026-07-26 14:16 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | AI 转换器输出语言修复:5 个 convertermd/txt/excel/docx/pptx)和 import-service.ts fallback prompt 去掉字段描述中的(中文)硬编码,追加 `\n输出语言:${getAIOutputLanguage()}` | 无 | src/rules/converters/md-converter.ts src/rules/converters/txt-converter.ts src/rules/converters/excel-converter.ts src/rules/converters/docx-converter.ts src/rules/converters/pptx-converter.ts src/rules/import-service.ts | deepseek-v4-flash |
| 2026-07-26 14:54 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 规则名称空值时输入框红框+错误提示文字 | 无 | src/i18n/messages.ts src/views/setupView.ts src/views/setupView.js | deepseek-v4-flash-free |
| 2026-07-26 15:26 | ① 用户提出 → ⑤ 编码实现 → ⑥ 审查验证 | repairJsonEscapes 去掉 replace 无差别解引号,防止 AI 正确转义的 \" 被破坏导致 JSON 解析失败 | 无 | src/ai/engine.ts | deepseek-v4-flash-free |
| 2026-07-26 21:43 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 自定义规则导入体验改进:提示词增强(集中到 prompt-builder.ts 的 buildSystemPrompt,增加输入容忍说明/非规则过滤/id+severity推断/description+message互推/对照示例)+ 解析侧兜底(severity缺/非法→warningid缺→rule-Ndesc+msg皆缺→丢弃)+ 预览 id 可编辑(header+panel 双 input 同步,占位 id 橙色高亮,校验加 id 非空)+ 5 个 converter 移除本地 buildSystemPrompt 改为调用公共函数 + 8 个新增测试 | 设计文档 docs/superpowers/specs/2026-07-26-custom-rule-import-ux-design.md(用户已提供);Stage ② 澄清:导入入口指引去掉/desc-message互填策略「AI提示词要求,解析侧仅两人皆缺才丢弃」/提示词抽公共/id两处同步/占位 id 保留高亮 | src/rules/converters/prompt-builder.ts src/rules/converters/md-converter.ts src/rules/converters/txt-converter.ts src/rules/converters/docx-converter.ts src/rules/converters/pptx-converter.ts src/rules/converters/excel-converter.ts src/rules/import-service.ts src/rules/import-preview.ts src/i18n/messages.ts src/test/import-dedup.test.ts | deepseek-v4-pro |
| 2026-07-26 22:00 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 语言下拉框选项文本固定为母语名称(中文(简体)/English/日本語),不再随界面语言切换而翻译;删除未使用的 getLanguageLabel 函数和 languageLabel 字段 | 无 | src/views/setupView.ts | deepseek-v4-flash |
| 2026-07-26 22:28 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | AI 规则导入提示词三语支持:prompt-builder.ts 重构为多语言模板(zh-CN/en/ja),AI 直接看到目标语言的完整提示词,不再依赖单行语言指令切换;import-service.ts fallback prompt 和 YAML 注释(重复/重叠/手动注释等)通过 t() 本地化 | 配置项设计:先用 Record<Language, PromptStrings> 结构组织多语言文本 | src/rules/converters/prompt-builder.ts src/rules/import-service.ts src/i18n/messages.ts | deepseek-v4-flash |
| 2026-07-26 22:41 | ⑤ 编码实现 → ⑥ 审查验证 | 修复日语提示词遗漏:角色描述开头追加「すべての説明とメッセージは日本語で出力してください」;finalInstruction 追加日语输出指令;示例标签 输入/输出/Input/Output 按语言本地化 | 无 | src/rules/converters/prompt-builder.ts | deepseek-v4-flash |
| 2026-07-26 22:51 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | AI 引擎提示词三语化:engine.ts 的 CUSTOM_RULE_SYSTEM_PROMPT 和 DEEP_REVIEW_SYSTEM_PROMPT 从硬编码中文改为三语模板(zh-CN/en/ja),内置输出语言指令;parseJsonResponse 错误消息改为 t() 本地化;用户消息标签(自定义规则/代码/静态分析结果)按语言本地化 | 无 | src/ai/engine.ts src/i18n/messages.ts | deepseek-v4-flash |
+896
View File
@@ -0,0 +1,896 @@
import * as vscode from 'vscode';
export type Language = 'zh-CN' | 'en' | 'ja';
const defaultLang: Language = 'zh-CN';
const messages: Record<string, Record<Language, string>> = {
'review.noEditor': {
'zh-CN': '请先打开一个文件',
en: 'Please open a file first',
ja: '最初にファイルを開いてください',
},
'review.noSelection': {
'zh-CN': '请先选中要审查的代码',
en: 'Please select code to review first',
ja: 'レビューするコードを選択してください',
},
'review.needApiKey': {
'zh-CN': '请先在设置面板中配置 API Key',
en: 'Please configure API Key in Setup first',
ja: '最初に設定パネルでAPIキーを設定してください',
},
'review.running': {
'zh-CN': '正在审查...',
en: 'Reviewing...',
ja: 'レビュー中...',
},
'review.staticAnalysis': {
'zh-CN': '运行静态分析...',
en: 'Running static analysis...',
ja: '静的解析を実行中...',
},
'review.aiReview': {
'zh-CN': '运行 AI 审查...',
en: 'Running AI review...',
ja: 'AIレビューを実行中...',
},
'review.reviewingSelection': {
'zh-CN': '审查选中代码...',
en: 'Reviewing selected code...',
ja: '選択したコードをレビュー中...',
},
'review.selectionComplete': {
'zh-CN': '选中代码审查完成: {0} 个问题',
en: 'Selection review complete: {0} issue(s)',
ja: '選択コードのレビュー完了: {0} 件の問題',
},
'review.fixNotAvailable': {
'zh-CN': '单条修复功能开发中',
en: 'Single fix is under development',
ja: '単一修正機能は開発中です',
},
'review.fixAllNotAvailable': {
'zh-CN': '批量修复功能开发中',
en: 'Batch fix is under development',
ja: '一括修正機能は開発中です',
},
'export.needRunFirst': {
'zh-CN': '请先运行完整审查生成报告',
en: 'Run a full review first to generate a report',
ja: '最初に完全レビューを実行してレポートを生成してください',
},
'export.copyToClipboard': {
'zh-CN': '📋 复制到剪贴板',
en: '📋 Copy to Clipboard',
ja: '📋 クリップボードにコピー',
},
'export.copyDescription': {
'zh-CN': '将报告内容以 Markdown 格式复制到剪贴板',
en: 'Copy report in Markdown format to clipboard',
ja: 'レポートをMarkdown形式でクリップボードにコピー',
},
'export.downloadMarkdown': {
'zh-CN': '📄 下载 Markdown 文件',
en: '📄 Download Markdown File',
ja: '📄 Markdownファイルをダウンロード',
},
'export.saveDescription': {
'zh-CN': '将报告保存为 .md 文件',
en: 'Save report as .md file',
ja: 'レポートを.mdファイルとして保存',
},
'export.selectMethod': {
'zh-CN': '选择导出方式',
en: 'Select export method',
ja: 'エクスポート方法を選択',
},
'export.copied': {
'zh-CN': '报告已复制到剪贴板',
en: 'Report copied to clipboard',
ja: 'レポートをクリップボードにコピーしました',
},
'export.saveDialogTitle': {
'zh-CN': '保存审查报告',
en: 'Save Review Report',
ja: 'レビューレポートを保存',
},
'export.saved': {
'zh-CN': '报告已保存到 {0}',
en: 'Report saved to {0}',
ja: 'レポートを {0} に保存しました',
},
'setup.noWorkspace': {
'zh-CN': '请先打开工作区',
en: 'Please open a workspace first',
ja: '最初にワークスペースを開いてください',
},
'setup.manageRulesHint': {
'zh-CN': '请在设置面板中管理自定义规则',
en: 'Manage custom rules in the Setup panel',
ja: '設定パネルでカスタムルールを管理してください',
},
'setup.openSetupFail': {
'zh-CN': '无法打开设置面板',
en: 'Cannot open setup panel',
ja: '設定パネルを開けません',
},
'setup.openSettingsJson': {
'zh-CN': '打开设置 (JSON)',
en: 'Open Settings (JSON)',
ja: '設定を開く (JSON)',
},
'setup.setApiKeyFirst': {
'zh-CN': '请先设置 API Key',
en: 'Please set API Key first',
ja: '最初にAPIキーを設定してください',
},
'setup.setBaseUrlFirst': {
'zh-CN': '请先设置 Base URL',
en: 'Please set Base URL first',
ja: '最初にベースURLを設定してください',
},
'setup.testSuccess': {
'zh-CN': '✓ 连接成功',
en: '✓ Connection successful',
ja: '✓ 接続成功',
},
'setup.testFail': {
'zh-CN': '✗ 连接失败: {0}',
en: '✗ Connection failed: {0}',
ja: '✗ 接続失敗: {0}',
},
'setup.selectRuleFile': {
'zh-CN': '选择规则文件',
en: 'Select rule file',
ja: 'ルールファイルを選択',
},
'setup.fileExists': {
'zh-CN': '文件 {0} 已存在',
en: 'File {0} already exists',
ja: 'ファイル {0} は既に存在します',
},
'setup.importSuccess': {
'zh-CN': '规则文件已导入: {0}',
en: 'Rule file imported: {0}',
ja: 'ルールファイルをインポートしました: {0}',
},
'setup.importing': {
'zh-CN': '正在导入自定义规则...',
en: 'Importing custom rules...',
ja: 'カスタムルールをインポート中...',
},
'setup.importCancelled': {
'zh-CN': '导入已取消',
en: 'Import cancelled',
ja: 'インポートをキャンセルしました',
},
'setup.importDedupResult': {
'zh-CN': '规则已导入: {0}({1} 条,{2} 条完全重复已注释,{3} 条部分重叠已标注)',
en: 'Rules imported: {0} ({1} total, {2} exact duplicate(s) commented, {3} partial overlap(s) marked)',
ja: 'ルールをインポートしました: {0}({1} 件、{2} 件の完全重複をコメントアウト、{3} 件の部分重複をマーク)',
},
'setup.importFail': {
'zh-CN': '规则导入失败: {0}',
en: 'Rule import failed: {0}',
ja: 'ルールのインポートに失敗しました: {0}',
},
'setup.header': {
'zh-CN': '净码特工 · 代码审查 · 设置',
en: 'Code Purifier · Setup',
ja: 'コードピュリファイア · 設定',
},
'setup.quickStart': {
'zh-CN': '快速开始',
en: 'Quick Start',
ja: 'クイックスタート',
},
'setup.gettingStarted': {
'zh-CN': '三步启用代码审核',
en: '3 Steps to Enable Code Review',
ja: '3ステップでコードレビューを有効化',
},
'setup.step1': {
'zh-CN': '安装插件后,<b>配置 AI 模型</b>及 API Key,激活智能审核能力',
en: 'After installing, <b>configure AI model</b> and API Key to activate intelligent review',
ja: 'インストール後、<b>AIモデル</b>とAPIキーを設定してインテリジェントレビューを有効化',
},
'setup.step2': {
'zh-CN': '启用 <b>自定义规则</b>,补充团队特有的编码规范',
en: 'Enable <b>custom rules</b> to add team-specific coding standards',
ja: '<b>カスタムルール</b>を有効にしてチーム固有のコーディング規約を追加',
},
'setup.step3': {
'zh-CN': '<b>保存并测试连接</b>,验证配置无误后即可触发审核',
en: '<b>Save and test connection</b>, verify config then trigger review',
ja: '<b>保存して接続テスト</b>、設定を確認してレビューを開始',
},
'setup.step3Hint': {
'zh-CN': '按 <b>Ctrl + Shift + R</b> 快捷键触发审核,结果实时显示在 <b>审核结果报告</b>页面中',
en: 'Press <b>Ctrl + Shift + R</b> to trigger review, results appear in the <b>Review Report</b> panel',
ja: '<b>Ctrl + Shift + R</b> でレビューを実行、結果は<b>レビューレポート</b>に表示',
},
'setup.engineSection': {
'zh-CN': '审核引擎',
en: 'Review Engines',
ja: 'レビューエンジン',
},
'setup.commonRules': {
'zh-CN': '共通规则',
en: 'Common Rules',
ja: '共通ルール',
},
'setup.linterStatic': {
'zh-CN': 'Linter 静态分析',
en: 'Linter Static Analysis',
ja: 'リンター静的解析',
},
'setup.customRules': {
'zh-CN': '自定义规则',
en: 'Custom Rules',
ja: 'カスタムルール',
},
'setup.teamCoding': {
'zh-CN': '团队编码规范',
en: 'Team Coding Standards',
ja: 'チームコーディング規約',
},
'setup.aiReview': {
'zh-CN': 'AI 审核',
en: 'AI Review',
ja: 'AIレビュー',
},
'setup.deepReview': {
'zh-CN': '深度代码审查',
en: 'Deep Code Review',
ja: '詳細コードレビュー',
},
'setup.aiConfig': {
'zh-CN': 'AI 模型配置',
en: 'AI Model Configuration',
ja: 'AIモデル設定',
},
'setup.provider': {
'zh-CN': '模型提供商',
en: 'Model Provider',
ja: 'モデルプロバイダー',
},
'setup.notConfigured': {
'zh-CN': '未配置',
en: 'Not configured',
ja: '未設定',
},
'setup.model': {
'zh-CN': '模型名称',
en: 'Model Name',
ja: 'モデル名',
},
'setup.modelHint': {
'zh-CN': '建议使用支持结构化输出的模型。',
en: 'Use a model that supports structured output.',
ja: '構造化出力をサポートするモデルの使用を推奨。',
},
'setup.apiKey': {
'zh-CN': 'API Key',
en: 'API Key',
ja: 'APIキー',
},
'setup.baseUrl': {
'zh-CN': 'Base URL',
en: 'Base URL',
ja: 'ベースURL',
},
'setup.keyStorageHint': {
'zh-CN': 'Key 仅存储在本地 VS Code 安全存储中。',
en: 'Key is stored locally in VS Code secure storage.',
ja: 'キーはVS Codeの安全なストレージにのみ保存されます。',
},
'setup.outputLang': {
'zh-CN': '语言',
en: 'Language',
ja: '言語',
},
'setup.outputLangHint': {
'zh-CN': '更改插件的显示语言',
en: 'Change plugin display language',
ja: 'プラグインの表示言語を変更する',
},
'setup.customRulesSection': {
'zh-CN': '自定义规则',
en: 'Custom Rules',
ja: 'カスタムルール',
},
'setup.ruleList': {
'zh-CN': '规则列表',
en: 'Rule List',
ja: 'ルール一覧',
},
'setup.ruleCount': {
'zh-CN': '{0} 条',
en: '{0} rule(s)',
ja: '{0} 件',
},
'setup.ruleNamePlaceholder': {
'zh-CN': '输入规则名称...',
en: 'Enter rule name...',
ja: 'ルール名を入力...',
},
'setup.add': {
'zh-CN': '+ 添加',
en: '+ Add',
ja: '+ 追加',
},
'setup.ruleNameHint': {
'zh-CN': '建议使用英文名称,无需输入 .yaml 后缀(如 security-rules',
en: 'Use English names, no .yaml suffix needed (e.g. security-rules)',
ja: '英語名を使用してください、.yaml拡張子は不要です(例: security-rules',
},
'setup.ruleNameRequired': {
'zh-CN': '请输入规则名称',
en: 'Please enter a rule name',
ja: 'ルール名を入力してください',
},
'setup.reset': {
'zh-CN': '重置',
en: 'Reset',
ja: 'リセット',
},
'setup.saveAndTest': {
'zh-CN': '保存并测试连接',
en: 'Save & Test Connection',
ja: '保存して接続テスト',
},
'importPreview.title': {
'zh-CN': '规则导入预览',
en: 'Rule Import Preview',
ja: 'ルールインポートプレビュー',
},
'importPreview.source': {
'zh-CN': '来源:{0} · 检测到 {1} 条规则',
en: 'Source: {0} · {1} rule(s) detected',
ja: 'ソース: {0} · {1} 件のルールを検出',
},
'importPreview.keep': {
'zh-CN': '保留',
en: 'Keep',
ja: '保持',
},
'importPreview.comment': {
'zh-CN': '注释',
en: 'Comment Out',
ja: 'コメントアウト',
},
'importPreview.cancel': {
'zh-CN': '取消',
en: 'Cancel',
ja: 'キャンセル',
},
'importPreview.confirm': {
'zh-CN': '确认导入',
en: 'Confirm Import',
ja: 'インポートを確認',
},
'import.customRulePrefix': {
'zh-CN': '自定义规则',
en: 'Custom rule',
ja: 'カスタムルール',
},
'import.duplicateOf': {
'zh-CN': '重复:{0}',
en: 'Duplicate: {0}',
ja: '重複:{0}',
},
'import.overlapWith': {
'zh-CN': '与 {0} 部分重叠',
en: 'Overlap with {0}',
ja: '{0} と部分的に重複',
},
'import.overlapReason': {
'zh-CN': '重叠原因:{0}',
en: 'Overlap reason: {0}',
ja: '重複理由:{0}',
},
'import.badgeRestored': {
'zh-CN': '已恢复',
en: 'Restored',
ja: '復元済み',
},
'import.badgeWillComment': {
'zh-CN': '将注释',
en: 'Will comment out',
ja: 'コメントアウト予定',
},
'import.idLabel': {
'zh-CN': 'id(规则唯一标识)',
en: 'id (unique identifier)',
ja: 'id(一意識別子)',
},
'import.severityLabel': {
'zh-CN': 'severity(严重级别)',
en: 'severity',
ja: 'severity(重要度)',
},
'import.descriptionLabel': {
'zh-CN': 'description(规则描述)',
en: 'description',
ja: 'description(ルール説明)',
},
'import.messageLabel': {
'zh-CN': 'message(触发提示消息)',
en: 'message',
ja: 'message(トリガーメッセージ)',
},
'import.languagesLabel': {
'zh-CN': 'languages(适用语言)',
en: 'languages',
ja: 'languages(対象言語)',
},
'import.excludeLanguagesLabel': {
'zh-CN': 'excludeLanguages(排除语言)',
en: 'excludeLanguages',
ja: 'excludeLanguages(除外言語)',
},
'import.tagPlaceholder': {
'zh-CN': '输入语言名,Enter 添加',
en: 'Type language name, Enter to add',
ja: '言語名を入力、Enterで追加',
},
'import.ruleCount': {
'zh-CN': '{0} 条',
en: '{0}',
ja: '{0} 件',
},
'import.exactDuplicate': {
'zh-CN': '⛔ 完全重复 {0} 条',
en: '⛔ Exact duplicate {0}',
ja: '⛔ 完全重複 {0} 件',
},
'import.overlapDuplicate': {
'zh-CN': '⚠️ 部分重叠 {0} 条',
en: '⚠️ Partial overlap {0}',
ja: '⚠️ 部分的重複 {0} 件',
},
'import.noDuplicate': {
'zh-CN': '✅ 无重复 {0} 条',
en: '✅ No duplicate {0}',
ja: '✅ 重複なし {0} 件',
},
'import.statusBar': {
'zh-CN': '将保留 {0} 条规则,注释 {1} 条规则',
en: 'Keep {0} rule(s), comment out {1} rule(s)',
ja: '{0} 件保持、{1} 件コメントアウト',
},
'import.editedHint': {
'zh-CN': ' · 已编辑 {0} 条规则',
en: ' · Edited {0} rule(s)',
ja: ' · {0} 件を編集済み',
},
'import.sectionExact': {
'zh-CN': '完全重复',
en: 'Exact Duplicate',
ja: '完全重複',
},
'import.sectionOverlap': {
'zh-CN': '部分重叠',
en: 'Partial Overlap',
ja: '部分的重複',
},
'import.sectionNone': {
'zh-CN': '无重复',
en: 'No Duplicate',
ja: '重複なし',
},
'import.validationDescEmpty': {
'zh-CN': '规则 "{0}" 的 description 不能为空',
en: 'Description of rule "{0}" cannot be empty',
ja: 'ルール "{0}" の description は必須です',
},
'import.validationMsgEmpty': {
'zh-CN': '规则 "{0}" 的 message 不能为空',
en: 'Message of rule "{0}" cannot be empty',
ja: 'ルール "{0}" の message は必須です',
},
'import.placeholderIdHint': {
'zh-CN': '占位 ID,请修改为有意义的标识',
en: 'Placeholder ID, please change to a meaningful identifier',
ja: 'プレースホルダーID、意味のある識別子に変更してください',
},
'import.validationIdEmpty': {
'zh-CN': '规则 id 不能为空',
en: 'Rule id cannot be empty',
ja: 'ルールIDは必須です',
},
'report.panelTitle': {
'zh-CN': '净码特工 · 代码审查报告',
en: 'Code Purifier · Review Report',
ja: 'コードピュリファイア · レビューレポート',
},
'report.totalIssues': {
'zh-CN': '总计问题',
en: 'Total Issues',
ja: '問題の総数',
},
'report.errors': {
'zh-CN': '错误',
en: 'Errors',
ja: 'エラー',
},
'report.warnings': {
'zh-CN': '警告',
en: 'Warnings',
ja: '警告',
},
'report.info': {
'zh-CN': '建议',
en: 'Info',
ja: '情報',
},
'report.fixAll': {
'zh-CN': '全部修复',
en: 'Fix All',
ja: 'すべて修正',
},
'report.rerun': {
'zh-CN': '重新审查',
en: 'Re-run Review',
ja: '再レビュー',
},
'report.export': {
'zh-CN': '导出报告',
en: 'Export Report',
ja: 'レポートをエクスポート',
},
'report.sourceLinter': {
'zh-CN': 'Linter',
en: 'Linter',
ja: 'リンター',
},
'report.sourceCustom': {
'zh-CN': '自定义',
en: 'Custom',
ja: 'カスタム',
},
'report.sourceAI': {
'zh-CN': 'AI',
en: 'AI',
ja: 'AI',
},
'report.noIssues': {
'zh-CN': '未发现任何问题',
en: 'No issues found',
ja: '問題は見つかりませんでした',
},
'report.noRuleViolations': {
'zh-CN': '未发现规则违规',
en: 'No rule violations found',
ja: 'ルール違反は見つかりませんでした',
},
'report.noAIFindings': {
'zh-CN': '无 AI 审查建议',
en: 'No AI review findings',
ja: 'AIレビューによる指摘はありません',
},
'report.skipCustomRules': {
'zh-CN': '当前文件语言无匹配的自定义规则,已跳过规则评估',
en: 'No custom rules match this file language, rule evaluation skipped',
ja: 'このファイル言語に一致するカスタムルールがありません、ルール評価をスキップしました',
},
'report.executionErrors': {
'zh-CN': '执行错误',
en: 'Execution Errors',
ja: '実行エラー',
},
'report.degradedBanner': {
'zh-CN': '部分 AI 功能不可用,报告已降级',
en: 'Some AI features unavailable, report degraded',
ja: '一部のAI機能が利用できません、レポートは縮退しています',
},
'report.title': {
'zh-CN': '代码审查报告',
en: 'Code Review Report',
ja: 'コードレビューレポート',
},
'report.file': {
'zh-CN': '文件',
en: 'File',
ja: 'ファイル',
},
'report.language': {
'zh-CN': '语言',
en: 'Language',
ja: '言語',
},
'report.duration': {
'zh-CN': '耗时',
en: 'Duration',
ja: '所要時間',
},
'report.tools': {
'zh-CN': '分析工具',
en: 'Analysis Tools',
ja: '解析ツール',
},
'report.totalSummary': {
'zh-CN': '总计: {0} | 错误: {1} | 警告: {2} | 建议: {3}',
en: 'Total: {0} | Errors: {1} | Warnings: {2} | Info: {3}',
ja: '合計: {0} | エラー: {1} | 警告: {2} | 情報: {3}',
},
'report.staticSection': {
'zh-CN': '静态分析 · {0} 个问题',
en: 'Static Analysis · {0} issue(s)',
ja: '静的解析 · {0} 件の問題',
},
'report.customSection': {
'zh-CN': '自定义规则 · {0} 个问题',
en: 'Custom Rules · {0} issue(s)',
ja: 'カスタムルール · {0} 件の問題',
},
'report.aiSection': {
'zh-CN': 'AI 审查 · {0} 条建议',
en: 'AI Review · {0} finding(s)',
ja: 'AIレビュー · {0} 件の指摘',
},
'report.suggestion': {
'zh-CN': '建议',
en: 'Suggestion',
ja: '提案',
},
'report.noProblems': {
'zh-CN': '未发现问题',
en: 'No issues found',
ja: '問題は見つかりませんでした',
},
'report.issuesCount': {
'zh-CN': '{0} 个问题',
en: '{0} issue(s)',
ja: '{0} 件の問題',
},
'report.itemsCount': {
'zh-CN': '{0} 条',
en: '{0} item(s)',
ja: '{0} 件',
},
'report.injectedCount': {
'zh-CN': '(注入 {0}/{1} 条)',
en: '(injected {0}/{1})',
ja: '(注入 {0}/{1} 件)',
},
'report.injectedRules': {
'zh-CN': '(注入 {0}/{1} 条规则)',
en: '(injected {0}/{1} rules)',
ja: '(注入 {0}/{1} 件ルール)',
},
'import.unsupportedFormat': {
'zh-CN': '不支持的文件格式: {0}',
en: 'Unsupported file format: {0}',
ja: 'サポートされていないファイル形式: {0}',
},
'import.conversionFailed': {
'zh-CN': '转换失败,未生成规则内容',
en: 'Conversion failed, no rule content generated',
ja: '変換に失敗しました、ルール内容が生成されませんでした',
},
'import.emptyFile': {
'zh-CN': '所选文件为空',
en: 'Selected file is empty',
ja: '選択したファイルは空です',
},
'import.needApiKey': {
'zh-CN': '请先在设置面板中配置 API Key',
en: 'Please configure API Key in Setup first',
ja: '最初に設定パネルでAPIキーを設定してください',
},
'import.timeout': {
'zh-CN': 'AI 生成规则超时,请检查网络或增大 ai.timeout 配置',
en: 'AI rule generation timed out, check network or increase ai.timeout',
ja: 'AIルール生成がタイムアウトしました、ネットワークを確認するかai.timeoutを増やしてください',
},
'import.aiFail': {
'zh-CN': 'AI 生成规则失败: {0}',
en: 'AI rule generation failed: {0}',
ja: 'AIルール生成に失敗しました: {0}',
},
'import.emptyResponse': {
'zh-CN': 'AI 返回内容为空',
en: 'AI returned empty response',
ja: 'AIが空の応答を返しました',
},
'import.excelReadFail': {
'zh-CN': '读取 Excel 文件失败: {0}',
en: 'Failed to read Excel file: {0}',
ja: 'Excelファイルの読み込みに失敗しました: {0}',
},
'import.excelEmpty': {
'zh-CN': 'Excel 文件没有工作表',
en: 'Excel file has no worksheets',
ja: 'Excelファイルにワークシートがありません',
},
'import.excelNoData': {
'zh-CN': 'Excel 工作表中没有数据',
en: 'No data in Excel worksheet',
ja: 'Excelワークシートにデータがありません',
},
'import.docxReadFail': {
'zh-CN': '读取 Word 文件失败: {0}',
en: 'Failed to read Word file: {0}',
ja: 'Wordファイルの読み込みに失敗しました: {0}',
},
'import.docxEmpty': {
'zh-CN': 'Word 文件中没有可提取的文本内容',
en: 'No extractable text in Word file',
ja: 'Wordファイルに抽出可能なテキストがありません',
},
'import.pptxReadFail': {
'zh-CN': '读取 PowerPoint 文件失败: {0}',
en: 'Failed to read PowerPoint file: {0}',
ja: 'PowerPointファイルの読み込みに失敗しました: {0}',
},
'import.pptxEmpty': {
'zh-CN': 'PowerPoint 文件中没有可提取的文本内容',
en: 'No extractable text in PowerPoint file',
ja: 'PowerPointファイルに抽出可能なテキストがありません',
},
'yaml.duplicateExact': {
'zh-CN': '# [DUPLICATE: exact] 重复 {0}(检测目标完全一致)',
en: '# [DUPLICATE: exact] duplicate of {0} (identical detection target)',
ja: '# [DUPLICATE: exact] {0} と重複(検出対象が完全に一致)',
},
'yaml.duplicateOverlap': {
'zh-CN': '# [DUPLICATE: overlap] 与 {0} 部分重叠',
en: '# [DUPLICATE: overlap] overlap with {0}',
ja: '# [DUPLICATE: overlap] {0} と部分的に重複',
},
'yaml.overlapReason': {
'zh-CN': '# 重叠原因:{0}',
en: '# Overlap reason: {0}',
ja: '# 重複理由:{0}',
},
'yaml.manualComment': {
'zh-CN': '# [手动注释] 用户选择不启用此规则',
en: '# [manual] User chose not to enable this rule',
ja: '# [手動コメント] ユーザーがこのルールを有効にしないことを選択',
},
'yaml.enableHint': {
'zh-CN': '# 如需启用,删除以下每行开头的 # 即可',
en: '# To enable, remove the leading # from each line below',
ja: '# 有効にするには、以下の各行の先頭の # を削除してください',
},
'yaml.duplicateExactCustom': {
'zh-CN': '# [DUPLICATE: exact] 重复自定义规则 {0}(检测目标完全一致)',
en: '# [DUPLICATE: exact] duplicate of custom rule {0} (identical detection target)',
ja: '# [DUPLICATE: exact] カスタムルール {0} と重複(検出対象が完全に一致)',
},
'yaml.duplicateExactGeneric': {
'zh-CN': '# [DUPLICATE: exact] 重复 {0}(检测目标完全一致)',
en: '# [DUPLICATE: exact] duplicate of {0} (identical detection target)',
ja: '# [DUPLICATE: exact] {0} と重複(検出対象が完全に一致)',
},
'yaml.duplicateOverlapCustom': {
'zh-CN': '# [DUPLICATE: {0}] 与自定义规则 {1} 部分重叠',
en: '# [DUPLICATE: {0}] overlap with custom rule {1}',
ja: '# [DUPLICATE: {0}] カスタムルール {1} と部分的に重複',
},
'yaml.duplicateOverlapGeneric': {
'zh-CN': '# [DUPLICATE: {0}] 与 {1} 部分重叠',
en: '# [DUPLICATE: {0}] overlap with {1}',
ja: '# [DUPLICATE: {0}] {1} と部分的に重複',
},
'engine.jsonNotFound': {
'zh-CN': '响应中未找到 JSON。原始响应(前200字符):{0}',
en: 'JSON not found in response. Raw response (first 200 chars): {0}',
ja: 'レスポンスにJSONが見つかりません。生のレスポンス(先頭200文字):{0}',
},
'engine.jsonParseFail': {
'zh-CN': 'JSON 解析失败。原始响应(前200字符):{0}',
en: 'JSON parse failed. Raw response (first 200 chars): {0}',
ja: 'JSON解析に失敗しました。生のレスポンス(先頭200文字):{0}',
},
'adapter.javaNotInstalled': {
'zh-CN': 'Java 11+ 未安装或不在 PATH 中',
en: 'Java 11+ not installed or not in PATH',
ja: 'Java 11+ がインストールされていないかPATHにありません',
},
'adapter.sqlfluffNotInstalled': {
'zh-CN': 'sqlfluff 未安装,请执行 pip install sqlfluff',
en: 'sqlfluff not installed, run: pip install sqlfluff',
ja: 'sqlfluffがインストールされていません、pip install sqlfluff を実行してください',
},
'adapter.invalidApiKey': {
'zh-CN': 'API Key 无效,请重新设置',
en: 'Invalid API Key, please reconfigure',
ja: 'APIキーが無効です、再設定してください',
},
'adapter.noApiKey': {
'zh-CN': '未配置 API Key',
en: 'API Key not configured',
ja: 'APIキーが設定されていません',
},
'adapter.createProviderFail': {
'zh-CN': '创建 Provider 失败: {0}',
en: 'Failed to create provider: {0}',
ja: 'プロバイダーの作成に失敗しました: {0}',
},
'adapter.customRuleParseFail': {
'zh-CN': '自定义规则响应解析失败: {0}',
en: 'Custom rule response parse failed: {0}',
ja: 'カスタムルール応答の解析に失敗しました: {0}',
},
'adapter.customRuleRequestFail': {
'zh-CN': '自定义规则请求失败: {0}',
en: 'Custom rule request failed: {0}',
ja: 'カスタムルールリクエストに失敗しました: {0}',
},
'adapter.aiReviewParseFail': {
'zh-CN': 'AI 审查响应解析失败: {0}',
en: 'AI review response parse failed: {0}',
ja: 'AIレビュー応答の解析に失敗しました: {0}',
},
'adapter.aiReviewRequestFail': {
'zh-CN': 'AI 审查请求失败: {0}',
en: 'AI review request failed: {0}',
ja: 'AIレビューリクエストに失敗しました: {0}',
},
'extension.activated': {
'zh-CN': '净码特工 · Code Purifier 已激活',
en: 'Code Purifier activated',
ja: 'コードピュリファイアが有効化されました',
},
'lang.zhCN': {
'zh-CN': '中文(简体)',
en: 'Chinese (Simplified)',
ja: '中国語(簡体字)',
},
'lang.en': {
'zh-CN': 'English',
en: 'English',
ja: 'English',
},
'lang.ja': {
'zh-CN': '日本語',
en: '日本語',
ja: '日本語',
},
};
let currentLang: Language = defaultLang;
const languageChangeEmitter = new vscode.EventEmitter<Language>();
export function t(key: string, vars?: Record<string, string | number>): string {
const msg = messages[key]?.[currentLang] ?? messages[key]?.[defaultLang];
let result = msg ?? key;
if (vars) {
for (const [k, v] of Object.entries(vars)) {
result = result.replace(`{${k}}`, String(v));
}
}
return result;
}
export function setLanguage(lang: Language): void {
if (currentLang === lang) { return; }
currentLang = lang;
languageChangeEmitter.fire(lang);
}
export function onLanguageChange(listener: (lang: Language) => void): vscode.Disposable {
return languageChangeEmitter.event(listener);
}
export function getLanguage(): Language {
return currentLang;
}
export function getMessageKeys(): string[] {
return Object.keys(messages);
}
+7
View File
@@ -0,0 +1,7 @@
import * as vscode from 'vscode';
import type { CustomRule } from '../../types';
export interface RuleConverter {
supportedExtensions: string[];
convert(srcPath: string, context: vscode.ExtensionContext, existingRules?: CustomRule[]): Promise<string | null>;
}
+30
View File
@@ -0,0 +1,30 @@
import * as vscode from 'vscode';
import * as mammoth from 'mammoth';
import { RuleConverter } from './converter';
import { convertContentWithAI } from '../import-service';
import { buildSystemPrompt } from './prompt-builder';
import type { CustomRule } from '../../types';
import { t } from '../../i18n/messages';
export class DocxConverter implements RuleConverter {
supportedExtensions = ['.docx'];
async convert(srcPath: string, context: vscode.ExtensionContext, existingRules?: CustomRule[]): Promise<string | null> {
let result: mammoth.Result;
try {
result = await mammoth.extractRawText({ path: srcPath });
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(t('import.docxReadFail', { 0: msg }));
return null;
}
const content = result.value.trim();
if (!content) {
vscode.window.showErrorMessage(t('import.docxEmpty'));
return null;
}
return convertContentWithAI(content, context, buildSystemPrompt('freeform', existingRules));
}
}
+64
View File
@@ -0,0 +1,64 @@
import * as vscode from 'vscode';
import * as XLSX from 'xlsx';
import { RuleConverter } from './converter';
import { convertContentWithAI } from '../import-service';
import { buildSystemPrompt } from './prompt-builder';
import type { CustomRule } from '../../types';
import { t } from '../../i18n/messages';
function buildMarkdownTable(rows: Record<string, unknown>[], sheetName: string): string {
const keys = Object.keys(rows[0]);
const header = `| ${keys.join(' | ')} |`;
const separator = `| ${keys.map(() => '---').join(' | ')} |`;
const dataLines = rows.map(row => {
const cells = keys.map(k => String(row[k] ?? ''));
return `| ${cells.join(' | ')} |`;
});
return [`## ${sheetName}`, header, separator, ...dataLines].join('\n');
}
export class ExcelConverter implements RuleConverter {
supportedExtensions = ['.xlsx', '.xls'];
async convert(srcPath: string, context: vscode.ExtensionContext, existingRules?: CustomRule[]): Promise<string | null> {
let workbook: XLSX.WorkBook;
try {
workbook = XLSX.readFile(srcPath);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(t('import.excelReadFail', { 0: msg }));
return null;
}
if (workbook.SheetNames.length === 0) {
vscode.window.showErrorMessage(t('import.excelEmpty'));
return null;
}
let parts: string[];
try {
parts = [];
for (const sheetName of workbook.SheetNames) {
const sheet = workbook.Sheets[sheetName];
if (!sheet) { continue; }
const rows = XLSX.utils.sheet_to_json<Record<string, string>>(sheet);
if (rows.length === 0) {
continue;
}
parts.push(buildMarkdownTable(rows, sheetName));
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(t('import.excelReadFail', { 0: msg }));
return null;
}
if (parts.length === 0) {
vscode.window.showErrorMessage(t('import.excelNoData'));
return null;
}
const combined = parts.join('\n\n');
return convertContentWithAI(combined, context, buildSystemPrompt('spreadsheet', existingRules));
}
}
+15
View File
@@ -0,0 +1,15 @@
import * as fs from 'fs';
import * as vscode from 'vscode';
import { RuleConverter } from './converter';
import { convertContentWithAI } from '../import-service';
import { buildSystemPrompt } from './prompt-builder';
import type { CustomRule } from '../../types';
export class MdConverter implements RuleConverter {
supportedExtensions = ['.md'];
async convert(srcPath: string, context: vscode.ExtensionContext, existingRules?: CustomRule[]): Promise<string | null> {
const content = fs.readFileSync(srcPath, 'utf-8');
return convertContentWithAI(content, context, buildSystemPrompt('freeform', existingRules));
}
}
+30
View File
@@ -0,0 +1,30 @@
import * as vscode from 'vscode';
import { OfficeParser, type OfficeParserAST } from 'officeparser';
import { RuleConverter } from './converter';
import { convertContentWithAI } from '../import-service';
import { buildSystemPrompt } from './prompt-builder';
import type { CustomRule } from '../../types';
import { t } from '../../i18n/messages';
export class PptxConverter implements RuleConverter {
supportedExtensions = ['.pptx'];
async convert(srcPath: string, context: vscode.ExtensionContext, existingRules?: CustomRule[]): Promise<string | null> {
let ast: OfficeParserAST;
try {
ast = await OfficeParser.parseOffice(srcPath);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(t('import.pptxReadFail', { 0: msg }));
return null;
}
const text = ast.toText().trim();
if (!text) {
vscode.window.showErrorMessage(t('import.pptxEmpty'));
return null;
}
return convertContentWithAI(text, context, buildSystemPrompt('freeform', existingRules));
}
}
+446
View File
@@ -0,0 +1,446 @@
import staticRules from '../static-rules.json';
import type { CustomRule } from '../../types';
import { getLanguage } from '../../i18n/messages';
export type PromptInputType = 'freeform' | 'spreadsheet';
type Lang = 'zh-CN' | 'en' | 'ja';
interface PromptStrings {
role: (inputDesc: string) => string;
inputToleranceTitle: string;
inputToleranceLines: string[];
nonRuleFilterTitle: string;
nonRuleFilterLines: string[];
fieldDefsTitle: string;
fieldDefsLines: string[];
languageRulesTitle: string;
languageRulesLines: string[];
exampleTitle: string;
example1Input: string;
example1Output: string;
example2Input: string;
example2Output: string;
staticAnalysisTitle: string;
staticAnalysisLines: string[];
finalInstruction: string;
dedupHeader: string;
dedupLinterLabel: (name: string, count: number) => string;
dedupCustomLabel: (count: number) => string;
dedupFooter: string;
outputLang: string;
}
const p: Record<Lang, PromptStrings> = {
'zh-CN': {
role: (inputDesc) => `你是一个代码审查规则转换器。将用户提供的${inputDesc},转换为结构化的 YAML 格式,用于代码审查工具。`,
inputToleranceTitle: '## 输入容忍说明',
inputToleranceLines: [
'用户输入可能有多种形态,你必须接受并处理以下任一形式:',
'- 自然语言段落(一段或多段话描述规则)',
'- 无序列表(每条规则一行或一段)',
'- 表格(列名不固定,从语义推断)',
'- 混合形式(段落 + 列表 + 表格组合)',
'',
'不得因输入格式非标准而拒绝转换。应主动从松散描述中提取规则语义。',
],
nonRuleFilterTitle: '## 非规则内容过滤',
nonRuleFilterLines: [
'用户输入中可能混入项目介绍、背景说明、代码示例、章节标题等非规则内容。你必须:',
'- 识别并跳过非规则内容,仅将真正的编码规则转为 YAML 条目',
'- 代码示例、项目介绍等仅作为理解规则语义的上下文,自身不输出为规则',
'- 若某段内容无法判断为规则(既无规则意图也无违反提示),直接忽略,不强行转换',
],
fieldDefsTitle: '## 字段定义',
fieldDefsLines: [
'每条规则需要包含以下字段:',
'- id: 规则唯一标识(kebab-case 英文,语义化、简短,如 no-console-log、avoid-magic-number',
' **必须**基于规则描述内容自动生成语义化的 id',
' 即使输入中无显式 id 标识,也必须根据 description/message 的语义推断出合适的 id',
' 多条规则之间 id 不得重复',
'- severity: 严重级别(error / warning / info',
' **必须**输出。按规则语义推断:',
' error: 会导致 bug / 安全问题 / 数据损坏',
' warning: 潜在问题 / 不良实践',
' info: 风格 / 可读性建议',
' 即使输入中无显式严重级别,也必须根据规则后果的严重程度推断',
'- description: 规则简短描述',
' **必须**输出。若输入中不明显,从 message 的内容反向推导出简短描述',
'- message: 违反时的提示消息',
' **必须**输出。若输入中不明显,从 description 的内容推导出违反提示',
' description 与 message 语义可相近,无需强行区分口吻,但两者都必须填写',
'- languages: 适用语言数组(可选,如 [javascript, typescript]',
'- excludeLanguages: 明确排除的语言数组(可选,如 [css, sql]',
'- duplicateOf: 重复的规则 IDlinter 如 eslint/no-console;自定义如 custom/my-rule',
'- duplicateLevel: 重复程度(exact / overlap / none',
'- duplicateReason: 重复/重叠原因说明(overlap 档必填)',
],
languageRulesTitle: '## 语言字段规则(严格遵守)',
languageRulesLines: [
'对于每条规则的 languages 字段:',
'1. 规则描述中含明确语言关键词(如 "Java"、"JavaScript"、"CSS")→ 使用 languages 白名单',
'2. 规则适用于大多数语言,只有少数不适用 → 使用 excludeLanguages 黑名单',
'3. 无法确定适用语言,或规则为通用规范 → languages 与 excludeLanguages 均留空',
'4. languages 和 excludeLanguages 不可同时非空',
'5. 语言名使用小写:java, javascript, typescript, css, sql, plsql, jsp',
'6. 严禁猜测。留空比猜测错误更安全。',
],
exampleTitle: '## 输入输出示例',
example1Input: '不要用 console.log,生产环境会泄露信息。还有不要留下未使用的变量,看着乱。',
example1Output: [
'- id: no-console-log',
' severity: warning',
' description: 禁止使用 console.log',
' message: 请使用 logger 工具替代 console.log',
' duplicateLevel: none',
'- id: no-unused-vars',
' severity: warning',
' description: 禁止未使用的变量',
' message: 未使用的变量应删除或注释',
' duplicateOf: eslint/no-unused-vars',
' duplicateLevel: exact',
].join('\n'),
example2Input: [
'本项目是一个电商后台管理系统,主要使用 Java + Spring Boot 开发。',
'代码规范要求:Service 层方法必须有日志记录,方便排查问题。',
'示例代码:',
' public void createOrder(Order order) { ... }',
'另外,Controller 层返回值统一用 Result 包装,不要直接返回 Map。',
].join('\n'),
example2Output: [
'- id: require-service-logging',
' severity: warning',
' description: Service 层方法必须有日志记录',
' message: Service 方法缺少日志记录,请补充以便排查问题',
' languages: [java]',
' duplicateLevel: none',
'- id: require-result-wrapper',
' severity: warning',
' description: Controller 返回值必须用 Result 包装',
' message: 请用 Result 包装返回值,不要直接返回 Map',
' languages: [java]',
' duplicateLevel: none',
].join('\n'),
staticAnalysisTitle: '## 静态分析重复检测',
staticAnalysisLines: [
'对于每条规则,判断其检测目标与触发条件是否与上述某个 linter 规则或自定义规则重复:',
'',
'- **exact**:检测目标与触发条件完全一致(会报出同样的问题行)→ 输出 duplicateOf + duplicateLevel: exact',
'- **overlap**:检测目标相同,但本规则有额外要求或更窄范围 → 输出 duplicateOf + duplicateLevel: overlap + duplicateReason',
'- **none**:检测目标不同 → 输出 duplicateLevel: none',
'',
'仅"话题相似"不算重复。例如:',
'- "未使用变量应删除" → exact(重复 eslint/no-unused-vars',
'- "禁止在 console.log 中输出敏感信息" → none(检测目标不同)',
'',
'仅输出 YAML,不要额外说明。',
],
finalInstruction: '',
dedupHeader: '## 已知规则清单(用于重复检测)',
dedupLinterLabel: (name, count) => `### ${name} (${count} 条)`,
dedupCustomLabel: (count) => `### 已导入的自定义规则 (${count} 条)`,
dedupFooter: '判定时请精确匹配上述规则 ID,而非模糊匹配分类。',
outputLang: '输出语言:zh-CN',
},
en: {
role: (inputDesc) => `You are a code review rule converter. Convert the ${inputDesc} provided by the user into structured YAML format for a code review tool. All descriptions and messages must be in English.`,
inputToleranceTitle: '## Input Tolerance',
inputToleranceLines: [
'User input may come in various forms. You must accept and process any of the following:',
'- Natural language paragraphs (one or more paragraphs describing rules)',
'- Unordered lists (one rule per line or paragraph)',
'- Tables (column names may vary; infer from semantics)',
'- Mixed forms (paragraphs + lists + tables)',
'',
'Do not reject conversion due to non-standard input format. Actively extract rule semantics from loose descriptions.',
],
nonRuleFilterTitle: '## Non-Rule Content Filtering',
nonRuleFilterLines: [
'User input may contain project introductions, background info, code examples, section titles, etc. You must:',
'- Identify and skip non-rule content; only convert actual coding rules into YAML entries',
'- Code examples, project descriptions etc. serve only as context for understanding rule semantics; do not output them as rules',
'- If content cannot be identified as a rule (no rule intent or violation hint), ignore it; do not force conversion',
],
fieldDefsTitle: '## Field Definitions',
fieldDefsLines: [
'Each rule must include the following fields:',
'- id: Unique rule identifier (kebab-case English, semantic and concise, e.g., no-console-log, avoid-magic-number)',
' **Must** generate a semantic id based on the rule description content',
' Even if no explicit id is present in the input, infer a suitable id from the description/message semantics',
' IDs must not be duplicated across rules',
'- severity: Severity level (error / warning / info)',
' **Must** output. Infer based on rule semantics:',
' error: causes bugs / security issues / data corruption',
' warning: potential issues / bad practices',
' info: style / readability suggestions',
' Even if no explicit severity is given, infer from the rule\'s impact',
'- description: Short rule description',
' **Must** output. If not obvious from input, derive from message content',
'- message: Violation message',
' **Must** output. If not obvious from input, derive from description content',
' description and message may be semantically similar; no need to force different tones, but both must be filled',
'- languages: Applicable language array (optional, e.g., [javascript, typescript])',
'- excludeLanguages: Explicitly excluded language array (optional, e.g., [css, sql])',
'- duplicateOf: Duplicate rule ID (linter e.g., eslint/no-console; custom e.g., custom/my-rule)',
'- duplicateLevel: Duplicate level (exact / overlap / none)',
'- duplicateReason: Duplicate/overlap reason (required for overlap)',
],
languageRulesTitle: '## Language Field Rules (Strict)',
languageRulesLines: [
'For each rule\'s languages field:',
'1. If rule description mentions specific languages (e.g., "Java", "JavaScript", "CSS") → use languages whitelist',
'2. If rule applies to most languages, with few exceptions → use excludeLanguages blacklist',
'3. If applicable language cannot be determined, or rule is general → leave both languages and excludeLanguages empty',
'4. languages and excludeLanguages must not both be non-empty simultaneously',
'5. Use lowercase language names: java, javascript, typescript, css, sql, plsql, jsp',
'6. Never guess. Leaving empty is safer than guessing incorrectly.',
],
exampleTitle: '## Input/Output Examples',
example1Input: 'Do not use console.log, it leaks information in production. Also do not leave unused variables, they look messy.',
example1Output: [
'- id: no-console-log',
' severity: warning',
' description: Forbid using console.log',
' message: Use a logger tool instead of console.log',
' duplicateLevel: none',
'- id: no-unused-vars',
' severity: warning',
' description: Forbid unused variables',
' message: Unused variables should be deleted or commented out',
' duplicateOf: eslint/no-unused-vars',
' duplicateLevel: exact',
].join('\n'),
example2Input: [
'This project is an e-commerce backend, mainly using Java + Spring Boot.',
'Coding rules: Service layer methods must have logging for debugging.',
'Example code:',
' public void createOrder(Order order) { ... }',
'Also, Controller layer return values should use Result wrapper, do not return Map directly.',
].join('\n'),
example2Output: [
'- id: require-service-logging',
' severity: warning',
' description: Service layer methods must have logging',
' message: Service method missing logging, add for debugging',
' languages: [java]',
' duplicateLevel: none',
'- id: require-result-wrapper',
' severity: warning',
' description: Controller return values must use Result wrapper',
' message: Use Result wrapper for return values, do not return Map directly',
' languages: [java]',
' duplicateLevel: none',
].join('\n'),
staticAnalysisTitle: '## Static Analysis Duplicate Detection',
staticAnalysisLines: [
'For each rule, determine whether its detection target and trigger conditions duplicate any linter rule or custom rule above:',
'',
'- **exact**: Detection target and trigger conditions are completely identical (would flag the same line) → output duplicateOf + duplicateLevel: exact',
'- **overlap**: Same detection target but this rule has additional requirements or narrower scope → output duplicateOf + duplicateLevel: overlap + duplicateReason',
'- **none**: Different detection targets → output duplicateLevel: none',
'',
'"Same topic" alone does not count as duplicate. For example:',
'- "Unused variables should be deleted" → exact (duplicate of eslint/no-unused-vars)',
'- "Do not output sensitive info in console.log" → none (different detection target)',
'',
'Output YAML only, no extra explanation.',
],
finalInstruction: 'All descriptions and messages must be written in English.',
dedupHeader: '## Known Rules (for duplicate detection)',
dedupLinterLabel: (name, count) => `### ${name} (${count} rules)`,
dedupCustomLabel: (count) => `### Imported custom rules (${count} rules)`,
dedupFooter: 'Match exactly by rule ID above, not by fuzzy category matching.',
outputLang: 'Output language: en',
},
ja: {
role: (inputDesc) => `あなたはコードレビュールール変換ツールです。ユーザーが提供した${inputDesc}を、コードレビューツール用の構造化YAML形式に変換してください。すべての説明とメッセージは日本語で出力してください。`,
inputToleranceTitle: '## 入力許容について',
inputToleranceLines: [
'ユーザー入力は様々な形式である可能性があります。以下の形式を受け入れ、処理する必要があります:',
'- 自然言語の段落(1つ以上の段落でルールを記述)',
'- 順不同リスト(各ルールが1行または1段落)',
'- テーブル(列名は固定されていません。意味から推測してください)',
'- 混合形式(段落 + リスト + テーブルの組み合わせ)',
'',
'非標準的な入力形式であっても変換を拒否してはいけません。緩やかな記述からルールの意味を積極的に抽出してください。',
],
nonRuleFilterTitle: '## 非ルールコンテンツのフィルタリング',
nonRuleFilterLines: [
'ユーザー入力には、プロジェクト紹介、背景説明、コード例、セクションタイトルなどの非ルールコンテンツが混入している可能性があります。以下を行う必要があります:',
'- 非ルールコンテンツを識別してスキップし、実際のコーディングルールのみをYAMLエントリに変換する',
'- コード例やプロジェクト紹介などはルール意味理解のコンテキストとしてのみ使用し、これら自体をルールとして出力しない',
'- ルールと判断できない内容(ルール意図も違反のヒントもない場合)は無視し、無理に変換しない',
],
fieldDefsTitle: '## フィールド定義',
fieldDefsLines: [
'各ルールには以下のフィールドが必要です:',
'- id: ルールの一意識別子(kebab-caseの英語、意味的で簡潔、例:no-console-log、avoid-magic-number',
' **必須** ルール説明内容に基づいて意味的なidを自動生成する',
' 入力に明示的なidがない場合でも、description/messageの意味から適切なidを推測する',
' 複数ルール間でidが重複してはいけない',
'- severity: 重大度(error / warning / info',
' **必須**で出力。ルールの意味に従って推測:',
' error: バグ/セキュリティ問題/データ破損を引き起こす',
' warning: 潜在的な問題/悪い慣行',
' info: スタイル/可読性の提案',
' 入力に明示的な重大度がない場合でも、ルールの影響の重大さから推測する',
'- description: ルールの簡単な説明',
' **必須**で出力。入力で不明確な場合、messageの内容から逆算して短い説明を導出',
'- message: 違反時のメッセージ',
' **必須**で出力。入力で不明確な場合、descriptionの内容から違反メッセージを導出',
' descriptionとmessageは意味的に近くても構いません。口調を無理に区別する必要はありませんが、両方とも必須です',
'- languages: 対象言語配列(オプション、例:[javascript, typescript]',
'- excludeLanguages: 明示的に除外する言語配列(オプション、例:[css, sql]',
'- duplicateOf: 重複するルールID(リンター例:eslint/no-console、カスタム例:custom/my-rule',
'- duplicateLevel: 重複レベル(exact / overlap / none',
'- duplicateReason: 重複/重複理由の説明(overlapの場合は必須)',
],
languageRulesTitle: '## 言語フィールドルール(厳守)',
languageRulesLines: [
'各ルールのlanguagesフィールドについて:',
'1. ルール説明に明確な言語キーワードがある場合(例:「Java」「JavaScript」「CSS」)→ languagesにホワイトリストを使用',
'2. ルールがほとんどの言語に適用され、一部のみ適用外の場合 → excludeLanguagesにブラックリストを使用',
'3. 適用言語が判断できない場合、またはルールが汎用の場合 → languagesとexcludeLanguagesの両方を空にする',
'4. languagesとexcludeLanguagesは同時に空であってはいけない',
'5. 言語名は小文字を使用:java, javascript, typescript, css, sql, plsql, jsp',
'6. 推測は厳禁。空のままにする方が誤った推測より安全です。',
],
exampleTitle: '## 入出力例',
example1Input: 'console.logは本番環境で情報漏洩するため使用しないでください。また、未使用の変数は残さないでください。散らかって見えます。',
example1Output: [
'- id: no-console-log',
' severity: warning',
' description: console.logの使用禁止',
' message: loggerツールを使用してconsole.logを代替してください',
' duplicateLevel: none',
'- id: no-unused-vars',
' severity: warning',
' description: 未使用変数の禁止',
' message: 未使用の変数は削除またはコメントアウトしてください',
' duplicateOf: eslint/no-unused-vars',
' duplicateLevel: exact',
].join('\n'),
example2Input: [
'本プロジェクトはECサイト管理システムで、主にJava + Spring Bootを使用しています。',
'コード規約:Service層のメソッドには必ずログ記録が必要です。問題調査のためです。',
'コード例:',
' public void createOrder(Order order) { ... }',
'また、Controller層の戻り値は統一してResultでラップし、Mapを直接返さないでください。',
].join('\n'),
example2Output: [
'- id: require-service-logging',
' severity: warning',
' description: Service層メソッドにはログ記録が必須',
' message: Serviceメソッドにログ記録がありません。問題調査のため追加してください',
' languages: [java]',
' duplicateLevel: none',
'- id: require-result-wrapper',
' severity: warning',
' description: Controllerの戻り値はResultでラップすること',
' message: Resultで戻り値をラップし、Mapを直接返さないでください',
' languages: [java]',
' duplicateLevel: none',
].join('\n'),
staticAnalysisTitle: '## 静的解析重複検出',
staticAnalysisLines: [
'各ルールについて、その検出対象とトリガー条件が上記のリンタールールまたはカスタムルールと重複するか判断:',
'',
'- **exact**: 検出対象とトリガー条件が完全に一致(同じ問題行を報告する)→ duplicateOf + duplicateLevel: exact を出力',
'- **overlap**: 検出対象は同じだが、このルールに追加要件やより狭い範囲がある → duplicateOf + duplicateLevel: overlap + duplicateReason を出力',
'- **none**: 検出対象が異なる → duplicateLevel: none を出力',
'',
'単に「トピックが類似している」だけでは重複とみなされません。例:',
'- 「未使用変数は削除すべき」→ exacteslint/no-unused-varsと重複)',
'- 「console.logで機密情報を出力しない」→ none(検出対象が異なる)',
'',
'YAMLのみを出力し、追加説明は不要です。',
],
finalInstruction: 'すべてのdescriptionとmessageは日本語で出力してください。',
dedupHeader: '## 既知ルール一覧(重複検出用)',
dedupLinterLabel: (name, count) => `### ${name}${count} 件)`,
dedupCustomLabel: (count) => `### インポート済みカスタムルール(${count} 件)`,
dedupFooter: '上記ルールIDで正確にマッチングしてください。曖昧なカテゴリマッチングは避けてください。',
outputLang: '出力言語:ja',
},
};
function getLang(): Lang {
const lang = getLanguage();
if (lang === 'en' || lang === 'ja') { return lang; }
return 'zh-CN';
}
function buildDedupPromptSection(existingCustomRules?: CustomRule[], lang?: Lang): string {
const l = lang ?? getLang();
const s = p[l];
const lines: string[] = [s.dedupHeader];
for (const [linter, rules] of Object.entries(staticRules.rules)) {
lines.push(s.dedupLinterLabel(linter, rules.length));
for (const rule of rules) {
lines.push(`- ${rule.id}: ${rule.description}`);
}
lines.push('');
}
if (existingCustomRules && existingCustomRules.length > 0) {
lines.push(s.dedupCustomLabel(existingCustomRules.length));
for (const rule of existingCustomRules) {
lines.push(`- custom/${rule.id}: ${rule.description}`);
}
lines.push('');
}
lines.push(s.dedupFooter);
return lines.join('\n');
}
export function buildSystemPrompt(inputType: PromptInputType, existingRules?: CustomRule[]): string {
const lang = getLang();
const s = p[lang];
const inputDesc = inputType === 'spreadsheet'
? (lang === 'zh-CN' ? '表格规则数据' : lang === 'ja' ? '表形式のルールデータ' : 'spreadsheet rule data')
: (lang === 'zh-CN' ? '自然语言规则描述' : lang === 'ja' ? '自然言語のルール記述' : 'natural language rule description');
const parts: string[] = [
s.role(inputDesc),
'',
s.inputToleranceTitle,
...s.inputToleranceLines,
'',
s.nonRuleFilterTitle,
...s.nonRuleFilterLines,
'',
s.fieldDefsTitle,
...s.fieldDefsLines,
'',
s.languageRulesTitle,
...s.languageRulesLines,
'',
s.exampleTitle,
'',
`${lang === 'zh-CN' ? '示例 1 — 松散段落输入:' : lang === 'ja' ? '例1 — 緩やかな段落入力:' : 'Example 1 — Loose paragraph input:'}`,
`${lang === 'zh-CN' ? '输入' : lang === 'ja' ? '入力' : 'Input'}${s.example1Input}`,
`${lang === 'zh-CN' ? '输出' : lang === 'ja' ? '出力' : 'Output'}`,
s.example1Output,
'',
`${lang === 'zh-CN' ? '示例 2 — 含非规则内容的混合输入:' : lang === 'ja' ? '例2 — 非ルールコンテンツを含む混合入力:' : 'Example 2 — Mixed input with non-rule content:'}`,
`${lang === 'zh-CN' ? '输入' : lang === 'ja' ? '入力' : 'Input'}${s.example2Input}`,
`${lang === 'zh-CN' ? '输出' : lang === 'ja' ? '出力' : 'Output'}`,
s.example2Output,
'',
buildDedupPromptSection(existingRules, lang),
'',
s.staticAnalysisTitle,
...s.staticAnalysisLines,
'',
s.finalInstruction,
s.outputLang,
];
return parts.join('\n');
}
+15
View File
@@ -0,0 +1,15 @@
import * as fs from 'fs';
import * as vscode from 'vscode';
import { RuleConverter } from './converter';
import { convertContentWithAI } from '../import-service';
import { buildSystemPrompt } from './prompt-builder';
import type { CustomRule } from '../../types';
export class TxtConverter implements RuleConverter {
supportedExtensions = ['.txt'];
async convert(srcPath: string, context: vscode.ExtensionContext, existingRules?: CustomRule[]): Promise<string | null> {
const content = fs.readFileSync(srcPath, 'utf-8');
return convertContentWithAI(content, context, buildSystemPrompt('freeform', existingRules));
}
}
+12
View File
@@ -0,0 +1,12 @@
import * as fs from 'fs';
import * as vscode from 'vscode';
import { RuleConverter } from './converter';
import type { CustomRule } from '../../types';
export class YamlConverter implements RuleConverter {
supportedExtensions = ['.yaml', '.yml'];
async convert(srcPath: string, _context: vscode.ExtensionContext, _existingRules?: CustomRule[]): Promise<string | null> {
return fs.readFileSync(srcPath, 'utf-8');
}
}
+593
View File
@@ -0,0 +1,593 @@
import * as vscode from 'vscode';
import type { ConversionResult, PreviewDecision, ImportableRule } from './import-types';
import { t } from '../i18n/messages';
export async function showImportPreview(
result: ConversionResult,
): Promise<PreviewDecision | null> {
return new Promise((resolve) => {
const panel = vscode.window.createWebviewPanel(
'ruleImportPreview',
t('importPreview.title'),
vscode.ViewColumn.Active,
{ enableScripts: true },
);
const keepRule: Record<string, boolean> = {};
for (const rule of result.rules) {
keepRule[rule.id] = rule.duplicateLevel !== 'exact';
}
panel.webview.html = renderPreviewHtml(result, keepRule);
panel.webview.onDidReceiveMessage((msg) => {
if (msg.type === 'toggleRule') {
keepRule[msg.ruleId] = msg.keep;
} else if (msg.type === 'confirm') {
resolve({
keepRule,
confirmed: true,
editedRules: msg.editedRules as ImportableRule[] | undefined,
});
panel.dispose();
} else if (msg.type === 'cancel') {
resolve(null);
panel.dispose();
}
});
panel.onDidDispose(() => resolve(null));
});
}
const SEVERITY_OPTIONS = ['error', 'warning', 'info'];
const SEVERITY_COLORS: Record<string, string> = {
error: '#f48771',
warning: '#d29922',
info: '#58a6ff',
};
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(
r => r.duplicateLevel !== 'exact' && r.duplicateLevel !== 'overlap'
);
const totalKept = Object.values(keepRule).filter(Boolean).length;
const totalCommented = Object.values(keepRule).filter(v => !v).length;
function renderRuleCard(rule: ImportableRule): string {
const kept = keepRule[rule.id];
const color = SEVERITY_COLORS[rule.severity] || '#8b949e';
const editRule = rule;
let duplicateInfo = '';
if (rule.duplicateLevel === 'exact') {
const prefix = rule.duplicateOf?.startsWith('custom/') ? `${t('import.customRulePrefix')} ${rule.duplicateOf.slice(7)}` : (rule.duplicateOf ?? 'unknown');
duplicateInfo = `<div style="color:#8b949e;font-size:12px;margin-top:4px;">${t('import.duplicateOf', { 0: prefix })}</div>`;
} else if (rule.duplicateLevel === 'overlap') {
const prefix = rule.duplicateOf?.startsWith('custom/') ? `${t('import.customRulePrefix')} ${rule.duplicateOf.slice(7)}` : (rule.duplicateOf ?? 'unknown');
duplicateInfo = `
<div style="color:#d29922;font-size:12px;margin-top:4px;">${t('import.overlapWith', { 0: prefix })}</div>
${rule.duplicateReason ? `<div style="color:#8b949e;font-size:12px;margin-top:2px;">${t('import.overlapReason', { 0: rule.duplicateReason })}</div>` : ''}
`;
}
const statusBadge = rule.duplicateLevel === 'exact'
? `<span class="badge badge-exact">${kept ? t('import.badgeRestored') : t('import.badgeWillComment')}</span>`
: rule.duplicateLevel === 'overlap'
? `<span class="badge badge-overlap">${kept ? t('importPreview.keep') : t('importPreview.comment')}</span>`
: `<span class="badge badge-none">${t('importPreview.keep')}</span>`;
const tagValue = (tags: string[] | undefined) => tags && tags.length > 0 ? tags.join(',') : '';
const tagDisplay = (tags: string[] | undefined) => tags && tags.length > 0 ? tags.map(t => `<span class="tag" data-value="${t}">${t}<span class="tag-remove" data-tag="${t}">×</span></span>`).join('') : '';
return `
<div class="rule-card" data-ruleid="${rule.id}">
<div class="rule-card-header" onclick="toggleCard('${rule.id}')">
<div class="rule-card-summary">
<input class="rule-id-input${/^rule-\d+$/.test(rule.id) ? ' placeholder-id' : ''}" value="${rule.id}" onchange="syncId('${rule.id}', this.value)" onclick="event.stopPropagation()">
<span class="rule-severity-tag" style="background:${color}20;color:${color};border:1px solid ${color}40;">${editRule.severity}</span>
<span class="rule-desc-preview">${editRule.description}</span>
</div>
<div class="rule-card-meta">
${statusBadge}
<span class="expand-icon">▼</span>
</div>
</div>
<div class="rule-card-body" id="body-${rule.id}" style="display:none;">
<div class="edit-header">
<div class="id-row">
<span class="id-display-label">${t('import.idLabel')}</span>
<input class="id-display-input${/^rule-\d+$/.test(rule.id) ? ' placeholder-id' : ''}" value="${rule.id}" onchange="syncId('${rule.id}', this.value)">
${/^rule-\d+$/.test(rule.id) ? `<span class="placeholder-hint">${t('import.placeholderIdHint')}</span>` : ''}
</div>
<div class="keep-toggle">
<button class="toggle-btn ${kept ? 'active' : ''}" data-action="keep" onclick="event.stopPropagation();toggleKeep('${rule.id}', true)">${t('importPreview.keep')}</button>
<button class="toggle-btn ${!kept ? 'active' : ''}" data-action="comment" onclick="event.stopPropagation();toggleKeep('${rule.id}', false)">${t('importPreview.comment')}</button>
</div>
</div>
${duplicateInfo}
<div class="edit-field">
<label>${t('import.severityLabel')}</label>
<select onchange="updateRule('${rule.id}','severity',this.value)">
${SEVERITY_OPTIONS.map(s => `<option value="${s}" ${s === editRule.severity ? 'selected' : ''}>${s}</option>`).join('')}
</select>
</div>
<div class="edit-field">
<label>${t('import.descriptionLabel')}</label>
<textarea rows="2" onchange="updateRule('${rule.id}','description',this.value)">${editRule.description}</textarea>
</div>
<div class="edit-field">
<label>${t('import.messageLabel')}</label>
<textarea rows="2" onchange="updateRule('${rule.id}','message',this.value)">${editRule.message}</textarea>
</div>
<div class="edit-field">
<label>${t('import.languagesLabel')}</label>
<div class="tag-input-wrapper">
<div class="tag-list" data-ruleid="${rule.id}" data-field="languages">
${tagDisplay(editRule.languages)}
</div>
<input class="tag-input" data-ruleid="${rule.id}" data-field="languages" placeholder="${t('import.tagPlaceholder')}" value="">
</div>
</div>
<div class="edit-field">
<label>${t('import.excludeLanguagesLabel')}</label>
<div class="tag-input-wrapper">
<div class="tag-list" data-ruleid="${rule.id}" data-field="excludeLanguages">
${tagDisplay(editRule.excludeLanguages)}
</div>
<input class="tag-input" data-ruleid="${rule.id}" data-field="excludeLanguages" placeholder="${t('import.tagPlaceholder')}" value="">
</div>
</div>
</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, '')}`;
const show = rules.some(r => keepRule[r.id] !== undefined);
return `
<div style="margin-bottom:12px;">
<div class="section-header" onclick="toggleSection('${sectionId}')">
<span style="font-size:14px;">${icon}</span>
<span class="section-title">${title}${t('import.ruleCount', { 0: rules.length })}</span>
<span class="section-arrow">▶</span>
</div>
<div id="${sectionId}" style="display:${show ? 'block' : 'none'};">
${rules.map(renderRuleCard).join('')}
</div>
</div>
`;
}
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--vscode-font-family);
font-size: var(--vscode-font-size);
color: var(--vscode-foreground);
background: var(--vscode-editor-background);
padding: 16px; line-height: 1.5;
}
.header {
border-bottom: 1px solid var(--vscode-panel-border);
padding-bottom: 12px; margin-bottom: 12px;
}
.header-title { font-size: 16px; font-weight: 700; margin-bottom: 4px; }
.header-sub { font-size: 12px; color: var(--vscode-descriptionForeground); }
.summary {
display: flex; gap: 12px; margin-bottom: 16px;
}
.summary-item {
padding: 8px 12px; border-radius: 6px; font-size: 12px;
border: 1px solid var(--vscode-panel-border);
background: var(--vscode-sideBar-background, var(--vscode-editor-background));
}
.summary-bar {
margin-bottom: 16px; padding: 8px 12px; border-radius: 6px;
font-size: 12px; background: rgba(139,92,246,0.1);
border: 1px solid rgba(139,92,246,0.3); color: #a78bfa;
}
.actions {
display: flex; gap: 8px; padding-top: 12px;
border-top: 1px solid var(--vscode-panel-border);
justify-content: flex-end;
}
.btn {
padding: 6px 16px; border: 1px solid var(--vscode-panel-border);
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border-radius: 6px; cursor: pointer; font-size: 12px;
}
.btn:hover { background: var(--vscode-button-secondaryHoverBackground); }
.btn-primary { background: #7c3aed; color: #fff; border-color: #7c3aed; }
.btn-primary:hover { background: #8b5cf6; }
.section-header {
display: flex; align-items: center; gap: 8px; margin-bottom: 8px;
cursor: pointer; padding: 4px 0;
}
.section-title { font-weight: 600; font-size: 13px; }
.section-arrow { font-size: 10px; color: var(--vscode-descriptionForeground); }
.rule-card {
border: 1px solid var(--vscode-panel-border);
border-radius: 8px; margin-bottom: 8px; overflow: hidden;
}
.rule-card-header {
display: flex; align-items: center; justify-content: space-between;
padding: 10px 12px; cursor: pointer; gap: 8px;
}
.rule-card-header:hover { background: var(--vscode-list-hoverBackground); }
.rule-card-summary {
display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0;
}
.rule-id-input {
font-family: monospace; font-size: 13px; font-weight: 600; white-space: nowrap;
background: transparent;
border: 1px solid transparent;
border-radius: 3px;
color: var(--vscode-foreground);
padding: 1px 4px; outline: none;
width: auto; min-width: 120px;
}
.rule-id-input:focus {
border-color: var(--vscode-focusBorder, #7c3aed);
background: var(--vscode-input-background);
}
.rule-id-input.placeholder-id {
border-color: #f59e0b !important;
box-shadow: 0 0 0 1px rgba(245, 158, 11, 0.3);
}
.rule-severity-tag {
padding: 1px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; white-space: nowrap;
}
.rule-desc-preview {
font-size: 12px; color: var(--vscode-descriptionForeground);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rule-card-meta {
display: flex; align-items: center; gap: 8px; flex-shrink: 0;
}
.expand-icon { font-size: 10px; color: var(--vscode-descriptionForeground); }
.badge {
padding: 1px 8px; border-radius: 10px; font-size: 11px; font-weight: 600;
}
.badge-exact { background: rgba(248,81,73,0.15); color: #f48771; }
.badge-overlap { background: rgba(210,153,34,0.15); color: #d29922; }
.badge-none { background: rgba(35,134,54,0.15); color: #3fb950; }
.rule-card-body {
padding: 0 12px 12px; border-top: 1px solid var(--vscode-panel-border);
}
.edit-header {
display: flex; align-items: center; justify-content: space-between;
padding: 10px 0 8px;
}
.id-row {
display: flex; align-items: center; gap: 8px;
}
.keep-toggle { display: flex; gap: 4px; }
.toggle-btn {
padding: 3px 12px; border-radius: 4px; cursor: pointer; font-size: 11px;
border: 1px solid var(--vscode-panel-border);
background: transparent; color: var(--vscode-foreground);
}
.toggle-btn.active {
background: rgba(35,134,54,0.15); color: #3fb950; border-color: rgba(35,134,54,0.3);
}
.toggle-btn.active[data-action="comment"] {
background: rgba(248,81,73,0.15); color: #f48771; border-color: rgba(248,81,73,0.3);
}
.edit-field { margin-top: 10px; }
.edit-field label {
display: block; font-size: 11px; font-weight: 600;
color: var(--vscode-descriptionForeground); margin-bottom: 4px;
text-transform: uppercase; letter-spacing: 0.5px;
}
.id-display-label {
font-size: 11px; font-weight: 600;
color: var(--vscode-descriptionForeground);
text-transform: uppercase; letter-spacing: 0.5px;
}
.id-display-input {
font-family: monospace; font-size: 13px; font-weight: 600;
background: var(--vscode-input-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 3px;
color: var(--vscode-input-foreground);
padding: 4px 8px; outline: none;
width: auto; min-width: 200px;
}
.id-display-input:focus {
border-color: var(--vscode-focusBorder, #7c3aed);
}
.id-display-input.placeholder-id {
border-color: #f59e0b !important;
box-shadow: 0 0 0 1px rgba(245, 158, 11, 0.3);
}
.placeholder-hint {
color: #d29922; font-size: 11px;
}
.edit-field select, .edit-field textarea {
width: 100%; padding: 6px 8px; border-radius: 4px;
border: 1px solid var(--vscode-panel-border);
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
font-family: var(--vscode-font-family);
font-size: var(--vscode-font-size);
}
.edit-field textarea { resize: vertical; }
.tag-input-wrapper {
border: 1px solid var(--vscode-panel-border);
border-radius: 4px; padding: 4px 6px;
background: var(--vscode-input-background);
display: flex; flex-wrap: wrap; gap: 4px; align-items: center;
}
.tag-list { display: flex; flex-wrap: wrap; gap: 4px; }
.tag {
display: inline-flex; align-items: center; gap: 3px;
padding: 1px 6px; border-radius: 3px; font-size: 11px;
background: rgba(88,166,255,0.15); color: #58a6ff;
border: 1px solid rgba(88,166,255,0.3);
}
.tag-remove {
cursor: pointer; font-size: 13px; line-height: 1; opacity: 0.7;
}
.tag-remove:hover { opacity: 1; }
.tag-input {
border: none; outline: none; flex: 1; min-width: 80px;
background: transparent; color: var(--vscode-input-foreground);
font-size: 12px; padding: 2px 0;
}
.tag-input::placeholder { color: var(--vscode-input-placeholderForeground); }
.validation-error {
padding: 8px 12px; margin-bottom: 12px; border-radius: 6px;
background: rgba(248,81,73,0.15); color: #f48771;
border: 1px solid rgba(248,81,73,0.3); font-size: 12px;
}
</style>
</head>
<body>
<div class="header">
<div class="header-title">${t('importPreview.title')}</div>
<div class="header-sub">${t('importPreview.source', { 0: result.sourceFileName, 1: String(result.rules.length) })}</div>
</div>
<div class="summary">
<div class="summary-item" style="border-color:rgba(248,81,73,0.3);color:#f48771;">${t('import.exactDuplicate', { 0: exactRules.length })}</div>
<div class="summary-item" style="border-color:rgba(210,153,34,0.3);color:#d29922;">${t('import.overlapDuplicate', { 0: overlapRules.length })}</div>
<div class="summary-item" style="border-color:rgba(35,134,54,0.3);color:#3fb950;">${t('import.noDuplicate', { 0: noneRules.length })}</div>
</div>
<div class="summary-bar" id="statusBar">
${t('import.statusBar', { 0: `<b id="keepCount">${totalKept}</b>`, 1: `<b id="commentCount">${totalCommented}</b>` })}
<span id="editHint" style="display:none;">${t('import.editedHint', { 0: '<b id="editCount">0</b>' })}</span>
</div>
<div id="validationError" class="validation-error" style="display:none;"></div>
${renderSection(t('import.sectionExact'), '⛔', exactRules, false)}
${renderSection(t('import.sectionOverlap'), '⚠️', overlapRules, true)}
${renderSection(t('import.sectionNone'), '✅', noneRules, false)}
<div class="actions">
<button class="btn" onclick="cancel()">${t('importPreview.cancel')}</button>
<button class="btn btn-primary" onclick="doConfirm()">${t('importPreview.confirm')}</button>
</div>
<script>
const vscode = acquireVsCodeApi();
const editedRules = {};
const VALIDATION_DESC_EMPTY = ${JSON.stringify(t('import.validationDescEmpty'))};
const VALIDATION_MSG_EMPTY = ${JSON.stringify(t('import.validationMsgEmpty'))};
const VALIDATION_ID_EMPTY = ${JSON.stringify(t('import.validationIdEmpty'))};
function syncId(originalId, newValue) {
const card = document.querySelector('.rule-card[data-ruleid="' + originalId + '"]');
if (!card) return;
const headerInput = card.querySelector('.rule-id-input');
const panelInput = card.querySelector('.id-display-input');
if (headerInput) headerInput.value = newValue;
if (panelInput) panelInput.value = newValue;
if (!editedRules[originalId]) editedRules[originalId] = {};
editedRules[originalId].id = newValue;
updateEditHint();
const isPlaceholder = /^rule-\\d+$/.test(newValue);
[headerInput, panelInput].forEach(el => {
if (el) {
el.classList.toggle('placeholder-id', isPlaceholder);
}
});
}
function toggleCard(ruleId) {
const body = document.getElementById('body-' + ruleId);
const card = body.closest('.rule-card');
const icon = card.querySelector('.expand-icon');
if (body.style.display === 'none') {
body.style.display = 'block';
icon.textContent = '▲';
} else {
body.style.display = 'none';
icon.textContent = '▼';
}
}
function toggleSection(id) {
const el = document.getElementById(id);
const arrow = el.previousElementSibling.querySelector('.section-arrow');
if (el.style.display === 'none') {
el.style.display = 'block';
arrow.textContent = '▼';
} else {
el.style.display = 'none';
arrow.textContent = '▶';
}
}
function toggleKeep(ruleId, keep) {
vscode.postMessage({ type: 'toggleRule', ruleId, keep });
const card = document.querySelector('.rule-card[data-ruleid="' + ruleId + '"]');
if (!card) return;
const btns = card.querySelectorAll('.toggle-btn');
btns.forEach(b => b.classList.toggle('active', (keep && b.dataset.action === 'keep') || (!keep && b.dataset.action === 'comment')));
updateSummary();
}
function updateRule(ruleId, field, value) {
if (!editedRules[ruleId]) {
editedRules[ruleId] = {};
}
editedRules[ruleId][field] = value;
updateEditHint();
}
function collectEditedRules() {
const result = [];
document.querySelectorAll('.rule-card').forEach(card => {
const originalId = card.dataset.ruleid;
const idInput = card.querySelector('.id-display-input');
const ruleId = idInput ? idInput.value.trim() || originalId : originalId;
const severityEl = card.querySelector('.edit-field select');
const textareas = card.querySelectorAll('.edit-field textarea');
const descEl = textareas[0];
const msgEl = textareas[1];
const langList = card.querySelector('.tag-list[data-field="languages"]');
const exclList = card.querySelector('.tag-list[data-field="excludeLanguages"]');
const rule = {
id: ruleId,
severity: severityEl ? severityEl.value : 'warning',
description: descEl ? descEl.value : '',
message: msgEl ? msgEl.value : '',
languages: langList ? Array.from(langList.querySelectorAll('.tag')).map(t => t.dataset.value) : [],
excludeLanguages: exclList ? Array.from(exclList.querySelectorAll('.tag')).map(t => t.dataset.value) : [],
};
result.push(rule);
});
return result;
}
function validate() {
const rules = collectEditedRules();
for (const rule of rules) {
if (!rule.id || !rule.id.trim()) {
return VALIDATION_ID_EMPTY;
}
if (!rule.description || !rule.description.trim()) {
return VALIDATION_DESC_EMPTY.replace('{0}', rule.id);
}
if (!rule.message || !rule.message.trim()) {
return VALIDATION_MSG_EMPTY.replace('{0}', rule.id);
}
}
return null;
}
function doConfirm() {
const err = validate();
if (err) {
const errEl = document.getElementById('validationError');
errEl.textContent = err;
errEl.style.display = 'block';
return;
}
const edited = collectEditedRules();
const hasEdits = Object.keys(editedRules).length > 0;
vscode.postMessage({ type: 'confirm', editedRules: hasEdits ? edited : undefined });
}
function cancel() {
vscode.postMessage({ type: 'cancel' });
}
function updateSummary() {
let keepCount = 0, commentCount = 0;
document.querySelectorAll('.rule-card').forEach(card => {
const ruleId = card.dataset.ruleid;
const keepBtns = card.querySelectorAll('.toggle-btn');
let isKept = true;
keepBtns.forEach(b => {
if (b.classList.contains('active') && b.dataset.action === 'comment') isKept = false;
});
if (isKept) keepCount++; else commentCount++;
});
document.getElementById('keepCount').textContent = keepCount;
document.getElementById('commentCount').textContent = commentCount;
}
function updateEditHint() {
const count = Object.keys(editedRules).length;
const hint = document.getElementById('editHint');
const countEl = document.getElementById('editCount');
if (count > 0) {
hint.style.display = 'inline';
countEl.textContent = count;
} else {
hint.style.display = 'none';
}
}
document.addEventListener('keydown', function(e) {
if (e.target.classList.contains('tag-input') && e.key === 'Enter') {
e.preventDefault();
const input = e.target;
const val = input.value.trim();
if (!val) return;
const ruleId = input.dataset.ruleid;
const field = input.dataset.field;
const list = input.parentElement.querySelector('.tag-list');
const existing = list.querySelectorAll('.tag');
const exists = Array.from(existing).some(t => t.dataset.value === val);
if (exists) { input.value = ''; return; }
const tag = document.createElement('span');
tag.className = 'tag';
tag.dataset.value = val;
tag.innerHTML = val + '<span class="tag-remove">×</span>';
list.appendChild(tag);
input.value = '';
const tags = Array.from(list.querySelectorAll('.tag')).map(t => t.dataset.value);
if (!editedRules[ruleId]) editedRules[ruleId] = {};
editedRules[ruleId][field] = tags;
updateEditHint();
}
});
document.addEventListener('click', function(e) {
if (e.target.classList.contains('tag-remove')) {
const tag = e.target.closest('.tag');
const list = tag.closest('.tag-list');
const ruleId = list.dataset.ruleid;
const field = list.dataset.field;
tag.remove();
const remaining = Array.from(list.querySelectorAll('.tag')).map(t => t.dataset.value);
if (!editedRules[ruleId]) editedRules[ruleId] = {};
editedRules[ruleId][field] = remaining;
updateEditHint();
}
});
</script>
</body>
</html>`;
}
+390
View File
@@ -0,0 +1,390 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import { getApiKey } from '../config/secret';
import { getAIConfig, getAITimeout } from '../config/ai';
import { createProvider } from '../ai/factory';
import { RuleConverter } from './converters/converter';
import { loadActiveRules } from './yaml-parser';
import type { ConversionResult, ImportableRule, PreviewDecision } from './import-types';
import { t, getLanguage } from '../i18n/messages';
interface ParsedYamlItem {
id?: string;
severity?: string;
description?: string;
message?: string;
languages?: string[];
excludeLanguages?: string[];
duplicateOf?: string;
duplicateLevel?: string;
duplicateReason?: string;
[key: string]: unknown;
}
function parseSimpleYaml(content: string): ParsedYamlItem[] {
const items: ParsedYamlItem[] = [];
let current: ParsedYamlItem | null = null;
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) { continue; }
if (trimmed.startsWith('- ')) {
if (current) { items.push(current); }
current = {};
const indentMatch = trimmed.match(/^- (\w[\w-]*)\s*:\s*(.*)$/);
if (indentMatch) {
const key = indentMatch[1];
const raw = indentMatch[2].trim();
if (raw.startsWith('[') && raw.endsWith(']')) {
current[key] = raw.slice(1, -1).split(',').map(s =>
s.trim().replace(/^['"]|['"]$/g, '')
);
} else {
current[key] = raw;
}
}
} else if (current) {
const propMatch = trimmed.match(/^(\w[\w-]*)\s*:\s*(.*)$/);
if (propMatch) {
const key = propMatch[1];
const raw = propMatch[2].trim();
if (!raw || raw === '[]') {
current[key] = [];
} else if (raw.startsWith('[') && raw.endsWith(']')) {
current[key] = raw.slice(1, -1).split(',').map(s =>
s.trim().replace(/^['"]|['"]$/g, '')
);
} else {
current[key] = raw;
}
}
}
}
if (current) { items.push(current); }
return items;
}
export function parseImportableYaml(content: string): ImportableRule[] {
const items = parseSimpleYaml(content);
const validSeverities = new Set(['error', 'warning', 'info']);
return items
.filter(item => item.description || item.message)
.map((item, idx) => ({
id: (item.id && item.id.trim()) ? item.id.trim() : `rule-${idx + 1}`,
severity: item.severity && validSeverities.has(item.severity)
? item.severity as 'error' | 'warning' | 'info'
: 'warning',
description: (item.description ?? item.message ?? ''),
message: (item.message ?? item.description ?? ''),
languages: item.languages as string[] | undefined,
excludeLanguages: item.excludeLanguages as string[] | undefined,
duplicateOf: item.duplicateOf as string | undefined,
duplicateLevel: item.duplicateLevel as 'exact' | 'overlap' | 'none' | undefined,
duplicateReason: item.duplicateReason as string | undefined,
}));
}
export function buildFinalYaml(
yamlContent: string,
rules: ImportableRule[],
decision: PreviewDecision,
): string {
if (decision.editedRules && decision.editedRules.length > 0) {
return renderRulesToYaml(decision.editedRules, decision);
}
return buildFinalYamlFromRaw(yamlContent, rules, decision);
}
function renderRulesToYaml(
rules: ImportableRule[],
decision: PreviewDecision,
): string {
const lines: string[] = [];
for (const rule of rules) {
const keep = decision.keepRule[rule.id] ?? rule.duplicateLevel !== 'exact';
const ruleLines: string[] = [];
ruleLines.push(`- id: ${rule.id}`);
ruleLines.push(` severity: ${rule.severity}`);
ruleLines.push(` description: ${rule.description}`);
ruleLines.push(` message: ${rule.message}`);
if (rule.languages && rule.languages.length > 0) {
ruleLines.push(` languages: [${rule.languages.join(', ')}]`);
}
if (rule.excludeLanguages && rule.excludeLanguages.length > 0) {
ruleLines.push(` excludeLanguages: [${rule.excludeLanguages.join(', ')}]`);
}
if (keep) {
lines.push(...ruleLines);
} else {
const dupLevel = rule.duplicateLevel ?? 'none';
const dupOf = rule.duplicateOf ?? 'manual';
if (dupLevel === 'exact') {
lines.push(t('yaml.duplicateExact', { 0: dupOf }));
} else if (dupLevel === 'overlap') {
lines.push(t('yaml.duplicateOverlap', { 0: dupOf }));
if (rule.duplicateReason) {
lines.push(t('yaml.overlapReason', { 0: rule.duplicateReason }));
}
} else {
lines.push(t('yaml.manualComment'));
}
lines.push(t('yaml.enableHint'));
for (const rl of ruleLines) {
lines.push(`# ${rl}`);
}
}
lines.push('');
}
return lines.join('\n');
}
function buildFinalYamlFromRaw(
yamlContent: string,
rules: ImportableRule[],
decision: PreviewDecision,
): string {
const defaultKeep = (rule: ImportableRule) => rule.duplicateLevel !== 'exact';
const shouldKeep = (rule: ImportableRule) =>
decision.keepRule[rule.id] ?? defaultKeep(rule);
const lines = yamlContent.split('\n');
const output: string[] = [];
let currentRuleId: string | null = null;
let ruleLines: string[] = [];
function flushRule(): void {
if (currentRuleId === null) {
output.push(...ruleLines);
} else {
const rule = rules.find(r => r.id === currentRuleId);
const keep = rule ? shouldKeep(rule) : true;
if (keep) {
const filtered = ruleLines.filter(
line => !line.trimStart().startsWith('duplicateOf:') &&
!line.trimStart().startsWith('duplicateLevel:') &&
!line.trimStart().startsWith('duplicateReason:')
);
output.push(...filtered);
} else {
const level = rule?.duplicateLevel ?? 'exact';
const dupInfo = rule?.duplicateOf ?? 'unknown';
const reason = rule?.duplicateReason ?? '';
if (level === 'exact') {
if (dupInfo.startsWith('custom/')) {
output.push(t('yaml.duplicateExactCustom', { 0: dupInfo.slice(7) }));
} else {
output.push(t('yaml.duplicateExactGeneric', { 0: dupInfo }));
}
} else {
if (dupInfo.startsWith('custom/')) {
output.push(t('yaml.duplicateOverlapCustom', { 0: level, 1: dupInfo.slice(7) }));
} else {
output.push(t('yaml.duplicateOverlapGeneric', { 0: level, 1: dupInfo }));
}
if (reason) { output.push(t('yaml.overlapReason', { 0: reason })); }
}
output.push(t('yaml.enableHint'));
for (const line of ruleLines) {
output.push(line.trim() ? `# ${line}` : '#');
}
}
}
ruleLines = [];
}
for (const line of lines) {
const ruleStart = line.match(/^-\s+id:\s*(.+)/);
if (ruleStart) {
flushRule();
currentRuleId = ruleStart[1].trim();
ruleLines = [line];
} else if (currentRuleId) {
ruleLines.push(line);
} else {
ruleLines.push(line);
}
}
flushRule();
return output.join('\n');
}
export class ImportService {
private converters: Map<string, RuleConverter> = new Map();
registerConverter(converter: RuleConverter): void {
for (const ext of converter.supportedExtensions) {
this.converters.set(ext, converter);
}
}
async convert(
srcPath: string,
context: vscode.ExtensionContext,
): Promise<ConversionResult> {
const ext = path.extname(srcPath).toLowerCase();
const converter = this.converters.get(ext);
if (!converter) {
throw new Error(t('import.unsupportedFormat', { 0: ext }));
}
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
const existingRules = workspaceRoot ? loadActiveRules(workspaceRoot) : [];
const yamlContent = await converter.convert(srcPath, context, existingRules);
if (!yamlContent) {
throw new Error(t('import.conversionFailed'));
}
const rules = parseImportableYaml(yamlContent);
const exactCount = rules.filter(r => r.duplicateLevel === 'exact').length;
const overlapCount = rules.filter(r => r.duplicateLevel === 'overlap').length;
return {
rules,
yamlContent,
sourceFileName: path.basename(srcPath),
exactCount,
overlapCount,
};
}
applyConversion(
result: ConversionResult,
decision: PreviewDecision,
targetPath: string,
): void {
const finalYaml = buildFinalYaml(result.yamlContent, result.rules, decision);
const dir = path.dirname(targetPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(targetPath, finalYaml, 'utf-8');
}
}
function buildFallbackPrompt(): string {
const lang = getLanguage();
if (lang === 'ja') {
return `あなたはコードレビュールール変換ツールです。ユーザーが提供した自然言語のルール記述を、構造化されたYAML形式に変換してください。
各ルールには以下のフィールドが必要です:
- id: ルールの一意識別子(kebab-case英語)
- severity: 重要度(error / warning / info
- description: ルールの簡単な説明
- message: 違反時のメッセージ
- languages: 対象言語配列(オプション、例:[javascript, typescript]
出力形式の例:
- id: no-console-log
severity: warning
description: console.logの使用禁止
message: loggerツールを使用してconsole.logを代替してください
languages: [javascript, typescript]
YAMLのみを出力し、追加説明は不要です。
出力言語:ja`;
}
if (lang === 'en') {
return `You are a code review rule converter. Convert the natural language rule description provided by the user into structured YAML format for a code review tool.
Each rule must include the following fields:
- id: Unique rule identifier (kebab-case English)
- severity: Severity level (error / warning / info)
- description: Short rule description
- message: Violation message
- languages: Applicable language array (optional, e.g., [javascript, typescript])
Output format example:
- id: no-console-log
severity: warning
description: Forbid using console.log
message: Use a logger tool instead of console.log
languages: [javascript, typescript]
Output YAML only, no extra explanation.
Output language: en`;
}
return `你是一个代码审查规则转换器。将用户提供的自然语言规则描述,转换为结构化的 YAML 格式,用于代码审查工具。
每条规则需要包含以下字段:
- id: 规则唯一标识(kebab-case 英文)
- severity: 严重级别(error / warning / info
- description: 规则简短描述
- message: 违反时的提示消息
- languages: 适用语言数组(可选,如 [javascript, typescript]
输出格式示例:
- id: no-console-log
severity: warning
description: 禁止使用 console.log
message: 请使用 logger 工具替代 console.log
languages: [javascript, typescript]
仅输出 YAML,不要额外说明。
输出语言:zh-CN`;
}
export async function convertContentWithAI(
content: string,
context: vscode.ExtensionContext,
systemPrompt?: string,
): Promise<string | null> {
if (!content.trim()) {
vscode.window.showErrorMessage(t('import.emptyFile'));
return null;
}
const apiKey = await getApiKey(context);
if (!apiKey) {
vscode.window.showErrorMessage(t('import.needApiKey'));
return null;
}
const config = getAIConfig();
const provider = createProvider(config.provider, apiKey, config.baseUrl);
const prompt = systemPrompt ?? buildFallbackPrompt();
let yamlOutput: string;
try {
yamlOutput = await provider.chat(prompt, content, {
model: config.model,
temperature: 0.1,
maxTokens: 4096,
timeoutMs: getAITimeout() * 1000,
});
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') {
vscode.window.showErrorMessage(t('import.timeout'));
} else {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(t('import.aiFail', { 0: msg }));
}
return null;
}
const cleaned = yamlOutput
.replace(/```(yaml|yml)?\s*/gi, '')
.replace(/```\s*$/gm, '')
.trim();
if (!cleaned) {
vscode.window.showErrorMessage(t('import.emptyResponse'));
return null;
}
return cleaned;
}
+21
View File
@@ -0,0 +1,21 @@
import type { CustomRule } from '../types';
export interface ImportableRule extends CustomRule {
duplicateOf?: string;
duplicateLevel?: 'exact' | 'overlap' | 'none';
duplicateReason?: string;
}
export interface ConversionResult {
rules: ImportableRule[];
yamlContent: string;
sourceFileName: string;
exactCount: number;
overlapCount: number;
}
export interface PreviewDecision {
keepRule: Record<string, boolean>;
confirmed: boolean;
editedRules?: ImportableRule[];
}
+68
View File
@@ -0,0 +1,68 @@
import * as vscode from 'vscode';
import type { CustomRule } from '../types';
const LANGUAGE_ALIASES: Record<string, string[]> = {
typescriptreact: ['typescript', 'typescriptreact', 'tsx'],
javascriptreact: ['javascript', 'javascriptreact', 'jsx'],
};
const LANGUAGE_GROUPS: Record<string, string[]> = {
sql: ['sql', 'plsql'],
plsql: ['sql', 'plsql'],
};
const JSP_SUB_LANGUAGES = ['java', 'javascript', 'typescript', 'css', 'jsp', 'html'];
const JSP_EXTENSIONS = ['.jsp', '.jspx'];
function expandLanguageId(languageId: string): string[] {
const aliases = LANGUAGE_ALIASES[languageId] ?? [languageId];
const groups = LANGUAGE_GROUPS[languageId] ?? [];
return [...new Set([...aliases, ...groups, languageId])];
}
function isJspFile(document: vscode.TextDocument): boolean {
return JSP_EXTENSIONS.some(ext => document.fileName.toLowerCase().endsWith(ext));
}
function matchesLanguage(rule: CustomRule, expandedLangs: string[]): boolean {
if (rule.excludeLanguages && rule.excludeLanguages.length > 0) {
if (rule.excludeLanguages.some(l => expandedLangs.includes(l))) {
return false;
}
}
if (!rule.languages || rule.languages.length === 0) {
return true;
}
return rule.languages.some(l => expandedLangs.includes(l));
}
export function filterForDocument(
rules: CustomRule[],
document: vscode.TextDocument,
): CustomRule[] {
const langId = document.languageId;
if (langId === 'html' && isJspFile(document)) {
return rules.filter(rule => matchesLanguage(rule, JSP_SUB_LANGUAGES));
}
const expandedLangs = expandLanguageId(langId);
return rules.filter(rule => matchesLanguage(rule, expandedLangs));
}
export interface FilterResult {
relevant: CustomRule[];
filteredOut: CustomRule[];
skippedRequestA: boolean;
}
export function filterAndSummarize(
rules: CustomRule[],
document: vscode.TextDocument,
): FilterResult {
const relevant = filterForDocument(rules, document);
const filteredOut = rules.filter(r => !relevant.includes(r));
return {
relevant,
filteredOut,
skippedRequestA: relevant.length === 0,
};
}
+1788
View File
@@ -0,0 +1,1788 @@
{
"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)"
},
"rules": {
"eslint": [
{
"id": "eslint/constructor-super",
"description": "Verify calls of super() in constructors"
},
{
"id": "eslint/for-direction",
"description": "Enforce for loop update clause moving the counter in the right direction"
},
{
"id": "eslint/getter-return",
"description": "Enforce return statements in getters"
},
{
"id": "eslint/no-async-promise-executor",
"description": "Disallow using an async function as a Promise executor"
},
{
"id": "eslint/no-case-declarations",
"description": "Disallow lexical declarations in case clauses"
},
{
"id": "eslint/no-class-assign",
"description": "Disallow reassigning class members"
},
{
"id": "eslint/no-compare-neg-zero",
"description": "Disallow comparing against -0"
},
{
"id": "eslint/no-cond-assign",
"description": "Disallow assignment operators in conditional expressions"
},
{
"id": "eslint/no-const-assign",
"description": "Disallow reassigning const variables"
},
{
"id": "eslint/no-constant-binary-expression",
"description": "Disallow constant binary expressions"
},
{
"id": "eslint/no-constant-condition",
"description": "Disallow constant expressions in conditions"
},
{
"id": "eslint/no-control-regex",
"description": "Disallow control characters in regular expressions"
},
{
"id": "eslint/no-debugger",
"description": "Disallow the use of debugger"
},
{
"id": "eslint/no-delete-var",
"description": "Disallow deleting variables"
},
{
"id": "eslint/no-dupe-args",
"description": "Disallow duplicate arguments in function definitions"
},
{
"id": "eslint/no-dupe-class-members",
"description": "Disallow duplicate class members"
},
{
"id": "eslint/no-dupe-else-if",
"description": "Disallow duplicate conditions in if-else-if chains"
},
{
"id": "eslint/no-dupe-keys",
"description": "Disallow duplicate keys in object literals"
},
{
"id": "eslint/no-duplicate-case",
"description": "Disallow duplicate case labels"
},
{
"id": "eslint/no-empty",
"description": "Disallow empty block statements"
},
{
"id": "eslint/no-empty-character-class",
"description": "Disallow empty character classes in regular expressions"
},
{
"id": "eslint/no-empty-pattern",
"description": "Disallow empty destructuring patterns"
},
{
"id": "eslint/no-empty-static-block",
"description": "Disallow empty static blocks"
},
{
"id": "eslint/no-ex-assign",
"description": "Disallow reassigning exceptions in catch clauses"
},
{
"id": "eslint/no-extra-boolean-cast",
"description": "Disallow unnecessary boolean casts"
},
{
"id": "eslint/no-fallthrough",
"description": "Disallow fallthrough of case statements"
},
{
"id": "eslint/no-func-assign",
"description": "Disallow reassigning function declarations"
},
{
"id": "eslint/no-global-assign",
"description": "Disallow assignments to native objects or read-only global variables"
},
{
"id": "eslint/no-import-assign",
"description": "Disallow assigning to imported bindings"
},
{
"id": "eslint/no-invalid-regexp",
"description": "Disallow invalid regular expression strings in RegExp constructors"
},
{
"id": "eslint/no-irregular-whitespace",
"description": "Disallow irregular whitespace"
},
{
"id": "eslint/no-loss-of-precision",
"description": "Disallow literal numbers that lose precision"
},
{
"id": "eslint/no-misleading-character-class",
"description": "Disallow characters which are made with multiple code points in character class syntax"
},
{
"id": "eslint/no-new-native-nonconstructor",
"description": "Disallow new operators with global non-constructor functions"
},
{
"id": "eslint/no-nonoctal-decimal-escape",
"description": "Disallow \\8 and \\9 escape sequences in string literals"
},
{
"id": "eslint/no-obj-calls",
"description": "Disallow calling global object properties as functions"
},
{
"id": "eslint/no-octal",
"description": "Disallow octal literals"
},
{
"id": "eslint/no-prototype-builtins",
"description": "Disallow calling some Object.prototype methods directly on objects"
},
{
"id": "eslint/no-redeclare",
"description": "Disallow variable redeclaration"
},
{
"id": "eslint/no-regex-spaces",
"description": "Disallow multiple spaces in regular expression literals"
},
{
"id": "eslint/no-self-assign",
"description": "Disallow assignments where both sides are exactly the same"
},
{
"id": "eslint/no-setter-return",
"description": "Disallow returning values from setters"
},
{
"id": "eslint/no-shadow-restricted-names",
"description": "Disallow identifiers from shadowing restricted names"
},
{
"id": "eslint/no-sparse-arrays",
"description": "Disallow sparse arrays"
},
{
"id": "eslint/no-this-before-super",
"description": "Disallow this/super before calling super() in constructors"
},
{
"id": "eslint/no-undef",
"description": "Disallow undeclared variables"
},
{
"id": "eslint/no-unexpected-multiline",
"description": "Disallow confusing multiline expressions"
},
{
"id": "eslint/no-unreachable",
"description": "Disallow unreachable code after return, throw, continue, and break statements"
},
{
"id": "eslint/no-unsafe-finally",
"description": "Disallow control flow statements in finally blocks"
},
{
"id": "eslint/no-unsafe-negation",
"description": "Disallow negating the left operand of relational operators"
},
{
"id": "eslint/no-unsafe-optional-chaining",
"description": "Disallow use of optional chaining in contexts where undefined is not allowed"
},
{
"id": "eslint/no-unused-labels",
"description": "Disallow unused labels"
},
{
"id": "eslint/no-unused-private-class-members",
"description": "Disallow unused private class members"
},
{
"id": "eslint/no-unused-vars",
"description": "Disallow unused variables"
},
{
"id": "eslint/no-useless-backreference",
"description": "Disallow useless backreferences in regular expressions"
},
{
"id": "eslint/no-useless-catch",
"description": "Disallow unnecessary catch clauses"
},
{
"id": "eslint/no-useless-escape",
"description": "Disallow unnecessary escape characters"
},
{
"id": "eslint/no-with",
"description": "Disallow with statements"
},
{
"id": "eslint/require-yield",
"description": "Require generator functions to contain yield"
},
{
"id": "eslint/use-isnan",
"description": "Require calls to isNaN() when checking for NaN"
},
{
"id": "eslint/valid-typeof",
"description": "Enforce comparing typeof expressions against valid strings"
}
],
"ts-eslint": [
{
"id": "ts-eslint/ban-ts-comment",
"description": "Disallow @ts-<directive> comments"
},
{
"id": "ts-eslint/no-array-constructor",
"description": "Disallow generic Array constructors"
},
{
"id": "ts-eslint/no-duplicate-enum-values",
"description": "Disallow duplicate enum member values"
},
{
"id": "ts-eslint/no-empty-object-type",
"description": "Disallow empty object types"
},
{
"id": "ts-eslint/no-explicit-any",
"description": "Disallow the any type"
},
{
"id": "ts-eslint/no-extra-non-null-assertion",
"description": "Disallow extra non-null assertions"
},
{
"id": "ts-eslint/no-misused-new",
"description": "Enforce valid definition of new and constructor"
},
{
"id": "ts-eslint/no-namespace",
"description": "Disallow custom TypeScript modules and namespaces"
},
{
"id": "ts-eslint/no-non-null-asserted-optional-chain",
"description": "Disallow non-null assertions after optional chain"
},
{
"id": "ts-eslint/no-require-imports",
"description": "Disallow invocation of require()"
},
{
"id": "ts-eslint/no-this-alias",
"description": "Disallow aliasing this"
},
{
"id": "ts-eslint/no-unnecessary-type-constraint",
"description": "Disallow unnecessary constraints on generic types"
},
{
"id": "ts-eslint/no-unsafe-declaration-merging",
"description": "Disallow unsafe declaration merging"
},
{
"id": "ts-eslint/no-unsafe-function-type",
"description": "Disallow using Function as a type"
},
{
"id": "ts-eslint/no-unused-expressions",
"description": "Disallow unused expressions"
},
{
"id": "ts-eslint/no-unused-vars",
"description": "Disallow unused variables"
},
{
"id": "ts-eslint/no-wrapper-object-types",
"description": "Disallow wrapper object types (String, Number, Boolean)"
},
{
"id": "ts-eslint/prefer-as-const",
"description": "Prefer as const over literal type annotation"
},
{
"id": "ts-eslint/prefer-namespace-keyword",
"description": "Require using namespace keyword over module keyword"
},
{
"id": "ts-eslint/triple-slash-reference",
"description": "Disallow certain triple slash directives"
},
{
"id": "ts-eslint/no-var",
"description": "Require let or const instead of var"
},
{
"id": "ts-eslint/prefer-const",
"description": "Require const declarations for never-reassigned variables"
},
{
"id": "ts-eslint/prefer-rest-params",
"description": "Require rest parameters instead of arguments"
},
{
"id": "ts-eslint/prefer-spread",
"description": "Require spread operator instead of .apply()"
}
],
"stylelint": [
{
"id": "stylelint/color-hex-length",
"description": "Specify short or long hexadecimal color values"
},
{
"id": "stylelint/color-named",
"description": "Require (where possible) or disallow named colors"
},
{
"id": "stylelint/color-no-invalid-hex",
"description": "Disallow invalid hex colors"
},
{
"id": "stylelint/length-zero-no-unit",
"description": "Disallow units for zero lengths"
},
{
"id": "stylelint/font-family-no-missing-generic-family-keyword",
"description": "Disallow missing generic families in font-family"
},
{
"id": "stylelint/block-no-empty",
"description": "Disallow empty blocks"
},
{
"id": "stylelint/declaration-block-no-duplicate-properties",
"description": "Disallow duplicate properties within declaration blocks"
},
{
"id": "stylelint/no-descending-specificity",
"description": "Disallow selectors of lower specificity from overriding higher specificity"
},
{
"id": "stylelint/unit-no-unknown",
"description": "Disallow unknown units"
},
{
"id": "stylelint/property-no-unknown",
"description": "Disallow unknown properties"
},
{
"id": "stylelint/selector-pseudo-class-no-unknown",
"description": "Disallow unknown pseudo-class selectors"
},
{
"id": "stylelint/selector-pseudo-element-no-unknown",
"description": "Disallow unknown pseudo-element selectors"
}
],
"pmd": [
{
"id": "pmd/AbstractClassWithoutAbstractMethod",
"description": "Abstract class does not contain any abstract methods"
},
{
"id": "pmd/AccessorClassGeneration",
"description": "Avoid instantiation through private constructors from outside"
},
{
"id": "pmd/AccessorMethodGeneration",
"description": "Avoid synthetic accessor methods"
},
{
"id": "pmd/ArrayIsStoredDirectly",
"description": "Clone objects before storing in constructors/methods"
},
{
"id": "pmd/AssertStatementInTest",
"description": "Assert statements should not be used in test code"
},
{
"id": "pmd/AvoidMessageDigestField",
"description": "Don't declare MessageDigest as field (thread safety)"
},
{
"id": "pmd/AvoidPrintStackTrace",
"description": "Use logger instead of printStackTrace()"
},
{
"id": "pmd/AvoidReassigningCatchVariables",
"description": "Don't reassign caught exception variables"
},
{
"id": "pmd/AvoidReassigningLoopVariables",
"description": "Don't reassign loop control variables"
},
{
"id": "pmd/AvoidReassigningParameters",
"description": "Don't reassign method parameters"
},
{
"id": "pmd/AvoidStringBufferField",
"description": "Avoid StringBuffer/StringBuilder as fields"
},
{
"id": "pmd/AvoidUsingHardCodedIP",
"description": "Externalize IP addresses"
},
{
"id": "pmd/CheckResultSet",
"description": "Always check navigation method return values of ResultSet"
},
{
"id": "pmd/ConstantsInInterface",
"description": "Avoid constants in interfaces"
},
{
"id": "pmd/DefaultLabelNotLastInSwitch",
"description": "Default label should be last in switch"
},
{
"id": "pmd/DoubleBraceInitialization",
"description": "Avoid double-brace initialization"
},
{
"id": "pmd/EnumComparison",
"description": "Compare enums with == not equals()"
},
{
"id": "pmd/ExhaustiveSwitchHasDefault",
"description": "Exhaustive switch should not have default case"
},
{
"id": "pmd/ForLoopCanBeForeach",
"description": "Replace for loop with foreach"
},
{
"id": "pmd/ForLoopVariableCount",
"description": "Limit control variables in for loops"
},
{
"id": "pmd/GuardLogStatement",
"description": "Check log level before logging"
},
{
"id": "pmd/ImplicitFunctionalInterface",
"description": "Annotate functional interfaces with @FunctionalInterface"
},
{
"id": "pmd/JUnit4SuitesShouldUseSuiteAnnotation",
"description": "Use @RunWith(Suite.class) annotation"
},
{
"id": "pmd/JUnitJupiterTestShouldBePackagePrivate",
"description": "JUnit 5 tests should be package-private"
},
{
"id": "pmd/JUnitUseExpected",
"description": "Use @Test(expected) annotation"
},
{
"id": "pmd/LabeledStatement",
"description": "Avoid labeled statements"
},
{
"id": "pmd/LiteralsFirstInComparisons",
"description": "Position literals first in String comparisons"
},
{
"id": "pmd/LooseCoupling",
"description": "Use interfaces instead of implementation types"
},
{
"id": "pmd/MethodReturnsInternalArray",
"description": "Return copy of internal array"
},
{
"id": "pmd/MissingOverride",
"description": "Add @Override annotation"
},
{
"id": "pmd/NonExhaustiveSwitch",
"description": "Switch should be exhaustive"
},
{
"id": "pmd/OneDeclarationPerLine",
"description": "One declaration per line"
},
{
"id": "pmd/PreserveStackTrace",
"description": "Preserve stack trace when rethrowing exceptions"
},
{
"id": "pmd/PrimitiveWrapperInstantiation",
"description": "Use valueOf() instead of new Type()"
},
{
"id": "pmd/RelianceOnDefaultCharset",
"description": "Specify charset explicitly"
},
{
"id": "pmd/ReplaceEnumerationWithIterator",
"description": "Use Iterator instead of Enumeration"
},
{
"id": "pmd/ReplaceHashtableWithMap",
"description": "Use Map instead of Hashtable"
},
{
"id": "pmd/ReplaceVectorWithList",
"description": "Use List/ArrayList instead of Vector"
},
{
"id": "pmd/SimplifiableTestAssertion",
"description": "Use more specific assertion methods"
},
{
"id": "pmd/SystemPrintln",
"description": "Use logger instead of System.out/err"
},
{
"id": "pmd/UnitTestAssertionsShouldIncludeMessage",
"description": "Include message in assertions"
},
{
"id": "pmd/UnitTestContainsTooManyAsserts",
"description": "Limit asserts per test"
},
{
"id": "pmd/UnitTestShouldIncludeAssert",
"description": "Test should include assertions"
},
{
"id": "pmd/UnitTestShouldUseAfterAnnotation",
"description": "Use @After/@AfterEach annotation"
},
{
"id": "pmd/UnitTestShouldUseBeforeAnnotation",
"description": "Use @Before/@BeforeEach annotation"
},
{
"id": "pmd/UnitTestShouldUseTestAnnotation",
"description": "Use @Test annotation"
},
{
"id": "pmd/UnnecessaryVarargsArrayCreation",
"description": "Don't create explicit array for varargs"
},
{
"id": "pmd/UnnecessaryWarningSuppression",
"description": "Remove unused PMD suppressions"
},
{
"id": "pmd/UnusedAssignment",
"description": "Remove unused assignments"
},
{
"id": "pmd/UnusedFormalParameter",
"description": "Remove unused parameters"
},
{
"id": "pmd/UnusedLabel",
"description": "Remove unused labels"
},
{
"id": "pmd/UnusedLocalVariable",
"description": "Remove unused local variables"
},
{
"id": "pmd/UnusedPrivateField",
"description": "Remove unused private fields"
},
{
"id": "pmd/UnusedPrivateMethod",
"description": "Remove unused private methods"
},
{
"id": "pmd/UseCollectionIsEmpty",
"description": "Use isEmpty() instead of size()==0"
},
{
"id": "pmd/UseEnumCollections",
"description": "Use EnumSet/EnumMap instead of HashSet/HashMap"
},
{
"id": "pmd/UseStandardCharsets",
"description": "Use StandardCharsets constants"
},
{
"id": "pmd/UseTryWithResources",
"description": "Use try-with-resources"
},
{
"id": "pmd/UseVarargs",
"description": "Use varargs instead of array parameter"
},
{
"id": "pmd/WhileLoopWithLiteralBoolean",
"description": "Simplify while loops with literal booleans"
},
{
"id": "pmd/AtLeastOneConstructor",
"description": "Each class should have a constructor"
},
{
"id": "pmd/AvoidDollarSigns",
"description": "Avoid $ in names"
},
{
"id": "pmd/AvoidProtectedFieldInFinalClass",
"description": "Don't use protected fields in final classes"
},
{
"id": "pmd/AvoidProtectedMethodInFinalClassNotExtending",
"description": "Don't use protected methods in final classes not extending"
},
{
"id": "pmd/AvoidUsingNativeCode",
"description": "Avoid JNI calls"
},
{
"id": "pmd/BooleanGetMethodName",
"description": "Boolean getters should be named isX()"
},
{
"id": "pmd/CallSuperInConstructor",
"description": "Call super() in constructor"
},
{
"id": "pmd/ClassNamingConventions",
"description": "PascalCase naming"
},
{
"id": "pmd/CommentDefaultAccessModifier",
"description": "Comment default access modifier"
},
{
"id": "pmd/ConfusingTernary",
"description": "Avoid negation in if with else"
},
{
"id": "pmd/ControlStatementBraces",
"description": "Require braces on control statements"
},
{
"id": "pmd/EmptyControlStatement",
"description": "Report empty control statements"
},
{
"id": "pmd/EmptyMethodInAbstractClassShouldBeAbstract",
"description": "Empty methods in abstract classes should be abstract"
},
{
"id": "pmd/ExtendsObject",
"description": "No need to explicitly extend Object"
},
{
"id": "pmd/FieldDeclarationsShouldBeAtStartOfClass",
"description": "Fields at top of class"
},
{
"id": "pmd/FieldNamingConventions",
"description": "Configurable field naming conventions"
},
{
"id": "pmd/FinalParameterInAbstractMethod",
"description": "Final parameter in abstract method is useless"
},
{
"id": "pmd/ForLoopShouldBeWhileLoop",
"description": "Simplify for loops to while"
},
{
"id": "pmd/FormalParameterNamingConventions",
"description": "Parameter naming conventions"
},
{
"id": "pmd/GenericsNaming",
"description": "Single uppercase letter for generics"
},
{
"id": "pmd/IdenticalCatchBranches",
"description": "Collapse identical catch branches"
},
{
"id": "pmd/LambdaCanBeMethodReference",
"description": "Replace lambda with method reference"
},
{
"id": "pmd/LinguisticNaming",
"description": "Method name/return type consistency"
},
{
"id": "pmd/LocalHomeNamingConvention",
"description": "EJB LocalHome suffix"
},
{
"id": "pmd/LocalInterfaceSessionNamingConvention",
"description": "EJB Local suffix"
},
{
"id": "pmd/LocalVariableCouldBeFinal",
"description": "Declare local variables final when possible"
},
{
"id": "pmd/LocalVariableNamingConventions",
"description": "Variable naming conventions"
},
{
"id": "pmd/LongVariable",
"description": "Avoid excessively long variable names (>17 chars)"
},
{
"id": "pmd/MDBAndSessionBeanNamingConvention",
"description": "EJB Bean suffix"
},
{
"id": "pmd/MethodArgumentCouldBeFinal",
"description": "Declare parameters final when possible"
},
{
"id": "pmd/MethodNamingConventions",
"description": "Method naming conventions"
},
{
"id": "pmd/ModifierOrder",
"description": "Enforce JLS modifier order"
},
{
"id": "pmd/NoPackage",
"description": "All types must belong to a named package"
},
{
"id": "pmd/OnlyOneReturn",
"description": "Single exit point per method"
},
{
"id": "pmd/PackageCase",
"description": "Package names lowercase"
},
{
"id": "pmd/PrematureDeclaration",
"description": "Declare variables close to usage"
},
{
"id": "pmd/UselessParentheses",
"description": "Remove unnecessary parentheses"
},
{
"id": "pmd/UselessQualifiedThis",
"description": "Remove unnecessary qualified this"
},
{
"id": "pmd/UnnecessaryAnnotationValueElement",
"description": "Remove unnecessary annotation value element"
},
{
"id": "pmd/UnnecessaryBoxing",
"description": "Avoid unnecessary boxing"
},
{
"id": "pmd/UnnecessaryCast",
"description": "Remove unnecessary casts"
},
{
"id": "pmd/UnnecessaryConstructor",
"description": "Remove unnecessary constructors"
},
{
"id": "pmd/UnnecessaryFullyQualifiedName",
"description": "Remove unnecessary fully qualified names"
},
{
"id": "pmd/UnnecessaryLocalBeforeReturn",
"description": "Remove unnecessary local before return"
},
{
"id": "pmd/UnnecessaryModifier",
"description": "Remove unnecessary modifiers"
},
{
"id": "pmd/UnnecessaryReturn",
"description": "Remove unnecessary returns"
},
{
"id": "pmd/UnnecessarySemicolon",
"description": "Remove unnecessary semicolons"
},
{
"id": "pmd/UnnecessaryUnboxing",
"description": "Avoid unnecessary unboxing"
},
{
"id": "pmd/UpperLowerCaseNamingConventions",
"description": "Naming conventions for cases"
},
{
"id": "pmd/UseShortArrayInitializer",
"description": "Use short array initializer"
},
{
"id": "pmd/AbstractClassWithoutAnyMethod",
"description": "Abstract class without methods should use private constructor"
},
{
"id": "pmd/AvoidDeeplyNestedIfStmts",
"description": "Avoid deeply nested if statements"
},
{
"id": "pmd/AvoidRethrowingException",
"description": "Avoid catch-and-rethrow"
},
{
"id": "pmd/AvoidThrowingNewInstanceOfSameException",
"description": "Avoid wrapping same exception type"
},
{
"id": "pmd/AvoidThrowingNullPointerException",
"description": "Don't throw NPE manually"
},
{
"id": "pmd/AvoidThrowingRawExceptionTypes",
"description": "Don't throw raw Exception/RuntimeException/Throwable/Error"
},
{
"id": "pmd/AvoidUncheckedExceptionsInSignatures",
"description": "Don't declare unchecked exceptions in throws"
},
{
"id": "pmd/ClassWithOnlyPrivateConstructorsShouldBeFinal",
"description": "Make class final if only private constructors"
},
{
"id": "pmd/CognitiveComplexity",
"description": "Methods with high cognitive complexity"
},
{
"id": "pmd/CollapsibleIfStatements",
"description": "Merge nested if statements"
},
{
"id": "pmd/CouplingBetweenObjects",
"description": "High coupling threshold"
},
{
"id": "pmd/CyclomaticComplexity",
"description": "High cyclomatic complexity"
},
{
"id": "pmd/DataClass",
"description": "Suspected Data Class"
},
{
"id": "pmd/DoNotExtendJavaLangError",
"description": "Don't extend Error"
},
{
"id": "pmd/ExceptionAsFlowControl",
"description": "Don't use exceptions for flow control"
},
{
"id": "pmd/ExcessiveImports",
"description": "Too many imports"
},
{
"id": "pmd/ExcessiveParameterList",
"description": "Too many parameters"
},
{
"id": "pmd/ExcessivePublicCount",
"description": "Too many public methods/attributes"
},
{
"id": "pmd/FinalFieldCouldBeStatic",
"description": "Make final field static if compile-time constant"
},
{
"id": "pmd/GodClass",
"description": "God Class detection"
},
{
"id": "pmd/ImmutableField",
"description": "Field could be final"
},
{
"id": "pmd/InvalidJavaBean",
"description": "Bean doesn't follow JavaBeans spec"
},
{
"id": "pmd/LawOfDemeter",
"description": "Potential LoD violation"
},
{
"id": "pmd/LogicInversion",
"description": "Use opposite operator instead of !"
},
{
"id": "pmd/LoosePackageCoupling",
"description": "Avoid using classes from outside package hierarchy"
},
{
"id": "pmd/MutableStaticState",
"description": "Non-private non-final static fields"
},
{
"id": "pmd/NcssCount",
"description": "Non-Commenting Source Statements metric"
},
{
"id": "pmd/NPathComplexity",
"description": "NPath complexity threshold"
},
{
"id": "pmd/PublicMemberInNonPublicType",
"description": "Public member in non-public type"
},
{
"id": "pmd/SignatureDeclareThrowsException",
"description": "Don't declare throws Exception"
},
{
"id": "pmd/SimplifiedTernary",
"description": "Simplify ternary with boolean literals"
},
{
"id": "pmd/SimplifyBooleanExpressions",
"description": "Remove unnecessary boolean comparisons"
},
{
"id": "pmd/SimplifyBooleanReturns",
"description": "Simplify boolean returns"
},
{
"id": "pmd/SimplifyConditional",
"description": "Simplify conditional expressions"
},
{
"id": "pmd/SingularField",
"description": "Field may be local variable"
},
{
"id": "pmd/TooManyFields",
"description": "Too many fields"
},
{
"id": "pmd/TooManyMethods",
"description": "Too many methods"
},
{
"id": "pmd/UselessOverridingMethod",
"description": "Useless overriding method"
},
{
"id": "pmd/AssertEqualsArgumentOrder",
"description": "assertEquals expected/actual swapped"
},
{
"id": "pmd/AssignmentInOperand",
"description": "Avoid assignments in operands"
},
{
"id": "pmd/AssignmentToNonFinalStatic",
"description": "Unsafe static field assignment in constructor"
},
{
"id": "pmd/AvoidAccessibilityAlteration",
"description": "Don't use setAccessible(true)"
},
{
"id": "pmd/AvoidAssertAsIdentifier",
"description": "assert is reserved word (Java <1.4)"
},
{
"id": "pmd/AvoidBranchingStatementAsLastInLoop",
"description": "Branching statement as last in loop"
},
{
"id": "pmd/AvoidCallingFinalize",
"description": "Don't call finalize() explicitly"
},
{
"id": "pmd/AvoidCatchingGenericException",
"description": "Don't catch generic exceptions"
},
{
"id": "pmd/AvoidDecimalLiteralsInBigDecimalConstructor",
"description": "Use String constructor for BigDecimal"
},
{
"id": "pmd/AvoidDuplicateLiterals",
"description": "Avoid duplicate String literals"
},
{
"id": "pmd/AvoidEnumAsIdentifier",
"description": "enum is reserved word (Java <1.5)"
},
{
"id": "pmd/AvoidFieldNameMatchingMethodName",
"description": "Field name matching method name"
},
{
"id": "pmd/AvoidFieldNameMatchingTypeName",
"description": "Field name matching type name"
},
{
"id": "pmd/AvoidInstanceofChecksInCatchClause",
"description": "Use separate catch clauses"
},
{
"id": "pmd/AvoidLiteralsInIfCondition",
"description": "Avoid magic numbers in if conditions"
},
{
"id": "pmd/AvoidMultipleUnaryOperators",
"description": "Avoid multiple unary operators"
},
{
"id": "pmd/AvoidUsingOctalValues",
"description": "Avoid octal literals"
},
{
"id": "pmd/BrokenNullCheck",
"description": "Broken null check (|| vs &&)"
},
{
"id": "pmd/CallSuperFirst",
"description": "super should be called first"
},
{
"id": "pmd/CallSuperLast",
"description": "super should be called last"
},
{
"id": "pmd/CheckSkipResult",
"description": "Check skip() return value"
},
{
"id": "pmd/ClassCastExceptionWithToArray",
"description": "Collection.toArray() ClassCastException"
},
{
"id": "pmd/CloneMethodMustBePublic",
"description": "clone() must be public if Cloneable"
},
{
"id": "pmd/CloneMethodMustImplementCloneable",
"description": "clone() only if Cloneable"
},
{
"id": "pmd/CloneMethodReturnTypeMustMatchClassName",
"description": "clone() covariant return type"
},
{
"id": "pmd/CloseResource",
"description": "Ensure resources are closed"
},
{
"id": "pmd/CollectionTypeMismatch",
"description": "Type mismatch in collection methods"
},
{
"id": "pmd/CompareObjectsWithEquals",
"description": "Use equals() not == for objects"
},
{
"id": "pmd/ComparisonWithNaN",
"description": "NaN comparisons always return false"
},
{
"id": "pmd/ConfusingArgumentToVarargsMethod",
"description": "Clarify varargs intent"
},
{
"id": "pmd/ConstructorCallsOverridableMethod",
"description": "Constructor calls overridable method"
},
{
"id": "pmd/DataflowAnomalyAnalysis",
"description": "Data flow anomalies"
},
{
"id": "pmd/DoNotCallGarbageCollectionExplicitly",
"description": "Don't call System.gc()"
},
{
"id": "pmd/DoNotCallSystemExit",
"description": "Don't call System.exit()"
},
{
"id": "pmd/DoNotHardCodeSDCard",
"description": "Don't hardcode /sdcard path"
},
{
"id": "pmd/DoNotThrowExceptionInFinally",
"description": "Don't throw in finally"
},
{
"id": "pmd/DoNotUseThreads",
"description": "Don't use Threads"
},
{
"id": "pmd/DontImportSun",
"description": "Don't import sun.* packages"
},
{
"id": "pmd/EmptyCatchBlock",
"description": "Empty catch blocks"
},
{
"id": "pmd/EqualsNull",
"description": "Equal comparison to null"
},
{
"id": "pmd/FinallyBlockDoesNothing",
"description": "Finally block does nothing"
},
{
"id": "pmd/IdempotentOperations",
"description": "Idempotent operations"
},
{
"id": "pmd/ImportFromSamePackage",
"description": "Import from same package"
},
{
"id": "pmd/InstantiationToGetClass",
"description": "Instantiation just to get class"
},
{
"id": "pmd/InvalidLogMessageFormat",
"description": "Invalid SLF4J message format"
},
{
"id": "pmd/JUnitSpelling",
"description": "JUnit method spelling"
},
{
"id": "pmd/JUnitStaticSuite",
"description": "JUnit static suite method"
},
{
"id": "pmd/JumbledIncrementer",
"description": "Jumbled incrementer"
},
{
"id": "pmd/LoggerIsNotStaticFinal",
"description": "Logger not static final"
},
{
"id": "pmd/MethodWithSameNameAsEnclosingClass",
"description": "Method same name as enclosing class"
},
{
"id": "pmd/MisplacedNullCheck",
"description": "Misplaced null check"
},
{
"id": "pmd/MissingBreakInSwitch",
"description": "Missing break in switch"
},
{
"id": "pmd/MissingSerialVersionUID",
"description": "Missing serialVersionUID"
},
{
"id": "pmd/MissingStaticMethodInNonInstantiatableClass",
"description": "Non-instantiatable class missing static method"
},
{
"id": "pmd/MoreThanOneLogger",
"description": "More than one logger"
},
{
"id": "pmd/NonCaseLabelInSwitch",
"description": "Non-case label in switch"
},
{
"id": "pmd/NonStaticInitializer",
"description": "Non-static initializer"
},
{
"id": "pmd/NullAssignment",
"description": "Null assignment"
},
{
"id": "pmd/NumberConstructor",
"description": "Number constructor (deprecated)"
},
{
"id": "pmd/ObjectFinalize",
"description": "Object finalize issues"
},
{
"id": "pmd/OperationWithCloning",
"description": "Operation with cloning"
},
{
"id": "pmd/PackageDeclaration",
"description": "Package declaration"
},
{
"id": "pmd/ProperCloneImplementation",
"description": "Proper clone implementation"
},
{
"id": "pmd/ProperLogger",
"description": "Proper logger"
},
{
"id": "pmd/ReturnFromFinallyBlock",
"description": "Return from finally"
},
{
"id": "pmd/SimpleDateFormatNeedsLocale",
"description": "SimpleDateFormat needs locale"
},
{
"id": "pmd/SingleMethodSingleton",
"description": "Singleton pattern issues"
},
{
"id": "pmd/SingletonClassReturningNewInstance",
"description": "Singleton returning new instance"
},
{
"id": "pmd/StaticEJBFieldShouldBeFinal",
"description": "Static EJB field should be final"
},
{
"id": "pmd/StringBufferInstantiationWithChar",
"description": "StringBuffer with char"
},
{
"id": "pmd/SuspiciousConstantFieldName",
"description": "Constant field naming"
},
{
"id": "pmd/SuspiciousEqualsMethodName",
"description": "equals() method signature"
},
{
"id": "pmd/SuspiciousHashcodeMethodName",
"description": "hashCode() method signature"
},
{
"id": "pmd/SuspiciousOctalEscape",
"description": "Suspicious octal escape"
},
{
"id": "pmd/TestClassWithoutTestCases",
"description": "Test class without test cases"
},
{
"id": "pmd/UnconditionalIfStatement",
"description": "Unconditional if statement"
},
{
"id": "pmd/UnnecessaryBooleanAssertion",
"description": "Unnecessary boolean assertion"
},
{
"id": "pmd/UnnecessaryCaseChange",
"description": "Unnecessary case change"
},
{
"id": "pmd/UnnecessaryConversionTemporal",
"description": "Unnecessary temporal conversion"
},
{
"id": "pmd/UnusedNullCheckInEquals",
"description": "Unused null check in equals"
},
{
"id": "pmd/UseCorrectExceptionLogging",
"description": "Correct exception logging"
},
{
"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/UseProperClassLoader",
"description": "Use proper classloader"
},
{
"id": "pmd/AddEmptyString",
"description": "Don't add empty strings"
},
{
"id": "pmd/AppendCharacterWithChar",
"description": "Append char not string in StringBuffer"
},
{
"id": "pmd/AvoidArrayLoops",
"description": "Use Arrays.copyOf or System.arraycopy"
},
{
"id": "pmd/AvoidCalendarDateCreation",
"description": "Avoid Calendar for current time"
},
{
"id": "pmd/AvoidFileStream",
"description": "Avoid FileInputStream/FileOutputStream/FileReader/FileWriter"
},
{
"id": "pmd/AvoidInstantiatingObjectsInLoops",
"description": "Don't instantiate objects in loops"
},
{
"id": "pmd/BigIntegerInstantiation",
"description": "Use BigInteger.ZERO/ONE/TEN"
},
{
"id": "pmd/ConsecutiveAppendsShouldReuse",
"description": "Chain StringBuilder.append calls"
},
{
"id": "pmd/ConsecutiveLiteralAppends",
"description": "Combine literal appends"
},
{
"id": "pmd/InefficientEmptyStringCheck",
"description": "Use isBlank() instead of trim().isEmpty()"
},
{
"id": "pmd/InefficientStringBuffering",
"description": "Avoid concatenating in StringBuffer constructor"
},
{
"id": "pmd/InsufficientStringBufferDeclaration",
"description": "Pre-size StringBuilder"
},
{
"id": "pmd/OptimizableToArrayCall",
"description": "Use new Foo[0] instead of new Foo[size]"
},
{
"id": "pmd/RedundantFieldInitializer",
"description": "Remove redundant field initializers"
},
{
"id": "pmd/StringInstantiation",
"description": "Avoid new String()"
},
{
"id": "pmd/StringToString",
"description": "Avoid toString() on String"
},
{
"id": "pmd/TooFewBranchesForSwitch",
"description": "Switch with less than 3 branches"
},
{
"id": "pmd/UseArrayListInsteadOfVector",
"description": "ArrayList instead of Vector"
},
{
"id": "pmd/UseArraysAsList",
"description": "Use Arrays.asList() instead of loop"
},
{
"id": "pmd/UseIndexOfChar",
"description": "Use indexOf(char) not indexOf(String)"
},
{
"id": "pmd/UseIOStreamsWithApacheCommonsFileItem",
"description": "Use getInputStream() not get()"
},
{
"id": "pmd/UselessStringValueOf",
"description": "Don't wrap with String.valueOf()"
},
{
"id": "pmd/UseStringBufferForStringAppends",
"description": "Use StringBuilder for concatenation"
},
{
"id": "pmd/UseStringBufferLength",
"description": "Use length() instead of toString().equals(\"\")"
},
{
"id": "pmd/HardCodedCryptoKey",
"description": "Don't hard code encryption keys"
},
{
"id": "pmd/InsecureCryptoIv",
"description": "Don't hard code initialization vectors"
}
],
"pmd-jsp": [
{
"id": "pmd-jsp/DontNestJsfInJstlIteration",
"description": "Do not nest JSF components inside JSTL iteration"
},
{
"id": "pmd-jsp/NoClassAttribute",
"description": "Use styleclass not class attribute"
},
{
"id": "pmd-jsp/NoHtmlComments",
"description": "Use JSP comments instead of HTML comments"
},
{
"id": "pmd-jsp/NoJspForward",
"description": "Do not forward from within a JSP"
},
{
"id": "pmd-jsp/DuplicateJspImports",
"description": "Avoid duplicate imports in JSP"
},
{
"id": "pmd-jsp/NoInlineScript",
"description": "Externalize HTML script content"
},
{
"id": "pmd-jsp/NoInlineStyleInformation",
"description": "Put styles in CSS files"
},
{
"id": "pmd-jsp/NoLongScripts",
"description": "Avoid long scripts in JSP"
},
{
"id": "pmd-jsp/NoScriptlets",
"description": "Avoid scriptlets in JSP"
},
{
"id": "pmd-jsp/JspEncoding",
"description": "JSP files should use UTF-8 encoding"
}
],
"sql-lint": [
{
"id": "sql-lint/AL01",
"description": "Implicit/explicit aliasing of table"
},
{
"id": "sql-lint/AL02",
"description": "Implicit/explicit aliasing of columns"
},
{
"id": "sql-lint/AL03",
"description": "Column expression without alias"
},
{
"id": "sql-lint/AL04",
"description": "Table aliases should be unique within each clause"
},
{
"id": "sql-lint/AL05",
"description": "Tables should not be aliased if unused"
},
{
"id": "sql-lint/AL06",
"description": "Enforce table alias lengths"
},
{
"id": "sql-lint/AL07",
"description": "Avoid table aliases"
},
{
"id": "sql-lint/AL08",
"description": "Column aliases should be unique within each clause"
},
{
"id": "sql-lint/AL09",
"description": "Column aliases should not alias to itself"
},
{
"id": "sql-lint/AL10",
"description": "Derived tables must have an alias"
},
{
"id": "sql-lint/AM01",
"description": "Ambiguous use of DISTINCT with GROUP BY"
},
{
"id": "sql-lint/AM02",
"description": "UNION DISTINCT/ALL preferred over just UNION"
},
{
"id": "sql-lint/AM03",
"description": "Ambiguous ordering directions"
},
{
"id": "sql-lint/AM04",
"description": "Query produces unknown number of result columns"
},
{
"id": "sql-lint/AM05",
"description": "Join clauses should be fully qualified"
},
{
"id": "sql-lint/AM06",
"description": "Inconsistent column references in GROUP BY/ORDER BY"
},
{
"id": "sql-lint/AM07",
"description": "Queries within set query produce different numbers of columns"
},
{
"id": "sql-lint/AM08",
"description": "Implicit cross join detected"
},
{
"id": "sql-lint/AM09",
"description": "LIMIT/OFFSET without ORDER BY non-deterministic"
},
{
"id": "sql-lint/CP01",
"description": "Inconsistent capitalisation of keywords"
},
{
"id": "sql-lint/CP02",
"description": "Inconsistent capitalisation of unquoted identifiers"
},
{
"id": "sql-lint/CP03",
"description": "Inconsistent capitalisation of function names"
},
{
"id": "sql-lint/CP04",
"description": "Inconsistent capitalisation of boolean/null literal"
},
{
"id": "sql-lint/CP05",
"description": "Inconsistent capitalisation of datatypes"
},
{
"id": "sql-lint/CV01",
"description": "Consistent usage of != or <>"
},
{
"id": "sql-lint/CV02",
"description": "Use COALESCE instead of IFNULL/NVL"
},
{
"id": "sql-lint/CV03",
"description": "Trailing commas within select clause"
},
{
"id": "sql-lint/CV04",
"description": "Consistent syntax for count number of rows"
},
{
"id": "sql-lint/CV05",
"description": "Comparisons with NULL should use IS or IS NOT"
},
{
"id": "sql-lint/CV06",
"description": "Statements must end with a semi-colon"
},
{
"id": "sql-lint/CV07",
"description": "Top-level statements should not be wrapped in brackets"
},
{
"id": "sql-lint/CV08",
"description": "Use LEFT JOIN instead of RIGHT JOIN"
},
{
"id": "sql-lint/CV09",
"description": "Block a list of configurable words"
},
{
"id": "sql-lint/CV10",
"description": "Consistent usage of preferred quotes for quoted literals"
},
{
"id": "sql-lint/CV11",
"description": "Enforce consistent type casting style"
},
{
"id": "sql-lint/CV12",
"description": "Use JOIN ... ON ... instead of WHERE ... for join conditions"
},
{
"id": "sql-lint/JJ01",
"description": "Jinja tags should have single whitespace on either side"
},
{
"id": "sql-lint/LT01",
"description": "Inappropriate Spacing"
},
{
"id": "sql-lint/LT02",
"description": "Incorrect Indentation"
},
{
"id": "sql-lint/LT03",
"description": "Operators before/after newlines"
},
{
"id": "sql-lint/LT04",
"description": "Leading/Trailing comma enforcement"
},
{
"id": "sql-lint/LT05",
"description": "Line is too long"
},
{
"id": "sql-lint/LT06",
"description": "Function name not followed by parenthesis"
},
{
"id": "sql-lint/LT07",
"description": "WITH clause closing bracket on new line"
},
{
"id": "sql-lint/LT08",
"description": "Blank line after CTE closing bracket"
},
{
"id": "sql-lint/LT09",
"description": "Select targets on new line"
},
{
"id": "sql-lint/LT10",
"description": "SELECT modifiers on same line as SELECT"
},
{
"id": "sql-lint/LT11",
"description": "Set operators surrounded by newlines"
},
{
"id": "sql-lint/LT12",
"description": "Files must end with single trailing newline"
},
{
"id": "sql-lint/LT13",
"description": "Files must not begin with newlines/whitespace"
},
{
"id": "sql-lint/LT14",
"description": "Keyword clauses before/after newlines"
},
{
"id": "sql-lint/LT15",
"description": "Too many consecutive blank lines"
},
{
"id": "sql-lint/OR01",
"description": "Remove empty batches"
},
{
"id": "sql-lint/PG01",
"description": "Avoid excessive locks in PostgreSQL DDL"
},
{
"id": "sql-lint/RF01",
"description": "References cannot reference objects not in FROM clause"
},
{
"id": "sql-lint/RF02",
"description": "References should be qualified if multiple tables"
},
{
"id": "sql-lint/RF03",
"description": "Column references consistent in single table statements"
},
{
"id": "sql-lint/RF04",
"description": "Keywords should not be used as identifiers"
},
{
"id": "sql-lint/RF05",
"description": "No special characters in identifiers"
},
{
"id": "sql-lint/RF06",
"description": "Unnecessary quoted identifier"
},
{
"id": "sql-lint/ST01",
"description": "Do not specify else null in CASE WHEN"
},
{
"id": "sql-lint/ST02",
"description": "Unnecessary CASE statement"
},
{
"id": "sql-lint/ST03",
"description": "Unused CTE"
},
{
"id": "sql-lint/ST04",
"description": "Nested CASE in ELSE clause can be flattened"
},
{
"id": "sql-lint/ST05",
"description": "Subqueries in Join/From clauses; use CTEs"
},
{
"id": "sql-lint/ST06",
"description": "Column order: wildcards, simple targets, then calculations"
},
{
"id": "sql-lint/ST07",
"description": "Prefer ON over USING for join keys"
},
{
"id": "sql-lint/ST08",
"description": "DISTINCT used with parentheses"
},
{
"id": "sql-lint/ST09",
"description": "Join condition order"
},
{
"id": "sql-lint/ST10",
"description": "Redundant constant expression"
},
{
"id": "sql-lint/ST11",
"description": "Joined table not referenced"
},
{
"id": "sql-lint/ST12",
"description": "Consecutive semicolons"
},
{
"id": "sql-lint/TQ01",
"description": "SP_ prefix should not be used for user-defined stored procedures"
},
{
"id": "sql-lint/TQ02",
"description": "Procedure bodies with multiple statements wrapped in BEGIN/END"
},
{
"id": "sql-lint/TQ03",
"description": "Remove empty batches"
}
]
}
}
+16
View File
@@ -0,0 +1,16 @@
declare module 'mammoth' {
interface Result {
value: string;
messages: unknown[];
}
interface ExtractRawTextOptions {
path: string;
}
export function extractRawText(input: ExtractRawTextOptions): Promise<Result>;
export function convertToHtml(input: ExtractRawTextOptions): Promise<Result>;
export function convertToMarkdown(input: ExtractRawTextOptions): Promise<Result>;
}