// 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');