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