- 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 效果总结由占位改为真实数据
266 lines
10 KiB
JavaScript
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);
|
|
});
|