import { ESLint } from 'eslint'; import js from '@eslint/js'; import ts from 'typescript-eslint'; import stylelint from 'stylelint'; import recommendedConfig from 'stylelint-config-recommended'; import { readFileSync, readdirSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { spawnSync } from 'child_process'; import os from 'os'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = join(__dirname, '..', '..'); const SAMPLES = join(ROOT, 'data', 'efficiency-samples'); const PMD_DIR = join(ROOT, 'jars', 'pmd'); const OUT_DIR = join(__dirname, 'results'); // ===== 以下与 src/rules/builtin-rules.ts 逐字一致 ===== const eslintExtraRules = { '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 eslintExtraTsRules = { '@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 stylelintExtraRules = { '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', }; 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'; const TS_FILES = ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts']; const JS_FILES = ['**/*.js', '**/*.jsx', '**/*.mjs', '**/*.cjs']; function buildSqlfluffConfig(dialect) { 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 `; } function timing(ms) { return `${ms.toFixed(1)}ms`; } async function measure(fn) { const start = process.hrtime.bigint(); const result = await fn(); const end = process.hrtime.bigint(); const ms = Number(end - start) / 1e6; return { ms, result }; } function runSync(cmd, args, opts = {}) { const res = spawnSync(cmd, args, { cwd: ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, ...opts }); return res; } function uniq(arr) { return [...new Set(arr)].sort(); } async function runEslint(files) { const config = [ js.configs.recommended, { files: JS_FILES, languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } }, ...ts.configs.recommended.map(cfg => ({ ...cfg, files: cfg.files ?? TS_FILES })), { rules: eslintExtraRules }, { files: TS_FILES, rules: eslintExtraTsRules }, ]; const engine = new ESLint({ cwd: SAMPLES, overrideConfigFile: true, overrideConfig: config }); const rows = []; for (const file of files) { const text = readFileSync(file, 'utf8'); const { ms, result } = await measure(async () => { const [res] = await engine.lintText(text, { filePath: file }); return res.messages.map(m => ({ rule: m.ruleId, severity: m.severity })); }); rows.push({ file: file.replace(/\\/g, '/').split('/').pop(), ms, diagnostics: result.length, rules: uniq(result.map(r => r.rule).filter(Boolean)) }); } return rows; } async function runStylelint(files) { const config = { ...recommendedConfig, rules: { ...recommendedConfig.rules, ...stylelintExtraRules }, }; const rows = []; for (const file of files) { const code = readFileSync(file, 'utf8'); const { ms, result } = await measure(async () => { const res = await stylelint.lint({ code, codeFilename: file, config, cwd: SAMPLES }); return res.results.flatMap(r => r.warnings).map(w => ({ rule: w.rule, severity: w.severity })); }); rows.push({ file: file.replace(/\\/g, '/').split('/').pop(), ms, diagnostics: result.length, rules: uniq(result.map(r => r.rule).filter(Boolean)) }); } return rows; } async function runPmd(files) { const classpath = `${join(PMD_DIR, 'lib', '*')};${PMD_DIR}`; const ruleset = join(PMD_DIR, 'pmd-java-ruleset.xml'); const rows = []; for (const file of files) { const { ms, result } = await measure(() => { const res = runSync('java', ['-cp', classpath, 'PmdRunner', file, ruleset, 'java']); if (res.status !== 0 && res.status !== 4) { throw new Error(`PMD failed on ${file}: ${res.stderr}`); } let data = []; try { data = JSON.parse(res.stdout); } catch { /* ignore */ } const violations = []; for (const f of data.files ?? []) { for (const v of f.violations ?? []) { violations.push({ rule: `pmd:${v.rule}`, severity: v.priority <= 2 ? 2 : v.priority === 3 ? 1 : 0 }); } } return violations; }); rows.push({ file: file.replace(/\\/g, '/').split('/').pop(), ms, diagnostics: result.length, rules: uniq(result.map(r => r.rule).filter(Boolean)) }); } return rows; } async function runSqlfluff(files) { const tmpCfg = join(os.tmpdir(), `vscode-code-reviewer-a3-sqlfluff-${Date.now()}.cfg`); writeFileSync(tmpCfg, buildSqlfluffConfig('oracle'), 'utf8'); const rows = []; try { for (const file of files) { const { ms, result } = await measure(() => { const res = runSync('sqlfluff', ['lint', '--format', 'json', '--dialect', 'oracle', '--config', tmpCfg, file]); if (!res.stdout) { throw new Error(`sqlfluff failed on ${file}: ${res.stderr}`); } let data = []; try { data = JSON.parse(res.stdout); } catch { /* ignore */ } const violations = []; for (const r of data) { for (const v of r.violations ?? []) { violations.push({ rule: `sqlfluff:${v.code}`, severity: v.code === 'PRS' ? 2 : 1 }); } } return violations; }); rows.push({ file: file.replace(/\\/g, '/').split('/').pop(), ms, diagnostics: result.length, rules: uniq(result.map(r => r.rule).filter(Boolean)) }); } } finally { try { unlinkSync(tmpCfg); } catch { /* ignore */ } } return rows; } function listFiles(lang, extRe) { const dir = join(SAMPLES, lang); if (!existsSync(dir)) { return []; } return readdirSync(dir).filter(f => extRe.test(f)).sort().map(f => join(dir, f)); } async function main() { if (!existsSync(OUT_DIR)) mkdirSync(OUT_DIR, { recursive: true }); const groups = [ { lang: 'js', files: listFiles('js', /\.js$/) }, { lang: 'css', files: listFiles('css', /\.css$/) }, { lang: 'java', files: listFiles('java', /\.java$/) }, { lang: 'sql', files: listFiles('sql', /\.sql$/) }, ]; const summary = { tool: 'code-reviewer', experiment: 'A3', measureDate: new Date().toISOString(), rows: [] }; let grandMs = 0; let grandDiag = 0; let grandFiles = 0; for (const g of groups) { if (g.files.length === 0) { continue; } let rows; if (g.lang === 'js') { rows = await runEslint(g.files); } else if (g.lang === 'css') { rows = await runStylelint(g.files); } else if (g.lang === 'java') { rows = await runPmd(g.files); } else if (g.lang === 'sql') { rows = await runSqlfluff(g.files); } const totalMs = rows.reduce((s, r) => s + r.ms, 0); const totalDiag = rows.reduce((s, r) => s + r.diagnostics, 0); const allRules = uniq(rows.flatMap(r => r.rules)); grandMs += totalMs; grandDiag += totalDiag; grandFiles += rows.length; summary.rows.push({ lang: g.lang, files: rows.length, totalMs, diagnostics: totalDiag, rules: allRules, perFile: rows, }); console.log(`--- ${g.lang} (${rows.length} 文件) ---`); for (const r of rows) { console.log(` ${r.file}: ${r.diagnostics} 条, ${timing(r.ms)}, 规则=${r.rules.length} ${r.rules.join(',')}`); } console.log(` 小计: ${totalDiag} 条, ${timing(totalMs)}`); } summary.totalFiles = grandFiles; summary.totalDiagnostics = grandDiag; summary.totalMs = grandMs; const outFile = join(OUT_DIR, 'a3-plugin-results.json'); writeFileSync(outFile, JSON.stringify(summary, null, 2)); console.log(`\n=== 合计: ${grandFiles} 文件, ${grandDiag} 条诊断, ${timing(grandMs)} ===`); console.log(`结果已写入 ${outFile}`); } main().catch(err => { console.error(err); process.exit(1); });