- 新增三个 demo 工程(demo-eslint / demo-sqlfluff / demo-stylelint):各含 src/ 样例代码与覆盖率映射(run-coverage.mjs、coverage_map.json、stylelint_line_map.json) - 归档各 demo 的插件实测导出报告(reports/): - demo-eslint:common.js / esm-demo.js / legacysyntax.cjs / typescript.ts 审查报告 + 覆盖率报告(292/292 条零偏差,126 种规则全覆盖) - demo-sqlfluff:11 个 SQL 样例审查报告 + 覆盖率报告(113/113 条零偏差,57 种规则全覆盖,含 JJ01 行号缺陷修复后的复测验证) - demo-stylelint:common.css / empty-source.css 审查报告 + 覆盖率报告(145/145 条零偏差,68 种规则全覆盖)
167 lines
7.8 KiB
JavaScript
167 lines
7.8 KiB
JavaScript
// ESLint 覆盖率测试 v3:复刻内置配置(与 ESLintAdapter.getDefaultConfig 逐字一致)
|
||
// + legacysyntax.js script 模式(对应 demo 工程配置 eslint.config.mjs)
|
||
// + 以 calculateConfigForFile 计算各文件类型实际生效(severity != off)的规则集
|
||
import { ESLint } from 'eslint';
|
||
import js from '@eslint/js';
|
||
import ts from 'typescript-eslint';
|
||
import { readFileSync, readdirSync, writeFileSync } from 'fs';
|
||
import { join, dirname } from 'path';
|
||
import { fileURLToPath } from 'url';
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
const SRC = join(__dirname, 'src');
|
||
|
||
// ===== 以下与 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 TS_FILES = ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'];
|
||
const JS_FILES = ['**/*.js', '**/*.jsx', '**/*.mjs', '**/*.cjs'];
|
||
|
||
const tsConfigs = ts.configs.recommended.map(cfg => ({
|
||
...cfg,
|
||
files: (cfg).files ?? TS_FILES,
|
||
}));
|
||
|
||
// 复刻内置默认配置(与 ESLintAdapter.getDefaultConfig 一致)+ legacysyntax script 模式
|
||
const defaultConfig = [
|
||
js.configs.recommended,
|
||
{ files: JS_FILES, languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } },
|
||
...tsConfigs,
|
||
{ rules: eslintExtraRules },
|
||
{ files: TS_FILES, rules: eslintExtraTsRules },
|
||
{ files: ['**/legacysyntax.js'], languageOptions: { sourceType: 'script' } },
|
||
];
|
||
|
||
const files = readdirSync(SRC).filter(f => /\.(js|ts|mjs|cjs)$/.test(f));
|
||
const engine = new ESLint({ cwd: SRC, overrideConfigFile: true, overrideConfig: defaultConfig });
|
||
|
||
// ===== 实际生效规则(severity != off)=====
|
||
async function effectiveRules(sampleFile) {
|
||
const cfg = await engine.calculateConfigForFile(join(SRC, sampleFile));
|
||
const out = new Set();
|
||
for (const [name, val] of Object.entries(cfg.rules ?? {})) {
|
||
const sev = Array.isArray(val) ? val[0] : val;
|
||
if (sev !== 'off' && sev !== 0) out.add(name);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
const jsEnabled = await effectiveRules('common.js');
|
||
const tsEnabled = await effectiveRules('typescript.ts');
|
||
const builtinAll = new Set([...jsEnabled, ...tsEnabled]);
|
||
|
||
// ===== 逐文件 lint =====
|
||
const actualByFile = {};
|
||
const parseErrors = {};
|
||
let violationTotal = 0;
|
||
for (const f of files) {
|
||
const text = readFileSync(join(SRC, f), 'utf8');
|
||
const results = await engine.lintText(text, { filePath: join(SRC, f) });
|
||
const rulesHit = new Set();
|
||
for (const r of results) {
|
||
for (const m of r.messages) {
|
||
if (m.fatal) { parseErrors[f] = m.message; continue; }
|
||
violationTotal++;
|
||
if (m.ruleId) rulesHit.add(m.ruleId);
|
||
}
|
||
}
|
||
actualByFile[f] = rulesHit;
|
||
}
|
||
|
||
const actualRaw = new Set();
|
||
for (const s of Object.values(actualByFile)) for (const r of s) actualRaw.add(r);
|
||
|
||
// ===== 期望规则提取(demo 注释标注)=====
|
||
const NOISE = new Set(['eslint', '本文件', '说明', '保留', '合并', '用于', '异步', '比较', '该文件', 'eslint.config.mjs', '顺手在此以', '文件', '演示', '规则',
|
||
// 注释噪音:文件名 / 指令 / 未启用规则名 / 笔误(真实规则为 no-unused-vars)
|
||
'esm-demo', 'ts-ignore', 'no-empty-function', 'no-useless-vars']);
|
||
const expectByFile = {};
|
||
for (const f of files) {
|
||
const text = readFileSync(join(SRC, f), 'utf8');
|
||
const isTs = /\.ts$/.test(f);
|
||
const expects = new Set();
|
||
for (const line of text.split('\n')) {
|
||
const m = line.match(/^\s*\/\/\s*(.+)$/);
|
||
if (!m) continue;
|
||
const body = m[1];
|
||
// 候选 token:kebab-case 规则名 或 @typescript-eslint/ 前缀规则名
|
||
for (const tm of body.matchAll(/(@typescript-eslint\/[a-z0-9-]+|[a-z][a-z0-9]*(-[a-z0-9]+)+)/g)) {
|
||
const name = tm[1];
|
||
if (NOISE.has(name)) continue;
|
||
if (!isTs && name.startsWith('@typescript-eslint/')) continue;
|
||
expects.add(name);
|
||
}
|
||
}
|
||
expectByFile[f] = expects;
|
||
}
|
||
const expectAll = new Set();
|
||
for (const s of Object.values(expectByFile)) for (const r of s) expectAll.add(r);
|
||
|
||
// ===== 统计 =====
|
||
// 名称归一化:注释里通常省略 @typescript-eslint/ 前缀,比较时两边都剥掉前缀
|
||
const normalize = r => r.replace(/^@typescript-eslint\//, '');
|
||
const actualNorm = new Set([...actualRaw].map(normalize));
|
||
const jsHit = [...jsEnabled].filter(r => actualRaw.has(r));
|
||
const tsHit = [...tsEnabled].filter(r => actualRaw.has(r));
|
||
const hitInBuiltin = [...builtinAll].filter(r => actualRaw.has(r));
|
||
const expectHit = [...expectAll].filter(r => actualNorm.has(normalize(r)));
|
||
const expectMissed = [...expectAll].filter(r => !actualNorm.has(normalize(r))).sort();
|
||
|
||
console.log('=== ESLint 覆盖率实测结果(v3,生效口径)===');
|
||
console.log(`测试文件: ${files.join(', ')}`);
|
||
console.log(`解析错误: ${Object.keys(parseErrors).length ? JSON.stringify(parseErrors) : '无'}`);
|
||
console.log(`JS 生效规则: ${jsEnabled.size} 条,命中 ${jsHit.length} = ${(jsHit.length / jsEnabled.size * 100).toFixed(1)}%`);
|
||
console.log(`TS 生效规则: ${tsEnabled.size} 条,命中 ${tsHit.length} = ${(tsHit.length / tsEnabled.size * 100).toFixed(1)}%`);
|
||
console.log(`合并生效规则: ${builtinAll.size} 条,命中 ${hitInBuiltin.length} = ${(hitInBuiltin.length / builtinAll.size * 100).toFixed(1)}%`);
|
||
console.log(`demo 期望规则: ${expectAll.size} 条,命中 ${expectHit.length}`);
|
||
console.log(`违规总数: ${violationTotal}`);
|
||
console.log(`\nJS 未命中 (${jsEnabled.size - jsHit.length}): ${[...jsEnabled].filter(r => !actualRaw.has(r)).sort().join(', ') || '无'}`);
|
||
console.log(`\nTS 未命中 (${tsEnabled.size - tsHit.length}): ${[...tsEnabled].filter(r => !actualRaw.has(r)).sort().join(', ') || '无'}`);
|
||
console.log(`\n期望未达成: ${expectMissed.join(', ') || '无'}`);
|
||
|
||
writeFileSync('/data/user/work/results/eslint_result.json', JSON.stringify({
|
||
tool: 'eslint',
|
||
builtinJsTotal: jsEnabled.size,
|
||
builtinTsTotal: tsEnabled.size,
|
||
builtinAllTotal: builtinAll.size,
|
||
jsHit: jsHit.sort(),
|
||
tsHit: tsHit.sort(),
|
||
jsMissed: [...jsEnabled].filter(r => !actualRaw.has(r)).sort(),
|
||
tsMissed: [...tsEnabled].filter(r => !actualRaw.has(r)).sort(),
|
||
actualHitRaw: [...actualRaw].sort(),
|
||
expectTotal: expectAll.size,
|
||
expectHit: expectHit.sort(),
|
||
expectMissed,
|
||
violationTotal,
|
||
parseErrors,
|
||
}, null, 2));
|
||
console.log('\n结果已写入 /data/user/work/results/eslint_result.json');
|