Skip to content

fix(formatter): break call args instead of = for hugged template assignments#21913

Open
jsmecham wants to merge 1 commit intooxc-project:mainfrom
jsmecham:worktree-fixissue-21908
Open

fix(formatter): break call args instead of = for hugged template assignments#21913
jsmecham wants to merge 1 commit intooxc-project:mainfrom
jsmecham:worktree-fixissue-21908

Conversation

@jsmecham
Copy link
Copy Markdown
Contributor

@jsmecham jsmecham commented Apr 28, 2026

Summary

Fixes #21908

When a long-LHS variable declaration assigns the result of a single-template-arg call (e.g. graphql(/* GraphQL */ `...`)), oxfmt broke at = and kept the call hugged. Prettier instead keeps the assignment on one line and breaks the call's arguments out around the template.

Input

const xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx = graphql(
  /* GraphQL */ `
    query Q {
      x
    }
  `
);

Before (incorrect)

const xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx =
  graphql(/* GraphQL */ `
    query Q {
      x
    }
  `);

After (matches Prettier)

const xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx = graphql(
  /* GraphQL */ `
    query Q {
      x
    }
  `
);

Short-LHS cases stay hugged (matches Prettier):

const a = graphql(`
  query { x }
`);

Root cause

In print/call_like_expression/arguments.rs, the three template-hug predicates (is_multiline_template_only_args, is_graphql_call_with_single_template_arg, is_huggable_html_embed_single_arg) emitted (template) directly with no group. The template's hard newlines made the parent assignment's Fluid group exceed the print width, so the assignment broke at = — even though breaking the call's args would have produced a shorter, Prettier-matching layout.

In Prettier, shouldExpandLastArg returns true for embedded single-template args and uses conditionalGroup([hugged, hugged-with-break, allArgsBrokenOut]), so the assignment never breaks at =; the call args break instead when the hugged form overflows.

Fix

Split the existing hug branch into two paths:

  1. Always-hug (require, test calls, react hook deps) — unchanged, emits (args) directly. These paths never deal with multiline content, so unconditional hug is still correct.

  2. Template-hug (graphql / multiline template / html embed) — when the call is the immediate RHS of a VariableDeclarator or AssignmentExpression and the template was already multi-line in the source, wrap the args in best_fitting!:

    • Variant 1 (preferred): (template) — hugged
    • Variant 2 (fallback): (soft_block_indent(template)) — broken-out

    best_fitting acts as a "fits" boundary, so the assignment's Fluid group sees the args as fitting and doesn't break at =. When the hugged variant doesn't fit on the first line, the printer falls through to the broken-out variant.

    Single-line templates that get rewritten to multi-line by the embedded formatter still fall back to the unconditional hug, which lets will_break propagate to break the assignment at = — matching Prettier's breakParent behavior for embed labels.

    Non-assignment contexts (member chains, plain expression statements) keep the unconditional hug so the surrounding chain group decides how to break — preserving the existing expect(content).toMatch(`...`) chain-break behavior.

Tests

A new fixture crates/oxc_formatter/tests/fixtures/js/calls/template-arg-hug.js exercises five layout scenarios at both printWidth: 80 and printWidth: 100:

  • Short LHS hugs, no = break
  • Short LHS with leading comment still hugs
  • Long LHS at width 80 → broken-out args; at width 100 → hugs (validates the width-aware decision)
  • Member chain breaks at the dot, args stay hugged
  • Generic (non-graphql) multi-line template arg behaves the same way

Prettier conformance: 746/753 JS, 591/601 TS — no regressions vs. main.

AI Disclosure

This PR was co-authored with Claude Code (AI assistant), as noted in the commit. The fix was reviewed, tested against the formatter test suite and full Prettier conformance suite, and verified to produce no regressions.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 28, 2026

Merging this PR will not alter performance

✅ 44 untouched benchmarks
⏩ 7 skipped benchmarks1


Comparing jsmecham:worktree-fixissue-21908 (1aadd72) with main (4380812)

Open in CodSpeed

Footnotes

  1. 7 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@jsmecham jsmecham force-pushed the worktree-fixissue-21908 branch 4 times, most recently from d7b6bd0 to 7ba5543 Compare April 28, 2026 20:21
…signments

When the right-hand side of an assignment is a graphql/template hug call
(`graphql(`...`)`, `fn(/* lang */ `...`)`, etc.) where the template was
already multi-line in the source, prefer breaking the call's arguments out
over breaking at `=` when the hugged form would overflow the print width.

Single-line templates that get rewritten to multi-line by the embedded
formatter still fall back to the unconditional hug, which lets will_break
propagate to break the assignment at `=` — matching Prettier's `breakParent`
behavior for embed labels.

Member chains keep an unconditional hug so the surrounding group decides.

Fixes oxc-project#21908

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jsmecham jsmecham force-pushed the worktree-fixissue-21908 branch from 7ba5543 to 1aadd72 Compare April 28, 2026 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

formatter: long var + graphql() call breaks at = instead of inside, even with embedded GraphQL formatting

1 participant