Files
Lee Jaeha / Adamandleaysgur 255c5cca26 fix(formatter): measure Tailwind class argument after collapsing whitespace (#28745)
## Summary

Follow-up to #28744, same kind of issue in a different place.

With `sortTailwindcss` enabled, an assignment whose right side is a
Tailwind function call can take two runs to reach a stable output,
because the "short argument" check measures the argument's **source**
text while the printed text has its whitespace collapsed.

Input (`{"sortTailwindcss": {"functions": ["cn"]}}`, default
`printWidth: 100`):

```ts
const vaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = cn("p-4                           flex");
```

1st run:

```ts
const vaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = cn(
  "flex p-4",
);
```

2nd run (also what Prettier + prettier-plugin-tailwindcss prints in one
run):

```ts
const vaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa =
  cn("flex p-4");
```

The same happens for `a.b = cn(...)`, `{ key: cn(...) }` and
`cn(...).trim()`.

### Cause

`is_poorly_breakable_member_or_call_chain` uses `is_short_argument`
(Prettier's `isLoneShortArgument`) to check if the only argument is at
most `printWidth / 4` wide. For a string or template literal it measures
the source text. When the argument is sorted as Tailwind classes,
whitespace is collapsed when printed, so the source can be "long" while
the printed text is "short". Prettier sees the already rewritten value,
so it decides on the printed text.

### Fix

When the call is a Tailwind function call and `preserveWhitespace` is
off, measure the string / template literal argument after collapsing
whitespace (`collapsed_class_list_width`).

### Known limitation: duplicate classes

Duplicate removal also shortens the printed text, e.g. `cn("flex flex
flex flex flex p-4")` still takes two runs. I didn't handle it, because
the sorter removes duplicates only for classes known to Tailwind CSS
(`sortClassList` in prettier-plugin-tailwindcss: `if (order !== null)
seenClasses.add(cls)`), and the formatter can't know which classes are
known while building the IR.

The options I could think of:

1. Leave it (this PR). Duplicates are rare in real code, and the result
is still valid. It just settles on the 2nd run.
2. Also dedupe all classes when measuring. This fixes the common case
but can measure too short when unknown classes are duplicated.
3. Make the sorted text available before the layout decision, which is a
larger change to how sorting is batched.

I went with 1 to keep this small. If you prefer another approach, please
let me know and I'll update it.

## Test

- Added cases to `apps/oxfmt/test/api/sort_tailwindcss.test.ts`:
- string and template literal arguments with extra whitespace format in
one run, and a 2nd run doesn't change the output. These fail without
this fix.
- `preserveWhitespace: true`: the source text is still measured
(unchanged).
- `apps/oxfmt`: `vitest run` → 553 passed
- `cargo test -p oxc_formatter`, `CARGO_BUILD_WARNINGS=deny cargo lint
-p oxc_formatter`, `cargo fmt --check`, `node --run fmt` pass
- I also formatted 7 code shapes × 5 class strings × identifier lengths
1–100 twice and compared the runs. All whitespace cases are now stable;
only the duplicate-class cases above remain.

---------

Co-authored-by: leaysgur <6259812+leaysgur@users.noreply.github.com>
2026-11-04 05:48:27 +00:00
..
…