|
|
@@ -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)
|
|
|
+ })
|
|
|
+})
|