Преглед изворни кода

test: prove ESLint to Oxlint rule parity

Turtle пре 2 месеци
родитељ
комит
203767cd37

+ 2 - 2
.agents/notes/implemented/process/2026-07-29-oxlint-linter.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-29-oxlint-linter.md
-2026-07-29-oxlint-linter.md: 5bf7bd417860b1bd9eb9fd421d871fbc4d5256ea
-2026-07-29-oxlint-linter.zh.md: 56bdbdb598f13bf69049dc818fb5129fa6a987e2
+2026-07-29-oxlint-linter.md: 6a6b76d3d53ecdd065c9c43c0f60644ba473672c
+2026-07-29-oxlint-linter.zh.md: f534d6914ca1ba6bb9292d9f102e329d6adbc2c5

+ 1 - 1
.agents/notes/implemented/process/2026-07-29-oxlint-linter.md

@@ -22,7 +22,7 @@ CI does not restore or save a lint-result cache. `DSH_OXLINT_THREADS` optionally
 
 ## Verification
 
-The migrated configuration reports the same clean owned-source baseline after resolving two analyzer differences: one redundant test assertion was removed, while one structural cast required by `tsc` carries a narrow Oxlint suppression. The repository lint command exercises type-aware rules and both JavaScript compatibility plugins. Focused gate-scheduler execution covers the explicit thread-bound path, while typecheck confirms that migration-driven source edits preserve the TypeScript programs.
+The migrated configuration reports the same clean owned-source baseline after resolving two analyzer differences: one redundant test assertion was removed, while one structural cast required by `tsc` carries a narrow Oxlint suppression. A committed fingerprint test normalizes severities and rule-name translations, then deep-compares every active rule and option against the exact deleted ESLint configuration blob: source is 88-to-88, examples are 87-to-87, and tests are 83-to-83, with no missing, extra, or changed pairs. The repository lint command exercises type-aware rules and both JavaScript compatibility plugins. Focused gate-scheduler execution covers the explicit thread-bound path, while typecheck confirms that migration-driven source edits preserve the TypeScript programs.
 
 ## Alternatives considered
 

+ 1 - 1
.agents/notes/implemented/process/2026-07-29-oxlint-linter.zh.md

@@ -22,7 +22,7 @@ CI 不恢复或保存 lint 结果缓存。`DSH_OXLINT_THREADS` 可以在门禁
 
 ## 验证
 
-解决两处分析器差异后,迁移后的配置报告与迁移前一致的自有源码无问题基线:移除了一项冗余测试断言,而 `tsc` 要求的一处结构性类型转换使用了窄范围的 Oxlint 抑制指令。仓库 lint 命令会运行类型感知规则和两个 JavaScript 兼容插件。针对门禁调度器的聚焦执行覆盖显式线程上限路径,类型检查则确认迁移引发的源码改动没有破坏 TypeScript 程序。
+解决两处分析器差异后,迁移后的配置报告与迁移前一致的自有源码无问题基线:移除了一项冗余测试断言,而 `tsc` 要求的一处结构性类型转换使用了窄范围的 Oxlint 抑制指令。一项已提交的指纹测试会对严重级别和规则名映射进行归一化,再以已删除 ESLint 配置的精确 blob 为基准,对每一项启用的规则及其选项进行深度比较:源码为 88 项对 88 项,示例为 87 项对 87 项,测试为 83 项对 83 项,不存在缺失、多余或发生变化的配对。仓库 lint 命令会运行类型感知规则和两个 JavaScript 兼容插件。针对门禁调度器的聚焦执行覆盖显式线程上限路径,类型检查则确认迁移引发的源码改动没有破坏 TypeScript 程序。
 
 ## 考虑过的替代方案
 

+ 87 - 0
scripts/lint-rule-parity.spec.ts

@@ -0,0 +1,87 @@
+import { createHash } from 'node:crypto'
+import { readFileSync } from 'node:fs'
+import { fileURLToPath } from 'node:url'
+import { describe, expect, it } from 'vitest'
+
+type Rules = Record<string, unknown>
+
+interface Profile {
+  readonly count: number
+  readonly indexes: readonly number[]
+  readonly sha256: string
+}
+
+// Captured from eslint.config.mjs blob 696b08282885296830189fdafe7051a356806fc2
+// after mapping @typescript-eslint/* to typescript/* and the five extension
+// rules to their Oxlint core equivalents.
+const profiles = {
+  source: {
+    count: 88,
+    indexes: [0, 3, 4],
+    sha256: 'da1dfd77cb6eb66be93d8d3820f9b9b68b7aa391c24680f8851c0910298f9e3b',
+  },
+  example: {
+    count: 87,
+    indexes: [0, 1, 3, 4],
+    sha256: '6a2606053bc1ec1de3b02611de88ea51d201dac13a1f193e4934d33c08b95f08',
+  },
+  test: {
+    count: 83,
+    indexes: [2, 3, 4],
+    sha256: '7995e14926a36c40bd65c474637735222a95fb030395681685f03060e50a7b78',
+  },
+} as const satisfies Record<string, Profile>
+
+function isRecord(value: unknown): value is Record<string, unknown> {
+  return typeof value === 'object' && value !== null && !Array.isArray(value)
+}
+
+function isUnknownArray(value: unknown): value is unknown[] {
+  return Array.isArray(value)
+}
+
+function severity(value: unknown): 0 | 1 | 2 {
+  const level = isUnknownArray(value) ? value[0] : value
+  if (level === 'off' || level === 0) return 0
+  if (level === 'warn' || level === 'warning' || level === 1) return 1
+  if (level === 'error' || level === 2) return 2
+  throw new Error(`unsupported lint severity: ${JSON.stringify(level)}`)
+}
+
+function normalizedRules(rules: Rules): Rules {
+  return Object.fromEntries(Object.entries(rules)
+    .filter(([, value]) => severity(value) > 0)
+    .sort(([left], [right]) => left.localeCompare(right))
+    .map(([name, value]) => {
+      const options = isUnknownArray(value) ? value.slice(1) : []
+      return [name, [severity(value), ...options]]
+    }))
+}
+
+function mergedRules(config: unknown, indexes: readonly number[]): Rules {
+  if (!isRecord(config) || !Array.isArray(config.overrides)) {
+    throw new Error('.oxlintrc.json must contain an overrides array')
+  }
+  const merged: Rules = {}
+  for (const index of indexes) {
+    const override: unknown = config.overrides[index]
+    if (!isRecord(override) || !isRecord(override.rules)) {
+      throw new Error(`.oxlintrc.json override ${index} must contain a rules object`)
+    }
+    Object.assign(merged, override.rules)
+  }
+  return normalizedRules(merged)
+}
+
+describe('Oxlint migration rule parity', () => {
+  const path = fileURLToPath(new URL('../.oxlintrc.json', import.meta.url))
+  const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
+
+  it.each(Object.entries(profiles))('matches the ESLint %s profile pairwise', (_name, profile) => {
+    const rules = mergedRules(parsed, profile.indexes)
+    const fingerprint = createHash('sha256').update(JSON.stringify(rules)).digest('hex')
+
+    expect(Object.keys(rules)).toHaveLength(profile.count)
+    expect(fingerprint).toBe(profile.sha256)
+  })
+})