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 = { '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 = { '@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 = { '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 = { '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 = { '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> = { 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; 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 { const l = HEADER[lang]; const rec = (await import('stylelint-config-recommended')).default; const rules: Record = { ...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 { 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 (//.test(line)) { out.push(line); out.push(` `); continue; } const ruleRefMatch = line.match(/`); out.push(line); continue; } const excludeMatch = line.match(//); if (excludeMatch) { const name = excludeMatch[1]; const desc = getRuleDescription('pmd', name, lang); if (desc) { out.push(` `); } out.push(line); continue; } out.push(line); } return out.join('\n'); }