tsv_ts 0.8.0

TypeScript/JS formatter, type stripper and acorn-compatible parser — part of tsv
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
// Chain-specific argument building for call expressions
//
// Handles building call arguments in chain contexts where the callee
// is handled separately by the chain printer.
//
// Every state LADDER here is the plain-call / `new` one read through `ArgOpener` — the chain
// spelling differs only in what a state opens with (its own `(` / `?.(` rather than a callee
// doc), so `arg_wrapping.rs` owns the layouts and this file owns the strategy selection. What
// is still this file's alone is the selection ORDER and the arms the other trees have no twin
// for (the force-expanded hugs, the classify-and-wrap fallback).

use super::super::chain::{
    ChainCall, transparent_callee_sealed_chain, transparent_instantiation_callee,
};
use super::super::comments::CommentSpacing;
use super::super::{Printer, ShareTag, is_curried_arrow_chain, is_multiline_template_expression};
use super::arg_comments::{
    PartitionedComments, any_arg_empty_line, any_comment_forces_expansion, build_arg_gap_docs,
    build_empty_args_parens_doc, emit_last_arg_trailing_comments, first_arg_has_any_comments,
    has_inter_argument_comments, has_trailing_comments_on_args, last_arg_has_comments,
};
use super::arg_predicates::{
    arrow_body_breaks_after_space, arrow_body_is_call_through_non_null, is_block_function,
    is_function_composition_args, is_ternary_arrow_body, last_arg_is_array_or_object,
    lone_arg_params_render_flat, mark_boolean_coercion_argument,
};
use super::arg_wrapping::{
    ArgOpener, AssumedHugKind, AssumedParamHug, ChainArgKind, arrow_body_expands_internally,
    arrow_body_tail_has_comments, arrow_hug_refused_by_comments, build_args_split_last,
    build_arrow_gap_break_multi_arg_doc, build_arrow_gap_break_single_arg_doc, build_arrow_sig_doc,
    build_break_body_ladder, build_expand_first_arg_doc, build_expand_last_obj_array_doc,
    build_flat_params_arg_doc, build_hugged_arrow_sig_doc, build_joined_argument_doc,
    build_printed_argument_doc, build_single_arrow_hug_doc, build_ternary_arrow_hug_ladder,
    classify_chain_arg, first_arg_signature_refuses_expand_first, last_arg_arrow_gap_break,
    last_two_args_same_type, prebuild_expand_last_break_body, prebuild_expand_last_obj_array_body,
    prepend_arrow_body_comments, prepend_hugged_arrow_body_comments, should_expand_first_arg,
    sole_template_on_its_own_line, try_hook_deps_args_doc,
};
use crate::ast::{self, Expression, ExpressionKind};
use crate::printer::expressions::functions::{
    arrow_signature_refuses_hug, callback_signature_refuses_hug, prepend_leading,
};
use tsv_lang::Span;
use tsv_lang::doc::DocBuf;
use tsv_lang::doc::arena::DocArena;
use tsv_lang::doc::arena::DocId;

/// The doc a chain call prints ahead of its `(`: an optional call's `?.` half, the
/// callee→type-arguments comments and the type arguments themselves — `None` when the call
/// has neither. One builder for the call's own layouts and for a probe measuring the line up
/// to that `(` (the member chain's type-argument hug state, through
/// [`Printer::chain_call_head_doc`]).
// with two callers LLVM outlines it, a call on every chain call for a head that is rarely there
#[expect(clippy::inline_always)]
#[inline(always)]
pub(super) fn chain_call_head_doc(
    printer: &Printer<'_>,
    call: &ast::CallExpression<'_>,
    gap: super::CalleeGap,
) -> Option<DocId> {
    let d = printer.d();
    let optional_doc = super::optional_callee_gap_doc(printer, gap);
    // A transparent instantiation callee's list (`T<U>?.(a)`) belongs to the INSTANTIATION:
    // it prints ahead of the `?.` (behind it, `T?.<U>(a)` moves the list onto the call, a
    // different program), and its comment window is the expression→`<` gap the linearized
    // base leaves unclaimed — the callee gap opens past the `>`, so a window opened there
    // would invert and drop every comment in it.
    if let Some(inst) = transparent_instantiation_callee(call) {
        let ta = &inst.type_arguments;
        let mut parts = DocBuf::new();
        parts.extend(printer.build_name_to_type_params_comments_opt(
            instantiation_window_start(printer, call, inst),
            ta.span.start,
            CommentSpacing::Trailing,
        ));
        parts.push(shared_chain_type_args_doc(printer, ta));
        parts.extend(optional_doc);
        return Some(d.concat(&parts));
    }
    let type_args = call.type_arguments.as_ref();
    if optional_doc.is_none() && type_args.is_none() {
        return None;
    }
    let mut parts = DocBuf::new();
    parts.extend(optional_doc);
    if let Some(ta) = type_args {
        // Emit comments between callee and type args: `obj.fn/* c */ <string>()`
        // Uses build_name_to_type_params_comments for safe line comment handling
        parts.extend(printer.build_name_to_type_params_comments_opt(
            gap.start,
            ta.span.start,
            CommentSpacing::Trailing,
        ));
        parts.push(shared_chain_type_args_doc(printer, ta));
    }
    Some(d.concat(&parts))
}

/// A chain call's type arguments, built once for every layout candidate of the chain that
/// prints the call's head ([`ShareTag::ChainTypeArgs`]) — the one-line, expanded and
/// type-argument hug states and the hug's probe line each print it. Under the hug's assumed
/// break the key declines, so that printing builds its own.
fn shared_chain_type_args_doc(
    printer: &Printer<'_>,
    ta: &ast::TSTypeParameterInstantiation<'_>,
) -> DocId {
    let key = printer.chain_share_key_of(ta, ta.span, ShareTag::ChainTypeArgs);
    printer.chain_shared_doc(key, || printer.build_type_parameter_instantiation_doc(ta))
}

/// Where a transparent instantiation callee's expression→`<` window opens: past the `)`
/// of a sealed optional chain's pair (`(a?.b /* t */)<T>?.(c).d`), whose base emits the
/// operand→`)` gap inside that pair ([`transparent_callee_sealed_chain`]), else at the
/// expression's end. Opened at the expression's end, the window would print the comment twice,
/// inside the pair and again ahead of the `<`.
fn instantiation_window_start(
    printer: &Printer<'_>,
    call: &ast::CallExpression<'_>,
    inst: &ast::TSInstantiationExpression<'_>,
) -> u32 {
    let end = inst.expression.span().end;
    Printer::gap_start_after_owned_pair(
        end,
        printer.owned_pair_trailing_gap(end, transparent_callee_sealed_chain(call).is_some()),
    )
}

/// The `(`→first-argument gap for the chain's **body-line** paths, as a doc: the blocks the
/// author left on the `(`'s line and the rest of the gap, as ONE leading run
/// ([`PartitionedComments::emit_unpulled_gap`] — the same answer
/// [`super::arg_comments::emit_first_arg_leading_comments`] gives a block-only gap).
///
/// The force-expanded builder takes it too, for the gap its own delimiter-line pull declined
/// ([`PartitionedComments::pulls_to_delimiter_line`]) — the whole gap leads the argument
/// there, which is exactly this doc.
///
/// ⚠️ **Emitting a FILTERED subset here is a DROP waiting for its gate to move.** The loop
/// this replaces kept only the comments an item-boundary predicate called inline with the
/// argument and discarded the rest, on the standing promise that anything else had already
/// forced the expanded path. The moment the expansion gate stopped calling a glued run given
/// its own line "own-line" — which is what prettier's own soft `line` says about it —
/// `obj.m1().m2(⏎/* c1 */ /* c2 */⏎a)` arrived here and BOTH comments vanished, with the
/// whole fixture suite and every audit green. A gap emitter that can only print PART of what
/// it is handed is a claim about its gate, and no gate stated that claim.
///
/// ⚠️ **It still cannot delegate to
/// [`super::arg_comments::emit_first_arg_leading_comments`]**, and the reason is
/// the shape rather than the rules: that emitter has a second output channel for the
/// `(`-**line** run (a `//` trailing the `(` stays on it), while this one returns a single
/// doc its callers splice into the argument body. Callers that can reach such a comment
/// place it themselves first (the body-line arms under `!comments_force_expansion`, the
/// force-expanded builder under a declined pull) and consume this doc for the rest — but
/// `build_call_args_doc_for_chain_impl` builds it EAGERLY, for arms that then never run, so
/// "no `(`-line comment reaches here" is false as an invariant on this function even though
/// it holds at every use. Asserting it fails on `calls/chain_open_paren_comment`, which is
/// how this note came to be measured rather than guessed.
fn build_inline_leading_comments(
    printer: &Printer<'_>,
    paren_open: u32,
    arg_start: u32,
) -> Option<DocId> {
    let mut parts = DocBuf::new();
    // A `//` trailing the `(` stays out of the run — its callers place it (see above).
    PartitionedComments::new(printer, paren_open, arg_start).emit_unpulled_gap(&mut parts, printer);
    (!parts.is_empty()).then(|| printer.d().concat(&parts))
}

/// Build inline trailing block comments for an argument (non-expansion path).
///
/// Used for the last arg (comments before closing paren) and single-arg paths
/// where there's no comma to split around — i.e. always a **closer** gap, which is why it
/// takes [`PartitionedComments::for_closer_gap`] rather than the inter-item walk. The two
/// differ only in their claim on a **line** comment, and this emitter reads only
/// `trailing_block`, so the constructor is output-neutral here today; it is the contract
/// that matters, since the inter-item walk's rule is wrong for a gap holding the comma
/// `trailingComma: 'none'` deletes and would start mattering the moment this emitted more.
fn build_inline_trailing_comments(
    printer: &Printer<'_>,
    arg_end: u32,
    next_boundary: u32,
) -> Option<DocId> {
    let d = printer.d();
    let pc = PartitionedComments::for_closer_gap(printer, arg_end, next_boundary);

    if !pc.has_trailing_block() {
        return None;
    }

    let mut parts = DocBuf::new();
    for comment in &pc.trailing_block {
        parts.push(d.text(" "));
        parts.push(printer.build_comment_doc(comment));
    }
    Some(d.concat(&parts))
}

/// Build a Doc for call arguments only (for chain printing)
///
/// Uses proper group wrapping so args can break independently from the chain.
/// This allows the chain's conditionalGroup to try:
/// 1. Everything inline
/// 2. Args broken but chain inline (if args are in their own group)
/// 3. Chain broken (if args broken still doesn't fit)
pub(super) fn build_call_args_doc_for_chain(
    printer: &Printer<'_>,
    call: &ast::CallExpression<'_>,
    span: Span,
    facts: ChainCall,
) -> DocId {
    build_call_args_doc_for_chain_impl(printer, call, span, facts, false)
}

/// A lone arrow argument with an EXPRESSION body — the shape every hug arm in
/// [`build_chain_args_force_expand`] shares, before each asks its own body kind
/// (an expandable literal, or the breakable call / ternary that takes the
/// `(sig =>\n body,\n)` layout even when the chain forces expansion).
fn single_arrow_expression_body<'a>(
    call: &'a ast::CallExpression<'a>,
) -> Option<(
    &'a ast::ArrowFunctionExpression<'a>,
    Span,
    &'a Expression<'a>,
)> {
    if call.arguments.len() != 1 {
        return None;
    }
    let arg = &call.arguments[0];
    let ExpressionKind::ArrowFunctionExpression(arrow) = &arg.kind else {
        return None;
    };
    match &arrow.body {
        ast::ArrowFunctionBody::Expression(body) => Some((arrow, arg.span, body)),
        ast::ArrowFunctionBody::BlockStatement(_) => None,
    }
}

/// The hugged-arrow layout both expandable-literal arms emit: `(sig => <body>)`,
/// the signature hugged to the call paren and the body expanding internally behind
/// its own delimiters. `body_doc` arrives already built — the object arm's
/// grammar-required parens are the only thing the two assemble differently.
fn build_hugged_arrow_arg_doc(
    printer: &Printer<'_>,
    head: ChainHead,
    ctx: ChainArgsContext,
    arrow: &ast::ArrowFunctionExpression<'_>,
    span: Span,
    body_expr: &Expression<'_>,
    body_doc: DocId,
) -> DocId {
    let d = printer.d();
    let body_doc =
        prepend_hugged_arrow_body_comments(printer, arrow, body_expr.span().start, body_doc);
    let sig_doc = prepend_leading(
        d,
        ctx.leading_comment_doc,
        build_arrow_sig_doc(printer, arrow, span),
    );
    with_chain_head(
        d,
        head,
        d.concat(&[ctx.prefix, sig_doc, d.text(" => "), body_doc, d.text(")")]),
    )
}

/// Build a Doc for call arguments with forced expansion (hardlines instead of softlines)
///
/// Used for the "args expanded, chain inline" state in conditionalGroup.
pub(super) fn build_call_args_doc_for_chain_expanded(
    printer: &Printer<'_>,
    call: &ast::CallExpression<'_>,
    span: Span,
    facts: ChainCall,
) -> DocId {
    build_call_args_doc_for_chain_impl(printer, call, span, facts, true)
}

/// Shared per-call state computed once in `build_call_args_doc_for_chain_impl`'s
/// prologue and threaded into the `build_chain_args_*` branch builders — the
/// `(`/`?.(` prefix, the opening-paren position, the precomputed comment flags,
/// and the inline leading-comment doc. Built once, then moved into whichever
/// branch runs (mirrors `ClassMemberHeader` in the class parser).
#[derive(Clone, Copy)]
#[expect(clippy::struct_excessive_bools)] // independent prologue flags, not a state machine
struct ChainArgsContext {
    paren_open: u32,
    /// The `(` / `?.(` opener as a doc, built once where its spelling is a literal (a
    /// constant there; every layout below emits it as a node, none re-spells it).
    prefix: DocId,
    /// [`ChainCall::own_call_layout`] — whether the call-level layout rules apply to this
    /// link at all. Threaded here rather than re-derived, because it is a fact about the
    /// link's POSITION in the chain and no branch builder can see one.
    own_call_layout: bool,
    /// [`ChainCall::ungroup_args`] — the default tail prints the list without its own group.
    /// Threaded here for the reason `own_call_layout` is: whether the call is the callee of a
    /// call taking fewer arguments is a fact about its position in the chain.
    ungroup_args: bool,
    /// **to emit**: a non-owned leading comment before the first argument (emitted here).
    has_leading_comments: bool,
    /// **on page**: any leading comment before the first argument, **owned or not** — an
    /// owned comment rides inside the argument's own doc but still defeats the argument
    /// hug (prettier's `shouldExpandLastArg` returns false for a leading-commented arg). A
    /// to-emit gate would go blind to it and wrongly hug.
    has_leading_comment_on_page: bool,
    has_any_comments: bool,
    /// Whether the argument window puts any comment text on the page, **including** a
    /// comment a node owns and prints itself. The superset of `has_any_comments`, which is
    /// built from emit-keyed scans and so cannot see an owned comment. Layout gates take
    /// this one — an owned annotation on the last argument must refuse the expand-last hug
    /// exactly as an ordinary leading comment does.
    has_any_comment_text: bool,
    /// **on page**: a comment attaches to the last argument — leading it or trailing it
    /// before the `)`. The one predicate every argument-hug arm in all three branch
    /// builders asks, because prettier's `shouldExpandLastArg` returns false for a
    /// commented argument and because the hug layouts reassemble the argument from its
    /// signature and body docs, leaving its trailing gap with no emitter at all. Computed
    /// here rather than per builder: it was three identical re-derivations, and an arm that
    /// asked its own narrower question is exactly how the argument→`)` gap came to be
    /// DROPPED under forced expansion.
    last_arg_commented: bool,
    has_trailing_block_comments: bool,
    comments_force_expansion: bool,
    leading_comment_doc: Option<DocId>,
}

/// Implementation for call args doc building
fn build_call_args_doc_for_chain_impl(
    printer: &Printer<'_>,
    call: &ast::CallExpression<'_>,
    span: Span,
    facts: ChainCall,
    force_expand: bool,
) -> DocId {
    let ChainCall {
        own_call_layout,
        ungroup_args,
    } = facts;
    // Prettier's `anyArgEmptyLine` — the shared predicate, so a member chain's arguments
    // answer the blank question exactly as a plain call's do.
    let any_arg_empty_line = any_arg_empty_line(call.arguments, printer);

    // Every window past the callee, off the family's ONE derivation ([`super::CalleeGap`]) —
    // past the `)` of an IIFE callee's own pair, which emitted its trailing gap itself, and
    // past an optional call's `?.`, whose callee-side half this function emits below. This
    // was open-coded here, a third site of the derivation that struct's ⚠️ warns about; the
    // hand-rolled copy had no `?.` step at all, which is how the callee-side comment reached
    // the argument-leading scan and printed inside the parens.
    let gap = super::callee_gap(printer, call, span);
    let paren_open = gap.paren_open(call);

    // Check for any comments in arguments (leading, inter-argument, or trailing)
    // Note: presence of comments doesn't necessarily mean expansion - only line comments
    // and block comments on their own line force expansion.
    //
    // Whole-call comment-presence gate (one binary search over [paren_open,
    // span.end]): every sub-scan below lies within that window, so with no
    // comment they are all provably false — skip them. Canonical reference:
    // build_params_doc_with_comments.
    // Counts owned comments — it only short-circuits the sub-scans below, and an owned
    // comment still puts text on the page. The sub-scans themselves stay emit-keyed
    // (`has_comments_to_emit_between`): the owning node prints those, so there is nothing to emit.
    let call_has_comments = printer.has_comments_on_page_between(paren_open, span.end);
    let has_leading_comments = call_has_comments
        && !call.arguments.is_empty()
        && printer.has_comments_to_emit_between(paren_open, call.arguments[0].span().start);
    // Layout counterpart of `has_leading_comments`: counts an owned leading comment too.
    let has_leading_comment_on_page = call_has_comments
        && !call.arguments.is_empty()
        && printer.has_comments_on_page_between(paren_open, call.arguments[0].span().start);
    let has_inter_arg_comments = call_has_comments && has_inter_argument_comments(call, printer);
    let has_trailing_comments =
        call_has_comments && has_trailing_comments_on_args(call, span, printer);
    // Also check for trailing block comments on last arg (for inline handling).
    let has_trailing_block_comments = call_has_comments
        && call
            .arguments
            .last()
            .is_some_and(|last| printer.has_comments_to_emit_between(last.span().end, span.end));
    // A spread's stripped parens can hide a comment *before* the argument's own end,
    // where no scan above reaches it and the collapsing paths below drop it.
    let has_spread_paren_comments =
        call_has_comments && printer.any_paren_interior_forces_expansion(call.arguments);
    let has_any_comments = has_leading_comments
        || has_inter_arg_comments
        || has_trailing_comments
        || has_trailing_block_comments
        || has_spread_paren_comments;
    // The argument-hug refusal, asked by every branch builder — see `ChainArgsContext`.
    // Guarded by the same window search, so a comment-free call pays nothing for it.
    let last_arg_commented =
        call_has_comments && last_arg_has_comments(call.arguments, printer, span.end, paren_open);

    // Build leading comment doc once for reuse in single-arg arrow paths
    // (e.g., /** @param {any} x */ before arrow function parameters)
    let leading_comment_doc = if has_leading_comments && !call.arguments.is_empty() {
        build_inline_leading_comments(printer, paren_open, call.arguments[0].span().start)
    } else {
        None
    };

    // Check if any comments require expansion (line comments or block comments on own line)
    // Inline block comments don't force expansion. `has_any_comments` is a superset —
    // forced expansion needs a comment to exist — so gate this scan on it.
    let comments_force_expansion =
        has_any_comments && any_comment_forces_expansion(call, span, printer, paren_open);

    // Function composition: call arg contains a callback → expand all args
    // e.g., x.y(arr.map((e) => e[0]), ['foo']) — matches Prettier's isFunctionCompositionArgs.
    // That predicate also subsumes the all-arrows case (2+ arguments that are all arrows means
    // `function_count > 1`, which it short-circuits `true` on), so there is no separate
    // arrow-only disjunct here — the twin in `call_formatting.rs` states the same.
    let force_expand = force_expand
        || any_arg_empty_line
        || comments_force_expansion
        || is_function_composition_args(call.arguments);

    // `?.` fuses with the paren only in the spelling where nothing follows it — an empty
    // argument list and no type arguments ([`super::CallOptional::Fused`], the one spelling
    // both call printers ask). Everywhere else this link prints the `?.` itself, behind the
    // callee-side half of its own gap.
    let prefix = if gap.optional.fused() {
        printer.d().text("?.(")
    } else {
        printer.d().text("(")
    };

    // The HEAD every layout below opens with ([`chain_call_head_doc`]). Absent on nearly
    // every chain call, so it travels as an `Option<DocId>` the layouts prepend
    // ([`with_chain_head`]) rather than a buffer each of them assembles into.
    let head = chain_call_head_doc(printer, call, gap);
    // The pre-paren run, when the callee→`(` gap splits at the `(` — built here, where the
    // printer is in scope, and closed around whichever layout the arms below pick
    // ([`with_chain_head`]).
    let head = ChainHead {
        doc: head,
        hang: gap
            .paren_split
            .map(|(start, paren)| printer.continuation_indent_prefix(start, paren)),
    };

    let ctx = ChainArgsContext {
        paren_open,
        prefix,
        own_call_layout,
        ungroup_args,
        has_leading_comments,
        has_leading_comment_on_page,
        has_any_comments,
        has_any_comment_text: call_has_comments,
        last_arg_commented,
        has_trailing_block_comments,
        comments_force_expansion,
        leading_comment_doc,
    };

    // A `Boolean()` coercion heading this chain: its argument is a `shouldNotIndent`
    // position, marked here exactly as the plain call marks it, so the chain's flat, expanded
    // and standard-expanded candidates all print the argument flat (each re-enters here, so
    // each re-marks; every branch builder below bottoms out in the builder that reads it).
    // Only the chain HEAD can be one — a link's callee is a member expression — and with one
    // argument only the single-argument and force-expanded builders ever hold it.
    mark_boolean_coercion_argument(printer, call);
    printer.mark_relational_sibling_authored_pairs(call.arguments.iter().copied().map(Some));

    // Prettier's React-hook deps-array layout — the FIRST thing `printCallArguments` asks,
    // above `anyArgEmptyLine` and every specialized layout, and above the chain's own
    // forced expansion (prettier's member-chain printer reprints the group's arguments
    // through the same function, so the shape wins there too).
    if let Some(doc) = try_hook_deps_args_doc(
        printer,
        call.arguments,
        paren_open,
        span.end,
        call_has_comments,
        prefix,
    ) {
        return with_chain_head(printer.d(), head, doc);
    }

    if call.arguments.is_empty() {
        build_chain_args_empty(printer, span, ctx, head)
    } else if force_expand {
        build_chain_args_force_expand(printer, call, span, ctx, head)
    } else if call.arguments.len() == 1 {
        build_chain_args_single(printer, call, span, ctx, head)
    } else {
        build_chain_args_multi(printer, call, span, ctx, head)
    }
}

/// What a chain link prints between its callee and its argument list.
///
/// Two independent pieces, and they sit on OPPOSITE sides of the list, which is the whole
/// reason this is a struct rather than one `Option<DocId>`: `doc` goes before it, `hang`
/// wraps it.
#[derive(Clone, Copy)]
pub(super) struct ChainHead {
    /// An optional call's `?.` half and the type arguments, in that order — `None` on nearly
    /// every call. Prepended to the list.
    doc: Option<DocId>,
    /// The callee→`(` gap's pre-paren run and its separator, when that gap splits at the `(`
    /// ([`super::CalleeGap::paren_split`]): the whole argument list hangs one level under it
    /// (`a.b // c⏎\t(e)`). Prebuilt by the caller through
    /// [`Printer::continuation_indent_prefix`], because the sites that assemble the list hold
    /// only the arena and this needs the printer.
    hang: Option<DocId>,
}

/// Prepend a chain call's head (see `build_call_args_doc_for_chain_impl`) to its argument
/// layout — the layout alone when there is none, which is nearly every call — and hang the
/// layout under the pre-paren run when the callee→`(` gap split.
///
/// The `hang` closes INSIDE `doc`: the type arguments are written before the comment
/// (`call<A> // c⏎\t(c)`), so they stay on the callee's line and only the list moves.
fn with_chain_head(d: &DocArena, head: ChainHead, args: DocId) -> DocId {
    let args = match head.hang {
        None => args,
        Some(hang) => d.indent(d.concat(&[hang, args])),
    };
    match head.doc {
        None => args,
        Some(doc) => d.concat(&[doc, args]),
    }
}

/// Empty argument list (`()` / `?.()` / `<T>()`), preserving dangling comments
/// between the callee/type-args and the `(` and inside the parens.
fn build_chain_args_empty(
    printer: &Printer<'_>,
    span: Span,
    ctx: ChainArgsContext,
    head: ChainHead,
) -> DocId {
    let ChainArgsContext {
        paren_open, prefix, ..
    } = ctx;
    with_chain_head(
        printer.d(),
        head,
        build_empty_args_parens_doc(printer, paren_open, span.end, prefix),
    )
}

/// Forced-expansion argument layout (`force_expand` true): hardlines instead of
/// softlines. Single object/array and single expression-arrow args get hugged
/// special-cases; everything else uses the blank-line- and comment-aware loop.
fn build_chain_args_force_expand(
    printer: &Printer<'_>,
    call: &ast::CallExpression<'_>,
    span: Span,
    ctx: ChainArgsContext,
    head: ChainHead,
) -> DocId {
    let d = printer.d();
    let ChainArgsContext {
        paren_open,
        prefix,
        has_leading_comments,
        has_leading_comment_on_page,
        last_arg_commented,
        has_trailing_block_comments,
        comments_force_expansion,
        leading_comment_doc,
        ..
    } = ctx;

    // Special case: single object/array arg should hug the parens
    // and expand internally with hardlines, not softlines around it.
    // e.g., `.push({\n  ...\n})` not `.push(\n  {...},\n)`
    //
    // We use build_arg_expression_doc_expanded which produces hardlines,
    // allowing fits() to correctly measure the first line: `chain.call({`
    // and return true (since hardlines end the fits check).
    //
    // Exception: when there are trailing comments, we use the full expansion
    // path which produces the extra-indented style that Prettier uses:
    // `fn(\n  {...} /* comment */,\n)` not `fn({...} /* comment */)`
    //
    // …and the leading half of that same refusal, which this arm is the last builder in the
    // family to ask. It reassembles the call from `prefix` + the argument's doc + `)`, so the
    // whole `callee`→argument gap reaches no emitter here (`docs/comments.md` hazard 4) — and
    // the hug is not tsv's to keep anyway: prettier's `shouldExpandLastArg` refuses on
    // `hasComment(lastArg, Leading)`, which `call_formatting.rs`'s `try_single_arg_comment_paths`
    // and `new_expression.rs` both honor. **on page**, not to-emit: a comment the argument owns
    // and prints itself still defeats the hug, and the gate that cannot see it hugs blind.
    if call.arguments.len() == 1
        && !has_leading_comment_on_page
        && !has_trailing_block_comments
        && !comments_force_expansion
    {
        let arg = &call.arguments[0];
        if matches!(
            arg.kind,
            ExpressionKind::ObjectExpression(_) | ExpressionKind::ArrayExpression(_)
        ) {
            // Build the object/array with forced internal expansion (hardlines)
            let arg_doc = printer.build_arg_expression_doc_expanded(arg);
            return with_chain_head(d, head, d.concat(&[prefix, arg_doc, d.text(")")]));
        }
    }

    // Special case: a single arrow arg the signature can hug to the call paren (prettier's
    // couldExpandArg) — three body kinds, each with its own layout below. For the two
    // expandable LITERAL bodies fits() measures only `chain.map((sig) => [` / `=> ({` and
    // truncates at the body's first hardline; the breakable ones break after `=>` instead.
    // Skip when comments_force_expansion — these layouts have no line to put a comment that
    // must own one on, and the `(`-line channel `leading_comment_doc` cannot carry has no
    // home here either.
    // Skip on a commented last argument — see `last_arg_commented` above.
    if !last_arg_commented
        && !comments_force_expansion
        && let Some((arrow, arrow_span, body_expr)) = single_arrow_expression_body(call)
    {
        // ARRAY body: `(sig => [\n  items,\n])` — array content on new lines, bracket
        // hugged.
        //
        // TODO: this is the one reassembling arm that asks half the refusal pair — the
        // body-tail question alone, where its OBJECT twin below asks
        // `arrow_hug_refused_by_comments`. Instrumenting the arm's entry over ~23k files
        // (`tests/fixtures` + the prettier / svelte / kit / zzz / gro trees) reaches it 6
        // times, all of them inside `tests/fixtures`, and the signature question is false
        // at EVERY hit, so adding it cannot fire. No input separates the two halves —
        // hence no fixture, hence no change. Close it the moment one turns up.
        if matches!(body_expr.kind, ExpressionKind::ArrayExpression(_))
            && !arrow_body_tail_has_comments(printer, arrow_span, body_expr)
        {
            let body_doc = printer.build_arg_expression_doc_expanded(body_expr);
            return build_hugged_arrow_arg_doc(
                printer, head, ctx, arrow, arrow_span, body_expr, body_doc,
            );
        }

        // OBJECT body: `(sig => ({\n  props\n}))` — the object expands behind its
        // grammar-required parens, synthesized here. Skipped when a break forced inside
        // the signature invalidates the hug (`arrow_signature_has_breaking_comments`).
        if matches!(body_expr.kind, ExpressionKind::ObjectExpression(_))
            && !arrow_hug_refused_by_comments(printer, arrow, arrow_span, body_expr)
        {
            let body_doc = d.parens(printer.build_arg_expression_doc_expanded(body_expr));
            return build_hugged_arrow_arg_doc(
                printer, head, ctx, arrow, arrow_span, body_expr, body_doc,
            );
        }

        // BREAKABLE body (call, ternary): the arrow-hugging break state directly,
        // `(sig =>\n  body,\n)` — prettier keeps the signature hugged even when forcing
        // expansion. couldExpandArg keys on the body type (looking through the return-type
        // annotation and a trailing `!`), so typed-return arrows hug too. Skipped on a
        // signature break and on a body-tail comment for the same reasons as the object
        // arm above.
        if (arrow_body_is_call_through_non_null(body_expr) || is_ternary_arrow_body(body_expr))
            && !arrow_hug_refused_by_comments(printer, arrow, arrow_span, body_expr)
        {
            let body_doc = printer.build_arrow_arg_body_doc_unclaimed(body_expr);
            let body_doc =
                prepend_arrow_body_comments(printer, arrow, body_expr.span().start, body_doc);
            let sig_doc = build_arrow_sig_doc(printer, arrow, arrow_span);
            let sig_doc = prepend_leading(d, leading_comment_doc, sig_doc);

            return with_chain_head(
                d,
                head,
                d.concat(&[
                    prefix,
                    sig_doc,
                    d.text(" =>"),
                    d.indent_hardline(body_doc),
                    d.hardline(),
                    d.text(")"),
                ]),
            );
        }
    }

    // Forced expansion: use hardlines instead of softlines
    // Build arguments with blank line preservation and full comment handling
    let mut arg_parts = DocBuf::new();
    // Comments trailing the `(` on its own line, kept on the `(` line
    // (divergence from prettier, which relocates them to their own line).
    // Injected after the `(` in the wrap below.
    let mut paren_line_prefix_parts: DocBuf = DocBuf::new();
    // Whether the gap just closed — between `arguments[i - 1]` and `arguments[i]` —
    // carries an author blank line. Computed once at the bottom of the previous
    // iteration (the no-comment branch) and reused here, since the top of this
    // iteration and that bottom look at the same gap under the same no-comment guard.
    let mut prev_gap_has_blank = false;

    for (i, arg) in call.arguments.iter().enumerate() {
        let arg_start = arg.span().start;

        // Handle leading comments before first argument
        if i == 0 && has_leading_comments {
            let first_pc = PartitionedComments::new(printer, paren_open, arg_start);

            // The delimiter-line question, in the conjunction the force-expanded builders
            // spell (`docs/comments.md`; `emit_first_arg_leading_comments`' rustdoc names
            // this caller): pull onto the `(` line only when a comment in the gap is what
            // forces the container open. A block-only run forces nothing, so it leads the
            // argument — which is prettier's answer and the plain call's
            // (`build_call_with_arg_comments` asks the same pair). Asking
            // `has_trailing_comments()` alone made this the one builder in the family that
            // pulled a lone block, and a `(`-line block is not even a fixed point where the
            // argument's own layout is what expanded the list.
            if first_pc.pulls_to_delimiter_line(printer) {
                // Comments trailing the `(` stay on the `(` line, author blank included;
                // the own-line set then leads the first arg (source order preserved — see
                // conformance_prettier_ts_comments.md §Comment relocation, Call open paren `(`).
                // A block hugging the arg stays inline (`/* b */ a`), an own-line block /
                // line comment takes its own line with author blanks preserved.
                first_pc.emit_pulled_gap(&mut paren_line_prefix_parts, &mut arg_parts, printer);
            } else if let Some(doc) = build_inline_leading_comments(printer, paren_open, arg_start)
            {
                // Nothing pulled, so the WHOLE gap leads the first argument — which is the
                // chain's own body-line emitter, asked here rather than re-derived: a
                // broke-after run takes its soft `line`, anything else glues to the
                // argument. Emitting only part of the gap is the DROP its doc warns about.
                arg_parts.push(doc);
            }
        }

        // Check for blank line before this arg (from previous arg)
        // Only add blank line preservation when there are no comments between args,
        // since comments with blank lines are handled in the separator logic below.
        if i > 0 {
            let prev_end = call.arguments[i - 1].span().end;
            let has_comments_before = printer.has_comments_to_emit_between(prev_end, arg_start);
            // Nothing to emit in the gap, but an owned annotation can still physically be
            // there — `prev_gap_has_blank` was measured with `blank_scan_end`, so its own
            // newlines don't read as a blank line yet an authored blank *before* it is kept.
            if !has_comments_before && prev_gap_has_blank {
                arg_parts.push(d.literalline());
                arg_parts.push(d.hardline());
            }
        }

        // An own-line format-ignore directive in this argument's gap freezes it verbatim
        // (Rule A). The three hugging special cases above all decline once a comment
        // forces expansion, so this is the only argument builder a directive reaches in
        // the chain family.
        //
        // A lone expression-body arrow renders its signature flat here, the same way the
        // expand-last arm does (prettier's `expandLastArg` `removeLines`): the argument is
        // already broken out onto its own line, so the remaining break belongs after `=>`,
        // on the body — never inside the parameter list. Without this the signature group
        // is the first break candidate this hardline-wrapped layout offers, and a
        // one-parameter arrow shatters into three lines. The non-chain path keeps the
        // signature intact by construction (`call_formatting.rs`'s `try_single_arg_hug`);
        // this is the chain path answering the question the same way rather than
        // differently.
        let flat_sig = call.arguments.len() == 1
            && matches!(&arg.kind, ExpressionKind::ArrowFunctionExpression(arrow)
                if arrow.body.is_expression() && !is_curried_arrow_chain(arg));
        let build_arg = || build_joined_argument_doc(printer, paren_open, call.arguments, i);
        arg_parts.push(if flat_sig {
            build_flat_params_arg_doc(printer, build_arg)
        } else {
            build_arg()
        });

        // Handle trailing comments and comma placement
        let arg_end = arg.span().end;
        let next_boundary = if i < call.arguments.len() - 1 {
            call.arguments[i + 1].span().start
        } else {
            span.end
        };

        if i < call.arguments.len() - 1 {
            // Not the last argument
            let next_arg_start = call.arguments[i + 1].span().start;

            // Reclassify a hugging after-comma block as leading, emit the
            // before/after-comma trailing comments + comma, then the spread interior's
            // own-line share; the separator + leading comments below finish the gap.
            // This layout is hardline-joined throughout, so the gap's `forces_expansion`
            // obligation is already met and nothing reads it.
            let interior = printer.arg_paren_interior(paren_open, call.arguments, i);
            let pc = printer
                .open_inter_arg_gap(&mut arg_parts, arg, interior, next_arg_start)
                .comments;

            // Skip hardline if next arg has blank line
            // (blank line preservation at the top of the loop handles the line break)
            let has_comments_before_next =
                printer.has_comments_to_emit_between(arg_end, next_arg_start);
            let next_has_blank = if has_comments_before_next {
                pc.has_blank_line_in_gap(printer)
            } else {
                // Measure the no-comment gap's blank once; the top of the next iteration
                // reuses it (same gap, same guard) instead of re-scanning the window.
                printer.is_next_line_empty(arg_end, printer.blank_scan_end(arg_end, next_arg_start))
            };
            // Carry the no-comment gap's blank forward; a comment gap's blank is emitted
            // here, and the next iteration's top guard skips the reuse, so leave it false.
            prev_gap_has_blank = next_has_blank && !has_comments_before_next;
            if next_has_blank && has_comments_before_next {
                // Blank line before next arg's leading comments — emit literalline
                // before the hardline separator. When there are no comments, the
                // blank line is handled at the top of the next iteration.
                arg_parts.push(d.literalline());
                arg_parts.push(d.hardline());
            } else if !next_has_blank {
                arg_parts.push(d.hardline());
            }
            // else: next_has_blank && !has_comments_before_next — skip hardline,
            // blank line preservation at top of next iteration adds literalline + hardline
            pc.emit_leading_comments_inline_aware(&mut arg_parts, printer);
        } else {
            // Last argument — the parent's share of a spread's stripped-paren interior,
            // then same-line trailing comments in source order (a block that sat after
            // the source comma just trails past where the comma was; a line comment
            // follows via `line_suffix`), then own-line dangling comments. No trailing
            // comma (trailingComma: 'none').
            let interior = printer.arg_paren_interior(paren_open, call.arguments, i);
            emit_last_arg_trailing_comments(printer, &mut arg_parts, arg, interior, next_boundary);
        }
    }

    // No trailing comma after the last arg (trailingComma: 'none') — the last-arg
    // comment emit trails same-line comments after the arg and emits no comma, so
    // nothing is appended here.
    with_chain_head(
        d,
        head,
        d.concat(&[
            prefix,
            d.concat(&paren_line_prefix_parts),
            d.indent_hardline(d.concat(&arg_parts)),
            d.hardline(),
            d.text(")"),
        ]),
    )
}

/// Single non-force-expand argument: arrow special-cases (call/ternary/object/array
/// expression bodies, block arrows, multiline templates) and the general
/// classify-and-wrap path. Always returns.
fn build_chain_args_single(
    printer: &Printer<'_>,
    call: &ast::CallExpression<'_>,
    span: Span,
    ctx: ChainArgsContext,
    head: ChainHead,
) -> DocId {
    let d = printer.d();
    let ChainArgsContext {
        paren_open,
        prefix,
        own_call_layout,
        has_leading_comments,
        has_leading_comment_on_page,
        has_any_comments,
        has_any_comment_text,
        last_arg_commented,
        leading_comment_doc,
        ..
    } = ctx;
    // The chain's spelling of what a call-argument state opens with — its own `(` / `?.(`,
    // the callee being a separate group the chain printer places. Every ladder below is the
    // plain-call / `new` one read through it.
    let opener = ArgOpener::chain(prefix);

    let arg = &call.arguments[0];

    // A broken `=>`→body gap forces the closing paren onto its own
    // line. Above every body-kind arm below, because the rule is the gap's and not the arms'
    // body-kind split — the one body question it does owe, prettier's `couldExpandArg`, the
    // helper applies itself — see [`last_arg_arrow_gap_break`]. The argument doc is built
    // here rather than reused from an arm because the arms below never run for this shape;
    // asking the cheap comment question first keeps that build off every other path.
    if has_any_comment_text
        && !last_arg_commented
        && let Some(gap_break) = last_arg_arrow_gap_break(printer, arg)
    {
        // ⚠️ Building ONE doc with flat parameters here is right for a state
        // that can fall through and wrong for one that cannot: with no `allArgsBrokenOut()`
        // behind it, a flattened signature too wide for the line simply overflowed. The two
        // printings and the ladder go together — the shared entry point owns that pairing.
        return with_chain_head(
            d,
            head,
            build_arrow_gap_break_single_arg_doc(
                printer,
                opener,
                arg,
                &gap_break,
                leading_comment_doc,
                || printer.build_arg_expression_doc(arg),
            ),
        );
    }

    // Special case: arrow function with call expression body
    // Prettier keeps `(sig =>` hugged, breaking after `=>` to the body.
    // Structure: `(sig =>\n  body\n)` instead of `(\n  sig =>\n    body\n)`
    //
    // couldExpandArg keys on the body type and looks through the return-type
    // annotation plus a trailing non-null `!` (its `stripChainElementWrappers`),
    // so typed-return and `=> call()!` arrows are call-body arrows too.
    //
    // Leading comments on the arg block expand-last (prettier's shouldExpandLastArg
    // returns false when hasComment(lastArg, Leading)).
    if let ExpressionKind::ArrowFunctionExpression(arrow) = &arg.kind
        && let ast::ArrowFunctionBody::Expression(body_expr) = &arrow.body
        && arrow_body_is_call_through_non_null(body_expr)
        // `has_any_comment_text`: refusing the hug is a LAYOUT decision, so it must see a
        // comment the argument owns and prints itself (see `build_chain_args_multi`).
        && !last_arg_commented
        // …and the reassembling arm's refusal pair, the shared rule the object/array arm
        // below and the general gate already ask (`arrow_hug_refused_by_comments`). Its
        // body-tail half bites even though the flat state prints the whole arrow and would
        // keep the comment: a state set that is only conditionally lossless is one width away
        // from dropping it.
        && !(has_any_comment_text && arrow_hug_refused_by_comments(printer, arrow, arg.span, body_expr))
        // The body is built ONCE, injected into the whole-arrow argument doc and reused by
        // the break-body state — as the multi-argument arm and the plain-call spelling do.
        // Built in both places, each chain level built its callback's body twice and a
        // chain nested in that body (`a.b().c(() => a.b().c(() => …))`) compounded to
        // 2^depth. Last in the chain because it builds, and never refused: a call body is
        // exactly what the prebuild takes. The break-body state therefore prints the body
        // as the whole arrow does, a multi-line comment the body owns outside the body's
        // group (`Printer::build_arrow_arg_body_doc`), so `(x) =>⏎/* c⏎c */ a.b().c(…)`
        // keeps its chain on the comment's last line as prettier does.
        && let Some(body_reuse) = prebuild_expand_last_break_body(printer, Some(arg))
    {
        let arrow_doc = printer
            .with_arrow_body_inject(Some(body_reuse), || printer.build_arg_expression_doc(arg));
        let arrow_doc = prepend_leading(d, leading_comment_doc, arrow_doc);
        let (_, body_doc) = body_reuse;
        let body_doc =
            prepend_arrow_body_comments(printer, arrow, body_expr.span().start, body_doc);
        let sig_doc = build_hugged_arrow_sig_doc(printer, arrow, arg.span)
            .map(|sig_doc| prepend_leading(d, leading_comment_doc, sig_doc));

        // The ladder — flat, the signature hugged with the body broken after `=>`, then the
        // argument broken out — shared with the plain-call / `new` spelling of this same
        // layout ([`build_break_body_ladder`]). The broken-out state is what a signature too
        // wide for the call's line falls through to, and all that is left when the signature
        // will break; the flat one is dropped when the argument will break.
        return with_chain_head(
            d,
            head,
            build_break_body_ladder(
                d,
                opener,
                &[],
                sig_doc,
                body_doc,
                false,
                arrow_doc,
                arrow_doc,
            ),
        );
    }

    // Special case: arrow function with ternary body
    // Prettier uses conditional parens:
    // - Flat: `map((x) => (x ? y : z))` - with parens
    // - Break: `map((x) =>\n  x ? y : z)` - no parens, body indented
    // couldExpandArg keys on the body type (looking through the return-type
    // annotation), so typed-return arrows are eligible.
    // Leading comments block expand-last (prettier's shouldExpandLastArg).
    if let ExpressionKind::ArrowFunctionExpression(arrow) = &arg.kind
        && let ast::ArrowFunctionBody::Expression(body_expr) = &arrow.body
        && is_ternary_arrow_body(body_expr)
        // `has_any_comment_text`: see above — a layout gate counts an owned comment.
        && !last_arg_commented
        // …and the same refusal pair as above — here its body-tail half bites at every state,
        // since the flat one synthesizes its own `(`/`)` around the body rather than printing
        // the arrow (`arrow_hug_refused_by_comments`)…
        && !(has_any_comment_text && arrow_hug_refused_by_comments(printer, arrow, arg.span, body_expr))
        // …and a signature that will break, which every state prints flattened: the call
        // breaks every argument out below, the argument printed whole.
        && !arrow_signature_refuses_hug(printer, arrow, arg.span)
    {
        let body_doc = printer.build_arrow_arg_body_doc(body_expr);
        let sig_doc = build_arrow_sig_doc(printer, arrow, arg.span);
        let sig_doc = prepend_leading(d, leading_comment_doc, sig_doc);

        // The ladder, shared with the plain-call and `new` spellings
        // ([`build_ternary_arrow_hug_ladder`]).
        return with_chain_head(
            d,
            head,
            build_ternary_arrow_hug_ladder(
                printer,
                opener,
                arrow,
                body_expr,
                sig_doc,
                body_doc,
                || {
                    prepend_leading(
                        d,
                        leading_comment_doc,
                        printer.build_arg_expression_doc(arg),
                    )
                },
            ),
        );
    }

    // Special case: arrow function with object/array expression body
    // Prettier's shouldExpandLastArg path: produces a 3-state conditional_group
    // so fluid assignments can expand call args instead of breaking after =.
    // couldExpandArg keys on the body type (looking through the return-type
    // annotation), so typed-return arrows are eligible.
    // Leading comments block expand-last (prettier's shouldExpandLastArg).
    // The layout itself is shared with the non-chain and `new` spellings — see
    // [`build_single_arrow_hug_doc`].
    if let ExpressionKind::ArrowFunctionExpression(arrow) = &arg.kind
        && arrow_body_expands_internally(arrow)
        // `has_any_comment_text` (on page), not `has_any_comments` (to emit): an owned
        // leading comment defeats the expand-last hug just like an ordinary one, so the
        // hug must refuse it too — a to-emit gate would hug blind.
        && !last_arg_commented
        // A comment LEADING the arg isn't the only shape that defeats the hug: a signature
        // that will break is one the one-line hug can't honor. Same predicate as the
        // non-chain path — see `arrow_signature_refuses_hug`.
        && !arrow_signature_refuses_hug(printer, arrow, arg.span)
    {
        // The two printings and the state ladder they feed — the same entry point the
        // non-chain and `new` sites take, differing only in the chain's argument builder and
        // its leading run ([`build_single_arrow_hug_doc`]). A curried chain's terminal
        // object/array is reached here too ([`arrow_body_expands_internally`], the guard
        // above and the ladder's own body-kind key), and for it the two printings genuinely
        // differ: the `expandLastArg` print has no chain layout, so a run of heads too wide
        // to hug fails on width instead of breaking its own heads and fitting.
        return with_chain_head(
            d,
            head,
            build_single_arrow_hug_doc(printer, opener, arg, arrow, leading_comment_doc, || {
                printer.build_arg_expression_doc(arg)
            }),
        );
    }

    // Build arg doc in argument context. This is prettier's `printedArguments` — printed
    // with no `expandLastArg`, so a curried chain takes the progressive layout
    // (`build_printed_argument_doc`); the expand-last hug states above build their own.
    //
    // A block-bodied arrow the block-arrow arm below may hug takes its body built once, for
    // the convergence state that arm admits ([`AssumedParamHug`]).
    let assumed = match &arg.kind {
        ExpressionKind::ArrowFunctionExpression(arrow)
            if matches!(arrow.body, ast::ArrowFunctionBody::BlockStatement(_))
                && !(has_leading_comment_on_page || last_arg_commented) =>
        {
            AssumedParamHug::new(printer, arg)
        }
        _ => None,
    };
    let arg_doc = AssumedParamHug::sharing(assumed.as_ref(), printer, || {
        build_printed_argument_doc(printer, arg, || printer.build_arg_expression_doc(arg))
    });
    let arg_start = arg.span().start;
    let arg_end = arg.span().end;

    // Check for leading inline block comments before the arg
    let leading_comments_doc = if has_leading_comments {
        build_inline_leading_comments(printer, paren_open, arg_start)
    } else {
        None
    };

    // Check for trailing inline block comments (don't force expansion)
    let trailing_comments_doc = if has_any_comments {
        build_inline_trailing_comments(printer, arg_end, span.end)
    } else {
        None
    };

    // Build combined arg doc with leading/trailing comments.
    //
    // ⚠️ Every layout arm below must splice THIS doc, never `arg_doc`. An arm that composes a
    // state out of the bare argument drops the whole gap run whenever that state wins — the
    // `})⟨⟩)` gap-injection shape, and hazard 4 in docs/comments.md. It is invisible to the
    // fixture suite until some document puts a comment there, and it is exactly what the
    // measured hug below reintroduced the first time it was written.
    let arg_with_comments = match (leading_comments_doc, trailing_comments_doc) {
        (Some(leading), Some(trailing)) => d.concat(&[leading, arg_doc, trailing]),
        (Some(leading), None) => d.concat(&[leading, arg_doc]),
        (None, Some(trailing)) => d.concat(&[arg_doc, trailing]),
        (None, None) => arg_doc,
    };

    // Check if it's a block-bodied callback whose signature refuses the hug. These need
    // soft-break wrapping to expand the call. A `function` expression is always
    // block-bodied, so it asks the question unconditionally — the arrow's `is_expression`
    // guard is about the arrow's *body* kind, not about the callee, and reading it as the
    // whole gate is what left the `function` twin ungated here.
    let callback_signature_refuses = match &arg.kind {
        ExpressionKind::ArrowFunctionExpression(arrow) => {
            !arrow.body.is_expression() && callback_signature_refuses_hug(printer, arg, 1)
        }
        ExpressionKind::FunctionExpression(_) => callback_signature_refuses_hug(printer, arg, 1),
        _ => false,
    };

    if callback_signature_refuses {
        // Callback whose signature will break - force expansion
        return with_chain_head(d, head, opener.wrap_soft(d, arg_with_comments));
    }

    // Single multiline template literal — the expanded form, taken for two reasons.
    //
    // 1. The author put it on a line of its own, prettier's `isTemplateOnItsOwnLine`
    //    (walk backwards from the backtick and find a newline).
    // 2. This call is a LINK the member-chain redirect swallowed
    //    ([`ChainCall::own_call_layout`]), so prettier prints its arguments through
    //    `printCallArguments` and the hug — which lives at the TOP of
    //    `printCallExpression`, above that redirect — never runs for it. The two states
    //    that separate are `` a.b(`x⏎y`).c() `` (a link, expands) and `` a(`x⏎y`).b() ``
    //    (prettier's chain BASE, printed with `print()`, hugs).
    let template_expands = is_multiline_template_expression(arg)
        && (sole_template_on_its_own_line(printer, paren_open, arg_start) || !own_call_layout);

    if template_expands {
        // `arg_with_comments`, per the ⚠️ above — this arm rebuilt the argument from
        // `build_expression_doc` instead, which is the same defect one step further:
        // it discarded the gap runs AND the argument-context printing. Every comment
        // around such an argument was DROPPED (`o.toBe(/* c */⏎\`a⏎b\`)` and its
        // trailing twin), at the one call layout that reassembles rather than splices.
        return with_chain_head(
            d,
            head,
            d.concat(&[
                prefix,
                d.indent_hardline(arg_with_comments),
                d.hardline(),
                d.text(")"),
            ]),
        );
    }

    // Multiline template literal on same line as ( — hug it.
    // Mirrors call_formatting.rs's isTemplateOnItsOwnLine handling:
    // when the template starts on the same line as the opening paren,
    // prettier hugs it (no break between `(` and the backtick).
    //
    // ⚠️ Do NOT delete this as "the outer dispatcher already routes such a call to the flat
    // form" — that bypass only sees the OUTERMOST call, and the two answers are genuinely
    // different questions. Prettier reaches the hug here through `printCallee`'s recursion
    // into `printCallExpression` when a call's callee is a plain call
    // (`` template(`…`)({…}) `` — hugged in prettier's own snapshot), which tsv's linearizer
    // folds into a chain; it does NOT reach it for a link of a real member chain
    // (`` a.b(`x⏎y`).c().d(z) ``, where `printMemberChain` prints the group's arguments
    // itself and the template expands). Neutralizing this arm was measured over ~23k files:
    // exactly two movers, both prettier-suite files, and both AWAY from prettier's committed
    // output. The residual — the member-chain link, where tsv hugs and prettier expands — is
    // a chain-linearization difference, not this arm's.
    //
    // The `(`-line run takes the plain call's hug run ([`try_hug_multiline_template_arg`]'s
    // glued value-gap emitter, [`Printer::build_rhs_comments_glued_opt`]), not the list's:
    // the hug is one layout across the call families, and [`sole_template_on_its_own_line`]
    // decided it by what THAT run prints ahead of the backtick. The list's run keeps a
    // block the callee→`(` gap relocated on a line of its own where the plain call glues it
    // (`[] /* c */⏎(`⏎`).b()` printed `[](/* c */⏎`⏎`).b()`, which the next pass expands).
    if is_multiline_template_expression(arg) {
        let leading = if has_leading_comments {
            printer.build_rhs_comments_glued_opt(paren_open, arg_start)
        } else {
            None
        };
        let mut parts = DocBuf::new();
        parts.push(prefix);
        parts.extend(leading);
        parts.push(arg_doc);
        parts.extend(trailing_comments_doc);
        parts.push(d.text(")"));
        return with_chain_head(d, head, d.concat(&parts));
    }

    // Block-body arrows: use conditional_group to try hug first, then expand.
    // Cannot use `ArgOpener::wrap_soft` (a plain group) because will_break()
    // recurses into the block body's hardlines and forces break without trying
    // fits(). conditional_group uses fits() directly, correctly measuring whether
    // the hugged first line (e.g., `fn((params) => {`) fits.
    //
    // Exception: a commented argument is never hugged — see the arm below.
    if let ExpressionKind::ArrowFunctionExpression(arrow) = &arg.kind
        && matches!(arrow.body, ast::ArrowFunctionBody::BlockStatement(_))
    {
        let state_expand = d.concat(&[
            prefix,
            d.indent_hardline(arg_with_comments),
            d.hardline(),
            d.text(")"),
        ]);
        let doc = if has_leading_comment_on_page || last_arg_commented {
            // A comment anywhere on the argument prevents hugging — the same
            // `shouldExpandLastArg` rule the arms above ask. An owned comment (on page but
            // not in `has_leading_comments`) rides inside `arg_with_comments` via `arg_doc`,
            // so it still defeats the hug. What is left is prettier's default arm,
            // `group(allArgsBrokenOut, {shouldBreak: printedArguments.some(willBreak)})`:
            // an argument that will break (a non-empty body) forces the list out
            // (`(\n  /* l */ (x) => {…}\n)`), and one that will not (`/* l */ (q) => {}`)
            // stays on the call's line while it fits. The forced form is the explicit
            // hardlines rather than a soft group: a chain measuring this doc flat must stop
            // at the break, as prettier's `fits` stops at a group already marked broken,
            // where a soft group would be measured through the body's hardline and expand
            // the chain.
            if d.will_break(arg_with_comments) {
                state_expand
            } else {
                opener.wrap_soft(d, arg_with_comments)
            }
        } else {
            // No leading comments — try hug first, then expand
            let state_hug = d.concat(&[prefix, arg_with_comments, d.text(")")]);
            match assumed {
                Some(hug) => {
                    let build_arg = || {
                        build_printed_argument_doc(printer, arg, || {
                            printer.build_arg_expression_doc(arg)
                        })
                    };
                    let with_trailing = |assumed_arg| match trailing_comments_doc {
                        Some(trailing) => d.concat(&[assumed_arg, trailing]),
                        None => assumed_arg,
                    };
                    // The chain's own convergence state asks for this state alone
                    // (`chain::builder`'s assumed-hug middle state).
                    if let Some(contents) =
                        hug.take_forced(printer, AssumedHugKind::ParamLiteral, build_arg)
                    {
                        let assumed_arg = with_trailing(contents);
                        return with_chain_head(
                            d,
                            head,
                            d.concat(&[prefix, assumed_arg, d.text(")")]),
                        );
                    }
                    match hug.build(printer, false, build_arg).literal {
                        Some(assumed) => {
                            let state_assumed =
                                d.concat(&[prefix, with_trailing(assumed.doc), d.text(")")]);
                            d.conditional_group(&[
                                state_hug,
                                assumed.state(d, arg_with_comments, state_assumed),
                                state_expand,
                            ])
                        }
                        None => d.conditional_group(&[state_hug, state_expand]),
                    }
                }
                None => d.conditional_group(&[state_hug, state_expand]),
            }
        };
        return with_chain_head(d, head, doc);
    }

    // Leading comments prevent hugging — prettier's shouldExpandLastArg
    // returns false when hasComment(lastArg, Leading), so the default
    // expansion path is used instead of expand-last hugging.
    //
    // A comment that FORCES a break inside an arrow's signature does the same, and for the
    // same reason: hugging would strand the break mid-signature. This is the last of the
    // three places that must agree on when a hug is legal (the expand-last gate above, and
    // `call_formatting.rs`'s non-chain arm) — all asking the one predicate, because the
    // hug that mangled `arr.map(…)` while `fn(…)` stayed correct was precisely two of them
    // disagreeing. See `arrow_signature_refuses_hug`, which also answers a break the
    // signature's own docs carry.
    let signature_forces_break = matches!(&arg.kind, ExpressionKind::ArrowFunctionExpression(arrow)
            if arrow_signature_refuses_hug(printer, arrow, arg.span));
    let kind = if has_leading_comment_on_page || signature_forces_break {
        ChainArgKind::NeedsSoftWrap
    } else {
        classify_chain_arg(arg)
    };
    let doc = match kind {
        ChainArgKind::NeedsSoftWrap => {
            // Needs soft-break wrapping - e.g., long strings
            opener.wrap_soft(d, arg_with_comments)
        }
        ChainArgKind::NeedsWrapper if last_arg_commented => {
            // A comment TRAILING the argument defeats this hug exactly as it defeats the
            // natural one below: prettier's `shouldExpandLastArg` refuses on
            // `hasComment(lastArg, Trailing)`, so the call takes the default broken-out
            // layout — `.g(⏎\t(x) => (a ? b : c) /* c */⏎)`, the ternary parenthesized as
            // an unbroken arrow body is (`calls/chained/trailing_arg_comment_long`).
            opener.wrap_soft(d, arg_with_comments)
        }
        ChainArgKind::NeedsWrapper => {
            // Huggable with internal break points (ternary, etc.)
            // Hugs opening paren; breaks the closing paren onto its own line
            // when content breaks (no trailing comma; trailingComma: 'none').
            opener.hug_arg(d, arg_with_comments)
        }
        ChainArgKind::HugsNaturally if trailing_comments_doc.is_some() => {
            // A comment TRAILING the last argument defeats the hug, exactly as the leading
            // one two arms up does: prettier's `shouldExpandLastArg` refuses on both
            // (`!hasComment(lastArg, Leading) && !hasComment(lastArg, Trailing)`), so the
            // call takes the default broken-out layout. Only the two shapes that reach this
            // arm *with* a trailing comment were hugging — a `function` expression and a
            // block-terminal curried chain; every other trailing-comment argument
            // (object, array, block arrow, ternary, cast) is routed by an earlier path.
            opener.wrap_soft(d, arg_with_comments)
        }
        ChainArgKind::HugsNaturally => {
            // A curried arrow chain is the one shape here whose hug has to be MEASURED
            // against the SOFT-wrapped fallback rather than the ladder below. Prettier
            // reaches it through `shouldExpandLastArg`'s conditionalGroup, whose hug states
            // print the argument with `expandLastArg: true` — heads welded onto the callee's
            // line, which is what has to fit — and whose last state is `allArgsBrokenOut()`,
            // printed from `printedArguments`. An unconditional hug measures the wrong
            // thing: `arg_with_comments` already carries the progressive layout, whose short
            // first line always fits, so the call hugs where prettier breaks out.
            if is_curried_arrow_chain(arg) {
                let state_hug = d.concat(&[prefix, arg_with_comments, d.text(")")]);
                d.conditional_group(&[state_hug, opener.wrap_soft(d, arg_with_comments)])
            } else {
                // Objects, arrays, blocks and `function`s hug — but through prettier's own
                // ladder, not an unconditional hug. ⚠️ The bare `prefix arg )` concat that
                // stood here had NO state below the hug, so an argument with nothing to break
                // inside it — `{}`, `[]`, `function () {}` — ran past the print width
                // instead of dropping to its own line (`calls/chain_single_arg_hug_long`).
                // The plain-call and `new` spellings of this same question are
                // `call_formatting.rs`'s and `new_expression.rs`'s single-argument arms; one
                // `printCallArguments` prints all three.
                opener.lone_hug_ladder(d, arg_with_comments, lone_arg_params_render_flat(arg))
            }
        }
    };
    with_chain_head(d, head, doc)
}

/// Multiple non-force-expand arguments: the expand-last/expand-first strategy
/// trees (block-function last, expression-arrow last, expand-first, array/object
/// last) and the default soft-break-wrapped argument list. Always returns.
fn build_chain_args_multi(
    printer: &Printer<'_>,
    call: &ast::CallExpression<'_>,
    call_span: Span,
    ctx: ChainArgsContext,
    head: ChainHead,
) -> DocId {
    let d = printer.d();
    let ChainArgsContext {
        paren_open,
        prefix,
        has_any_comments,
        has_any_comment_text,
        last_arg_commented,
        comments_force_expansion,
        ungroup_args,
        ..
    } = ctx;
    // See [`build_chain_args_single`] — the chain's opener, through which every ladder below
    // is the plain-call / `new` one.
    let opener = ArgOpener::chain(prefix);

    // A broken `=>`→body gap on the last argument forces the closing
    // paren onto its own line. Above every body-kind arm below, because the rule is the
    // gap's and not the arms' body-kind split — the one body question it does owe,
    // prettier's `couldExpandArg`, the helper applies itself — see
    // [`last_arg_arrow_gap_break`]. The
    // cheap comment question gates the split-last build, so no other path pays for it.
    if call.arguments.len() >= 2
        && has_any_comment_text
        && !comments_force_expansion
        && !last_arg_commented
        && let Some(last) = call.arguments.last()
        && let Some(gap_break) = last_arg_arrow_gap_break(printer, last)
    {
        // The body both printings share, armed around each — built by the gate above.
        let body_reuse = gap_break.inject;
        let (head_parts, printed_last_arg_doc, all_args_broken) = printer
            .with_arrow_body_inject(body_reuse, || {
                build_args_split_last(call.arguments, printer, paren_open, has_any_comments)
            });
        return with_chain_head(
            d,
            head,
            build_arrow_gap_break_multi_arg_doc(
                printer,
                opener,
                body_reuse,
                &head_parts,
                printed_last_arg_doc,
                all_args_broken,
                || printer.build_arg_expression_doc(last),
            ),
        );
    }

    // Multiple arguments with block-body callback:
    // Use conditional_group to try inline first, then expand-all.
    // fits() checks actual width, handling both short and non-short preceding args.
    //
    // IMPORTANT: Cannot use `ArgOpener::wrap_soft` (a plain group) because
    // will_break() recurses into the block body's hardlines and forces break
    // without trying fits(). conditional_group uses fits() directly.
    if call.arguments.len() >= 2
        && call.arguments.last().copied().is_some_and(is_block_function)
        && !comments_force_expansion
        // `has_any_comment_text`, not `has_any_comments`: refusing the expand-last hug is a
        // LAYOUT decision, so it must see a comment the last argument owns and prints
        // itself (a bundler annotation) — prettier's `shouldExpandLastArg` sees it too.
        && !last_arg_commented
    {
        let assumed = call
            .arguments
            .last()
            .and_then(|last| AssumedParamHug::new(printer, last));
        let (head_parts, last_arg_doc, all_args_broken) =
            AssumedParamHug::sharing(assumed.as_ref(), printer, || {
                build_args_split_last(call.arguments, printer, paren_open, has_any_comments)
            });

        // Prettier: if (headArgs.some(willBreak)) return allArgsBrokenOut()
        if let Some(bail) = opener.expand_all_if_head_breaks(d, &head_parts, all_args_broken) {
            return with_chain_head(d, head, bail);
        }

        // The same refusal one argument over: a break forced inside the LAST argument's own
        // signature invalidates the inline state just as a breaking head argument does. The
        // expression-body arms below have always asked it; this one — the block-bodied
        // callbacks, both spellings — never did.
        if call
            .arguments
            .last()
            .is_some_and(|arg| callback_signature_refuses_hug(printer, arg, call.arguments.len()))
        {
            return with_chain_head(d, head, opener.expand_all(d, all_args_broken));
        }

        let last_index = call.arguments.len() - 1;
        // The chain's own convergence states ask for one hug state alone
        // (`chain::builder`'s assumed-hug and return-type hug states).
        let build_last =
            || build_joined_argument_doc(printer, paren_open, call.arguments, last_index);
        if let Some(contents) = assumed.as_ref().and_then(|hug| {
            hug.take_forced(printer, AssumedHugKind::ParamLiteral, build_last)
                .or_else(|| hug.take_forced(printer, AssumedHugKind::ReturnType, build_last))
        }) {
            return with_chain_head(d, head, opener.inline(d, &head_parts, contents));
        }
        // A plain call's last of several arguments: its hug flattens a `function`'s parameters.
        let assumed =
            assumed.map_or_else(Default::default, |hug| hug.build(printer, true, build_last));
        return with_chain_head(
            d,
            head,
            opener.inline_assumed_or_expand_all(
                d,
                &head_parts,
                last_arg_doc,
                assumed,
                all_args_broken,
            ),
        );
    }

    // Expression arrow with call/conditional expression body
    // Prettier keeps preceding args inline and breaks after =>
    // e.g., `a.b(c, (x) =>\n  fn(x, ...),\n);`
    // couldExpandArg keys only on the body — param/return type annotations don't disable
    // the hug, so a typed arrow expands the same way (its full signature is emitted via
    // build_arrow_sig_doc).
    if call.arguments.len() >= 2
        && !comments_force_expansion
        && !(has_any_comments && last_arg_commented)
        && let Some(Expression { span: arrow_span, kind: ExpressionKind::ArrowFunctionExpression(arrow) }) = call.arguments.last()
        && let ast::ArrowFunctionBody::Expression(body_expr) = &arrow.body
        // …and it reads the body through prettier's `stripChainElementWrappers`, so a call
        // reached through a trailing `!` is expandable too — the same pair the plain-call /
        // `new` twin asks ([`try_expand_last_arg`]). Asking the bare node kind here declined
        // the ladder for `(x) => fn(x)!` and broke every argument out, where both other
        // spellings — and prettier — keep the head inline.
        && (arrow_body_is_call_through_non_null(body_expr) || is_ternary_arrow_body(body_expr))
        // The reassembling arm's refusal pair, which bites here exactly as it does in the
        // single-argument arms above (`arrow_hug_refused_by_comments`).
        && !arrow_hug_refused_by_comments(printer, arrow, *arrow_span, body_expr)
    {
        // Expand-last arrow with a call body: build the body ONCE and inject it so the
        // whole-arrow arg doc reuses it (the break-body state below reuses it too) —
        // building it in both places recurses into itself → O(2^depth).
        let body_reuse = prebuild_expand_last_break_body(printer, call.arguments.last().copied());
        let inject_prev = body_reuse.map(|(span, doc)| printer.inject_arrow_body(span, doc));

        let (head_parts, last_arg_doc, all_args_broken) =
            build_args_split_last(call.arguments, printer, paren_open, has_any_comments);

        if let Some(prev) = inject_prev {
            printer.restore_arrow_body_inject(prev);
        }

        // Prettier: if (headArgs.some(willBreak)) return allArgsBrokenOut()
        if let Some(bail) = opener.expand_all_if_head_breaks(d, &head_parts, all_args_broken) {
            return with_chain_head(d, head, bail);
        }

        let sig_doc = build_hugged_arrow_sig_doc(printer, arrow, *arrow_span);
        // Reuse the pre-built call body (see above); conditional bodies build fresh.
        let body_doc = body_reuse.map_or_else(
            || printer.build_arrow_arg_body_doc_unclaimed(body_expr),
            |(_, doc)| doc,
        );
        let body_doc =
            prepend_arrow_body_comments(printer, arrow, body_expr.span().start, body_doc);

        // The ladder — inline → break body → expand all — shared with the plain-call /
        // `new` spelling of this same layout ([`build_break_body_ladder`]).
        return with_chain_head(
            d,
            head,
            build_break_body_ladder(
                d,
                opener,
                &head_parts,
                sig_doc,
                body_doc,
                arrow_body_breaks_after_space(printer, arrow, body_expr),
                last_arg_doc,
                all_args_broken,
            ),
        );
    }

    // Expression arrow with an object/array terminal
    // Prettier keeps preceding args inline and expands object/array internally
    // e.g., `a.b(c, (x) => ({\n  y: x,\n}));`
    // couldExpandArg keys only on the terminal's type — a typed arrow expands the same
    // way, and so does a curried run of heads, since both states below render the
    // argument's own `expandLastArg` printing rather than a reassembled signature.
    if call.arguments.len() >= 2
        && !comments_force_expansion
        && !(has_any_comments && last_arg_commented)
        && let Some(last_arg) = call.arguments.last()
        && let ExpressionKind::ArrowFunctionExpression(arrow) = &last_arg.kind
        && arrow_body_expands_internally(arrow)
        // A signature that will break — the hug renders the prefix and the signature run on
        // one line. NOT the body-tail question its reassembling siblings ask
        // ([`arrow_hug_refused_by_comments`]): these states render the argument's own
        // `expandLastArg` printing, so the body-end→arrow-end gap has an emitter here. See
        // [`arrow_body_tail_has_comments`] for why refusing it was a bug rather than
        // conservatism.
        && !arrow_signature_refuses_hug(printer, arrow, last_arg.span)
    {
        // Expand-last arrow with an object/array terminal: build the terminal body ONCE and
        // inject it so BOTH printings of the argument reuse it — building it per printing
        // recurses into itself → O(2^depth). Re-armed around each printing, the same shape
        // the non-chain twin uses.
        let obj_reuse = prebuild_expand_last_obj_array_body(printer, Some(last_arg));

        let (head_parts, _printed_last_arg_doc, all_args_broken) = printer
            .with_arrow_body_inject(obj_reuse, || {
                build_args_split_last(call.arguments, printer, paren_open, has_any_comments)
            });

        // The willBreak bail and the three states, shared with the non-chain twin.
        return with_chain_head(
            d,
            head,
            build_expand_last_obj_array_doc(
                printer,
                opener,
                obj_reuse,
                &head_parts,
                all_args_broken,
                || printer.build_arg_expression_doc(last_arg),
            ),
        );
    }

    // "Expand first arg" pattern: first arg is block function, rest are short
    // e.g., `.reduce((acc, item) => { ... }, {})` - callback hugs, tail args stay inline
    // Matches prettier's shouldExpandFirstArg behavior
    // NOTE: Must come before expand-last-array/object to match Prettier's ordering —
    // shouldExpandFirstArg is checked before shouldExpandLastArg for arrays/objects.
    //
    // The shape test (two args, block-function first, short/comment-free second) is the
    // shared `should_expand_first_arg` — an inlined copy here is how
    // one of the two gets a fix and the other doesn't. Only the chain-specific refusals are
    // spelled out here.
    // The refusals are one negated disjunction rather than a run of `&& !`: the shape test
    // says the layout applies, and everything inside the parens is a reason it cannot.
    if should_expand_first_arg(printer, call.arguments)
        && !(comments_force_expansion
            // `has_any_comment_text`, not `has_any_comments`: refusing the expand-FIRST hug is
            // a LAYOUT decision, so it must see a comment the first argument owns and prints
            // itself — the twin of the expand-last gate above.
            || (has_any_comment_text
                && first_arg_has_any_comments(call.arguments, printer, paren_open))
            // Prettier's `ArgExpansionBailout` reached through `expandFirstArg`; the plain-call
            // and `new` twins ask the same predicate behind the same shape test.
            || first_arg_signature_refuses_expand_first(printer, call.arguments))
    {
        // The expand-first ladder, shared with the plain-call and `new` twins — including
        // the tail's willBreak bail and the broken-out fallback
        // (`docs/comments.md` hazard 2 is answered there, once).
        return with_chain_head(
            d,
            head,
            build_expand_first_arg_doc(printer, opener, call.arguments, paren_open, call_span.end),
        );
    }

    // "Expand last arg" pattern for arrays/objects:
    // Keep preceding args inline, only expand the last array/object arg.
    // e.g., `assert.deepEqual(parse('/foo'), [{...}, {...}])` keeps parse('/foo') inline
    // Matches prettier's shouldExpandLastArg for array/object arguments.
    //
    // Skip when last two args have the same outer type - use expand-all instead.
    if call.arguments.len() >= 2
        && last_arg_is_array_or_object(call.arguments, printer)
        && !call
            .arguments
            .last()
            .is_some_and(|arg| printer.arg_is_concisely_printed_array(arg))
        && !comments_force_expansion
        // `has_any_comment_text` (on page), not `has_any_comments` (to emit): an owned
        // comment leading the last array/object argument defeats the expand-last hug just
        // like an ordinary one — a to-emit gate would hug blind.
        && !last_arg_commented
        // Prettier blocks expand-last for 2-arg arrow+array (React hook pattern)
        && !(call.arguments.len() == 2
            && matches!(
                call.arguments.first(),
                Some(Expression { kind: ExpressionKind::ArrowFunctionExpression(_), .. })
            )
            && matches!(
                call.arguments.last(),
                Some(Expression { kind: ExpressionKind::ArrayExpression(_), .. })
            ))
        && !last_two_args_same_type(call.arguments)
    {
        let (head_parts, last_arg_doc, all_args_broken) =
            build_args_split_last(call.arguments, printer, paren_open, has_any_comments);

        // Prettier: if (headArgs.some(willBreak)) return allArgsBrokenOut()
        if let Some(bail) = opener.expand_all_if_head_breaks(d, &head_parts, all_args_broken) {
            return with_chain_head(d, head, bail);
        }

        // The three-state ladder — inline → hug the last argument open → every argument on
        // its own line — shared with the plain-call / `new` spelling.
        return with_chain_head(
            d,
            head,
            opener.inline_hug_or_expand_all(d, &head_parts, last_arg_doc, all_args_broken),
        );
    }

    // Multiple arguments: wrap in group with softlines so they can break. Each gap's
    // after-comma block comment follows the respect-the-newline rule — hugging the next
    // arg → leads it (`C`); stranded on the comma line → stays there (`A`) — via the same
    // shared emit_* helpers the force-expanded paths use. A comment-free gap takes the
    // cheap `comma_line()` separator (no per-gap comment scan).
    let mut arg_parts = DocBuf::new();
    for (i, arg) in call.arguments.iter().enumerate() {
        let arg_start = arg.span().start;
        let arg_end = arg.span().end;
        let is_first = i == 0;
        let is_last = i == call.arguments.len() - 1;

        // Leading inline block comments before the first arg (paren → arg gap).
        if is_first
            && has_any_comments
            && let Some(l) = build_inline_leading_comments(printer, paren_open, arg_start)
        {
            arg_parts.push(l);
        }

        // Broken-out layout, so prettier's `printedArguments` shape — a curried chain takes
        // the progressive layout (`build_printed_argument_doc`), as on the no-comment path.
        arg_parts.push(build_printed_argument_doc(printer, arg, || {
            printer.build_arg_expression_doc(arg)
        }));

        if is_last {
            // Trailing inline block comments after the last arg (before `)`).
            if has_any_comments
                && let Some(t) = build_inline_trailing_comments(printer, arg_end, call_span.end)
            {
                arg_parts.push(t);
            }
        } else {
            let next_arg_start = call.arguments[i + 1].span().start;
            if has_any_comments && printer.has_comments_to_emit_between(arg_end, next_arg_start) {
                // The shared argument-gap seam: everything through the comma
                // (before-comma blocks, the comma, a stranded after-comma block `A`), the
                // break, then the next argument's leading run (hugging after-comma `C` +
                // own-line comments).
                let (through_comma, leading) = build_arg_gap_docs(printer, arg_end, next_arg_start);
                arg_parts.extend(through_comma);
                arg_parts.push(d.line());
                arg_parts.extend(leading);
            } else {
                arg_parts.push(d.comma_line());
            }
        }
    }
    // Prettier's default tail: the list in its own group, except for a long-curried call,
    // whose list breaks with the group around it ([`ChainCall::ungroup_args`]).
    let args = d.concat(&arg_parts);
    let list = if ungroup_args {
        opener.soft_list(d, args)
    } else {
        opener.wrap_soft(d, args)
    };
    with_chain_head(d, head, list)
}