Skip to content

feat(enforce-canonical): report canonical forms that also set --tw-* variables (#217) - #222

Merged
sergioazoc merged 1 commit into
mainfrom
feat/canonical-variable-reason
Oct 9, 2026
Merged

sergioazoc merged 1 commit into
mainfrom
feat/canonical-variable-reason

Conversation

@sergioazoc

Copy link
Copy Markdown
Owner

Closes #217.

Summary

Tailwind canonicalizes [line-height:var(--x)] → leading-(--x), but the canonical form also sets --tw-leading, which every text-* size reads for its line height. The two can render differently next to one, so the #78 guard leaves them as written. Until now it also said nothing, so CI never caught them.

  • Worker: CANONICALIZE_HANDLER strips mirrors before comparing.
    • A mirror is a --tw-* declaration that repeats a sibling real property's value verbatim, !important included, plus the @property of each stripped variable. This is what Tailwind's own signature drops.
    • If the CSS is equal once stripped, and the canonical form adds mirrors and drops none, the reason is the new 'variable', which carries the variables.
    • A project variant that also adds a mirror stays 'variant'.
    • 86 forms of the stock theme fall in this case.
  • Rule: with reportNonEquivalent: true, enforce-canonical reports them as nonEquivalentVariable. The message names the variable, and the report carries a suggestion and never an autofix. The option stays off by default.
  • utils/report.ts: reportClassSuggestion replaces the one-class suggestion pattern that prefer-scale-token and no-unknown-classes each had a copy of. It never joins the shared autofix.
  • Persistence: the canon tuple becomes [canonical, safe, reason?, variables?], and older entries still read. Only the handler changed, so the canon cache file is renamed and the precompute is untouched.
  • Docs: EN/ES rule pages, CLAUDE.md (R5, Suggestions API) and CHANGELOG (## Unreleased).

Test plan

  • canonicalize-reason.test.ts:
    • The real DS for 'variable', 'value' and the mixed variant case.
    • The shipped handler against a stub DS for every mirror edge case: !important, value, var(--tw-*) reads, a ; in a value, an @property-only difference, a dropped mirror. A mutation of the dropped guard was checked to fail.
  • canonicalize-persistence.test.ts: 4-element round-trip, invalid entries skipped, 3-element entries still read.
  • enforce-canonical.test.ts: off by default; suggestion without an output; ! position; a mixed string where the autofix excludes the suggestion.
  • template-fragments.test.ts and the new tests/utils/report.test.ts.
  • Lint, lint:types, format:check, typecheck, build and pnpm test (3552 + 312).
  • Bench npm test. The corpus and synthetic runs are identical to the baseline (--expect-identical).
  • Re-measured on shadcn/ui apps/v4 with the option on: still 23 reports, all data-[disabled]:.

🤖 Generated with Claude Code

https://claude.ai/code/session_019vTh6S5A41M1HLkmk3FaM1

…variables (#217)

Tailwind canonicalizes `[line-height:var(--x)]` to `leading-(--x)`, but the
canonical form also sets `--tw-leading`, which every `text-*` size reads for its
line height, so the two can render differently next to one. The #78 guard left
these as written (reason 'value') and said nothing, so a CI lint never caught
them.

The canonicalize worker now strips MIRRORS before comparing: a `--tw-*`
declaration repeating, verbatim with its `!important`, the value of a sibling
real property in the same block, plus the `@property` of each stripped variable
(what Tailwind's own signature drops). Equal once stripped, with the canonical
form adding mirrors and dropping none, is the new reason 'variable', carrying
the variables. The variant/selector check compares stripped CSS too, so a
project variant that also adds a mirror stays 'variant'. 86 forms of the stock
theme land here: border-dashed, duration-*, font-*, leading-*, tracking-*,
ease-*, outline-*, content-none.

With `reportNonEquivalent: true`, enforce-canonical reports them as
`nonEquivalentVariable`, naming the variable, with a suggestion and never an
autofix. The option stays off by default.

- utils/report: `reportClassSuggestion`, the one-class suggestion pattern
  prefer-scale-token and no-unknown-classes each had a copy of; it never joins
  the shared autofix.
- The persisted canon tuple grows to [canonical, safe, reason?, variables?],
  the variables with 'variable' and only with it; older entries still read.
  Only CANONICALIZE_HANDLER changed, so the canon cache is renamed and the
  precompute is untouched.
- On shadcn/ui apps/v4 the option still adds 23 reports, all data-[disabled]:;
  bench corpus and synthetic are identical with the default config.

Claude-Session: https://claude.ai/code/session_019vTh6S5A41M1HLkmk3FaM1
@sergioazoc
sergioazoc merged commit cf17fc7 into main Oct 9, 2026
11 checks passed
@sergioazoc
sergioazoc deleted the feat/canonical-variable-reason branch October 9, 2026 00:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

enforce-canonical: report (without autofixing) rewrites that only add --tw-* declarations

1 participant