Files
2026Technology-Competition/src/rules/builtin-rules.ts
T

378 lines
14 KiB
TypeScript

import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import js from '@eslint/js';
import staticRules from './static-rules.json';
import type { Language } from '../i18n/messages';
export const eslintExtraRules: 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',
};
export const eslintExtraTsRules: 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',
};
export const stylelintExtraRules: 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',
};
export const BUILTIN_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';
export function buildBuiltinSqlfluffConfig(dialect: string): string {
return `[sqlfluff]
rules = ${BUILTIN_SQLFLUFF_RULES}
dialect = ${dialect}
max_line_length = 80
indent_unit = space
tab_space_size = 4
[sqlfluff:rules:aliasing.length]
max_alias_length = 30
`;
}
interface StaticRuleEntry {
id: string;
description?: string;
descriptionZh?: string;
descriptionJa?: string;
}
export function getRuleDescription(linter: string, ruleId: string, lang: Language): string | undefined {
const section = staticRules.rules[linter as keyof typeof staticRules.rules] as StaticRuleEntry[] | undefined;
if (!section) { return undefined; }
const entry = section.find(r => r.id === `${linter}/${ruleId}`);
if (!entry) { return undefined; }
if (lang === 'zh-CN') { return entry.descriptionZh ?? entry.description; }
if (lang === 'ja') { return entry.descriptionJa ?? entry.description; }
return entry.description;
}
const ESLINT_RULES_URL = 'https://eslint.org/docs/latest/rules/';
const STYLELINT_RULES_URL = 'https://stylelint.io/user-guide/rules/';
const SQLFLUFF_RULES_URL = 'https://docs.sqlfluff.com/en/stable/reference/rules.html';
interface HeaderLabels {
projectConfig: (name: string) => string;
howToAdd: string;
addExample: string;
addExampleStyle: string;
allRules: string;
dialect: string;
enableAll: string;
ruleList: string;
excludePrefix: string;
}
const HEADER: Record<Language, HeaderLabels> = {
'zh-CN': {
projectConfig: name => `项目配置 — 与插件内置 ${name} 规则一致(由插件生成)`,
howToAdd: '如何添加规则:在 rules 中新增一行',
addExample: "如 'rule-name': 'warn' 或 'rule-name': ['error', 'always']",
addExampleStyle: "如 'indentation': 2 或 'rule-name': true",
allRules: '全部可用规则:',
dialect: '数据库方言:postgres / mysql / bigquery / snowflake 等',
enableAll: '想启用全部规则时改为 → rules = all',
ruleList: '内置精选规则清单:',
excludePrefix: '排除:',
},
en: {
projectConfig: name => `Project config — matches the extension's built-in ${name} rules (generated by the extension)`,
howToAdd: 'To add a rule: add a line in rules',
addExample: "e.g. 'rule-name': 'warn' or 'rule-name': ['error', 'always']",
addExampleStyle: "e.g. 'indentation': 2 or 'rule-name': true",
allRules: 'All available rules:',
dialect: 'Database dialect: postgres / mysql / bigquery / snowflake etc.',
enableAll: 'Change to rules = all to enable all rules',
ruleList: 'Built-in curated rule list:',
excludePrefix: 'Excluded: ',
},
ja: {
projectConfig: name => `プロジェクト設定 — 拡張機能の組み込み${name}ルールと一致(拡張機能が生成)`,
howToAdd: 'ルールを追加するには: rulesに1行追加します',
addExample: "例: 'rule-name': 'warn' または 'rule-name': ['error', 'always']",
addExampleStyle: "例: 'indentation': 2 または 'rule-name': true",
allRules: '利用可能な全ルール: ',
dialect: 'データベース方言: postgres / mysql / bigquery / snowflake など',
enableAll: '全ルールを有効にする場合は rules = all に変更',
ruleList: '組み込みの精選ルール一覧:',
excludePrefix: '除外:',
},
};
const TS_NOTE: Record<Language, string[]> = {
'zh-CN': [
'注意:本文件仅包含 JS 内置规则;TS 项目如需 TS 专项规则,',
' 请安装 typescript-eslint 后自行追加,例如:',
" const ts = require('typescript-eslint');",
' module.exports = [ ...本文件配置, ...ts.configs.recommended ];',
],
en: [
'Note: this file only contains JS built-in rules. For TypeScript-specific rules,',
' install typescript-eslint and extend, e.g.:',
" const ts = require('typescript-eslint');",
' module.exports = [ ...this config, ...ts.configs.recommended ];',
],
ja: [
'注: このファイルはJS組み込みルールのみです。TS専用ルールが必要な場合は、',
' typescript-eslintをインストールして追記してください。例:',
" const ts = require('typescript-eslint');",
' module.exports = [ ...本設定, ...ts.configs.recommended ];',
],
};
const PMD_CATEGORY_COMMENTS: Record<string, Record<Language, string>> = {
bestpractices: {
'zh-CN': '最佳实践类(避免空 catch、关闭流等)',
en: 'Best practices (avoid empty catch, close streams, etc.)',
ja: 'ベストプラクティス(空のcatch回避、ストリームクローズ等)',
},
codestyle: {
'zh-CN': '代码风格类(命名规范、花括号位置等)',
en: 'Code style (naming conventions, brace placement, etc.)',
ja: 'コードスタイル(命名規則、ブレース位置等)',
},
design: {
'zh-CN': '设计类(过度耦合、复杂度、设计缺陷等)',
en: 'Design (over-coupling, complexity, design flaws, etc.)',
ja: '設計(過度な結合、複雑度、設計上の欠陥等)',
},
errorprone: {
'zh-CN': '易错类(空 catch、错误处理遗漏等)',
en: 'Error-prone (empty catch, missed error handling, etc.)',
ja: 'エラーを起こしやすい(空のcatch、エラー処理の見落とし等)',
},
multithreading: {
'zh-CN': '多线程类(线程使用、并发问题等)',
en: 'Multithreading (thread usage, concurrency issues, etc.)',
ja: 'マルチスレッド(スレッド使用、並行性の問題等)',
},
performance: {
'zh-CN': '性能类(重复对象创建、低效操作等)',
en: 'Performance (repeated object creation, inefficient operations, etc.)',
ja: 'パフォーマンス(オブジェクトの再生成、非効率な操作等)',
},
security: {
'zh-CN': '安全类(不安全的编码实践等)',
en: 'Security (unsafe coding practices, etc.)',
ja: 'セキュリティ(安全でないコーディング慣行等)',
},
};
function extractPmdVersion(): string {
const v = staticRules.linterVersion.pmd as string | undefined;
const m = v?.match(/(\d+\.\d+\.\d+)/);
return m ? m[1] : 'latest';
}
function resolvePmdRulesetPath(): string {
const candidates: string[] = [];
try {
const ext = vscode.extensions.getExtension?.('vscode-code-reviewer');
if (ext?.extensionPath) {
candidates.push(path.join(ext.extensionPath, 'jars', 'pmd', 'pmd-java-ruleset.xml'));
}
} catch { /* ignore */ }
let dir = __dirname;
for (let i = 0; i < 5; i++) {
candidates.push(path.join(dir, 'jars', 'pmd', 'pmd-java-ruleset.xml'));
dir = path.dirname(dir);
}
const found = candidates.find(p => fs.existsSync(p));
if (found) { return found; }
throw new Error(`PMD bundled ruleset not found: ${candidates.join(', ')}`);
}
export function buildEslintProjectConfigText(lang: Language): string {
const l = HEADER[lang];
const jsRules = js.configs.recommended.rules as Record<string, unknown>;
const rules: Array<[string, unknown]> = [...Object.entries(jsRules), ...Object.entries(eslintExtraRules)];
const body = rules.map(([id, sev]) => {
const desc = getRuleDescription('eslint', id, lang);
return ` '${id}': ${JSON.stringify(sev)},${desc ? ` // ${desc}` : ''}`;
});
const lines = [
'/* global module */',
'// ============================================================',
`// ${l.projectConfig('ESLint')}`,
`// ${l.howToAdd}`,
`// ${l.addExample}`,
`// ${l.allRules}${ESLINT_RULES_URL}`,
...TS_NOTE[lang].map(n => `// ${n}`),
'// ============================================================',
'module.exports = [',
' {',
' rules: {',
...body,
' },',
' },',
'];',
'',
];
return lines.join('\n');
}
export async function buildStylelintProjectConfigText(lang: Language): Promise<string> {
const l = HEADER[lang];
const rec = (await import('stylelint-config-recommended')).default;
const rules: Record<string, unknown> = { ...rec.rules, ...stylelintExtraRules };
const body = Object.entries(rules).map(([id, val]) => {
const desc = getRuleDescription('stylelint', id, lang);
return ` '${id}': ${JSON.stringify(val)},${desc ? ` // ${desc}` : ''}`;
});
const lines = [
'/* global module */',
'// ============================================================',
`// ${l.projectConfig('Stylelint')}`,
`// ${l.howToAdd}`,
`// ${l.addExampleStyle}`,
`// ${l.allRules}${STYLELINT_RULES_URL}`,
'// ============================================================',
'module.exports = {',
' rules: {',
...body,
' },',
'};',
'',
];
return lines.join('\n');
}
export function buildSqlfluffProjectConfigText(dialect: string, lang: Language): string {
const l = HEADER[lang];
const ruleIds = BUILTIN_SQLFLUFF_RULES.split(',');
const ruleComments = ruleIds.map(id => {
const desc = getRuleDescription('sqlfluff', id, lang);
return `# ${id} ${desc ?? ''}`;
});
const lines = [
'[sqlfluff]',
`# ${l.dialect}`,
`dialect = ${dialect}`,
`# ${l.enableAll}`,
`# ${l.ruleList}`,
...ruleComments,
`# ${l.allRules}${SQLFLUFF_RULES_URL}`,
`rules = ${BUILTIN_SQLFLUFF_RULES}`,
'max_line_length = 80',
'indent_unit = space',
'tab_space_size = 4',
'',
'[sqlfluff:rules:aliasing.length]',
'max_alias_length = 30',
'',
];
return lines.join('\n');
}
export async function buildPmdProjectRulesetText(lang: Language): Promise<string> {
const l = HEADER[lang];
const rulesetPath = resolvePmdRulesetPath();
const content = await fs.promises.readFile(rulesetPath, 'utf-8');
const allRulesUrl = `https://docs.pmd-code.org/pmd-doc-${extractPmdVersion()}/pmd_rules_java.html`;
const out: string[] = [];
for (const line of content.split('\n')) {
if (/<description>/.test(line)) {
out.push(line);
out.push(` <!-- ${l.allRules}${allRulesUrl} -->`);
continue;
}
const ruleRefMatch = line.match(/<rule ref="category\/java\/(\w+)\.xml"/);
if (ruleRefMatch) {
const cat = ruleRefMatch[1];
const comment = PMD_CATEGORY_COMMENTS[cat]?.[lang] ?? cat;
out.push(` <!-- ${comment} -->`);
out.push(line);
continue;
}
const excludeMatch = line.match(/<exclude name="([A-Za-z0-9_]+)"\/>/);
if (excludeMatch) {
const name = excludeMatch[1];
const desc = getRuleDescription('pmd', name, lang);
if (desc) {
out.push(` <!-- ${l.excludePrefix}${desc} -->`);
}
out.push(line);
continue;
}
out.push(line);
}
return out.join('\n');
}