From d10df39ae6342d5e2dc7ec3eec3156f90256529f Mon Sep 17 00:00:00 2001 From: Dunqing <29533304+Dunqing@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:32:15 +0000 Subject: [PATCH] fix(formatter): resolve pending space in fits measurer before expanded-mode early exit (#20954) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #20519 Alternative to #20858 ## Summary The `fits()` measurer in the printer defers `Space` elements via a `pending_space` boolean flag. When the measurer exits early through an expanded-mode line break (`Fits::Yes`), any unresolved `pending_space` is silently lost — causing the measured width to be off by one. This caused the decorator in `@property(...) prop: Type` to incorrectly stay on the same line because the space after `:` was not counted in the width measurement. ### Root cause In `FitsMeasurer::fits_element`, `Space` sets `pending_space = true` (deferred), while `HardSpace` immediately increments `line_width`. When `fits()` returns `Fits::Yes` via the expanded-mode line break path (line 1040), the pending space is never resolved. For the decorator case: 1. After `@property(...)` + space + `tooltipPlacement` + `:` → `line_width = 80` 2. `Space` after `:` → `pending_space = true` (line_width stays 80) 3. Union type group inherits Expanded mode → `if_group_breaks(separator)` included 4. Separator's `SoftOrSpace` in Expanded mode → `return Fits::Yes` **without resolving pending_space** 5. The +1 char from the space is lost, making the decorator group incorrectly "fit" ### Fix Resolve `pending_space` before returning `Fits::Yes` from the expanded-mode early exit path. This matches Ruff's approach where `Space` is counted eagerly via `fits_text(Text::Token(" "))`. ### Conformance - JS: 746/753 (unchanged) - TS: 591/601 (unchanged — `18148.ts` fixed, `comment.ts` regressed) The `comment.ts` regression (`typescript/union/consistent-with-flow/comment.ts`) is a **pre-existing Prettier bug** — at `printWidth: 81`, Prettier itself produces the same double-indented output with a spurious blank line: ```ts // printWidth: 81 → Prettier also double-indents: type SuperLong...Blaa = ← spurious blank line | Fooo1000 ← 4 spaces (double indent) ``` At exactly 80 chars, Prettier avoids this only because its own fits check has the same off-by-one (the space is not counted), causing the Fluid layout's inner group to accidentally "fit". ## Test plan - [x] `cargo test -p oxc_formatter` — 262 passed - [x] `cargo test -p oxc_formatter -- fixtures` — 213 passed (including new `issue-20519.ts`) - [x] `cargo run -p oxc_prettier_conformance` — JS 746/753, TS 591/601 - [x] `cargo clippy -p oxc_formatter` — clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- apps/oxfmt/conformance/run.ts | 2 - .../src/formatter/printer/mod.rs | 11 +++ .../tests/fixtures/ts/class/issue-20519.ts | 23 +++++ .../fixtures/ts/class/issue-20519.ts.snap | 86 +++++++++++++++++++ .../snapshots/prettier.ts.snap.md | 2 +- 5 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 crates/oxc_formatter/tests/fixtures/ts/class/issue-20519.ts create mode 100644 crates/oxc_formatter/tests/fixtures/ts/class/issue-20519.ts.snap diff --git a/apps/oxfmt/conformance/run.ts b/apps/oxfmt/conformance/run.ts index 969a8b5591a2f..3d5ba834ad77f 100644 --- a/apps/oxfmt/conformance/run.ts +++ b/apps/oxfmt/conformance/run.ts @@ -96,8 +96,6 @@ const categories: Category[] = [ "js-in-html(`