Do not migrate a variant literal behind a conditional - #20436
Do not migrate a variant literal behind a conditional#20436HuzaifaChaudary wants to merge 5 commits into
Conversation
the shadcn variant guard only looked at the text right before the literal so any ternary or nullish branch between variant and the string defeated it and the upgrade rewrote a react prop value as if it were a class braces and commas stay excluded from the new part so the match cannot run past its own value into a neighbouring prop like className
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThe codemod now matches one-level object literals inside conditional Merge Risk: 🔵 Low · up to The PR fixes conditional variant handling, but a bounded correctness risk remains for valid object-form expressions containing braces inside quoted strings, where the upgrade may still rewrite a component variant incorrectly. The change is otherwise localized and mergeable with explicit owner awareness or follow-up. 🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
Full details: Linked Issues checkExplanation The changes address the conditional-expression cases in issue
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 2de9ab43-138d-47a0-b13e-f979998d0475
📒 Files selected for processing (2)
packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.test.tspackages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.
There was a problem hiding this comment.
Pull request overview
This PR updates the @tailwindcss/upgrade template migration safety heuristics to avoid rewriting shadcn/ui-style variant="outline" literals when they appear inside simple conditional expressions, preventing accidental breakage of React prop values during upgrades.
Changes:
- Expanded the
variantlook-behind guard inisSafeMigrationto allow one ternary/nullish operator between thevariantprop and the string literal. - Added regression tests covering the reported conditional
variantcases and ensuring conditional class strings still migrate as expected.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts | Broadens the variant guard regex used to decide when migrations are unsafe. |
| packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.test.ts | Adds negative cases for conditional variant literals and positive cases to ensure conditional class migration still works. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // shadcn/ui variants, including a ternary or nullish branch between the prop | ||
| // and the literal. Braces and commas are excluded so the match cannot reach | ||
| // out of its own value into a neighbouring prop such as `className`. |
| // shadcn/ui variants, including a ternary or nullish branch between the prop | ||
| // and the literal. Braces and commas are excluded so the match cannot reach | ||
| // out of its own value into a neighbouring prop such as `className`. | ||
| /variant\s*[:=]\s*\{?(?:[^{},]*?(?:\?\?|\?|:)\s*)?['"`]$/, |
Confidence Score: 4/5The PR should not merge until conditional variant values containing deeper nested object literals are protected from class migration. The new object-group pattern handles the reported flat object literal but cannot consume deeper nested braces, allowing the same variant-value rewrite to remain reachable. Files Needing Attention: packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts Reviews (5): Last reviewed commit: "let a condition contain an object litera..." | Re-trigger Greptile |
review pointed out two holes. a comma inside a call like isActive(foo, bar) ended the match early, and a branch wrapped in parens put a ( between the operator and the literal a parenthesised group is now consumed as a unit so its commas stay inside it, and parens are allowed right before the literal. a bare comma or brace still ends the match so a neighbouring prop is still out of reach
|
thanks, two of the three were real and are now fixed in a call in the condition. a parenthesised branch. the guard is now /variant\s*[:=]\s*\{?(?:(?:[^{},()]|\([^()]*\))*?(?:\?\?|\?|:)\s*)?\(*\s*['"`]$/a bare comma or brace still ends the match, which is what keeps a neighbouring on the brace comment. i think that one is a misread. on multiline values. correct, and it is not something this change introduces. tests are now 78 in |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 4414dc19-0856-45eb-ab38-21ef4aac62f7
📒 Files selected for processing (2)
packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.test.tspackages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.
review caught an over match i introduced. vue writes attributes without braces so the gap happily crossed variant="ghost" and carried on into :class, which suppressed a class that should have migrated quotes may now only be crossed inside a brace, where a quoted ternary branch is ordinary. outside one a quote closes the attribute value so the match stops there. nested parens in a condition are handled while i was in here
|
both of these were right, fixed in the vue one is the important one, and it was a regression i introduced. the rule i had wrong is when a quote may be crossed. inside a brace a quoted ternary branch is ordinary, so nested parens. i pulled the pattern out into named parts above the array rather than growing the inline literal. three reviewers have now had to reason about this regex and the one liner was getting hard to read, so the parts are named and commented. two new tests, one in each direction, and both fail on the previous commit:
thanks for the catch, the vue case is one i should have tested for myself given the whole point of the guard is not to suppress real classes. |
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 9036e261-589b-43a9-a864-bf4a836e59d5
📒 Files selected for processing (2)
packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.test.tspackages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.
three rounds of review kept finding holes in a single pattern because one regex was trying to know where the value ends in three different syntaxes at once each form now stops at what actually ends it. a brace for a jsx expression, the opening quote for an attribute so it cannot run into a neighbouring one, a comma or brace for an object literal where quotes are normal the array already tests each rule on its own so this needs no other change
|
all three were right. fixed in three rounds of review have now found holes in this guard, and looking at why, they were all the same underlying mistake: one regex trying to know where a value ends in three different syntaxes at once. every time i widened it for one syntax it either missed another or reached into a neighbouring attribute. so it is now one rule per form, each stopping at what actually ends that form. the array already tests each rule independently so nothing else had to change. that covers the three you found:
three more negative tests, all failing on the previous commit. happy to keep going if there is another hole, but if the maintainers would rather this whole heuristic move off regexes and onto a real parse of the surrounding expression, that is a bigger change than i should make unasked and i would open a separate issue for it instead. |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 9fc6a8b9-d9c1-40e4-807e-2531ee803f25
📒 Files selected for processing (2)
packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.test.tspackages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review.
review found two more, both an object literal inside the condition. a vue
binding calling isActive({ x: 1 }) and a jsx expression starting with an
inline object
an object group is now allowed as a unit the same way a parenthesised group
already was, so the braces inside it do not read as the end of the value
|
both fixed in
both are covered by tests and both fail on the previous commit. where i think this actually standsthis is the fifth round, and i want to be straight about the trend rather than keep quietly widening things. @coderabbitai is right that the real answer is scanning the expression with balanced brace, paren and quote state instead of matching it. a regex can only ever handle a bounded nesting depth. what i am less sure about is whether that scanner belongs in this pull request. every entry in so my suggestion, and it is only a suggestion since this is your file:
either is fine by me. i would rather ask than keep adding nesting levels until someone finds level four. |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 94bee8a8-3ea5-4017-ba50-d7f61fc37806
📒 Files selected for processing (2)
packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.test.tspackages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review.
| // An object literal with one level of nesting, so a condition may contain one | ||
| // without the surrounding value looking like it ended there. | ||
| const OBJECT_GROUP = String.raw`\{(?:[^{}]|\{[^{}]*\})*\}` |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Make OBJECT_GROUP quote-aware.
OBJECT_GROUP treats every { and } as structural. A valid one-level object literal can contain a brace inside a string, for example:
<Button variant={{ tone: "}" }.tone === "}" ? "outline" : "ghost"} />
For the outline candidate, the conditional rules using this helper do not match. isSafeMigration can then return true, and migrateCandidate can rewrite the variant value to outline-solid. Consume quoted strings, including escaped quotes, before treating braces as delimiters. Add regression tests for the JSX, quoted-attribute, and object-form variants.
| const PAREN_GROUP = String.raw`\((?:[^()]|\([^()]*\))*\)` | ||
| // An object literal with one level of nesting, so a condition may contain one | ||
| // without the surrounding value looking like it ended there. | ||
| const OBJECT_GROUP = String.raw`\{(?:[^{}]|\{[^{}]*\})*\}` |
There was a problem hiding this comment.
Nested variant objects bypass guard
When a conditional variant expression contains an object nested more deeply than OBJECT_GROUP supports, the guard cannot consume the nested braces and treats the variant literal as a utility candidate, causing the upgrade to rewrite application data such as outline.
Knowledge Base Used: Upgrade tool transformations
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
implementation=$(fd -a '^is-safe-migration\.ts$' packages)
tests=$(fd -a '^is-safe-migration\.test\.ts$' packages)
printf '%s\n' '=== Implementation outline ==='
ast-grep outline "$implementation" --items all --type variable,constant,function
printf '%s\n' '=== Conditional guard implementation ==='
rg -n -C 12 'OBJECT_GROUP|PAREN_GROUP|CONDITIONAL_TEMPLATE_SYNTAX|variant' "$implementation"
printf '%s\n' '=== Related tests ==='
rg -n -C 6 'isActive\(\{ x: 1 \}\)|tone: "dark"|variant=.*\?' "$tests"
printf '%s\n' '=== Commit availability and changed files ==='
git show --no-ext-diff --format=fuller --stat 75e4e2f6 --
git diff --no-ext-diff 75e4e2f6^ 75e4e2f6 -- \
"$implementation" "$tests"Length of output: 13586
The three syntax-specific rules use the correct value boundaries. The two new tests cover the reported object-literal cases. The existing valid-position test confirms that a neighboring Vue A stateful scanner would remove the bounded-nesting limit. That is a separate refactor. It should replace the broader conditional-syntax mechanism in a dedicated change, not extend this targeted fix. No additional blocking concern from my review. ✏️ Learnings added
If you found this review helpful, would you consider giving us a shout-out on X? Thank you for using CodeRabbit! You are interacting with an AI system. |
|
both of these are real, and both are the ceiling i described in the last comment rather than new defects. a brace inside a string and an object nested past one level are the same structural limit: a regex cannot track balanced state, so there is always a next case. i said i would not keep adding levels and i am going to hold to that. what i did do is measure which direction the remaining gaps fail in, because that is the part that decides whether this is mergeable as it stands. they only fail by under guarding. for both of your cases the guard does not fire, so the prop is rewritten as a class, which is exactly what nothing over guards. the failure i actually care about is a real class silently no longer migrating, since that is worse than the bug this fixes. i checked that direction explicitly, including with your new shapes: so the state of this pr is: fixes the four forms from #20435 plus eleven more the reviews surfaced, cannot suppress a class that migrates today, and leaves a shrinking tail of exotic shapes behaving exactly as they already do on that seems like a reasonable place to stop to me, but it is your call and i am happy either way:
i would rather do that once, deliberately, than keep going a level at a time. |
Closes #20435
problem
@tailwindcss/upgraderewritesvariant="outline"tovariant="outline-solid"when a conditional sits betweenvariantand the string. these are react prop values and not class names so the rewrite breaks them.#18922 added a guard for the direct forms and those still work. the guard is a look behind that only inspects the text immediately before the literal
/variant\s*[:=]\s*\{?['"`]$/so as soon as a ternary or a nullish branch comes between the two it no longer matches. the four cases from the issue
fix
allow one ternary or nullish branch between the prop and the literal.
/variant\s*[:=]\s*\{?(?:[^{},]*?(?:\?\?|\?|:)\s*)?['"`]$/the added part excludes braces and commas on purpose. that is what stops the match running out of its own value and into a neighbouring prop, so a real class in a conditional is still migrated. the three cases i was most worried about
all three still migrate
shadowtoshadow-sm. the last one is the reason commas are excluded rather than only braces.tests
seven cases in
is-safe-migration.test.tsmainis-safe-migrationbeforeis-safe-migrationafter@tailwindcss-upgradepackage before@tailwindcss-upgradepackage afterprettier is clean on both changed files.
not covered
the issue also notes that a comment mentioning
outline: nonegets rewritten. that one is candidate extraction rather than this safety guard, so it is a different fix and i have left it alone.disclaimer: this contribution was prepared with the help of an ai agent. i reviewed the change, reproduced the four failing cases against
mainfirst, and ran the package test suite locally before opening it.