378 lines
14 KiB
TypeScript
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');
|
|
}
|