# enforce-canonical

oxlint rule that rewrites Tailwind CSS classes to the canonical form Tailwind itself computes, like
`-m-0` → `m-0`, with an autofix.

## At a glance

| Autofix | Editor suggestions | Design system           | Options                             |
| ------- | ------------------ | ----------------------- | ----------------------------------- |
| Yes     | Yes                | Required (`entryPoint`) | `entryPoint`, `reportNonEquivalent` |

## What this rule does

Asks the design system for the canonical form of every utility in your code and rewrites the ones
that aren't already canonical. "Canonical" is whatever `canonicalizeCandidates()` from
`@tailwindcss/node` returns — the same source of truth used by prettier-plugin-tailwindcss, oxfmt,
and the official Tailwind tooling. Examples: `-m-0` → `m-0` (no negative is needed for a zero),
`start-2` → `inset-s-2`, `flex-grow-[2]` → `grow-2` (an arbitrary value whose named form emits
identical CSS), `text-[var(--color-text)]/90` → `text-(--color-text)/90`. Auto-fix lands the first
hit, suggestions cover the rest in the same string. The v3 renames (`bg-gradient-to-r` →
`bg-linear-to-r`, `break-words` → `wrap-break-word`) are owned by `no-deprecated-classes`, not this
rule — see Interactions.

Named classes resolve through a precomputed in-memory `canonicalMap` (sub-microsecond). Arbitrary
values (`p-[2px]`, `bg-(--c)`) and arbitrary variants (`data-[open]:`, `min-[40rem]:`) go through
the `canonicalize-service` worker because they need a live DS lookup; results are cached
process-wide per `(entryPoint, rem, class)`. The worker preserves the position of `!` (important
prefix vs suffix vs none).

DS-dependent — requires `settings.tailwindcss.entryPoint`. If the design system can't load, the rule
emits a single fatal `designSystemUnavailable` diagnostic per file instead of silently passing.

## Options

### `reportNonEquivalent`

`boolean`, default `false`.

Some canonical forms are **not the same CSS in your project**, and the rule leaves those classes as
written. With this option on, it also tells you about two kinds of them, never with an autofix:

- **A variant your CSS defines differently from stock Tailwind.** shadcn/ui, for instance, ships
  `data-disabled:` as a custom variant built on `:where([data-disabled="true"]), …`, so
  `data-[disabled]:opacity-50` — which stock Tailwind rewrites to `data-disabled:opacity-50` —
  matches different elements here. The report names both variants, and switching is your call.
- **A canonical form that also sets a variable other utilities read.** Tailwind rewrites
  `[line-height:var(--x)]` to `leading-(--x)`, but `leading-(--x)` also sets `--tw-leading`, which
  every `text-*` size reads for its line height, so next to one the two can render differently. The
  same goes for `[border-style:dashed]` → `border-dashed` (`--tw-border-style`, read by `border-2`),
  `[transition-duration:300ms]` → `duration-300` (`--tw-duration`, read by `transition`), and the
  `font-*`, `tracking-*`, `ease-*`, `outline-*` and `content-none` forms. The report names the
  variable and carries a suggestion your editor can apply.

A difference stock Tailwind has too (`has-[[data-slot=x]]:` vs `has-data-[slot=x]:` emit different
selectors anywhere) isn't reported, and neither is a value that reads the theme (`p-[2px]` vs
`p-0.5`, see [`prefer-scale-token`](./prefer-scale-token)). On shadcn/ui's `apps/v4` the option adds
23 reports, all `data-[disabled]:`, and none of the second kind.

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

### `entryPoint`

`string`, optional. Per-rule override of `settings.tailwindcss.entryPoint`. Configure the entry
point in `settings.tailwindcss.entryPoint` for the whole project instead whenever possible.

## Examples

### ✗ Incorrect

```tsx
// Negative-of-zero is just zero
<div className="-m-0 -mt-0" />
// → <div className="m-0 mt-0" />

// Logical inset shorthand → canonical inset-s-* / inset-e-*
<div className="start-2 end-4" />
// → <div className="inset-s-2 inset-e-4" />

// An arbitrary value whose named form emits identical CSS
<div className="flex-grow-[2]" />
// → <div className="grow-2" />

// Variants and important are preserved
<div className="hover:!flex-grow-[2]" />
// → <div className="hover:!grow-2" />

// Opacity modifiers and arbitrary variants, with or without other variants
<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 }
// Not the same CSS: leading-(--x) also sets --tw-leading, which text-* sizes read.
// Reported with a suggestion, never fixed
<div className="[line-height:var(--x)]" />
// → <div className="leading-(--x)" />
```

### ✓ Correct

```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)" />
```

## Interactions with other rules

- **`no-unnecessary-arbitrary-value`**: complementary, no double-fire. Both rewrite an arbitrary
  value to a named utility only when the two emit **identical** CSS; they split on shape.
  `no-unnecessary-arbitrary-value` owns the cases where the arbitrary maps directly to a single
  named utility (e.g. `h-[auto]` → `h-auto`). A value that is only _numerically_ equal to a scale
  step (`p-[2px]` → `p-0.5`, whose CSS text differs) is neither rule's business — that is
  `prefer-scale-token`, report-only.
- **`prefer-theme-tokens`**: the third partner of the arbitrary→named trio. It catches CSS-var
  references like `border-(--border)` → `border-border` where neither `enforce-canonical` (the CSS
  differs) nor `no-unnecessary-arbitrary-value` (no shared bracket equivalent) would fire.
- **`no-deprecated-classes`**: **it owns the v3 renames, and this rule skips them.** Tailwind
  canonicalizes them too, so `bg-gradient-to-r` → `bg-linear-to-r` used to be reported twice with
  the same fix; the other rule's message ("deprecated in v4") is the more actionable of the two.
  What stays here is everything that is current-but-not-canonical: `-m-0` → `m-0`, `start-2` →
  `inset-s-2`, and arbitrary-valued forms like `flex-grow-[2]` → `grow-2` (the rename list holds
  spellings, not values). Keep both rules on — with only this one enabled the renames go unreported.
- **`prefer-scale-token`**: the report-only half of what this rule gave up in #78. A rewrite whose
  emitted CSS differs textually (`p-[10px]` → `p-2.5`, where the token resolves through
  `var(--spacing)`) is not autofixed here and never will be; that rule reports it with a suggestion
  instead. The two can never both fire: this one only rewrites byte-identical pairs, which is
  exactly what the other one skips.
- **`enforce-consistent-important-position`**: this rule preserves the `!` position you wrote
  (prefix vs suffix vs none). `enforce-consistent-important-position` is the single source of truth
  for enforcing a particular position.

## When to disable it

- **You don't want any class rewrites at all** — for example, in a legacy file you're keeping
  verbatim for diffing.
- **Performance-sensitive lint runs** where the worker initialization cost is unwelcome. The cache
  makes subsequent runs cheap, but the first hit pays for `@tailwindcss/node` startup. Most projects
  will not notice.
- **You haven't migrated to Tailwind v4 yet**: the canonical forms produced here assume v4
  semantics.
