lint-rule-fingerprint.spec.ts 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import { createHash } from 'node:crypto'
  2. import { readFileSync } from 'node:fs'
  3. import { fileURLToPath } from 'node:url'
  4. import { flattenDiagnosticMessageText, parseConfigFileTextToJson } from 'typescript'
  5. import { describe, expect, it } from 'vitest'
  6. type Rules = Record<string, unknown>
  7. interface Profile {
  8. readonly count: number
  9. readonly indexes: readonly number[]
  10. readonly sha256: string
  11. }
  12. // A one-time audit against eslint.config.mjs blob 696b08282885296830189fdafe7051a356806fc2
  13. // mapped @typescript-eslint/* to typescript/* and four extension rules to their
  14. // Oxlint core equivalents. These fingerprints pin the resulting repository
  15. // contract; they do not re-evaluate that deleted baseline or track its preset.
  16. const profiles = {
  17. source: {
  18. count: 88,
  19. indexes: [0, 1, 4, 5],
  20. sha256: 'da1dfd77cb6eb66be93d8d3820f9b9b68b7aa391c24680f8851c0910298f9e3b',
  21. },
  22. example: {
  23. count: 87,
  24. indexes: [0, 1, 2, 4, 5],
  25. sha256: '6a2606053bc1ec1de3b02611de88ea51d201dac13a1f193e4934d33c08b95f08',
  26. },
  27. test: {
  28. count: 83,
  29. indexes: [0, 3, 4, 5],
  30. sha256: '7995e14926a36c40bd65c474637735222a95fb030395681685f03060e50a7b78',
  31. },
  32. } as const satisfies Record<string, Profile>
  33. function isRecord(value: unknown): value is Record<string, unknown> {
  34. return typeof value === 'object' && value !== null && !Array.isArray(value)
  35. }
  36. function isUnknownArray(value: unknown): value is unknown[] {
  37. return Array.isArray(value)
  38. }
  39. function severity(value: unknown): 0 | 1 | 2 {
  40. const level = isUnknownArray(value) ? value[0] : value
  41. if (level === 'off' || level === 0) return 0
  42. if (level === 'warn' || level === 'warning' || level === 1) return 1
  43. if (level === 'error' || level === 2) return 2
  44. throw new Error(`unsupported lint severity: ${JSON.stringify(level)}`)
  45. }
  46. function normalizedRules(rules: Rules): Rules {
  47. return Object.fromEntries(Object.entries(rules)
  48. .filter(([, value]) => severity(value) > 0)
  49. .sort(([left], [right]) => left.localeCompare(right))
  50. .map(([name, value]) => {
  51. const options = isUnknownArray(value) ? value.slice(1) : []
  52. return [name, [severity(value), ...options]]
  53. }))
  54. }
  55. function mergedRules(overrides: readonly unknown[], indexes: readonly number[]): Rules {
  56. const merged: Rules = {}
  57. for (const index of indexes) {
  58. const override = overrides[index]
  59. if (!isRecord(override) || !isRecord(override.rules)) {
  60. throw new Error(`.oxlintrc.json override ${index} must contain a rules object`)
  61. }
  62. Object.assign(merged, override.rules)
  63. }
  64. return normalizedRules(merged)
  65. }
  66. describe('Oxlint repository rule fingerprint', () => {
  67. const path = fileURLToPath(new URL('../.oxlintrc.json', import.meta.url))
  68. const result = parseConfigFileTextToJson(path, readFileSync(path, 'utf8'))
  69. if (result.error !== undefined) {
  70. throw new Error(flattenDiagnosticMessageText(result.error.messageText, '\n'))
  71. }
  72. const parsed: unknown = result.config
  73. if (!isRecord(parsed) || !Array.isArray(parsed.overrides)) {
  74. throw new Error('.oxlintrc.json must contain an overrides array')
  75. }
  76. const overrides: readonly unknown[] = parsed.overrides
  77. it('pins the complete override shape', () => {
  78. expect(overrides).toHaveLength(6)
  79. })
  80. it.each(Object.entries(profiles))('pins the %s rule profile', (_name, profile) => {
  81. const rules = mergedRules(overrides, profile.indexes)
  82. const fingerprint = createHash('sha256').update(JSON.stringify(rules)).digest('hex')
  83. expect(Object.keys(rules)).toHaveLength(profile.count)
  84. expect(fingerprint).toBe(profile.sha256)
  85. })
  86. })