From 7c233f42d2a2a6fa0ac6e4349cdd5ee3eb957cfc Mon Sep 17 00:00:00 2001 From: leaysgur <6259812+leaysgur@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:06:32 +0000 Subject: [PATCH] fix(formatter,oxfmt): Handle nested `BinaryExpression` for tailwind trailing spaces (#20450) Fixes #20397 `can_collapse_whitespace()` walks the ancestor chain looking for `BinaryExpression(+)` to determine if boundary whitespace must be preserved. The loop broke on the first non-`BinaryExpression` ancestor, so a `ConditionalExpression` (ternary) between the string and the outer `+` caused an immediate break. So, the `BinaryExpression` was never seen. Prettier's `canCollapseWhitespaceIn()` iterates the entire ancestor path without breaking, reacting only to `BinaryExpression(+)` and `TemplateLiteral` nodes via if checks. --- apps/oxfmt/test/api/sort_tailwindcss.test.ts | 14 ++++++ crates/oxc_formatter/src/utils/tailwindcss.rs | 45 +++++++++++-------- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/apps/oxfmt/test/api/sort_tailwindcss.test.ts b/apps/oxfmt/test/api/sort_tailwindcss.test.ts index 2e213e79523e2..496bc812975d6 100644 --- a/apps/oxfmt/test/api/sort_tailwindcss.test.ts +++ b/apps/oxfmt/test/api/sort_tailwindcss.test.ts @@ -634,6 +634,20 @@ shadow-lg\` : "font-normal"}\`} />;`; expect(result.errors).toStrictEqual([]); }); + // https://github.com/oxc-project/oxc/issues/20397 + it("should preserve trailing space in ternary inside binary concat", async () => { + const input = `const A =
;`; + + const result = await format("test.tsx", input, { + experimentalTailwindcss: {}, + }); + + expect(result.code).toContain('"m-1 h-fit w-full "'); + expect(result.code).toContain('"block "'); + expect(result.code).toContain('"hidden "'); + expect(result.errors).toStrictEqual([]); + }); + // Tests for template literals in binary expressions it("should sort template literal on right side of binary expression", async () => { const input = "const A =
;"; diff --git a/crates/oxc_formatter/src/utils/tailwindcss.rs b/crates/oxc_formatter/src/utils/tailwindcss.rs index 7b132a5d9c025..4b8313c2fcb70 100644 --- a/crates/oxc_formatter/src/utils/tailwindcss.rs +++ b/crates/oxc_formatter/src/utils/tailwindcss.rs @@ -188,24 +188,33 @@ where // 2. Check binary concat context (walk parent chain) for ancestor in ancestors { - let AstNodes::BinaryExpression(binary) = ancestor else { - break; - }; - - if binary.operator() != BinaryOperator::Addition { - break; - } - - let left = binary.left().span(); - let right = binary.right().span(); - - // Left operand needs trailing space for separation from `+ right` - if left.contains_inclusive(span) { - collapse.end = false; - } - // Right operand needs leading space for separation from `left +` - if right.contains_inclusive(span) { - collapse.start = false; + match ancestor { + AstNodes::BinaryExpression(binary) if binary.operator() == BinaryOperator::Addition => { + let left = binary.left().span(); + let right = binary.right().span(); + + // Left operand needs trailing space for separation from `+ right` + if left.contains_inclusive(span) { + collapse.end = false; + } + // Right operand needs leading space for separation from `left +` + if right.contains_inclusive(span) { + collapse.start = false; + } + + // Both flags are one-way latches; no need to continue once both are set. + if !collapse.start && !collapse.end { + break; + } + } + // Transparent nodes: skip through to find outer BinaryExpression(+) + AstNodes::ConditionalExpression(_) + | AstNodes::ParenthesizedExpression(_) + | AstNodes::TSAsExpression(_) + | AstNodes::TSSatisfiesExpression(_) + | AstNodes::TSNonNullExpression(_) + | AstNodes::TSTypeAssertion(_) => {} + _ => break, } }