Files
范智鹏 c92249a044 docs: 归档 demo 工程样例与插件实测报告
- 新增三个 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 种规则全覆盖)
2026-08-25 22:00:19 +08:00

167 lines
7.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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];
// 候选 tokenkebab-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');