fix(formatter): keep an arrow signature that exactly fills the line for any body (#27989)

Revert #24440 and reclassify DIVERGENCES.md#arrow-exact-fill-signature as "uniform-rule".

---

> It seems like oxc is correct here and Prettier has a bug?
> https://github.com/oxc-project/oxc/issues/24416

Having the opportunity to revisit the details..., I found that this is indeed the truth.
This commit is contained in:
leaysgur committed 2026-10-20 06:12:30 +00:00
1 parent 324432178d
commit 6912d1f70a
11 files changed
+222 -237

No files matched your search

+15 -9
View File
@@ -547,30 +547,36 @@ Prettier 3.9 changed this to first retry the whole union FLAT on the indented ne
Community pushback on the 3.9 style (users migrated to Oxfmt over it) put following on hold:
ours deliberately matches Prettier 3.8's output for this construct.
## arrow-chain-exact-fill-signature
## arrow-exact-fill-signature
- Why: cost
- Pin: `tests/fixtures/js/arrow-function/chain-exact-fill-signature.js`
- Why: uniform-rule (same construct, same output: arrow with a non-conditional body)
- Pin: `tests/fixtures/js/arrow-function/exact-fill-signature.js`
```js
/* input (printWidth 80) */
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLengthOkOkOkO = (x) => cond ? longlonglong : shortshort;
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLength = (x) => (y) => cond ? longlonglong : shortshort;
/* ours */
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLengthOkOkOkO = (x) =>
cond ? longlonglong : shortshort;
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLength = (x) => (y) =>
cond ? longlonglong : shortshort;
/* prettier */
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLengthOkOkOkO = (
x,
) => (cond ? longlonglong : shortshort);
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLength =
(x) => (y) => (cond ? longlonglong : shortshort);
```
An arrow chain with a parens-adding conditional body keeps a signature line that EXACTLY fills the print width (the signature alone is measured; both idempotent).
Prettier counts the hug layout's literal trailing space, so the exact fill overflows:
it breaks the assignment and retries the chain flat on the indented next line.
One char over and the outputs converge;
a naive port of the literal-space structure regresses `js/arrows/currying-4.js` (Prettier gates the hug on the chain's expand state),
so matching costs more than this exact-width edge is worth.
An arrow (single or chain) whose signature line EXACTLY fills the print width keeps it and puts the body on the next line, whatever the body.
Prettier breaks the signature only for a parens-adding conditional body:
its hug layout puts a literal space after `=>`, outside the group that may break the body onto the next line,
so the space is counted although it is trimmed whenever the body breaks.
As the last call argument, Prettier breaks all the arguments instead of hugging the arrow.
One char over and the outputs converge.
## suppressed-for-head-declaration
@@ -169,6 +169,8 @@ impl<'a, 'b> FormatJsArrowFunctionExpression<'a, 'b> {
}),
};
// A cast-wrapped body has no kind (see `is_cast_target`)
// Prettier also keeps a parens-adding conditional on the same line (its hug layout);
// we don't, see DIVERGENCES.md#arrow-exact-fill-signature
body_kind_hugs && !is_cast_target(expression.span(), f)
});
@@ -201,34 +203,18 @@ impl<'a, 'b> FormatJsArrowFunctionExpression<'a, 'b> {
|| (matches!(self.arrow.parent(), AstNodes::JSXExpressionContainer(container)
if !f.context().comments().has_comment_in_range(arrow.span.end, container.span.end)));
if should_add_parens {
// The leading space must be a literal space rather than a soft line:
// it is counted when measuring whether the signature fits,
// so a signature that fills the line exactly gets broken,
// matching Prettier's `printArrowFunctionBody`.
write!(
f,
[
space(),
group(&format_args!(
if_group_fits_on_line(&token("(")),
indent(&format_args!(soft_line_break(), format_body)),
if_group_fits_on_line(&token(")")),
is_last_call_arg.then_some(&FormatTrailingCommas::All),
should_add_soft_line.then_some(soft_line_break())
))
]
);
} else {
write!(
f,
group(&format_args!(
soft_line_indent_or_space(&format_body),
is_last_call_arg.then_some(&FormatTrailingCommas::All),
should_add_soft_line.then_some(soft_line_break())
))
);
}
write!(
f,
group(&format_args!(
soft_line_indent_or_space(&format_args!(
should_add_parens.then_some(if_group_fits_on_line(&token("("))),
format_body,
should_add_parens.then_some(if_group_fits_on_line(&token(")")))
)),
is_last_call_arg.then_some(&FormatTrailingCommas::All),
should_add_soft_line.then_some(soft_line_break())
))
);
}
}
}
@@ -442,6 +428,8 @@ impl<'a> Format<'a, JsFormatContext<'a>> for ArrowChain<'a, '_> {
// soft line break before the body so that it prints on a separate line
// in its entirety.
// A cast-wrapped body has no kind (see `is_cast_target`).
// Prettier also keeps a parens-adding conditional on the same line (its hug layout) unless the chain breaks;
// we don't, see DIVERGENCES.md#arrow-exact-fill-signature.
let body_on_separate_line = !tail.get_expression().is_none_or(|expression| {
matches!(
expression,
@@ -597,25 +585,14 @@ impl<'a> Format<'a, JsFormatContext<'a>> for ArrowChain<'a, '_> {
}
} else {
let should_add_parens = tail_body.as_expression().is_some_and(should_add_parens);
if should_add_parens {
// Known divergence from Prettier: with a signature that exactly fills the line,
// Prettier breaks it because the hug layout's literal space is counted
// when measuring fits (see the single-arrow branch above), while this soft-line
// wrapping (`soft_line_indent_or_space` in `format_tail_body`) stops the measurement.
// Porting the literal-space structure here is NOT enough: Prettier gates the hug
// on `!shouldBreakChain` (`expand_signatures` here) and otherwise breaks without
// parens; a naive port regresses `js/arrows/currying-4.js`.
write!(
f,
[
if_group_fits_on_line(&token("(")),
format_tail_body,
if_group_fits_on_line(&token(")"))
]
);
} else {
write!(f, [format_tail_body]);
}
write!(
f,
[
should_add_parens.then_some(if_group_fits_on_line(&token("("))),
format_tail_body,
should_add_parens.then_some(if_group_fits_on_line(&token(")")))
]
);
}
});
@@ -1,12 +0,0 @@
// An arrow chain with a parens-adding conditional body keeps a signature line
// that exactly fills the print width, body indented on the next line.
// DIVERGES from Prettier (DIVERGENCES.md#arrow-chain-exact-fill-signature),
// which counts the hug layout's literal trailing space when measuring the fit,
// breaks the assignment, and retries the chain flat on the indented next line
// (with parens); layout-only, triggers only on an exact width match.
// The first chain diverges at printWidth 80, the second at 100; at the
// respectively other width both formatters agree.
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLength = (x) => (y) =>
cond ? longlonglong : shortshort;
const exactlyFillsThePrintWidthOneHundredSignatureLinePaddedOutToTheFullLineLengthOk = (x) => (y) =>
cond ? longlonglong : shortshort;
@@ -1,51 +0,0 @@
---
source: crates/oxc_formatter/tests/fixtures/mod.rs
---
==================== Input ====================
// An arrow chain with a parens-adding conditional body keeps a signature line
// that exactly fills the print width, body indented on the next line.
// DIVERGES from Prettier (DIVERGENCES.md#arrow-chain-exact-fill-signature),
// which counts the hug layout's literal trailing space when measuring the fit,
// breaks the assignment, and retries the chain flat on the indented next line
// (with parens); layout-only, triggers only on an exact width match.
// The first chain diverges at printWidth 80, the second at 100; at the
// respectively other width both formatters agree.
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLength = (x) => (y) =>
cond ? longlonglong : shortshort;
const exactlyFillsThePrintWidthOneHundredSignatureLinePaddedOutToTheFullLineLengthOk = (x) => (y) =>
cond ? longlonglong : shortshort;
==================== Output ====================
------------------
{ printWidth: 80 }
------------------
// An arrow chain with a parens-adding conditional body keeps a signature line
// that exactly fills the print width, body indented on the next line.
// DIVERGES from Prettier (DIVERGENCES.md#arrow-chain-exact-fill-signature),
// which counts the hug layout's literal trailing space when measuring the fit,
// breaks the assignment, and retries the chain flat on the indented next line
// (with parens); layout-only, triggers only on an exact width match.
// The first chain diverges at printWidth 80, the second at 100; at the
// respectively other width both formatters agree.
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLength = (x) => (y) =>
cond ? longlonglong : shortshort;
const exactlyFillsThePrintWidthOneHundredSignatureLinePaddedOutToTheFullLineLengthOk =
(x) => (y) => (cond ? longlonglong : shortshort);
-------------------
{ printWidth: 100 }
-------------------
// An arrow chain with a parens-adding conditional body keeps a signature line
// that exactly fills the print width, body indented on the next line.
// DIVERGES from Prettier (DIVERGENCES.md#arrow-chain-exact-fill-signature),
// which counts the hug layout's literal trailing space when measuring the fit,
// breaks the assignment, and retries the chain flat on the indented next line
// (with parens); layout-only, triggers only on an exact width match.
// The first chain diverges at printWidth 80, the second at 100; at the
// respectively other width both formatters agree.
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLength = (x) => (y) =>
cond ? longlonglong : shortshort;
const exactlyFillsThePrintWidthOneHundredSignatureLinePaddedOutToTheFullLineLengthOk = (x) => (y) =>
cond ? longlonglong : shortshort;
===================== End =====================
@@ -0,0 +1,22 @@
// 10 20 30 40 50 60 70 80
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
// DIVERGENCES.md#arrow-exact-fill-signature
// Conditional body: diverges at printWidth 80
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLengthOkOkOkO = (x) =>
cond ? longlonglong : shortshort;
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLength = (x) => (y) =>
cond ? longlonglong : shortshort;
vi.mocked(useCommandStore().formatKeySequenceXY).mockImplementation((command) =>
cond ? longlonglong : shortshort,
);
// Call body: Prettier keeps the signature too
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLengthOkOkOkO = (x) =>
callSomething(longlonglong, shortshort);
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLength = (x) => (y) =>
callSomething(longlonglong, shortshort);
vi.mocked(useCommandStore().formatKeySequenceXY).mockImplementation((command) =>
callSomething(longlonglong, shortshort),
);
@@ -0,0 +1,81 @@
---
source: crates/oxc_formatter/tests/fixtures/mod.rs
---
==================== Input ====================
// 10 20 30 40 50 60 70 80
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
// DIVERGENCES.md#arrow-exact-fill-signature
// Conditional body: diverges at printWidth 80
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLengthOkOkOkO = (x) =>
cond ? longlonglong : shortshort;
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLength = (x) => (y) =>
cond ? longlonglong : shortshort;
vi.mocked(useCommandStore().formatKeySequenceXY).mockImplementation((command) =>
cond ? longlonglong : shortshort,
);
// Call body: Prettier keeps the signature too
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLengthOkOkOkO = (x) =>
callSomething(longlonglong, shortshort);
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLength = (x) => (y) =>
callSomething(longlonglong, shortshort);
vi.mocked(useCommandStore().formatKeySequenceXY).mockImplementation((command) =>
callSomething(longlonglong, shortshort),
);
==================== Output ====================
------------------
{ printWidth: 80 }
------------------
// 10 20 30 40 50 60 70 80
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
// DIVERGENCES.md#arrow-exact-fill-signature
// Conditional body: diverges at printWidth 80
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLengthOkOkOkO = (x) =>
cond ? longlonglong : shortshort;
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLength = (x) => (y) =>
cond ? longlonglong : shortshort;
vi.mocked(useCommandStore().formatKeySequenceXY).mockImplementation((command) =>
cond ? longlonglong : shortshort,
);
// Call body: Prettier keeps the signature too
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLengthOkOkOkO = (x) =>
callSomething(longlonglong, shortshort);
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLength = (x) => (y) =>
callSomething(longlonglong, shortshort);
vi.mocked(useCommandStore().formatKeySequenceXY).mockImplementation((command) =>
callSomething(longlonglong, shortshort),
);
-------------------
{ printWidth: 100 }
-------------------
// 10 20 30 40 50 60 70 80
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
// DIVERGENCES.md#arrow-exact-fill-signature
// Conditional body: diverges at printWidth 80
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLengthOkOkOkO = (x) =>
cond ? longlonglong : shortshort;
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLength = (x) => (y) =>
cond ? longlonglong : shortshort;
vi.mocked(useCommandStore().formatKeySequenceXY).mockImplementation((command) =>
cond ? longlonglong : shortshort,
);
// Call body: Prettier keeps the signature too
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLengthOkOkOkO = (x) =>
callSomething(longlonglong, shortshort);
const exactlyFillsThePrintWidthEightySignatureLinePaddedToLength = (x) => (y) =>
callSomething(longlonglong, shortshort);
vi.mocked(useCommandStore().formatKeySequenceXY).mockImplementation((command) =>
callSomething(longlonglong, shortshort),
);
===================== End =====================
@@ -0,0 +1,17 @@
// 10 20 30 40 50 60 70 80
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
// Each child line fills the print width (80) exactly, then `{" "}` overflows it:
// the child stays on its line, the fill entry boundary does not count the pending space
<p>
Run <span className="dynamic-info">{toolNameToolNameToolNameToolNameTo}</span>{" "}
with args:{" "}
</p>;
<p>
Abort {switchBlocked?.blockingOp === "merge" ? "MergeMergeMergeMe" : "Rebase"}{" "}
&amp; Switch
</p>;
<p>
<ValueText style={valueStyle}>{formatValue(num1)}</ValueText> {t("andandand")}{" "}
<ValueText style={valueStyle}>{formatValue(num2)}</ValueText>
</p>;
@@ -0,0 +1,62 @@
---
source: crates/oxc_formatter/tests/fixtures/mod.rs
---
==================== Input ====================
// 10 20 30 40 50 60 70 80
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
// Each child line fills the print width (80) exactly, then `{" "}` overflows it:
// the child stays on its line, the fill entry boundary does not count the pending space
<p>
Run <span className="dynamic-info">{toolNameToolNameToolNameToolNameTo}</span>{" "}
with args:{" "}
</p>;
<p>
Abort {switchBlocked?.blockingOp === "merge" ? "MergeMergeMergeMe" : "Rebase"}{" "}
&amp; Switch
</p>;
<p>
<ValueText style={valueStyle}>{formatValue(num1)}</ValueText> {t("andandand")}{" "}
<ValueText style={valueStyle}>{formatValue(num2)}</ValueText>
</p>;
==================== Output ====================
------------------
{ printWidth: 80 }
------------------
// 10 20 30 40 50 60 70 80
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
// Each child line fills the print width (80) exactly, then `{" "}` overflows it:
// the child stays on its line, the fill entry boundary does not count the pending space
<p>
Run <span className="dynamic-info">{toolNameToolNameToolNameToolNameTo}</span>{" "}
with args:{" "}
</p>;
<p>
Abort {switchBlocked?.blockingOp === "merge" ? "MergeMergeMergeMe" : "Rebase"}{" "}
&amp; Switch
</p>;
<p>
<ValueText style={valueStyle}>{formatValue(num1)}</ValueText> {t("andandand")}{" "}
<ValueText style={valueStyle}>{formatValue(num2)}</ValueText>
</p>;
-------------------
{ printWidth: 100 }
-------------------
// 10 20 30 40 50 60 70 80
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
// Each child line fills the print width (80) exactly, then `{" "}` overflows it:
// the child stays on its line, the fill entry boundary does not count the pending space
<p>
Run <span className="dynamic-info">{toolNameToolNameToolNameToolNameTo}</span> with args:{" "}
</p>;
<p>Abort {switchBlocked?.blockingOp === "merge" ? "MergeMergeMergeMe" : "Rebase"} &amp; Switch</p>;
<p>
<ValueText style={valueStyle}>{formatValue(num1)}</ValueText> {t("andandand")}{" "}
<ValueText style={valueStyle}>{formatValue(num2)}</ValueText>
</p>;
===================== End =====================
@@ -1,24 +0,0 @@
// 10 20 30 40 50 60 70 80
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
// Every signature below is exactly 80 chars including `=>`.
// A conditional body plans the hug layout `=> (cond ? a : b)`,
// whose literal space is counted when measuring the signature: 81 > 80 breaks the params.
const MachineConsoleLink = ({ ipAddress }: { ipAddress: string | undefined }) =>
ipAddress ? (
<a target={"_blank"} rel="noreferrer" href={`http://${ipAddress}`}>
{ipAddress}
</a>
) : (
"--"
);
// Non-JSX conditional body: params break, then the body hugs with parens.
const MachineConsoleLinkAA = ({ hostName }: { hostName: string | undefined }) =>
hostName ? hostName : "--";
// Call body: the body starts with a soft line instead of the hug space,
// so the measurement stops at 80 and the signature is kept.
const MachineConsoleLinkAB = ({ hostName }: { hostName: string | undefined }) =>
foo(hostName);
@@ -1,93 +0,0 @@
---
source: crates/oxc_formatter/tests/fixtures/mod.rs
---
==================== Input ====================
// 10 20 30 40 50 60 70 80
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
// Every signature below is exactly 80 chars including `=>`.
// A conditional body plans the hug layout `=> (cond ? a : b)`,
// whose literal space is counted when measuring the signature: 81 > 80 breaks the params.
const MachineConsoleLink = ({ ipAddress }: { ipAddress: string | undefined }) =>
ipAddress ? (
<a target={"_blank"} rel="noreferrer" href={`http://${ipAddress}`}>
{ipAddress}
</a>
) : (
"--"
);
// Non-JSX conditional body: params break, then the body hugs with parens.
const MachineConsoleLinkAA = ({ hostName }: { hostName: string | undefined }) =>
hostName ? hostName : "--";
// Call body: the body starts with a soft line instead of the hug space,
// so the measurement stops at 80 and the signature is kept.
const MachineConsoleLinkAB = ({ hostName }: { hostName: string | undefined }) =>
foo(hostName);
==================== Output ====================
------------------
{ printWidth: 80 }
------------------
// 10 20 30 40 50 60 70 80
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
// Every signature below is exactly 80 chars including `=>`.
// A conditional body plans the hug layout `=> (cond ? a : b)`,
// whose literal space is counted when measuring the signature: 81 > 80 breaks the params.
const MachineConsoleLink = ({
ipAddress,
}: {
ipAddress: string | undefined;
}) =>
ipAddress ? (
<a target={"_blank"} rel="noreferrer" href={`http://${ipAddress}`}>
{ipAddress}
</a>
) : (
"--"
);
// Non-JSX conditional body: params break, then the body hugs with parens.
const MachineConsoleLinkAA = ({
hostName,
}: {
hostName: string | undefined;
}) => (hostName ? hostName : "--");
// Call body: the body starts with a soft line instead of the hug space,
// so the measurement stops at 80 and the signature is kept.
const MachineConsoleLinkAB = ({ hostName }: { hostName: string | undefined }) =>
foo(hostName);
-------------------
{ printWidth: 100 }
-------------------
// 10 20 30 40 50 60 70 80
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
// Every signature below is exactly 80 chars including `=>`.
// A conditional body plans the hug layout `=> (cond ? a : b)`,
// whose literal space is counted when measuring the signature: 81 > 80 breaks the params.
const MachineConsoleLink = ({ ipAddress }: { ipAddress: string | undefined }) =>
ipAddress ? (
<a target={"_blank"} rel="noreferrer" href={`http://${ipAddress}`}>
{ipAddress}
</a>
) : (
"--"
);
// Non-JSX conditional body: params break, then the body hugs with parens.
const MachineConsoleLinkAA = ({ hostName }: { hostName: string | undefined }) =>
hostName ? hostName : "--";
// Call body: the body starts with a soft line instead of the hug space,
// so the measurement stops at 80 and the signature is kept.
const MachineConsoleLinkAB = ({ hostName }: { hostName: string | undefined }) => foo(hostName);
===================== End =====================
@@ -7,7 +7,7 @@ checker.ts:
sys peak growth: 2973842 # 2.97 MB
arena allocs: 1058560
arena reallocs: 927
arena size: 89926872 # 89.93 MB
arena size: 89923584 # 89.92 MB
App.tsx:
file size: 415342 # 415.34 kB