# enforce-canonical

Regla de oxlint que reescribe las clases de Tailwind CSS a la forma canónica que calcula el propio
Tailwind, como `-m-0` → `m-0`, con autofix.

## De un vistazo

| Autofix | Sugerencias en el editor | Design system              | Opciones                            |
| ------- | ------------------------ | -------------------------- | ----------------------------------- |
| Sí      | Sí                       | Obligatorio (`entryPoint`) | `entryPoint`, `reportNonEquivalent` |

## Qué hace esta regla

Le pregunta al design system cuál es la forma canónica de cada utility en tu código y reescribe las
que no lo son. "Canónico" es lo que devuelve `canonicalizeCandidates()` de `@tailwindcss/node` — la
misma fuente de verdad que usan prettier-plugin-tailwindcss, oxfmt y las herramientas oficiales de
Tailwind. Ejemplos: `-m-0` → `m-0` (no necesitas el negativo para un cero), `start-2` → `inset-s-2`,
`flex-grow-[2]` → `grow-2` (un valor arbitrario cuya forma nombrada emite el mismo CSS),
`text-[var(--color-text)]/90` → `text-(--color-text)/90`. El auto-fix corrige el primer hit, las
sugerencias cubren el resto en el mismo string. Los renombres de v3 (`bg-gradient-to-r` →
`bg-linear-to-r`, `break-words` → `wrap-break-word`) son propiedad de `no-deprecated-classes`, no de
esta regla — ver Interacciones.

Las clases con nombre resuelven contra un `canonicalMap` precomputado en memoria (sub-microsegundo).
Los valores arbitrarios (`p-[2px]`, `bg-(--c)`) y las variantes arbitrarias (`data-[open]:`,
`min-[40rem]:`) pasan por el worker `canonicalize-service` porque necesitan una consulta viva al DS;
los resultados se cachean a nivel proceso por `(entryPoint, rem, class)`. El worker preserva la
posición del `!` (prefix vs suffix vs ninguno).

DS-dependiente — requiere `settings.tailwindcss.entryPoint`. Si el design system no puede cargar, la
regla emite un único diagnóstico fatal `designSystemUnavailable` por archivo en vez de pasar en
silencio.

## Opciones

### `reportNonEquivalent`

`boolean`, default `false`.

Algunas formas canónicas **no son el mismo CSS en tu proyecto**, y la regla deja esas clases como
están. Con esta opción activa, además te avisa de dos tipos de ellas, nunca con autofix:

- **Una variante que tu CSS define distinto del Tailwind de stock.** shadcn/ui, por ejemplo, trae
  `data-disabled:` como una variante propia construida sobre `:where([data-disabled="true"]), …`,
  así que `data-[disabled]:opacity-50` — que el Tailwind de stock reescribe a
  `data-disabled:opacity-50` — selecciona otros elementos aquí. El reporte nombra las dos variantes,
  y cambiar es decisión tuya.
- **Una forma canónica que además fija una variable que leen otras utilidades.** Tailwind reescribe
  `[line-height:var(--x)]` a `leading-(--x)`, pero `leading-(--x)` también fija `--tw-leading`, que
  cada tamaño `text-*` lee para su interlineado, así que junto a uno las dos pueden verse distinto.
  Lo mismo pasa con `[border-style:dashed]` → `border-dashed` (`--tw-border-style`, que lee
  `border-2`), `[transition-duration:300ms]` → `duration-300` (`--tw-duration`, que lee
  `transition`) y las formas `font-*`, `tracking-*`, `ease-*`, `outline-*` y `content-none`. El
  reporte nombra la variable y trae una sugerencia que tu editor puede aplicar.

Una diferencia que también tiene el Tailwind de stock (`has-[[data-slot=x]]:` vs
`has-data-[slot=x]:` emiten selectores distintos en cualquier proyecto) no se reporta, y tampoco un
valor que lee el theme (`p-[2px]` vs `p-0.5`, mira [`prefer-scale-token`](./prefer-scale-token)). En
`apps/v4` de shadcn/ui la opción agrega 23 reportes, todos `data-[disabled]:`, y ninguno del segundo
tipo.

```jsonc
{ "tailwindcss/enforce-canonical": ["warn", { "reportNonEquivalent": true }] }
```

### `entryPoint`

`string`, opcional. Override por regla de `settings.tailwindcss.entryPoint`. Cuando puedas,
configura el entry point en `settings.tailwindcss.entryPoint` para todo el proyecto.

## Ejemplos

### ✗ Incorrecto

```tsx
// El negativo-de-cero es simplemente cero
<div className="-m-0 -mt-0" />
// → <div className="m-0 mt-0" />

// Shorthand de inset lógico → canónico inset-s-* / inset-e-*
<div className="start-2 end-4" />
// → <div className="inset-s-2 inset-e-4" />

// Un valor arbitrario cuya forma nombrada emite el mismo CSS
<div className="flex-grow-[2]" />
// → <div className="grow-2" />

// Variants e important se preservan
<div className="hover:!flex-grow-[2]" />
// → <div className="hover:!grow-2" />

// Modificadores de opacidad y variantes arbitrarias, con o sin otras variantes
<div className="dark:bg-white/[.08] data-[open]:flex min-[40rem]:flex" />
// → <div className="dark:bg-white/8 data-open:flex sm:flex" />

// options: { "reportNonEquivalent": true }
// No es el mismo CSS: leading-(--x) también fija --tw-leading, que leen los tamaños text-*.
// Se reporta con una sugerencia, nunca se arregla solo
<div className="[line-height:var(--x)]" />
// → <div className="leading-(--x)" />
```

### ✓ Correcto

```tsx
<div className="m-0 mt-0" />

<div className="inset-s-2 inset-e-4" />

<div className="grow-2" />

<div className="hover:!grow-2" />

<div className="dark:bg-white/8 data-open:flex sm:flex" />

<div className="leading-(--x)" />
```

## Interacciones con otras reglas

- **`no-unnecessary-arbitrary-value`**: complementaria, no hay doble-fire. Las dos actúan solo
  cuando el valor arbitrario y su reemplazo nombrado emiten CSS **idéntico**; se reparten por forma.
  `no-unnecessary-arbitrary-value` es dueña de los casos donde el arbitrario mapea directo a una
  única utility con nombre (e.g. `h-[auto]` → `h-auto`). Un valor que solo es _numéricamente_ igual
  a un paso de la escala (`p-[2px]` → `p-0.5`, cuyo texto CSS difiere) no es asunto de ninguna de
  las dos — eso es `prefer-scale-token`, solo-reporte.
- **`prefer-theme-tokens`**: el tercer partner del trío arbitrario→nombrado. Atrapa referencias a
  variables CSS como `border-(--border)` → `border-border` donde ni `enforce-canonical` (el CSS
  difiere) ni `no-unnecessary-arbitrary-value` (no hay equivalente bracket compartido) se
  dispararían.
- **`no-deprecated-classes`**: **es la dueña de los renombres de v3, y esta regla los omite.**
  Tailwind también los canonicaliza, así que `bg-gradient-to-r` → `bg-linear-to-r` antes se
  reportaba dos veces con el mismo fix; el mensaje de la otra regla ("deprecada en v4") es el más
  accionable de los dos. Aquí queda todo lo que es actual-pero-no-canónico: `-m-0` → `m-0`,
  `start-2` → `inset-s-2`, y las formas con valor arbitrario como `flex-grow-[2]` → `grow-2` (la
  lista de renombres tiene grafías, no valores). Mantén ambas activas — con solo esta los renombres
  quedan sin reportar.
- **`prefer-scale-token`**: la mitad solo-reporte de lo que esta regla cedió en #78. Una reescritura
  cuyo CSS emitido difiere textualmente (`p-[10px]` → `p-2.5`, donde el token llega por
  `var(--spacing)`) no se autofixea aquí y no lo hará nunca; esa regla lo reporta con una
  sugerencia. Las dos no pueden disparar juntas: esta solo reescribe pares byte-idénticos, que es
  exactamente lo que la otra salta.
- **`enforce-consistent-important-position`**: esta regla preserva la posición del `!` que
  escribiste (prefix vs suffix vs ninguno). `enforce-consistent-important-position` es la única
  fuente de verdad para imponer una posición en particular.

## Cuándo desactivarla

- **No quieres ninguna reescritura de clases** — por ejemplo, en un archivo legacy que estás
  manteniendo literal para diffear.
- **Lint runs sensibles a la performance** donde el costo de inicializar el worker molesta. El cache
  hace que las corridas siguientes sean baratas, pero el primer hit paga el startup de
  `@tailwindcss/node`. La mayoría de proyectos no lo van a notar.
- **No migraste a Tailwind v4 todavía**: las formas canónicas producidas aquí asumen semántica v4.
