feat: prefer-scale-token (#91), and three reliability fixes from the 1.5.0 review - #96
Merged
Conversation
Documented in the #94 review, and one of them got worse in #95. The declaration service degraded in silence. It swallowed its own failures and returned "no information", which was defensible while `no-conflicting-classes` was the only caller — no declarations, no comparison, no diagnostic. Since #95 `no-unknown-classes` asks it about VALIDITY, and there silence sends the rule back to the tolerant heuristic: a dead worker quietly reinstates the exact false negatives the service exists to remove, on a run that stays green. It throws now, like the sort and canonicalize services, and each caller takes the posture that already existed in the plugin — the two DS-dependent rules surface `designSystemUnavailable`, and `no-dark-without-light` (DS-OPTIONAL, which may never emit it) degrades to prefix-only grouping and says so via debugLog. `internDeclarations` appended lint-time values to `index.values`, which belongs to the cache artifact and is held by reference. Two caches built from one artifact would each push while holding their own text→id map, so the same value would get two ids — and `decidePair` compares ids, so two identical values would read as a conflict. Ids are now local to the cache; nothing reads them back out of the array (the decoder only resolves ids that came from `index.table`). Unreachable through the loader today, one shared object away from being a false positive nobody could explain, and the test interleaves the two caches so it fails on the old code. `definedVars` scanned only the entry CSS. Splitting the theme across files is the normal shadcn/ui layout, so `:root { --primary: … }` in an `@import` read as undefined — and that is precisely what tells `prefer-theme-tokens` a rewrite is safe, so it proposed `bg-primary` for `bg-(--primary)` and silently changed the colour. It now scans the entry and its resolved imports, reusing the collection `extractComponentClasses` already did.
…hout fixing it Closes the gap #91 reported. Since #78, `enforce-canonical` only rewrites when the emitted CSS is byte-identical — right for an autofixer, and it took the consistency signal with it for every value whose token resolves through `var()`. Nothing else covered it: the three related rules each key on something this case does not satisfy (byte-identical CSS, what canonicalizeCandidates proposes, the NAME of a variable the user wrote). Numeric equality is a fourth premise. Two families, both derived from the design system by the precompute: - the spacing scale — `p-[10px]` → `p-2.5`, `gap-[4px]` → `gap-1`, `h-[2rem]` → `h-8`, and `w-[140px]` → `w-35`, the issue's own example; - named theme tokens by value — `rounded-[0.5rem]` → `rounded-lg`, read off the emitted CSS plus the theme, so no namespace table exists to drift. Report-only, with a suggestion and NO autofix: the equivalence is numeric, so a `:root` override or a different root font size makes the two diverge. Fixing that automatically is the mistake #78 fixed, and the message says as much. The boundary is where the work was. Tailwind compiles ANY number — `w-8.425` is valid — so "has a scale equivalent" is true for practically every length, and reporting all of them would be `no-arbitrary-value` with extra steps. The cut is the granularity Tailwind's own enumerated steps use, computed by the precompute as the smallest gap between them (0.5 in the default theme) rather than chosen here. `step` only ever makes it finer. Two things the derivation gets right on its own, which is why it is a derivation: `text-[14px]` → `text-sm` is NOT reported, because `text-sm` also sets `line-height` and the single-declaration requirement drops the whole family; and the ~7 000 colour tokens are excluded because a colour can never match a literal numerically, leaving 253 comparable tokens and +8.5 KB on the cache artifact. Corrects the issue's premise, for the record: `w-[140px]` → `w-35` was never reported, before or after #78 — Tailwind does not propose it, since 35 is not one of the steps it enumerates.
Rule page in both locales, with the two things worth understanding up front: why there is no autofix (the equivalence is numeric, so a `:root` override or a different root font size makes it false — autofixing that is the bug #78 fixed), and why there is a granularity at all (Tailwind compiles any number, so without one the rule fires on essentially every length). Also fixes a doc error the same misconception produced: the rule index claimed `no-unnecessary-arbitrary-value` handles `w-[200px]` → `w-50`. It does not and cannot — those emit different CSS text — and `getNamedEquivalent('w-[200px]')` returns null. That example now points at the rule that does handle it. `enforce-canonical`'s page gains the pointer to its report-only half, since that is where the signal was lost. Unrelated flake found on the way: the precompute worker test scanned the WHOLE shared cache dir for `.tmp.` stragglers, so it failed whenever a peer isolate was mid-write — which this branch made likely by changing CACHE_KEY, since every fixture then recomputes at once. Scoped to its own artifact's temp names.
Found reviewing the diff. `p-[10]` compiles to `padding: 10` — no unit, which is not a length at all — and the rule treated the bare number as px, so it suggested `p-2.5` (`padding: 10px`). That is a change of meaning dressed up as a change of spelling, which is the one thing a rule with no autofix still has to get right, because an editor applies its suggestion on a click. Values now carry whether they had a unit, and a unitless number is never the same value as a length whatever the digits say. The scale path additionally requires both sides to be the same kind of quantity, so a hypothetical unitless `--spacing` would still compare correctly. Also tightened the prefix split to require the dash that joins a utility to its value, instead of assuming the character before `[` is one. Verified the fix discriminates: without the guard, `p-[10]` and `w-[140]` are reported.
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.
Closes #91, plus the three reliability findings the 1.5.0 review left documented and unfixed — one of which 1.5.0 itself made worse.
prefer-scale-token(#91)A new rule that reports a hardcoded value numerically equal to something the design system already names, and suggests the name. It fills a gap the other three arbitrary→named rules structurally cannot, because each keys on something this case does not satisfy:
no-unnecessary-arbitrary-valuew-[100%]→w-fullenforce-canonicalcanonicalizeCandidatesproposesprefer-theme-tokensbg-(--primary)→bg-primaryprefer-scale-tokenp-[10px]→p-2.5p-2.5compiles tocalc(var(--spacing) * 2.5)andp-[10px]to10px: same length, different text.Report-only, and it will stay that way. No
fixable— the token reaches its value throughvar(), so a:rootoverride or a differentrootFontSizemakes the equivalence false. Autofixing that is the bug #78 fixed. The message says so.Correcting the issue's premise
w-[140px]→w-35was never reported, before or after #78:canonicalizeCandidatesdoesn't propose it, because 35 isn't one of the steps Tailwind enumerates. What #78 silenced were the enumerated ones (p-[10px]→p-2.5,gap-[4px]→gap-1,h-[2rem]→h-8) plus the theme tokens (rounded-[0.5rem]→rounded-lg). Both are covered now, including the reporter's example — Tailwind compilesw-35, so it counts.Two families, both derived
--spacingcomes from the theme; the prefixes that read it are found by asking what<prefix>-1compiles to; named tokens come from the emitted CSS plus the theme values. No namespace table exists to drift, and two things fall out of the derivation rather than being special-cased:text-[14px]→text-smis NOT reported.text-smalso setsline-height, so it isn't the same declaration. Requiring one declaration drops the whole family on its own.var()classes gone, 253 comparable ones left, +8.5 KB on the cache artifact.The granularity is derived, not chosen
Tailwind compiles any number (
w-8.425is valid), so every length is N spacing units for some N — reporting all of them would beno-arbitrary-valuewith extra steps. Every step Tailwind's own scale enumerates is a multiple of0.5, and the precompute computes that from the steps themselves.w-[140px]→w-35passes;w-[33.7px]→w-8.425doesn't.steponly ever makes it finer.Three reliability fixes
no-conflicting-classeswas the only caller. Since fix: derive the remaining nine rules from the generated CSS #95no-unknown-classesasks it about VALIDITY, and there silence sends the rule back to the tolerant heuristic: a dead worker quietly reinstated the false negatives the service exists to remove, on a run that stayed green. It throws now, and each caller takes a posture that already existed — the two DS-dependent rules surfacedesignSystemUnavailable,no-dark-without-light(which may never emit it) degrades and logs underdebug. A transient worker failure now fails those two rules instead of weakening them quietly.prefer-theme-tokensproposed a colour-changing rewrite when the theme lived in an@import.definesVaris the guard that stops it, and it scanned only the entry CSS — so the normal shadcn/ui layout made:root { --primary: … }read as undefined. Reproduced with a fixture, then fixed.Testing
prefer-scale-token: 33 cases, and the valid half is the important one (the boundary, byte-identical hand-offs,text-[14px], variable references, bare numbers).precomputed-snapshot.test.ts, so a Tailwind release that changes the granularity or the token set shows up as a failure rather than silently moving the rule.Found while reviewing my own diff
p-[10]compiles topadding: 10— no unit, not a length — and the rule treated the bare number as px, so it suggestedp-2.5(padding: 10px). A change of meaning dressed as a change of spelling, which matters even without an autofix because editors apply suggestions on a click. Values now carry whether they had a unit.Also fixes a doc error the same misconception produced: the rule index claimed
no-unnecessary-arbitrary-valuehandlesw-[200px]→w-50. It can't —getNamedEquivalent('w-[200px]')is null — and that example now points at the rule that does.Version bumped to 1.6.0: new rule (off by default, no presets exist), plus the behaviour change above.