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

ESLint: +29 条 P1/P2 规则 + 12 条 TS 专属规则(含 no-shadow/no-array-constructor 冲突处理)
Stylelint: 集成 stylelint-config-recommended + 27 条额外规则
PMD: 排除 20 弃用 + 17 噪音规则,补启 Security/Multithreading,274+12 条精选
SQL-lint: 内置精选 57 条规则配置 + 按 tier 分级 severity + 无项目配置时自动注入临时配置
模板导出: export-service.ts 导出 2-sheet xlsx(复用 xlsx 零新依赖)
模板导入: template-converter.ts 固定列映射解析 + dedup-prompt.ts AI 语义去重
导入预览增强: 错误规则分组置顶只读、跳过行提示、空数据提示
i18n: 新增 18 条模板导入/导出相关翻译
WebView: 静态分析/自定义规则项默认可展开显示 suggestion
This commit is contained in:
范智鹏
2026-07-30 23:17:20 +08:00
parent d2dd16a043
commit 5848eaa82a
27 changed files with 6610 additions and 153 deletions
+5
View File
@@ -8,6 +8,7 @@ import { reportToMarkdown } from '../utils/report';
import { getApiKey } from '../config';
import { ReviewPanel } from '../panel/webview';
import { t } from '../i18n/messages';
import { exportTemplate } from '../rules/export-service';
let currentReport: MergedReport | null = null;
@@ -184,4 +185,8 @@ export function registerCommands(
}
})
);
context.subscriptions.push(
vscode.commands.registerCommand('codeReviewer.exportTemplate', () => exportTemplate())
);
}
+55
View File
@@ -7,6 +7,59 @@ import ts from 'typescript-eslint';
import type { LinterAdapter, AdapterResult, LinterDiagnostic } from './adapter';
import { getEslintConfigPath } from '../config';
const extraRules: Record<string, 'error' | 'warn'> = {
'eqeqeq': 'error',
'no-eq-null': 'error',
'no-self-compare': 'error',
'no-promise-executor-return': 'error',
'no-shadow': 'error',
'no-unassigned-vars': 'error',
'no-useless-assignment': 'error',
'block-scoped-var': 'error',
'default-case': 'error',
'default-case-last': 'error',
'no-unmodified-loop-condition': 'error',
'no-unreachable-loop': 'error',
'no-eval': 'error',
'no-extend-native': 'error',
'no-var': 'error',
'no-await-in-loop': 'warn',
'prefer-template': 'warn',
'prefer-object-spread': 'warn',
'prefer-rest-params': 'warn',
'prefer-spread': 'warn',
'prefer-object-has-own': 'warn',
'no-useless-concat': 'warn',
'no-useless-return': 'warn',
'no-useless-computed-key': 'warn',
'no-useless-rename': 'warn',
'no-param-reassign': 'warn',
'no-return-assign': 'error',
'no-throw-literal': 'error',
'camelcase': 'warn',
'new-cap': 'warn',
'no-array-constructor': 'error',
};
const extraTsRules: Record<string, 'error' | 'warn' | 'off'> = {
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/no-dynamic-delete': 'error',
'@typescript-eslint/no-useless-empty-export': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/unified-signatures': 'error',
'@typescript-eslint/no-extraneous-class': 'warn',
'@typescript-eslint/no-useless-constructor': 'warn',
'@typescript-eslint/no-non-null-asserted-nullish-coalescing': 'error',
'@typescript-eslint/no-invalid-void-type': 'warn',
'@typescript-eslint/prefer-literal-enum-member': 'warn',
'@typescript-eslint/prefer-enum-initializers': 'warn',
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'error',
'no-array-constructor': 'off',
};
const TS_FILES = ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'];
const PROJECT_CONFIG_FILES = [
'.eslintrc.js',
'.eslintrc.json',
@@ -52,6 +105,8 @@ export class ESLintAdapter implements LinterAdapter {
ESLintAdapter.defaultConfig = [
js.configs.recommended,
...ts.configs.recommended,
{ rules: extraRules },
{ files: TS_FILES, rules: extraTsRules },
];
}
return ESLintAdapter.defaultConfig;
+51 -6
View File
@@ -1,16 +1,56 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { spawn } from 'child_process';
import type { LinterAdapter, AdapterResult, LinterDiagnostic } from './adapter';
import type { LinterAdapter, AdapterResult, LinterDiagnostic, Severity } from './adapter';
import { getSqlLintConfigFile } from '../config';
import { t } from '../i18n/messages';
import staticRules from '../rules/static-rules.json';
const DIALECT_MAP: Record<string, string> = {
sql: 'ansi',
plsql: 'postgres',
};
const BUILTIN_SQLFLUFF_CONFIG = `[sqlfluff]
rules = core,AM03,AM05,AM08,CV01,CV02,CV06,CV08,CV12,LT13,LT14,LT15,ST01,ST02,ST04,ST05,ST06,ST07,ST09,ST10,ST11,ST12,RF02,RF04,RF05,RF06
dialect = ansi
max_line_length = 80
indent_unit = space
tab_space_size = 4
`;
interface RuleEntry { id: string; description: string; tier?: string; }
const tierMap = new Map<string, string>();
try {
const sqlfluffRules = (staticRules as any).rules?.['sql-lint'] as RuleEntry[] | undefined;
if (sqlfluffRules) {
for (const rule of sqlfluffRules) {
if (rule.id && rule.tier) {
tierMap.set(rule.id.replace('sql-lint/', ''), rule.tier);
}
}
}
} catch {}
function tierToSeverity(tier: string | undefined): Severity {
if (tier === 'P0' || tier === 'P1') { return 'error'; }
if (tier === 'P2') { return 'warning'; }
return 'warning';
}
function hasProjectSqlfluffConfig(workspaceRoot: string): boolean {
const candidates = ['.sqlfluff', '.sqlfluff.ini'];
for (const candidate of candidates) {
if (fs.existsSync(path.join(workspaceRoot, candidate))) {
return true;
}
}
return false;
}
interface SqlFluffViolation {
start_line_no: number;
start_line_pos: number;
@@ -77,15 +117,16 @@ export class SqlLintAdapter implements LinterAdapter {
const dialect = DIALECT_MAP[languageId] || 'ansi';
let configPath: string | undefined;
let tempConfigPath: string | undefined;
const globalConfig = getSqlLintConfigFile();
if (globalConfig && globalConfig.trim() !== '') {
configPath = globalConfig;
} else if (hasProjectSqlfluffConfig(workingDir)) {
} else {
const projectConfig = path.join(workingDir, '.sqlfluff');
if (fs.existsSync(projectConfig)) {
configPath = projectConfig;
}
tempConfigPath = path.join(os.tmpdir(), `vscode-code-reviewer-sqlfluff-${Date.now()}.cfg`);
fs.writeFileSync(tempConfigPath, BUILTIN_SQLFLUFF_CONFIG, 'utf-8');
configPath = tempConfigPath;
}
try {
@@ -96,7 +137,7 @@ export class SqlLintAdapter implements LinterAdapter {
for (const result of results) {
for (const v of result.violations) {
diagnostics.push({
severity: 'warning',
severity: tierToSeverity(tierMap.get(v.code)),
ruleId: `sql-lint:${v.code}`,
message: v.description,
range: new vscode.Range(
@@ -124,6 +165,10 @@ export class SqlLintAdapter implements LinterAdapter {
status: 'execution-failed',
errorMessage: message,
};
} finally {
if (tempConfigPath) {
try { fs.unlinkSync(tempConfigPath); } catch {}
}
}
}
}
+39 -12
View File
@@ -3,6 +3,7 @@ import * as fs from 'fs';
import * as path from 'path';
import type { LinterAdapter, AdapterResult, LinterDiagnostic, Severity } from './adapter';
import { getStylelintConfigPath } from '../config';
import recommendedConfig from 'stylelint-config-recommended';
const CONFIG_FILE_NAMES = [
'.stylelintrc',
@@ -15,20 +16,46 @@ const CONFIG_FILE_NAMES = [
'stylelint.config.cjs',
];
const extraRules: Record<string, unknown> = {
'color-no-invalid-hex': true,
'function-linear-gradient-no-nonstandard-direction': true,
'function-no-unknown': true,
'unit-no-unknown': true,
'no-unknown-animations': true,
'no-unknown-custom-media': true,
'no-unknown-custom-properties': true,
'at-rule-no-vendor-prefix': true,
'media-feature-name-no-vendor-prefix': true,
'property-no-vendor-prefix': true,
'selector-no-vendor-prefix': true,
'value-no-vendor-prefix': true,
'color-hex-length': 'short',
'color-function-notation': 'modern',
'length-zero-no-unit': true,
'selector-pseudo-element-colon-notation': 'double',
'import-notation': 'string',
'alpha-value-notation': 'number',
'hue-degree-notation': 'angle',
'keyframe-selector-notation': 'percentage',
'declaration-block-no-redundant-longhand-properties': true,
'shorthand-property-no-redundant-values': true,
'block-no-redundant-nested-style-rules': true,
'color-named': 'never',
'font-family-name-quotes': 'always-where-required',
'number-max-precision': 4,
'comment-whitespace-inside': 'always',
};
const DEFAULT_CONFIG: Record<string, unknown> = {
...(recommendedConfig as Record<string, unknown>),
rules: {
'color-hex-length': 'short',
'color-named': 'never',
'color-no-invalid-hex': true,
'length-zero-no-unit': true,
'font-family-no-missing-generic-family-keyword': true,
'block-no-empty': true,
'declaration-block-no-duplicate-properties': true,
'no-descending-specificity': true,
'unit-no-unknown': true,
'property-no-unknown': true,
'selector-pseudo-class-no-unknown': true,
'selector-pseudo-element-no-unknown': true,
...((recommendedConfig as Record<string, unknown>).rules as Record<string, unknown>),
...extraRules,
},
};
+85
View File
@@ -1070,6 +1070,91 @@ const messages: Record<string, Record<Language, string>> = {
en: '日本語',
ja: '日本語',
},
'setup.fromTemplate': {
'zh-CN': '从模板导入',
en: 'Import from template',
ja: 'テンプレートからインポート',
},
'setup.exportTemplate': {
'zh-CN': '↓ 导出模板',
en: '↓ Export Template',
ja: '↓ テンプレートをエクスポート',
},
'setup.selectTemplateFile': {
'zh-CN': '选择模板文件',
en: 'Select template file',
ja: 'テンプレートファイルを選択',
},
'setup.importingTemplate': {
'zh-CN': '正在导入模板...',
en: 'Importing template...',
ja: 'テンプレートをインポート中...',
},
'import.template.badFormat': {
'zh-CN': '文件格式错误,请使用 Excel 模板(.xlsx/.xls',
en: 'Bad file format, please use Excel template (.xlsx/.xls)',
ja: 'ファイル形式エラー、Excel テンプレートを使用してください',
},
'import.template.empty': {
'zh-CN': '文件为空',
en: 'File is empty',
ja: 'ファイルが空です',
},
'import.template.notTemplate': {
'zh-CN': '不是模板文件,缺少列: {0}',
en: 'Not a template file, missing columns: {0}',
ja: 'テンプレートファイルではありません、欠損列: {0}',
},
'import.template.skipped': {
'zh-CN': '已跳过 {0} 行空数据',
en: 'Skipped {0} empty rows',
ja: '{0} 行の空データをスキップしました',
},
'import.sectionInvalid': {
'zh-CN': '错误规则',
en: 'Error Rules',
ja: 'エラー行',
},
'import.cannotImport': {
'zh-CN': '将自动丢弃,请修改文件后重新导入',
en: 'Will be auto-discarded, please fix the file and retry',
ja: '自動破棄されます、ファイルを修正して再インポートしてください',
},
'import.dedupFailed': {
'zh-CN': 'AI 去重失败,规则将不带去重标记导入',
en: 'AI dedup failed, rules imported without dedup marks',
ja: 'AI 重複排除失敗、重複マークなしでインポート',
},
'import.emptyValidRules': {
'zh-CN': '无有效规则可导入,请修改文件后重新导入',
en: 'No valid rules to import, please fix the file and retry',
ja: '有効なルールがありません、ファイルを修正して再インポートしてください',
},
'import.issuePrefix': {
'zh-CN': '⚠',
en: '⚠',
ja: '⚠',
},
'exportTemplate.saveLabel': {
'zh-CN': '导出模板',
en: 'Export Template',
ja: 'エクスポート',
},
'exportTemplate.success': {
'zh-CN': '模板已导出',
en: 'Template exported',
ja: 'テンプレートをエクスポートしました',
},
'exportTemplate.fail': {
'zh-CN': '导出失败:{0}',
en: 'Export failed: {0}',
ja: 'エクスポート失敗: {0}',
},
'exportTemplate.openFolder': {
'zh-CN': '打开文件夹',
en: 'Reveal in Folder',
ja: 'フォルダを開く',
},
};
let currentLang: Language = defaultLang;
+2 -2
View File
@@ -306,7 +306,7 @@ ${errorBox}
const toolName = report.adapterNames.length > 0 ? report.adapterNames.join(' + ') : t('report.sourceLinter');
const hasFixable = fixableSet.size > 0;
return `<div class="section-header"><span class="section-header-title">${esc(toolName)} · ${t('report.issuesCount', { 0: report.linterCount })}</span>${hasFixable ? `<button class="btn" onclick="send('fixAll')">${t('report.fixAll')}</button>` : ''}</div>`
+ report.linterDiagnostics.map((d, i) => this.buildIssueItem(d.severity, d.ruleId, d.message, d.range.start.line, 'linter', d.suggestion, fixableSet.has(i), undefined, false)).join('');
+ report.linterDiagnostics.map((d, i) => this.buildIssueItem(d.severity, d.ruleId, d.message, d.range.start.line, 'linter', d.suggestion, fixableSet.has(i))).join('');
}
private buildCustomList(report: MergedReport, fixableSet: Set<number>): string {
@@ -322,7 +322,7 @@ ${errorBox}
? t('report.injectedRules', { 0: filterInfo.injected, 1: filterInfo.totalActive })
: '';
return `<div class="section-header"><span class="section-header-title">${t('report.sourceCustom')} · ${t('report.issuesCount', { 0: report.customRuleCount })}${filterLabel}</span>${hasFixable ? `<button class="btn" onclick="send('fixAll')">${t('report.fixAll')}</button>` : ''}</div>`
+ report.customRuleDiagnostics.map((d, i) => this.buildIssueItem(d.severity, d.ruleId, d.message, d.range.start.line, 'custom', d.suggestion, fixableSet.has(i), undefined, false)).join('');
+ report.customRuleDiagnostics.map((d, i) => this.buildIssueItem(d.severity, d.ruleId, d.message, d.range.start.line, 'custom', d.suggestion, fixableSet.has(i))).join('');
}
private buildAIList(report: MergedReport): string {
+132
View File
@@ -0,0 +1,132 @@
import type { CustomRule } from '../../types';
import { getLanguage, type Language } from '../../i18n/messages';
export function buildDedupOnlyPrompt(
yamlContent: string,
existingRules: CustomRule[],
): { system: string; user: string } {
const lang = getLanguage();
const s = PROMPTS[lang];
const existingList = existingRules.length === 0
? s.noExisting
: existingRules.map(r =>
`- id: ${r.id} | severity: ${r.severity} | description: ${r.description} | message: ${r.message}`
).join('\n');
const system = [
s.role,
s.taskTitle,
s.taskLines.join('\n'),
s.rulesTitle,
s.rulesLines.join('\n'),
s.constraintTitle,
s.constraintLines.join('\n'),
s.existingTitle,
existingList,
].join('\n\n');
const user = s.userPrefix + '\n\n' + yamlContent;
return { system, user };
}
const PROMPTS: Record<Language, {
role: string;
taskTitle: string;
taskLines: string[];
rulesTitle: string;
rulesLines: string[];
constraintTitle: string;
constraintLines: string[];
existingTitle: string;
noExisting: string;
userPrefix: string;
}> = {
'zh-CN': {
role: '你是规则去重判定助手。你只输出 YAML,不输出任何解释。',
taskTitle: '## 任务',
taskLines: [
'下面是已标准化的规则 YAML。你只负责对照"现有规则"为每条规则标注去重字段。',
'为每条规则补充以下字段(如果无重复则标注 none):',
'- duplicateOf: 重复的规则 ID(如 eslint/no-console、custom/my-rule',
'- duplicateLevel: exact(完全相同)/ overlap(部分重叠)/ none(无重复)',
'- duplicateReason: 仅 overlap 时必填,简要说明重叠原因',
],
rulesTitle: '## 判定规则',
rulesLines: [
'1. exact: id 完全相同,或 description + message 语义完全一致',
'2. overlap: 检测目标/场景部分重叠,但并非完全相同',
'3. none: 与现有规则无冲突',
],
constraintTitle: '## ⚠️ 严格约束(必须遵守)',
constraintLines: [
'1. 严禁修改任何已有字段的值(id、severity、description、message、languages、excludeLanguages',
'2. 严禁添加新规则,严禁删除或合并规则',
'3. 规则数量必须与输入完全一致,顺序必须与输入完全一致',
'4. 你只允许添加三个字段: duplicateOf、duplicateLevel、duplicateReason',
'5. 如果某条规则与现有规则无任何重复,设置 duplicateLevel: none 即可,不需要补充 duplicateOf',
'6. 输出纯 YAML,不要用 markdown 代码块包裹',
],
existingTitle: '## 现有规则',
noExisting: '(无)',
userPrefix: '## 待去重的规则 YAML',
},
'en': {
role: 'You are a rule deduplication assistant. Output YAML only, no explanations.',
taskTitle: '## Task',
taskLines: [
'Below is standardized rule YAML. Only annotate dedup fields against the "Existing Rules" list.',
'For each rule, add (mark none if no conflict):',
'- duplicateOf: duplicated rule ID (e.g. eslint/no-console, custom/my-rule)',
'- duplicateLevel: exact / overlap / none',
'- duplicateReason: required only for overlap',
],
rulesTitle: '## Judgement Rules',
rulesLines: [
'1. exact: identical id, or semantically identical description+message',
'2. overlap: partially overlapping target/scenario',
'3. none: no conflict with existing rules',
],
constraintTitle: '## ⚠️ Strict Constraints (MUST follow)',
constraintLines: [
'1. Do NOT modify any existing field values (id, severity, description, message, languages, excludeLanguages)',
'2. Do NOT add, delete, or merge rules',
'3. Rule count and order must exactly match the input',
'4. Only add three fields: duplicateOf, duplicateLevel, duplicateReason',
'5. If a rule has no duplication, set duplicateLevel: none without duplicateOf',
'6. Output pure YAML, do NOT wrap in markdown code fences',
],
existingTitle: '## Existing Rules',
noExisting: '(none)',
userPrefix: '## YAML to deduplicate',
},
'ja': {
role: 'あなたはルール重複判定アシスタントです。YAML のみ出力し、説明は不要です。',
taskTitle: '## タスク',
taskLines: [
'以下は標準化されたルール YAML です。既存ルールと照合し、重複フィールドのみ注釈してください。',
'各ルールに以下を追加(重複がない場合は none と表記):',
'- duplicateOf: 重複ルール ID(例: eslint/no-console, custom/my-rule',
'- duplicateLevel: exact / overlap / none',
'- duplicateReason: overlap 時のみ必須',
],
rulesTitle: '## 判定ルール',
rulesLines: [
'1. exact: ID が同一、または description+message が意味的に完全一致',
'2. overlap: 検出対象/シナリオが部分重複',
'3. none: 既存ルールと競合なし',
],
constraintTitle: '## ⚠️ 厳格な制約(必ず遵守)',
constraintLines: [
'1. 既存フィールド(id, severity, description, message, languages, excludeLanguages)の値を一切変更しない',
'2. ルールの追加、削除、統合を一切行わない',
'3. ルールの数と順序は入力と完全に一致させる',
'4. 追加できるフィールドは duplicateOf, duplicateLevel, duplicateReason のみ',
'5. 重複がないルールは duplicateLevel: none とし、duplicateOf は付けない',
'6. 純粋な YAML を出力し、markdown コードブロックで囲まない',
],
existingTitle: '## 既存ルール',
noExisting: '(なし)',
userPrefix: '## 重複排除対象の YAML',
},
};
+100
View File
@@ -0,0 +1,100 @@
import * as path from 'path';
import * as XLSX from 'xlsx';
import type { ImportableRule, ValidationIssue } from '../import-types';
import type { Severity } from '../../types';
import { t } from '../../i18n/messages';
const REQUIRED_HEADERS = ['id', 'severity', 'description', 'message'];
const VALID_SEVERITY = ['error', 'warning', 'info'];
function splitList(v: unknown): string[] {
const s = String(v ?? '').trim();
if (!s) { return []; }
return s.split(/[,;、\n]/).map(x => x.trim()).filter(Boolean);
}
export interface TemplateParseResult {
rules: ImportableRule[];
validRules: ImportableRule[];
yamlContent: string;
skippedCount: number;
}
export function parseTemplate(srcPath: string): TemplateParseResult {
const ext = path.extname(srcPath).toLowerCase();
if (!['.xlsx', '.xls'].includes(ext)) {
throw new Error(t('import.template.badFormat'));
}
let wb: XLSX.WorkBook;
try { wb = XLSX.readFile(srcPath); }
catch { throw new Error(t('import.template.badFormat')); }
const sheet = wb.Sheets[wb.SheetNames[0]];
const rows = XLSX.utils.sheet_to_json<Record<string, string>>(sheet, { defval: '' });
if (rows.length === 0) {
throw new Error(t('import.template.empty'));
}
const header = Object.keys(rows[0]).map(k => k.trim().toLowerCase());
const missing = REQUIRED_HEADERS.filter(h => !header.includes(h));
if (missing.length > 0) {
throw new Error(t('import.template.notTemplate', { 0: missing.join(', ') }));
}
const totalRows = rows.length;
const rules: ImportableRule[] = rows
.map((r, idx) => ({ r, rowNo: idx + 2 }))
.filter(({ r }) => String(r.id ?? '').trim() !== '')
.map(({ r, rowNo }) => {
const issues: ValidationIssue[] = [];
const sevRaw = String(r.severity ?? '').trim().toLowerCase();
const severity: Severity = VALID_SEVERITY.includes(sevRaw) ? (sevRaw as Severity) : 'warning';
if (!VALID_SEVERITY.includes(sevRaw)) {
issues.push({ field: 'severity', severity: 'warning', message: `severity 非法: "${r.severity ?? ''}"` });
}
const description = String(r.description ?? '').trim();
if (!description) {
issues.push({ field: 'description', severity: 'error', message: 'description 为空' });
}
const message = String(r.message ?? '').trim();
if (!message) {
issues.push({ field: 'message', severity: 'error', message: 'message 为空' });
}
return {
id: String(r.id).trim(),
severity,
description,
message,
languages: splitList(r.languages),
excludeLanguages: splitList(r.excludeLanguages),
rowNumber: rowNo,
validationIssues: issues.length > 0 ? issues : undefined,
};
});
const validRules = rules.filter(r => !r.validationIssues);
const yamlContent = buildYaml(validRules);
const skippedCount = totalRows - rules.length;
return { rules, validRules, yamlContent, skippedCount };
}
function buildYaml(rules: ImportableRule[]): string {
const lines: string[] = [];
for (const r of rules) {
lines.push(`- id: ${r.id}`);
lines.push(` severity: ${r.severity}`);
lines.push(` description: ${r.description}`);
lines.push(` message: ${r.message}`);
if (r.languages?.length) {
lines.push(` languages: [${r.languages.join(', ')}]`);
}
if (r.excludeLanguages?.length) {
lines.push(` excludeLanguages: [${r.excludeLanguages.join(', ')}]`);
}
}
return lines.join('\n');
}
+60
View File
@@ -0,0 +1,60 @@
import * as XLSX from 'xlsx';
import * as vscode from 'vscode';
import { t } from '../i18n/messages';
const RULES_HEADER = ['id', 'severity', 'description', 'message', 'languages', 'excludeLanguages'];
const RULES_EXAMPLE = [
'no-todo', 'warning', '禁止提交 TODO 注释',
'发现 TODO 注释,请清理后提交', 'javascript,typescript', '',
];
const GUIDE_AOA: string[][] = [
['字段', '含义', '取值/格式', '示例'],
['id', '规则唯一标识', '小写字母、数字、连字符,全局唯一', 'no-todo'],
['severity', '严重级别', 'error / warning / info', 'warning'],
['description', '规则简述(给人看)', '自由文本', '禁止提交 TODO 注释'],
['message', '命中时展示给开发者的提示语', '自由文本', '发现 TODO 注释,请清理后提交'],
['languages', '生效的语言', '逗号分隔,留空表示对所有语言生效', 'javascript,typescript'],
['excludeLanguages', '排除的语言', '逗号分隔,可留空', ''],
['', '', '', ''],
['填写说明', '', '', ''],
['1. severity 仅接受 error / warning / info 三个值', '', '', ''],
['2. languages / excludeLanguages 多值用英文逗号分隔', '', '', ''],
['3. 示例行可删除,仅作填写参考', '', '', ''],
['4. 该模板可直接用于「使用模板文件导入」功能回环校验', '', '', ''],
];
export async function exportTemplate(): Promise<void> {
const uri = await vscode.window.showSaveDialog({
defaultUri: vscode.Uri.file('code-review-rules-template.xlsx'),
filters: { 'Excel': ['xlsx'] },
saveLabel: t('exportTemplate.saveLabel'),
});
if (!uri) { return; }
const wb = XLSX.utils.book_new();
const wsRules = XLSX.utils.aoa_to_sheet([RULES_HEADER, RULES_EXAMPLE]);
wsRules['!cols'] = [
{ wch: 16 }, { wch: 10 }, { wch: 32 }, { wch: 40 }, { wch: 24 }, { wch: 20 },
];
XLSX.utils.book_append_sheet(wb, wsRules, '规则');
const wsGuide = XLSX.utils.aoa_to_sheet(GUIDE_AOA);
wsGuide['!cols'] = [{ wch: 22 }, { wch: 28 }, { wch: 42 }, { wch: 36 }];
XLSX.utils.book_append_sheet(wb, wsGuide, '说明');
try {
XLSX.writeFile(wb, uri.fsPath);
const openFolder = t('exportTemplate.openFolder');
const choice = await vscode.window.showInformationMessage(
t('exportTemplate.success'), openFolder,
);
if (choice === openFolder) {
vscode.commands.executeCommand('revealFileInOS', uri);
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(t('exportTemplate.fail', { 0: msg }));
}
}
+69 -5
View File
@@ -15,7 +15,9 @@ export async function showImportPreview(
const keepRule: Record<string, boolean> = {};
for (const rule of result.rules) {
keepRule[rule.id] = rule.duplicateLevel !== 'exact';
if (!rule.validationIssues?.length) {
keepRule[rule.id] = rule.duplicateLevel !== 'exact';
}
}
panel.webview.html = renderPreviewHtml(result, keepRule);
@@ -51,15 +53,31 @@ function renderPreviewHtml(
result: ConversionResult,
keepRule: Record<string, boolean>,
): string {
const exactRules = result.rules.filter(r => r.duplicateLevel === 'exact');
const overlapRules = result.rules.filter(r => r.duplicateLevel === 'overlap');
const noneRules = result.rules.filter(
const errorRules = result.rules.filter(r => r.validationIssues?.length);
const cleanRules = result.rules.filter(r => !r.validationIssues?.length);
const exactRules = cleanRules.filter(r => r.duplicateLevel === 'exact');
const overlapRules = cleanRules.filter(r => r.duplicateLevel === 'overlap');
const noneRules = cleanRules.filter(
r => r.duplicateLevel !== 'exact' && r.duplicateLevel !== 'overlap'
);
const totalKept = Object.values(keepRule).filter(Boolean).length;
const totalCommented = Object.values(keepRule).filter(v => !v).length;
const skippedHint = result.skippedCount
? `<div class="summary-bar" style="border-color:rgba(88,166,255,0.3);color:#58a6ff;">${t('import.template.skipped', { 0: String(result.skippedCount) })}</div>`
: '';
const hasValidRules = cleanRules.length > 0;
const emptyValidHint = !hasValidRules
? `<div class="validation-error" style="display:block;">${t('import.emptyValidRules')}</div>`
: '';
const confirmBtnAttrs = hasValidRules
? 'onclick="doConfirm()"'
: 'disabled style="opacity:0.5;cursor:not-allowed;"';
function renderRuleCard(rule: ImportableRule): string {
const kept = keepRule[rule.id];
const color = SEVERITY_COLORS[rule.severity] || '#8b949e';
@@ -156,6 +174,46 @@ function renderPreviewHtml(
`;
}
function renderErrorCard(rule: ImportableRule): string {
const issues = (rule.validationIssues || []).map(i =>
`<div style="color:#f48771;font-size:12px;margin-bottom:4px;">${t('import.issuePrefix')} ${i.message}</div>`
).join('');
return `
<div class="rule-card" data-error="true" style="opacity:0.7;border-color:rgba(248,81,73,0.3);">
<div class="rule-card-header" style="cursor:default;">
<div class="rule-card-summary">
<span style="font-family:monospace;font-size:13px;font-weight:600;">${rule.id}</span>
<span style="color:#f48771;font-size:11px;font-weight:600;">${t('import.cannotImport')}</span>
</div>
</div>
<div class="rule-card-body" style="border-top:1px solid rgba(248,81,73,0.15);padding-top:8px;">
${issues}
<div style="color:#8b949e;font-size:11px;margin-top:6px;">
severity: ${rule.severity} | description: ${rule.description} | message: ${rule.message}
</div>
</div>
</div>
`;
}
function renderErrorSection(rules: ImportableRule[]): string {
if (rules.length === 0) { return ''; }
const sectionId = 'section-error';
return `
<div style="margin-bottom:12px;">
<div class="section-header" onclick="toggleSection('${sectionId}')">
<span style="font-size:14px;">🚫</span>
<span class="section-title">${t('import.sectionInvalid')}${rules.length}</span>
<span class="section-arrow">▼</span>
</div>
<div id="${sectionId}">
${rules.map(renderErrorCard).join('')}
</div>
</div>
`;
}
function renderSection(title: string, icon: string, rules: ImportableRule[], _defaultExpanded: boolean): string {
if (rules.length === 0) { return ''; }
const sectionId = `section-${title.replace(/\s/g, '')}`;
@@ -381,13 +439,17 @@ body {
<div id="validationError" class="validation-error" style="display:none;"></div>
${skippedHint}
${renderErrorSection(errorRules)}
${renderSection(t('import.sectionExact'), '⛔', exactRules, false)}
${renderSection(t('import.sectionOverlap'), '⚠️', overlapRules, true)}
${renderSection(t('import.sectionNone'), '✅', noneRules, false)}
${emptyValidHint}
<div class="actions">
<button class="btn" onclick="cancel()">${t('importPreview.cancel')}</button>
<button class="btn btn-primary" onclick="doConfirm()">${t('importPreview.confirm')}</button>
<button class="btn btn-primary" ${confirmBtnAttrs}>${t('importPreview.confirm')}</button>
</div>
<script>
@@ -462,6 +524,7 @@ function updateRule(ruleId, field, value) {
function collectEditedRules() {
const result = [];
document.querySelectorAll('.rule-card').forEach(card => {
if (card.hasAttribute('data-error')) { return; }
const originalId = card.dataset.ruleid;
const idInput = card.querySelector('.id-display-input');
const ruleId = idInput ? idInput.value.trim() || originalId : originalId;
@@ -522,6 +585,7 @@ function cancel() {
function updateSummary() {
let keepCount = 0, commentCount = 0;
document.querySelectorAll('.rule-card').forEach(card => {
if (card.hasAttribute('data-error')) { return; }
const ruleId = card.dataset.ruleid;
const keepBtns = card.querySelectorAll('.toggle-btn');
let isKept = true;
+39
View File
@@ -6,6 +6,8 @@ import { getAIConfig, getAITimeout } from '../config/ai';
import { createProvider } from '../ai/factory';
import { RuleConverter } from './converters/converter';
import { loadActiveRules } from './yaml-parser';
import { parseTemplate } from './converters/template-converter';
import { buildDedupOnlyPrompt } from './converters/dedup-prompt';
import type { ConversionResult, ImportableRule, PreviewDecision } from './import-types';
import { t, getLanguage } from '../i18n/messages';
@@ -283,6 +285,43 @@ export class ImportService {
};
}
async importTemplate(
srcPath: string,
name: string,
context: vscode.ExtensionContext,
): Promise<ConversionResult> {
const { rules, validRules, yamlContent, skippedCount } = parseTemplate(srcPath);
let dedupedValidRules: ImportableRule[] = validRules;
if (validRules.length > 0) {
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
const existingRules = workspaceRoot ? loadActiveRules(workspaceRoot) : [];
const { system, user } = buildDedupOnlyPrompt(yamlContent, existingRules);
const dedupedYaml = await convertContentWithAI(user, context, system);
if (dedupedYaml) {
dedupedValidRules = parseImportableYaml(dedupedYaml);
} else {
vscode.window.showWarningMessage(t('import.dedupFailed'));
}
}
const errorRules = rules.filter(r => r.validationIssues?.length);
const allRules = [...dedupedValidRules, ...errorRules];
const exactCount = dedupedValidRules.filter(r => r.duplicateLevel === 'exact').length;
const overlapCount = dedupedValidRules.filter(r => r.duplicateLevel === 'overlap').length;
return {
rules: allRules,
yamlContent,
sourceFileName: path.basename(srcPath),
exactCount,
overlapCount,
skippedCount,
errorCount: errorRules.length,
};
}
applyConversion(
result: ConversionResult,
decision: PreviewDecision,
+10
View File
@@ -1,9 +1,17 @@
import type { CustomRule } from '../types';
export interface ValidationIssue {
field: string;
severity: 'error' | 'warning';
message: string;
}
export interface ImportableRule extends CustomRule {
duplicateOf?: string;
duplicateLevel?: 'exact' | 'overlap' | 'none';
duplicateReason?: string;
validationIssues?: ValidationIssue[];
rowNumber?: number;
}
export interface ConversionResult {
@@ -12,6 +20,8 @@ export interface ConversionResult {
sourceFileName: string;
exactCount: number;
overlapCount: number;
skippedCount?: number;
errorCount?: number;
}
export interface PreviewDecision {
+356 -94
View File
@@ -1,11 +1,11 @@
{
"version": "1.0.1",
"linterVersion": {
"eslint": "9.x (recommended)",
"ts-eslint": "8.x (recommended)",
"stylelint": "16.x (12 rules)",
"pmd": "7.26.0 (6 categories)",
"sql-lint": "4.2.2 (default)"
"eslint": "9.x (92 rules)",
"ts-eslint": "8.x (35 rules)",
"stylelint": "16.x (68 rules)",
"pmd": "7.26.0 (274 Java rules + 12 JSP rules)",
"sql-lint": "4.2.2 (57 recommended)"
},
"rules": {
"eslint": [
@@ -252,7 +252,38 @@
{
"id": "eslint/valid-typeof",
"description": "Enforce comparing typeof expressions against valid strings"
}
},
{"id": "eslint/eqeqeq", "description": "Require === and !=="},
{"id": "eslint/no-eq-null", "description": "Disallow null comparisons without type-checking"},
{"id": "eslint/no-self-compare", "description": "Disallow comparisons where both sides are the same"},
{"id": "eslint/no-await-in-loop", "description": "Disallow await inside loops"},
{"id": "eslint/no-promise-executor-return", "description": "Disallow returning values from Promise executor"},
{"id": "eslint/no-shadow", "description": "Disallow variable declarations from shadowing variables in outer scopes"},
{"id": "eslint/no-unassigned-vars", "description": "Disallow let or var variables that are read but never assigned"},
{"id": "eslint/no-useless-assignment", "description": "Disallow variable assignments where the value is not used"},
{"id": "eslint/block-scoped-var", "description": "Enforce variables within the scope they are defined"},
{"id": "eslint/default-case", "description": "Require default cases in switch statements"},
{"id": "eslint/default-case-last", "description": "Enforce default clauses in switch statements to be last"},
{"id": "eslint/no-unmodified-loop-condition", "description": "Disallow unmodified loop conditions"},
{"id": "eslint/no-unreachable-loop", "description": "Disallow loops with a body that allows only one iteration"},
{"id": "eslint/no-eval", "description": "Disallow the use of eval()"},
{"id": "eslint/no-extend-native", "description": "Disallow extending native types"},
{"id": "eslint/no-var", "description": "Require let or const instead of var"},
{"id": "eslint/prefer-template", "description": "Require template literals instead of string concatenation"},
{"id": "eslint/prefer-object-spread", "description": "Disallow Object.assign and prefer object spread"},
{"id": "eslint/prefer-rest-params", "description": "Require rest parameters instead of arguments"},
{"id": "eslint/prefer-spread", "description": "Require spread operator instead of .apply()"},
{"id": "eslint/prefer-object-has-own", "description": "Disallow Object.prototype.hasOwnProperty.call() and prefer Object.hasOwn()"},
{"id": "eslint/no-useless-concat", "description": "Disallow unnecessary concatenation of literals or template literals"},
{"id": "eslint/no-useless-return", "description": "Disallow redundant return statements"},
{"id": "eslint/no-useless-computed-key", "description": "Disallow unnecessary computed property keys in objects and classes"},
{"id": "eslint/no-useless-rename", "description": "Disallow renaming import, export, and destructured assignments to the same name"},
{"id": "eslint/no-param-reassign", "description": "Disallow reassigning function parameters"},
{"id": "eslint/no-return-assign", "description": "Disallow assignment operators in return statements"},
{"id": "eslint/no-throw-literal", "description": "Disallow throwing literals as exceptions"},
{"id": "eslint/camelcase", "description": "Enforce camelcase naming convention"},
{"id": "eslint/new-cap", "description": "Require constructor names to begin with a capital letter"},
{"id": "eslint/no-array-constructor", "description": "Disallow Array constructors"}
],
"ts-eslint": [
{
@@ -350,7 +381,19 @@
{
"id": "ts-eslint/prefer-spread",
"description": "Require spread operator instead of .apply()"
}
},
{"id": "ts-eslint/no-non-null-assertion", "description": "Disallow non-null assertions using the ! postfix operator"},
{"id": "ts-eslint/no-dynamic-delete", "description": "Disallow using the delete operator on computed key expressions"},
{"id": "ts-eslint/no-useless-empty-export", "description": "Disallow empty exports that don't change anything in a module"},
{"id": "ts-eslint/consistent-type-imports", "description": "Enforce consistent usage of type imports"},
{"id": "ts-eslint/unified-signatures", "description": "Disallow two overloads that could be unified into a single signature"},
{"id": "ts-eslint/no-extraneous-class", "description": "Disallow classes only being used as namespaces"},
{"id": "ts-eslint/no-useless-constructor", "description": "Disallow unnecessary constructors"},
{"id": "ts-eslint/no-non-null-asserted-nullish-coalescing", "description": "Disallow non-null assertions in the left operand of a nullish coalescing operator"},
{"id": "ts-eslint/no-invalid-void-type", "description": "Disallow void type outside of generic or return types"},
{"id": "ts-eslint/prefer-literal-enum-member", "description": "Require all enum members to be literal values"},
{"id": "ts-eslint/prefer-enum-initializers", "description": "Require each enum member value to be explicitly initialized"},
{"id": "ts-eslint/no-shadow", "description": "Disallow variable declarations from shadowing variables declared in the outer scope"}
],
"stylelint": [
{
@@ -400,6 +443,94 @@
{
"id": "stylelint/selector-pseudo-element-no-unknown",
"description": "Disallow unknown pseudo-element selectors"
},
{
"id": "stylelint/function-linear-gradient-no-nonstandard-direction",
"description": "Disallow non-standard directions in linear-gradient"
},
{
"id": "stylelint/function-no-unknown",
"description": "Disallow unknown functions"
},
{
"id": "stylelint/no-unknown-animations",
"description": "Disallow unknown animations"
},
{
"id": "stylelint/no-unknown-custom-media",
"description": "Disallow unknown custom media queries"
},
{
"id": "stylelint/no-unknown-custom-properties",
"description": "Disallow unknown custom properties"
},
{
"id": "stylelint/at-rule-no-vendor-prefix",
"description": "Disallow vendor prefixes for at-rules"
},
{
"id": "stylelint/media-feature-name-no-vendor-prefix",
"description": "Disallow vendor prefixes for media feature names"
},
{
"id": "stylelint/property-no-vendor-prefix",
"description": "Disallow vendor prefixes for properties"
},
{
"id": "stylelint/selector-no-vendor-prefix",
"description": "Disallow vendor prefixes for selectors"
},
{
"id": "stylelint/value-no-vendor-prefix",
"description": "Disallow vendor prefixes for values"
},
{
"id": "stylelint/color-function-notation",
"description": "Require modern or legacy notation for color-functions"
},
{
"id": "stylelint/selector-pseudo-element-colon-notation",
"description": "Use single or double colon notation for pseudo-elements"
},
{
"id": "stylelint/import-notation",
"description": "Require string or url notation for @import"
},
{
"id": "stylelint/alpha-value-notation",
"description": "Require percentage or number notation for alpha-values"
},
{
"id": "stylelint/hue-degree-notation",
"description": "Require number or angle notation for hue degrees"
},
{
"id": "stylelint/keyframe-selector-notation",
"description": "Require keyword or percentage notation for keyframe selectors"
},
{
"id": "stylelint/declaration-block-no-redundant-longhand-properties",
"description": "Disallow redundant longhand properties within declaration blocks"
},
{
"id": "stylelint/shorthand-property-no-redundant-values",
"description": "Disallow redundant values within shorthand properties"
},
{
"id": "stylelint/block-no-redundant-nested-style-rules",
"description": "Disallow redundant nested style rules within blocks"
},
{
"id": "stylelint/font-family-name-quotes",
"description": "Require quotes for font-family names"
},
{
"id": "stylelint/number-max-precision",
"description": "Limit the number of decimal places in numbers"
},
{
"id": "stylelint/comment-whitespace-inside",
"description": "Require or disallow whitespace inside comments"
}
],
"pmd": [
@@ -555,6 +686,10 @@
"id": "pmd/ReplaceVectorWithList",
"description": "Use List/ArrayList instead of Vector"
},
{
"id": "pmd/ReturnEmptyCollectionRatherThanNull",
"description": "Return empty collection rather than null"
},
{
"id": "pmd/SimplifiableTestAssertion",
"description": "Use more specific assertion methods"
@@ -595,6 +730,10 @@
"id": "pmd/UnnecessaryWarningSuppression",
"description": "Remove unused PMD suppressions"
},
{
"id": "pmd/UnsynchronizedStaticFormatter",
"description": "Static formatter should be synchronized"
},
{
"id": "pmd/UnusedAssignment",
"description": "Remove unused assignments"
@@ -635,10 +774,18 @@
"id": "pmd/UseTryWithResources",
"description": "Use try-with-resources"
},
{
"id": "pmd/UseUtilityClass",
"description": "Utility class should have private constructor"
},
{
"id": "pmd/UseVarargs",
"description": "Use varargs instead of array parameter"
},
{
"id": "pmd/VariableCanBeInlined",
"description": "Variable can be inlined"
},
{
"id": "pmd/WhileLoopWithLiteralBoolean",
"description": "Simplify while loops with literal booleans"
@@ -719,10 +866,6 @@
"id": "pmd/FormalParameterNamingConventions",
"description": "Parameter naming conventions"
},
{
"id": "pmd/GenericsNaming",
"description": "Single uppercase letter for generics"
},
{
"id": "pmd/IdenticalCatchBranches",
"description": "Collapse identical catch branches"
@@ -816,8 +959,8 @@
"description": "Remove unnecessary fully qualified names"
},
{
"id": "pmd/UnnecessaryLocalBeforeReturn",
"description": "Remove unnecessary local before return"
"id": "pmd/UnnecessaryImport",
"description": "Remove unnecessary imports"
},
{
"id": "pmd/UnnecessaryModifier",
@@ -1059,10 +1202,26 @@
"id": "pmd/AvoidMultipleUnaryOperators",
"description": "Avoid multiple unary operators"
},
{
"id": "pmd/AvoidSynchronizedStatement",
"description": "Avoid synchronized statements"
},
{
"id": "pmd/AvoidSynchronizedAtMethodLevel",
"description": "Avoid synchronized at method level"
},
{
"id": "pmd/AvoidThreadGroup",
"description": "Avoid using ThreadGroup"
},
{
"id": "pmd/AvoidUsingOctalValues",
"description": "Avoid octal literals"
},
{
"id": "pmd/AvoidUsingVolatile",
"description": "Avoid the volatile keyword"
},
{
"id": "pmd/BrokenNullCheck",
"description": "Broken null check (|| vs &&)"
@@ -1144,8 +1303,12 @@
"description": "Don't use Threads"
},
{
"id": "pmd/DontImportSun",
"description": "Don't import sun.* packages"
"id": "pmd/DontCallThreadRun",
"description": "Don't call Thread.run()"
},
{
"id": "pmd/DoubleCheckedLocking",
"description": "Double-checked locking is not thread-safe"
},
{
"id": "pmd/EmptyCatchBlock",
@@ -1163,6 +1326,10 @@
"id": "pmd/IdempotentOperations",
"description": "Idempotent operations"
},
{
"id": "pmd/ImplicitSwitchFallThrough",
"description": "Implicit switch fall through"
},
{
"id": "pmd/ImportFromSamePackage",
"description": "Import from same package"
@@ -1223,6 +1390,10 @@
"id": "pmd/NonStaticInitializer",
"description": "Non-static initializer"
},
{
"id": "pmd/NonThreadSafeSingleton",
"description": "Singleton is not thread-safe"
},
{
"id": "pmd/NullAssignment",
"description": "Null assignment"
@@ -1239,6 +1410,14 @@
"id": "pmd/OperationWithCloning",
"description": "Operation with cloning"
},
{
"id": "pmd/OverrideBothEqualsAndHashcode",
"description": "Override both equals() and hashCode()"
},
{
"id": "pmd/OverridingThreadRun",
"description": "Don't override Thread.run()"
},
{
"id": "pmd/PackageDeclaration",
"description": "Package declaration"
@@ -1315,22 +1494,30 @@
"id": "pmd/UnusedNullCheckInEquals",
"description": "Unused null check in equals"
},
{
"id": "pmd/UseConcurrentHashMap",
"description": "Use ConcurrentHashMap for concurrent access"
},
{
"id": "pmd/UseCorrectExceptionLogging",
"description": "Correct exception logging"
},
{
"id": "pmd/UseDiamondOperator",
"description": "Use diamond operator <>"
},
{
"id": "pmd/UseEqualsToCompareStrings",
"description": "Use equals() for strings"
},
{
"id": "pmd/UselessOperationOnImmutable",
"description": "Useless operation on immutable"
},
{
"id": "pmd/UseLocaleWithCaseConversions",
"description": "Use locale with case conversions"
},
{
"id": "pmd/UseNotifyAllInsteadOfNotify",
"description": "Use notifyAll() instead of notify()"
},
{
"id": "pmd/UseProperClassLoader",
"description": "Use proper classloader"
@@ -1485,303 +1672,378 @@
"sql-lint": [
{
"id": "sql-lint/AL01",
"description": "Implicit/explicit aliasing of table"
"description": "Implicit/explicit aliasing of table",
"tier": "P2"
},
{
"id": "sql-lint/AL02",
"description": "Implicit/explicit aliasing of columns"
"description": "Implicit/explicit aliasing of columns",
"tier": "P0"
},
{
"id": "sql-lint/AL03",
"description": "Column expression without alias"
"description": "Column expression without alias",
"tier": "P0"
},
{
"id": "sql-lint/AL04",
"description": "Table aliases should be unique within each clause"
"description": "Table aliases should be unique within each clause",
"tier": "P0"
},
{
"id": "sql-lint/AL05",
"description": "Tables should not be aliased if unused"
"description": "Tables should not be aliased if unused",
"tier": "P0"
},
{
"id": "sql-lint/AL06",
"description": "Enforce table alias lengths"
"description": "Enforce table alias lengths",
"tier": "P0"
},
{
"id": "sql-lint/AL07",
"description": "Avoid table aliases"
"description": "Avoid table aliases",
"tier": "excluded"
},
{
"id": "sql-lint/AL08",
"description": "Column aliases should be unique within each clause"
"description": "Column aliases should be unique within each clause",
"tier": "P0"
},
{
"id": "sql-lint/AL09",
"description": "Column aliases should not alias to itself"
"description": "Column aliases should not alias to itself",
"tier": "P0"
},
{
"id": "sql-lint/AL10",
"description": "Derived tables must have an alias"
"description": "Derived tables must have an alias",
"tier": "P0"
},
{
"id": "sql-lint/AM01",
"description": "Ambiguous use of DISTINCT with GROUP BY"
"description": "Ambiguous use of DISTINCT with GROUP BY",
"tier": "P0"
},
{
"id": "sql-lint/AM02",
"description": "UNION DISTINCT/ALL preferred over just UNION"
"description": "UNION DISTINCT/ALL preferred over just UNION",
"tier": "P0"
},
{
"id": "sql-lint/AM03",
"description": "Ambiguous ordering directions"
"description": "Ambiguous ordering directions",
"tier": "P1"
},
{
"id": "sql-lint/AM04",
"description": "Query produces unknown number of result columns"
"description": "Query produces unknown number of result columns",
"tier": "P2"
},
{
"id": "sql-lint/AM05",
"description": "Join clauses should be fully qualified"
"description": "Join clauses should be fully qualified",
"tier": "P1"
},
{
"id": "sql-lint/AM06",
"description": "Inconsistent column references in GROUP BY/ORDER BY"
"description": "Inconsistent column references in GROUP BY/ORDER BY",
"tier": "P0"
},
{
"id": "sql-lint/AM07",
"description": "Queries within set query produce different numbers of columns"
"description": "Queries within set query produce different numbers of columns",
"tier": "P2"
},
{
"id": "sql-lint/AM08",
"description": "Implicit cross join detected"
"description": "Implicit cross join detected",
"tier": "P1"
},
{
"id": "sql-lint/AM09",
"description": "LIMIT/OFFSET without ORDER BY non-deterministic"
"description": "LIMIT/OFFSET without ORDER BY non-deterministic",
"tier": "P2"
},
{
"id": "sql-lint/CP01",
"description": "Inconsistent capitalisation of keywords"
"description": "Inconsistent capitalisation of keywords",
"tier": "P0"
},
{
"id": "sql-lint/CP02",
"description": "Inconsistent capitalisation of unquoted identifiers"
"description": "Inconsistent capitalisation of unquoted identifiers",
"tier": "P0"
},
{
"id": "sql-lint/CP03",
"description": "Inconsistent capitalisation of function names"
"description": "Inconsistent capitalisation of function names",
"tier": "P0"
},
{
"id": "sql-lint/CP04",
"description": "Inconsistent capitalisation of boolean/null literal"
"description": "Inconsistent capitalisation of boolean/null literal",
"tier": "P0"
},
{
"id": "sql-lint/CP05",
"description": "Inconsistent capitalisation of datatypes"
"description": "Inconsistent capitalisation of datatypes",
"tier": "P0"
},
{
"id": "sql-lint/CV01",
"description": "Consistent usage of != or <>"
"description": "Consistent usage of != or <>",
"tier": "P1"
},
{
"id": "sql-lint/CV02",
"description": "Use COALESCE instead of IFNULL/NVL"
"description": "Use COALESCE instead of IFNULL/NVL",
"tier": "P1"
},
{
"id": "sql-lint/CV03",
"description": "Trailing commas within select clause"
"description": "Trailing commas within select clause",
"tier": "P0"
},
{
"id": "sql-lint/CV04",
"description": "Consistent syntax for count number of rows"
"description": "Consistent syntax for count number of rows",
"tier": "P0"
},
{
"id": "sql-lint/CV05",
"description": "Comparisons with NULL should use IS or IS NOT"
"description": "Comparisons with NULL should use IS or IS NOT",
"tier": "P0"
},
{
"id": "sql-lint/CV06",
"description": "Statements must end with a semi-colon"
"description": "Statements must end with a semi-colon",
"tier": "P1"
},
{
"id": "sql-lint/CV07",
"description": "Top-level statements should not be wrapped in brackets"
"description": "Top-level statements should not be wrapped in brackets",
"tier": "P2"
},
{
"id": "sql-lint/CV08",
"description": "Use LEFT JOIN instead of RIGHT JOIN"
"description": "Use LEFT JOIN instead of RIGHT JOIN",
"tier": "P1"
},
{
"id": "sql-lint/CV09",
"description": "Block a list of configurable words"
"description": "Block a list of configurable words",
"tier": "excluded"
},
{
"id": "sql-lint/CV10",
"description": "Consistent usage of preferred quotes for quoted literals"
"description": "Consistent usage of preferred quotes for quoted literals",
"tier": "excluded"
},
{
"id": "sql-lint/CV11",
"description": "Enforce consistent type casting style"
"description": "Enforce consistent type casting style",
"tier": "P2"
},
{
"id": "sql-lint/CV12",
"description": "Use JOIN ... ON ... instead of WHERE ... for join conditions"
"description": "Use JOIN ... ON ... instead of WHERE ... for join conditions",
"tier": "P1"
},
{
"id": "sql-lint/JJ01",
"description": "Jinja tags should have single whitespace on either side"
"description": "Jinja tags should have single whitespace on either side",
"tier": "P0"
},
{
"id": "sql-lint/LT01",
"description": "Inappropriate Spacing"
"description": "Inappropriate Spacing",
"tier": "P0"
},
{
"id": "sql-lint/LT02",
"description": "Incorrect Indentation"
"description": "Incorrect Indentation",
"tier": "P0"
},
{
"id": "sql-lint/LT03",
"description": "Operators before/after newlines"
"description": "Operators before/after newlines",
"tier": "excluded"
},
{
"id": "sql-lint/LT04",
"description": "Leading/Trailing comma enforcement"
"description": "Leading/Trailing comma enforcement",
"tier": "excluded"
},
{
"id": "sql-lint/LT05",
"description": "Line is too long"
"description": "Line is too long",
"tier": "P0"
},
{
"id": "sql-lint/LT06",
"description": "Function name not followed by parenthesis"
"description": "Function name not followed by parenthesis",
"tier": "P0"
},
{
"id": "sql-lint/LT07",
"description": "WITH clause closing bracket on new line"
"description": "WITH clause closing bracket on new line",
"tier": "P0"
},
{
"id": "sql-lint/LT08",
"description": "Blank line after CTE closing bracket"
"description": "Blank line after CTE closing bracket",
"tier": "P0"
},
{
"id": "sql-lint/LT09",
"description": "Select targets on new line"
"description": "Select targets on new line",
"tier": "excluded"
},
{
"id": "sql-lint/LT10",
"description": "SELECT modifiers on same line as SELECT"
"description": "SELECT modifiers on same line as SELECT",
"tier": "P0"
},
{
"id": "sql-lint/LT11",
"description": "Set operators surrounded by newlines"
"description": "Set operators surrounded by newlines",
"tier": "P0"
},
{
"id": "sql-lint/LT12",
"description": "Files must end with single trailing newline"
"description": "Files must end with single trailing newline",
"tier": "P0"
},
{
"id": "sql-lint/LT13",
"description": "Files must not begin with newlines/whitespace"
"description": "Files must not begin with newlines/whitespace",
"tier": "P1"
},
{
"id": "sql-lint/LT14",
"description": "Keyword clauses before/after newlines"
"description": "Keyword clauses before/after newlines",
"tier": "P1"
},
{
"id": "sql-lint/LT15",
"description": "Too many consecutive blank lines"
"description": "Too many consecutive blank lines",
"tier": "P1"
},
{
"id": "sql-lint/OR01",
"description": "Remove empty batches"
"description": "Remove empty batches",
"tier": "P2"
},
{
"id": "sql-lint/PG01",
"description": "Avoid excessive locks in PostgreSQL DDL"
"description": "Avoid excessive locks in PostgreSQL DDL",
"tier": "P2"
},
{
"id": "sql-lint/RF01",
"description": "References cannot reference objects not in FROM clause"
"description": "References cannot reference objects not in FROM clause",
"tier": "P0"
},
{
"id": "sql-lint/RF02",
"description": "References should be qualified if multiple tables"
"description": "References should be qualified if multiple tables",
"tier": "P1"
},
{
"id": "sql-lint/RF03",
"description": "Column references consistent in single table statements"
"description": "Column references consistent in single table statements",
"tier": "excluded"
},
{
"id": "sql-lint/RF04",
"description": "Keywords should not be used as identifiers"
"description": "Keywords should not be used as identifiers",
"tier": "P1"
},
{
"id": "sql-lint/RF05",
"description": "No special characters in identifiers"
"description": "No special characters in identifiers",
"tier": "P1"
},
{
"id": "sql-lint/RF06",
"description": "Unnecessary quoted identifier"
"description": "Unnecessary quoted identifier",
"tier": "P1"
},
{
"id": "sql-lint/ST01",
"description": "Do not specify else null in CASE WHEN"
"description": "Do not specify else null in CASE WHEN",
"tier": "P1"
},
{
"id": "sql-lint/ST02",
"description": "Unnecessary CASE statement"
"description": "Unnecessary CASE statement",
"tier": "P1"
},
{
"id": "sql-lint/ST03",
"description": "Unused CTE"
"description": "Unused CTE",
"tier": "P0"
},
{
"id": "sql-lint/ST04",
"description": "Nested CASE in ELSE clause can be flattened"
"description": "Nested CASE in ELSE clause can be flattened",
"tier": "P1"
},
{
"id": "sql-lint/ST05",
"description": "Subqueries in Join/From clauses; use CTEs"
"description": "Subqueries in Join/From clauses; use CTEs",
"tier": "P1"
},
{
"id": "sql-lint/ST06",
"description": "Column order: wildcards, simple targets, then calculations"
"description": "Column order: wildcards, simple targets, then calculations",
"tier": "P1"
},
{
"id": "sql-lint/ST07",
"description": "Prefer ON over USING for join keys"
"description": "Prefer ON over USING for join keys",
"tier": "P1"
},
{
"id": "sql-lint/ST08",
"description": "DISTINCT used with parentheses"
"description": "DISTINCT used with parentheses",
"tier": "P0"
},
{
"id": "sql-lint/ST09",
"description": "Join condition order"
"description": "Join condition order",
"tier": "P1"
},
{
"id": "sql-lint/ST10",
"description": "Redundant constant expression"
"description": "Redundant constant expression",
"tier": "P1"
},
{
"id": "sql-lint/ST11",
"description": "Joined table not referenced"
"description": "Joined table not referenced",
"tier": "P1"
},
{
"id": "sql-lint/ST12",
"description": "Consecutive semicolons"
"description": "Consecutive semicolons",
"tier": "P1"
},
{
"id": "sql-lint/TQ01",
"description": "SP_ prefix should not be used for user-defined stored procedures"
"description": "SP_ prefix should not be used for user-defined stored procedures",
"tier": "P2"
},
{
"id": "sql-lint/TQ02",
"description": "Procedure bodies with multiple statements wrapped in BEGIN/END"
"description": "Procedure bodies with multiple statements wrapped in BEGIN/END",
"tier": "P2"
},
{
"id": "sql-lint/TQ03",
"description": "Remove empty batches"
"description": "Remove empty batches",
"tier": "P2"
}
]
}
+4
View File
@@ -0,0 +1,4 @@
declare module 'stylelint-config-recommended' {
const config: { rules: Record<string, unknown> };
export default config;
}
+6 -1
View File
@@ -21,10 +21,15 @@
var name = input.value.trim();
if (!name) { showRuleNameError(true); return; }
showRuleNameError(false);
vscode.postMessage({ type: 'addRule', name: name });
var useTemplateMode = document.getElementById('useTemplateMode').checked;
vscode.postMessage({ type: 'addRule', name: name, useTemplateMode: useTemplateMode });
input.value = '';
}
window.exportTemplate = function() {
vscode.postMessage({ type: 'exportTemplate' });
};
var ruleInput = document.getElementById('newRuleInput');
if (ruleInput) {
ruleInput.addEventListener('input', function () { showRuleNameError(false); });
+63 -3
View File
@@ -8,6 +8,7 @@ import { createProvider, getAllProviderMeta, getProviderModels, invalidateProvid
import { listRuleFiles } from '../rules/yaml-parser';
import { ImportService } from '../rules/import-service';
import { showImportPreview } from '../rules/import-preview';
import { exportTemplate as exportTemplateService } from '../rules/export-service';
import { YamlConverter } from '../rules/converters/yaml-converter';
import { MdConverter } from '../rules/converters/md-converter';
import { TxtConverter } from '../rules/converters/txt-converter';
@@ -273,9 +274,12 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
await this.pushConfig();
break;
case 'addRule':
await this.addRule(msg.name);
await this.addRule(msg.name, msg.useTemplateMode);
await this.pushConfig();
break;
case 'exportTemplate':
await this.exportTemplate();
break;
case 'reset':
await this.resetConfig();
await this.pushConfig();
@@ -424,12 +428,57 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
}
}
private async addRule(name: string): Promise<void> {
private async addRule(name: string, useTemplateMode?: boolean): Promise<void> {
if (!name.trim()) { return; }
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspaceRoot) { return; }
if (useTemplateMode) {
const result = await vscode.window.showOpenDialog({
canSelectMany: false,
openLabel: t('setup.selectTemplateFile'),
filters: { 'Excel': ['xlsx', 'xls'] },
});
if (!result || result.length === 0) { return; }
try {
const conversion = await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: t('setup.importingTemplate'),
}, async () => {
return await this.importService.importTemplate(
result[0].fsPath, name, this.context,
);
});
const decision = await showImportPreview(conversion);
if (!decision || !decision.confirmed) {
vscode.window.showInformationMessage(t('setup.importCancelled'));
return;
}
const rulesDir = path.join(workspaceRoot, '.code-review', 'rules');
if (!fs.existsSync(rulesDir)) {
fs.mkdirSync(rulesDir, { recursive: true });
}
const yamlFileName = name.endsWith('.yaml') ? name : `${name}.yaml`;
const yamlPath = path.join(rulesDir, yamlFileName);
if (fs.existsSync(yamlPath)) {
vscode.window.showErrorMessage(t('setup.fileExists', { 0: yamlFileName }));
return;
}
this.importService.applyConversion(conversion, decision, yamlPath);
vscode.window.showInformationMessage(
t('setup.importDedupResult', { 0: yamlFileName, 1: String(conversion.rules.length), 2: String(conversion.exactCount), 3: String(conversion.overlapCount) })
);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(msg);
}
return;
}
const result = await vscode.window.showOpenDialog({
canSelectMany: false,
openLabel: t('setup.selectRuleFile'),
@@ -480,6 +529,10 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
}
}
private async exportTemplate(): Promise<void> {
await exportTemplateService();
}
private async resetConfig(): Promise<void> {
const config = vscode.workspace.getConfiguration('vscode-code-reviewer');
await config.update('ai.provider', undefined, vscode.ConfigurationTarget.Global);
@@ -985,10 +1038,17 @@ input::placeholder { color: var(--vscode-input-placeholderForeground, var(--vsco
<div class="field" style="margin-top:8px;">
<div class="input-group">
<input type="text" id="newRuleInput" placeholder="${t('setup.ruleNamePlaceholder')}">
<button class="btn btn-sm" style="background:#7c3aed;color:#fff;border-color:#7c3aed;" onclick="exportTemplate()">${t('setup.exportTemplate')}</button>
<button class="btn btn-sm" style="background:#7c3aed;color:#fff;border-color:#7c3aed;" onclick="addRule()">${t('setup.add')}</button>
</div>
<div class="error-hint" id="ruleNameError">${t('setup.ruleNameRequired')}</div>
<div class="field-hint">${t('setup.ruleNameHint')}</div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:4px;">
<div class="field-hint" style="margin-top:0;">${t('setup.ruleNameHint')}</div>
<label style="display:flex;align-items:center;gap:6px;font-size:11px;color:var(--vscode-descriptionForeground);cursor:pointer;white-space:nowrap;">
<input type="checkbox" id="useTemplateMode" style="width:auto;margin:0;">
使用模板文件导入
</label>
</div>
</div>
</div>