Repository navigation
fix(enforce-canonical): find the class wherever its selector puts it - #228
Merged
Merged
Conversation
The safe gate compares a class's CSS with its canonical form's, each with its own name neutralized. Only a name that OPENED a statement was (#156), and plenty of selectors don't start with it: in-* (`:where(:focus) .x`), *: and **: (`:is(.x > *)`), divide-* (`:where(.x > :not(:last-child))`) and, after Tailwind 4.3.3 (tailwindlabs/tailwindcss#20513), every group-* and peer-* (`:is(:where(.group):hover .x)`). Each of those rewrites read as a selector change and was never reported: in-focus:z-[10], *:z-[10] or divide-x-[2px] today, and every group-hover:z-[10] on Tailwind's next release. The token is now the one Tailwind prints, a `.` and the class serialized the way CSS.escape does it (Tailwind's escape() is that algorithm), matched whole: not as the start of a longer name, not after a `\`, never inside a string. Every other class in the selector is compared as written, so a canonicalization that changes the variant's selector still differs. The handler changed, so CANON_LOGIC_HASH renames the canonical cache; the precompute is untouched. Tests: the rule on in-*, *:, **:, divide-*, peer-*, -z- and @md:; the service on the real engine, and the handler on the CSS insiders prints for group-* and peer-*. Without the change 8 of them fail, and dropping the string skip, the whole-name check, the backslash check or the escape of a leading digit or a symbol each fails one. Claude-Session: https://claude.ai/code/session_019vTh6S5A41M1HLkmk3FaM1
The smoke installs the engine in a fresh consumer project and checks that every rule runs, coarse on purpose, so it can't see a change in the CSS Tailwind prints. It passed on 2026-09-28 and 2026-10-05 with the insiders build that moves group-*/peer-* classes inside their :is(), while enforce-canonical had stopped reporting every rewrite under those variants. The whole suite caught it on its first run. A `suite` job swaps tailwindcss@latest / @insiders in for the plugin's own engine and runs the package's tests. Two engine-guard tests now expect what the guard does with an insiders engine when one is installed: a single notice, and no drift check against it. Claude-Session: https://claude.ai/code/session_019vTh6S5A41M1HLkmk3FaM1
…AUDE.md Given a dist-tag, pnpm 12 saves `^0.0.0-insiders.<sha>` even with --save-exact, and that range resolves to whichever 0.0.0 prerelease sorts highest: tailwindcss@0.0.0-oxide-insiders.ff2c25f (2023) and @tailwindcss/node@0.0.0-insiders.ffb1d72 (2025), on which 345 tests failed. The suite job resolves the tag with `npm view` and adds that exact version; run that way on 0.0.0-insiders.fa81d69, the suite passes, 3782 of 3782. CHANGELOG (Unreleased): the enforce-canonical fix, with the reports it can add. CLAUDE.md: the safe gate as it is now (own(), escapeClass, the shapes that don't open the selector), and the canary's suite job with what it asks of a test that asserts on the installed engine. Claude-Session: https://claude.ai/code/session_019vTh6S5A41M1HLkmk3FaM1
sergioazoc
added a commit
that referenced
this pull request
Oct 9, 2026
- packages/oxlint-tailwindcss/package.json and the Claude Code plugin's plugin.json at 1.15.1. - CHANGELOG: Unreleased → 1.15.1 — Before you upgrade (enforce-canonical's reports under in-*, *: and **:, and on divide-*; none on shadcn/ui apps/v4) and Bug fixes (#228). - bench/results/latest.json regenerated after the bump (only the version and timings move), /benchmark and /comparison regenerated from it. Claude-Session: https://claude.ai/code/session_019vTh6S5A41M1HLkmk3FaM1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #213.
Summary
enforce-canonicalrewrites a class only when its canonical form prints the same CSS (#78). It compares the two with each one's own class name neutralized, but only a name that opened a statement was (#156), and plenty of selectors don't start with it:in-*:where(:focus) .in-focus\:x*:/**::is(.\*\:x > *)divide-*:where(.divide-x-2 > :not(:last-child))group-*/peer-*, after 4.3.3 (tailwindlabs/tailwindcss#20513):is(:where(.group):hover .group-hover\:x)Each of those rewrites read as a selector change and was never reported. On 4.3.3 today that means
in-focus:z-[10],*:z-[10],divide-x-[2px]and the like. On Tailwind's next release it would have been everygroup-*andpeer-*rewrite too, and consumers float on^4.x, so they would get that before this repo bumps..+ the class serialized like CSS.escape (Tailwind'sescape()is that algorithm). It is matched whole (not the start of a longer name, not after a\) and never inside a string. Every other class in the selector is compared as written, so a canonicalization that changes the variant's selector ([&>*]:→*:,has-[:checked]:→has-checked:) still differs and stays unreported.CANON_LOGIC_HASHrenames the canonical cache; the precompute is untouched.suitejob runs the package's tests withtailwindcss@latestand@insidersin place of the plugin's own engine. The smoke passed on 2026-09-28 and 2026-10-05 with the insiders build that carries #20513; the suite caught it on its first run.^0.0.0-insiders.<sha>even with--save-exact, and that range resolves to a 20230.0.0-oxide-insiders.*build (345 failures).Test plan
in-focus:,in-data-[open]:,*:,**:,divide-x-[2px],peer-checked:,-z-and@md:rewrite with their autofix;has-[:checked]:and[&>*]:still don't.group-*/peer-*(verbatim from0.0.0-insiders.fa81d69).escape()on 26 names (leading digit,-,-1, control characters, non-ASCII, emoji,!,@,/, brackets…), checked against the function extracted fromtailwindcss/dist.tailwindcss@0.0.0-insiders.fa81d69installed the way the canary does it: 3782/3782 (before: 3 failures, one of them this bug).lint:types,format:check, typecheck, build andpnpm test(3782 + 312); benchnpm test. The corpus and synthetic runs are identical to the baseline, soresults/latest.jsondoesn't move.🤖 Generated with Claude Code
https://claude.ai/code/session_019vTh6S5A41M1HLkmk3FaM1