diff --git a/_AI_USAGE_LOG.md b/_AI_USAGE_LOG.md index 9991483..9aa9e66 100644 --- a/_AI_USAGE_LOG.md +++ b/_AI_USAGE_LOG.md @@ -115,3 +115,8 @@ | 2026-07-26 14:16 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | AI 转换器输出语言修复:5 个 converter(md/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缺/非法→warning,id缺→rule-N,desc+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 结构组织多语言文本 | 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 | diff --git a/src/i18n/messages.ts b/src/i18n/messages.ts new file mode 100644 index 0000000..20a380d --- /dev/null +++ b/src/i18n/messages.ts @@ -0,0 +1,896 @@ +import * as vscode from 'vscode'; + +export type Language = 'zh-CN' | 'en' | 'ja'; + +const defaultLang: Language = 'zh-CN'; + +const messages: Record> = { + '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': '安装插件后,配置 AI 模型及 API Key,激活智能审核能力', + en: 'After installing, configure AI model and API Key to activate intelligent review', + ja: 'インストール後、AIモデルとAPIキーを設定してインテリジェントレビューを有効化', + }, + 'setup.step2': { + 'zh-CN': '启用 自定义规则,补充团队特有的编码规范', + en: 'Enable custom rules to add team-specific coding standards', + ja: 'カスタムルールを有効にしてチーム固有のコーディング規約を追加', + }, + 'setup.step3': { + 'zh-CN': '保存并测试连接,验证配置无误后即可触发审核', + en: 'Save and test connection, verify config then trigger review', + ja: '保存して接続テスト、設定を確認してレビューを開始', + }, + 'setup.step3Hint': { + 'zh-CN': '按 Ctrl + Shift + R 快捷键触发审核,结果实时显示在 审核结果报告页面中', + en: 'Press Ctrl + Shift + R to trigger review, results appear in the Review Report panel', + ja: 'Ctrl + Shift + R でレビューを実行、結果はレビューレポートに表示', + }, + '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(); + +export function t(key: string, vars?: Record): 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); +} diff --git a/src/rules/converters/converter.ts b/src/rules/converters/converter.ts new file mode 100644 index 0000000..98d344c --- /dev/null +++ b/src/rules/converters/converter.ts @@ -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; +} diff --git a/src/rules/converters/docx-converter.ts b/src/rules/converters/docx-converter.ts new file mode 100644 index 0000000..f4bf1a2 --- /dev/null +++ b/src/rules/converters/docx-converter.ts @@ -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 { + 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)); + } +} diff --git a/src/rules/converters/excel-converter.ts b/src/rules/converters/excel-converter.ts new file mode 100644 index 0000000..e58c354 --- /dev/null +++ b/src/rules/converters/excel-converter.ts @@ -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[], 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 { + 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>(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)); + } +} diff --git a/src/rules/converters/md-converter.ts b/src/rules/converters/md-converter.ts new file mode 100644 index 0000000..caefbce --- /dev/null +++ b/src/rules/converters/md-converter.ts @@ -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 { + const content = fs.readFileSync(srcPath, 'utf-8'); + return convertContentWithAI(content, context, buildSystemPrompt('freeform', existingRules)); + } +} diff --git a/src/rules/converters/pptx-converter.ts b/src/rules/converters/pptx-converter.ts new file mode 100644 index 0000000..4761d79 --- /dev/null +++ b/src/rules/converters/pptx-converter.ts @@ -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 { + 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)); + } +} diff --git a/src/rules/converters/prompt-builder.ts b/src/rules/converters/prompt-builder.ts new file mode 100644 index 0000000..f045dd7 --- /dev/null +++ b/src/rules/converters/prompt-builder.ts @@ -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 = { + '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: 重复的规则 ID(linter 如 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 を出力', + '', + '単に「トピックが類似している」だけでは重複とみなされません。例:', + '- 「未使用変数は削除すべき」→ exact(eslint/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'); +} diff --git a/src/rules/converters/txt-converter.ts b/src/rules/converters/txt-converter.ts new file mode 100644 index 0000000..a38c48a --- /dev/null +++ b/src/rules/converters/txt-converter.ts @@ -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 { + const content = fs.readFileSync(srcPath, 'utf-8'); + return convertContentWithAI(content, context, buildSystemPrompt('freeform', existingRules)); + } +} diff --git a/src/rules/converters/yaml-converter.ts b/src/rules/converters/yaml-converter.ts new file mode 100644 index 0000000..861c5fa --- /dev/null +++ b/src/rules/converters/yaml-converter.ts @@ -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 { + return fs.readFileSync(srcPath, 'utf-8'); + } +} diff --git a/src/rules/import-preview.ts b/src/rules/import-preview.ts new file mode 100644 index 0000000..bca2bc5 --- /dev/null +++ b/src/rules/import-preview.ts @@ -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 { + return new Promise((resolve) => { + const panel = vscode.window.createWebviewPanel( + 'ruleImportPreview', + t('importPreview.title'), + vscode.ViewColumn.Active, + { enableScripts: true }, + ); + + const keepRule: Record = {}; + 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 = { + error: '#f48771', + warning: '#d29922', + info: '#58a6ff', +}; + +function renderPreviewHtml( + result: ConversionResult, + keepRule: Record, +): 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 = `
${t('import.duplicateOf', { 0: prefix })}
`; + } else if (rule.duplicateLevel === 'overlap') { + const prefix = rule.duplicateOf?.startsWith('custom/') ? `${t('import.customRulePrefix')} ${rule.duplicateOf.slice(7)}` : (rule.duplicateOf ?? 'unknown'); + duplicateInfo = ` +
${t('import.overlapWith', { 0: prefix })}
+ ${rule.duplicateReason ? `
${t('import.overlapReason', { 0: rule.duplicateReason })}
` : ''} + `; + } + + const statusBadge = rule.duplicateLevel === 'exact' + ? `${kept ? t('import.badgeRestored') : t('import.badgeWillComment')}` + : rule.duplicateLevel === 'overlap' + ? `${kept ? t('importPreview.keep') : t('importPreview.comment')}` + : `${t('importPreview.keep')}`; + + const tagValue = (tags: string[] | undefined) => tags && tags.length > 0 ? tags.join(',') : ''; + const tagDisplay = (tags: string[] | undefined) => tags && tags.length > 0 ? tags.map(t => `${t}×`).join('') : ''; + + return ` +
+
+
+ + ${editRule.severity} + ${editRule.description} +
+
+ ${statusBadge} + +
+
+ + +
+ `; + } + + 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 ` +
+
+ ${icon} + ${title}(${t('import.ruleCount', { 0: rules.length })}) + +
+
+ ${rules.map(renderRuleCard).join('')} +
+
+ `; + } + + return ` + + + + + + + +
+
${t('importPreview.title')}
+
${t('importPreview.source', { 0: result.sourceFileName, 1: String(result.rules.length) })}
+
+
+
${t('import.exactDuplicate', { 0: exactRules.length })}
+
${t('import.overlapDuplicate', { 0: overlapRules.length })}
+
${t('import.noDuplicate', { 0: noneRules.length })}
+
+
+ ${t('import.statusBar', { 0: `${totalKept}`, 1: `${totalCommented}` })} + +
+ + + +${renderSection(t('import.sectionExact'), '⛔', exactRules, false)} +${renderSection(t('import.sectionOverlap'), '⚠️', overlapRules, true)} +${renderSection(t('import.sectionNone'), '✅', noneRules, false)} + +
+ + +
+ + + +`; +} diff --git a/src/rules/import-service.ts b/src/rules/import-service.ts new file mode 100644 index 0000000..2a0d365 --- /dev/null +++ b/src/rules/import-service.ts @@ -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 = 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 { + 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 { + 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; +} diff --git a/src/rules/import-types.ts b/src/rules/import-types.ts new file mode 100644 index 0000000..c6236f6 --- /dev/null +++ b/src/rules/import-types.ts @@ -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; + confirmed: boolean; + editedRules?: ImportableRule[]; +} diff --git a/src/rules/rule-filter.ts b/src/rules/rule-filter.ts new file mode 100644 index 0000000..7c8609a --- /dev/null +++ b/src/rules/rule-filter.ts @@ -0,0 +1,68 @@ +import * as vscode from 'vscode'; +import type { CustomRule } from '../types'; + +const LANGUAGE_ALIASES: Record = { + typescriptreact: ['typescript', 'typescriptreact', 'tsx'], + javascriptreact: ['javascript', 'javascriptreact', 'jsx'], +}; + +const LANGUAGE_GROUPS: Record = { + 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, + }; +} diff --git a/src/rules/static-rules.json b/src/rules/static-rules.json new file mode 100644 index 0000000..c186ab5 --- /dev/null +++ b/src/rules/static-rules.json @@ -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- 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" + } + ] + } +} diff --git a/src/types/mammoth.d.ts b/src/types/mammoth.d.ts new file mode 100644 index 0000000..97fc579 --- /dev/null +++ b/src/types/mammoth.d.ts @@ -0,0 +1,16 @@ +declare module 'mammoth' { + interface Result { + value: string; + messages: unknown[]; + } + + interface ExtractRawTextOptions { + path: string; + } + + export function extractRawText(input: ExtractRawTextOptions): Promise; + + export function convertToHtml(input: ExtractRawTextOptions): Promise; + + export function convertToMarkdown(input: ExtractRawTextOptions): Promise; +}