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 } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const DATA = join(__dirname, '..', '..', 'data'); 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 TS_FILES = ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts']; const JS_FILES = ['**/*.js', '**/*.jsx', '**/*.mjs', '**/*.cjs']; function timing(ms) { return `${ms.toFixed(1)}ms`; } async function measure(name, fn) { const start = process.hrtime.bigint(); const result = await fn(); const end = process.hrtime.bigint(); const ms = Number(end - start) / 1e6; return { name, ms, result }; } async function main() { if (!existsSync(OUT_DIR)) mkdirSync(OUT_DIR, { recursive: true }); const rows = []; const summary = { tool: 'code-reviewer', measureDate: new Date().toISOString() }; const eslintConfig = [ 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 eslintDemo = join(DATA, 'demo-eslint', 'src'); const eslintFiles = readdirSync(eslintDemo).filter(f => /\.(js|ts|mjs|cjs)$/.test(f)); if (eslintFiles.length > 0) { const engine = new ESLint({ cwd: eslintDemo, overrideConfigFile: true, overrideConfig: eslintConfig }); const t = measure('eslint static analysis (demo-eslint/src)', async () => { const results = []; for (const f of eslintFiles) { const text = readFileSync(join(eslintDemo, f), 'utf8'); const [res] = await engine.lintText(text, { filePath: join(eslintDemo, f) }); results.push({ file: f, messages: res.messages.length }); } return results; }); const { ms, result } = await t; rows.push({ target: 'demo-eslint', files: eslintFiles.length, ms, diagnostics: result.reduce((s, r) => s + r.messages, 0) }); } const stylelintDemo = join(DATA, 'demo-stylelint', 'src'); const stylelintFiles = readdirSync(stylelintDemo).filter(f => /\.css$/.test(f)); if (stylelintFiles.length > 0) { const styleConfig = { ...recommendedConfig, rules: { ...recommendedConfig.rules, ...stylelintExtraRules }, }; const t = measure('stylelint static analysis (demo-stylelint/src)', async () => { const results = []; for (const f of stylelintFiles) { const code = readFileSync(join(stylelintDemo, f), 'utf8'); const res = await stylelint.lint({ code, codeFilename: join(stylelintDemo, f), config: styleConfig, cwd: stylelintDemo }); const warnings = res.results.flatMap(r => r.warnings).length; results.push({ file: f, warnings }); } return results; }); const { ms, result } = await t; rows.push({ target: 'demo-stylelint', files: stylelintFiles.length, ms, diagnostics: result.reduce((s, r) => s + r.warnings, 0) }); } const totalMs = rows.reduce((s, r) => s + r.ms, 0); summary.rows = rows; summary.totalMs = totalMs; writeFileSync(join(OUT_DIR, 'measure-results.json'), JSON.stringify(summary, null, 2)); console.log('=== Code Purifier 静态分析耗时测量 ==='); for (const r of rows) { console.log(`${r.target}: ${r.files} 文件, ${r.diagnostics} 条诊断, ${timing(r.ms)}`); } console.log(`合计: ${timing(totalMs)}`); console.log(`结果已写入 tests/measure/results/measure-results.json`); } main().catch(err => { console.error(err); process.exit(1); });