Files
范智鹏 3280836124 data: A3 提效对比实验数据(23 样例 + 人工基线 + 插件实测,整体提速约 266 倍)
- data/efficiency-samples/ 23 个多语言样例 + data/manual-review-tool.html 人工审核工具
- tests/measure/measure-plugin-samples.mjs 四语言插件侧测量脚本
- tests/measure/compare-a3.mjs 聚合对比 + results(插件实测/人工基线/comparison)
- performance-comparison.md 填实基线对比与结论;README 效果总结由占位改为真实数据
2026-08-27 22:54:07 +08:00

266 lines
10 KiB
JavaScript

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);
});