feat(linter): detect token name collisions for flat vs grouped keys#150
feat(linter): detect token name collisions for flat vs grouped keys#150Arshgill01 wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Phase 2 can undo the collision guard. This one's a bit sneaky because it's not in your diff, but the new guard in Phase 1 interacts with the existing Phase 2 reference-resolution loop in a way that can silently corrupt the symbol table.
Phase 2 does a bare forEachLeaf walk over input.colors again with no awareness of seenKeys or seenNormalized. So in a case like this:
colors:
utility-info-50: "{colors.brand.primary}" # token reference
utility-info:50: "#FFFFFF"
Phase 1 does the right thing detects the collision, emits the error, skips utility-info.50. But then Phase 2 comes along, re-walks input.colors, and when it resolves the reference for utility-info-50 it calls symbolTable.set() again no guard, no check. Whatever Phase 1 decided gets quietly overwritten. Same issue exists in the rounded and spacing Phase 2 loops.
The straightforward fix is to not re-walk input.colors in Phase 2 at all — just iterate over symbolTable directly and resolve any entries that are still raw reference strings. That way Phase 2 only touches what Phase 1 actually registered.
Emp1500
left a comment
There was a problem hiding this comment.
Few changes and this is ready to go:
1)Extract the collision guard into a shared helper (DRY)
2)Phase 2 re-traverses without collision guards (data corruption risk)
| return; | ||
| } | ||
| seenKeys.add(name); | ||
| seenNormalized.set(normalized, name); |
There was a problem hiding this comment.
Extract the collision guard into a shared helper (DRY). The seenKeys / seenNormalized block is identical across colors, rounded, and spacing only the category string differs. Might be worth extracting before this merges, purely because typography is one refactor away from needing a fourth copy, and that's when these things tend to drift. Something like a small buildCollisionGuard(category, findings) factory that returns a (name) => boolean predicate would collapse all three blocks down to a one-liner each, with no changes to the existing messages or tests.
Adds duplicate and collision detection to the model builder for nested and flat token keys, preventing silent overwrites in the symbol table.
6c6ad33 to
f9dbecc
Compare
|
Both points addressed — just force-pushed. 1. DRY ( 2. Phase 2 symbol-table iteration (data-corruption fix) |
Emp1500
left a comment
There was a problem hiding this comment.
Does the colors test still pass after the refactor?
Noticed the old colors block said "Grouped color token flattens to..." (singular), but rounded/spacing just used the category name as-is. Now everything goes through buildCollisionGuard with the same ${category} interpolation, so wouldn't calling it with 'colors' give you "Grouped colors token..." instead?
|
Yes, caught that during the refactor and updated the test to match the new message. The old code had three separate inline blocks so the colors one happened to use the singular noun "color" while rounded/spacing used their category names as-is. The test on line 161 was updated from |
Emp1500
left a comment
There was a problem hiding this comment.
LGTM — both issues I flagged (Phase 2 corruption risk and the DRY refactor) are addressed, and the colors test fix matches the new uniform messaging. 288/288 tests passing. Nice work tightening this up.
Closes #149
Requested by @davideast in #68:
What this does
forEachLeaf(merged in #103) enables arbitrary nesting depth forcolors,rounded, andspacingtokens. As a side-effect it is now possible to write aDESIGN.mdwhere two differently-structured keys resolve to the same symbol table entry with no diagnostic emitted. Two collision shapes exist:Case 1 — Duplicate dot path
A flat string key whose literal value matches an existing nested token's dot-separated path:
Case 2 — Flat/grouped name collision
A nested token whose path, when dots are replaced by hyphens, matches a flat key that is already registered:
In both cases the offending token is skipped (not written to the symbol table) and an
error-severity finding is emitted so the user sees it at lint time, before the Tailwind v4 emitter processes the output. Behaviour is consistent across all three token categories.Changes
packages/cli/src/linter/model/handler.tsseenKeys(Set<string>) andseenNormalized(Map<string, string>) tracking inside thecolors,rounded, andspacingPhase 1forEachLeafcallbackspackages/cli/src/linter/model/handler.test.tsVerification