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
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
// Destructuring pattern printing for TypeScript
//
// This module handles all destructuring patterns:
// - Object patterns: `{a, b}` with width-based expansion
// - Array patterns: `[a, b]`
// - Assignment patterns: `a = 1`
// - Assignment expressions: `a = b` with width-based wrapping and chain detection
// - Rest elements: `...rest`
use super::assignment::{AssignmentLeft, RhsCommentInfo};
use super::objects::LiteralRole;
use crate::ast::{
self, ArrowFunctionBody, AssignmentOperator, Expression, ExpressionKind, ObjectPatternProperty,
};
use crate::printer::comments::ValueGap;
use crate::printer::comments::next_real_element_start;
use crate::printer::{
CommentVec, ContinuationValue, ParenContext, PatternContext, Printer,
object_pattern_should_expand,
};
use smallvec::{SmallVec, smallvec};
use tsv_lang::Span;
use tsv_lang::doc::DocBuf;
use tsv_lang::doc::after_operator::hang_after_operator;
use tsv_lang::doc::arena::DocId;
use tsv_lang::source_scan::find_char_skipping_comments;
/// Context for assignment expression printing (chain detection)
///
/// Matches prettier's `path.match()` logic for determining when to use chain formatting.
/// Chain formatting is ONLY used when the parent is another assignment expression.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AssignmentContext {
/// Parent is anything but an assignment expression — a statement, a declaration, an
/// argument, an operand. Regular grouped layout.
None,
/// Parent is an assignment expression
/// → Use chain formatting (ungrouped with line elements)
Chain,
}
/// The gap after array-pattern slot `i`, in this container's terms:
/// [`next_real_element_start`] with the pattern's own fallback — the body end its caller
/// already derived ([`Printer::array_pattern_tail`]), which stops at the closing `]`
/// rather than running on over a `?` / `: T` the pattern's span swallowed. The array
/// literal's twin is `array_gap_end`; each container names its own end once and the walk
/// past the holes is shared.
fn array_pattern_gap_end(arr: &ast::ArrayPattern<'_>, i: usize, body_end: u32) -> u32 {
next_real_element_start(arr.elements, i).unwrap_or(body_end)
}
/// Check if an arrow function has a nested arrow function as its body
/// Used for chain-tail-arrow-chain detection: `(x) => (y) => x + y`
fn is_nested_arrow_function(expr: &Expression<'_>) -> bool {
if let ExpressionKind::ArrowFunctionExpression(arrow) = &expr.kind
&& let ArrowFunctionBody::Expression(body_expr) = &arrow.body
{
return matches!(body_expr.kind, ExpressionKind::ArrowFunctionExpression(_));
}
false
}
/// A destructuring pattern's TAIL — the optional `?` marker and `: Type` annotation that
/// follow its closing `}`/`]` — paired with the body end they hang off.
///
/// The pairing is the whole point. `body_end` is both the bound every comment scan over the
/// pattern takes and the left edge of the tail's own first gap (`}`→`?`), so the two are one
/// fact about one node. Derived together ([`Printer::object_pattern_tail`] /
/// [`Printer::array_pattern_tail`]) rather than re-spelled at each site that needs both, where
/// one node's bound could be handed another's marker — the mismatch that a bound passed
/// alongside its node, instead of with it, makes expressible.
#[derive(Clone, Copy)]
struct PatternTail<'a> {
/// Just past the closing `}`/`]` — see [`Printer::pattern_body_end`].
body_end: u32,
optional: bool,
type_annotation: Option<&'a ast::TSTypeAnnotation<'a>>,
/// The pattern is a HUGGED sole parameter ([`Printer::build_hugged_object_pattern_doc`]),
/// so an object-type literal annotating it prints without a group of its own too.
hugged: bool,
}
/// Layout role of an assignment-chain segment's right side (`a = b = value`).
///
/// Three mutually exclusive roles, so an enum rather than a tail-ness / arrow-ness bool
/// pair: arrow-ness is meaningful only at the tail, and a pair makes the fourth
/// combination — an arrow chain that is not the tail — expressible and meaningless.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ChainSegment {
/// Chain middle (`a =` in `a = b = value`) — soft line break, no indent.
Middle,
/// Chain tail — indent the final value onto its own line when it breaks.
Tail,
/// Chain tail whose value is a nested arrow (`(x) => (y) => …`) — space, no indent.
ArrowChainTail,
}
/// Build chain formatting doc: [group(left), op, ...right parts]
fn build_chain_doc(
d: &tsv_lang::doc::arena::DocArena,
left_doc: DocId,
operator: DocId,
right_doc: DocId,
segment: ChainSegment,
) -> DocId {
let mut parts: DocBuf = smallvec![d.group(left_doc), operator];
match segment {
ChainSegment::ArrowChainTail => {
// Chain-tail-arrow-chain: (x) => (y) => x + y
parts.push(d.text(" "));
parts.push(right_doc);
}
ChainSegment::Tail => {
// Standard chain tail: indent the final value
parts.push(d.indent_line(right_doc));
}
ChainSegment::Middle => {
// Chain middle: soft line break, no indent
parts.push(d.line());
parts.push(right_doc);
}
}
d.concat(&parts)
}
impl<'a> Printer<'a> {
/// Build a Doc for an assignment expression
pub(super) fn build_assignment_doc(
&self,
assign: &ast::AssignmentExpression<'_>,
span: Span,
) -> DocId {
// Assignment RHS — an `ancestorNameMap` value position.
self.mark_ternary_extra_indent(assign.right);
// Every parent that reaches this entry point is a NON-assignment one: the chain
// context is set by exactly one caller, the recursive one below.
self.build_assignment_doc_with_context(assign, span, AssignmentContext::None)
}
/// Build a Doc for an assignment expression with chain context
fn build_assignment_doc_with_context(
&self,
assign: &ast::AssignmentExpression<'_>,
assign_span: Span,
context: AssignmentContext,
) -> DocId {
let d = self.d();
let rhs_is_assignment =
matches!(assign.right.kind, ExpressionKind::AssignmentExpression(_));
// A type-assertion target (`as` / `satisfies` / `<T>`) must be parenthesized
// to round-trip (`(x as T) = …`); non-null `x!` stays bare. The cast is kept
// in the internal AST so the formatter reproduces prettier's output, even
// though the public AST drops it from a simple `=` left. The shared target
// seam also emits the `^`→left gap — the authored `(`'s interior, which no
// other emitter reaches.
//
// A member chain in target position prints its `.prop` lookups with no break
// point (prettier's `printMemberExpression` `shouldInline`), so the mark goes up
// before the target is built and the chain root reads it — see
// [`Printer::mark_assignment_target_member_lookups`].
self.mark_assignment_target_member_lookups(assign.left);
// An object or array literal left (a compound operator's, which the parser never
// reparses as a pattern) prints in the target's role
// ([`Printer::build_compound_target_doc`]); every other left is its own doc.
let left_doc = self.build_shell_operand_doc_with(
assign_span.start,
assign.left,
ParenContext::AssignmentTarget {
operator: assign.operator,
},
|| self.build_compound_target_doc(assign.left),
);
// The VALUE is an assignment operand too, and prettier's call-object clause is
// gated on exactly that position ([`Printer::mark_member_call_tail_operand`]).
// Marked AFTER the target is built, since a nested assignment inside the target
// marks its own operands; every branch below builds the value after this point.
self.mark_member_call_tail_operand(assign.right);
// And the value is an assignment value — prettier's `shouldIndentIfInlining` keys
// on the PARENT being an `AssignmentExpression`, not on which layout tsv picked,
// so the mark belongs to the CONSTRUCT and not to one arm. It sits here for the
// same reason as the line above, and covers all five of this function's
// value-building arms: the operator→value continuation, the assignment chain, the
// two `=`-comment arms and the ordinary layout. Anchored to a single arm instead,
// the other four printed a binary value with the continuation indent of an unexempt
// position while their sibling printed the same construct flush — one construct,
// two answers, which is the exact drift this mark exists to prevent. Two of the
// five build their value outside `build_assignment_layout` and so read no other
// mark — the continuation and the chain, pinned by the `chain_binary_value_long`
// and `before_operator_line_comment_long_prettier_divergence` fixtures. The layout
// re-marks the same span when it is the arm that runs; a redundant store is the
// price of the mark being the construct's.
self.mark_assignment_value(assign.right);
// Extract inline comments between operator and RHS
// Uses line-comment-safe spacing: block comments get trailing space,
// line comments get hardline to prevent content absorption.
let rhs_comment_start = assign.left.span().end;
let rhs_comment_end = assign.right.span().start;
// One probe for the whole target→value gap gates every comment question in
// it, so the overwhelming common case (`a = b`) skips locating the operator —
// a byte-by-byte source scan — and both gap emitters. Sound because comments
// are start-sorted + disjoint and each sub-range lies within the gap, so an
// empty whole-gap window makes every sub-query provably empty. The same
// shape `build_variable_declaration_doc` uses for the declarator's gap.
let operator = assign.operator.as_str();
let op_pos = self
.has_comments_on_page_between(rhs_comment_start, rhs_comment_end)
.then(|| self.find_operator_in_source(rhs_comment_start, rhs_comment_end, operator))
.flatten();
// A comment in the target→operator gap that no inline position can hold — a
// `//` would swallow the operator, and a multiline block the author broke
// after would lose that break — keeps its place, and the operator and value
// take a continuation line indented one level, exactly as the declarator's
// name→`=` gap answers it (prettier instead relocates the comment past the
// value to end-of-statement, merging it with any comment already there).
// Asked BEFORE the promotion below, which would otherwise consume the very
// multiline block this gate reads. Bypasses the assignment-layout selection;
// the value is built lazily so the common path is unaffected.
if let Some(op_pos) = op_pos
&& let Some(continuation) = self.build_operator_value_continuation(
rhs_comment_start,
op_pos,
operator,
ContinuationValue::Expression(assign.right),
|| {
// The operator→RHS head freezes here exactly as it does on the
// ordinary path below — this arm decides where the operator and
// its value SIT, not what the value is. Skipping the verdict let
// an honored directive through with its effect silently dropped.
let gap_start = op_pos + operator.len() as u32;
let value_doc =
match self.value_head_frozen_span(gap_start, assign.right.span()) {
Some(frozen) => self.build_frozen_value_shell_doc(
assign.right,
frozen,
assign_span.end,
false,
),
None => self.build_value_doc_keeping_relational_sibling_pair(
assign.right,
assign_span.end,
),
};
self.prepend_rhs_comments(value_doc, gap_start, rhs_comment_end)
},
)
{
return d.concat(&[left_doc, continuation]);
}
// Promote comments that appear before the operator to the LHS.
// e.g., `a /* comment */ = b` → comment stays before `=`, not after.
let (left_doc, effective_rhs_start) = match op_pos
.and_then(|op| self.promote_comments_before_operator(rhs_comment_start, op))
{
Some((promoted, new_start)) => (d.concat(&[left_doc, promoted]), new_start),
None => (left_doc, rhs_comment_start),
};
let rhs_has_line_comment =
self.has_line_comments_between(effective_rhs_start, rhs_comment_end);
// A single-line block glued to the operator hugs the value even across a
// source newline (`x = /* c */⏎v` → `x = /* c */ v`), matching prettier's
// assignment layout.
let rhs_comments = self.build_rhs_comments_glued_opt(effective_rhs_start, rhs_comment_end);
// A run glued through to the value must not hang it even when a multiline
// member makes its doc break (`= /* x⏎y */ /* c */ v`) — see
// `RhsCommentInfo::glued_through`.
let rhs_glued_through =
self.comment_run_glued_through(effective_rhs_start, rhs_comment_end);
// prettier's `chooseLayout` fourth disjunct — an indentable block leading the RHS
// hangs it ([`Printer::indentable_block_leads_value`]). Two things make it see what
// a node-keyed reading of the same rule cannot: the RANGE is the gap, so a comment
// a discarded grouping paren separates from the RHS's first token still counts, and
// the AXIS is **on page**, so a comment the RHS owns counts even though this gap
// emits nothing for it.
let rhs_indentable_leads_value =
self.indentable_block_leads_value(effective_rhs_start, rhs_comment_end);
// The `=`→RHS head: an own-line directive there freezes the whole RHS — including a
// nested assignment, so a chain freezes from the directive down.
let rhs_frozen = self.value_head_frozen_span(effective_rhs_start, assign.right.span());
// The operator→RHS gap's facts, stated once for both of the layout's call sites
// below — the 2-segment chain and the ordinary value. They differ in nothing but
// the guard that reaches them.
let rhs_info = || RhsCommentInfo {
comments: rhs_comments,
has_line_comment: rhs_has_line_comment,
glued_through: rhs_glued_through,
gap: Some(ValueGap {
start: effective_rhs_start,
indentable_leads_value: rhs_indentable_leads_value,
}),
boundary: Some(assign_span.end),
frozen: rhs_frozen,
};
// What the LEFT is, for the layout's own arm ([`AssignmentLeft`]). A complex
// destructuring target answers the layout on its own, ahead of the value.
let left = if self.is_complex_destructuring_target(assign.left) {
AssignmentLeft::ComplexPattern
} else {
AssignmentLeft::Plain
};
// For 2-segment chains at top level (a = b = value), use unified assignment layout.
// Prettier only uses chain formatting for 3+ segments (`chooseLayout`, `print/assignment.js`).
// A 2-segment chain has rhs_is_assignment=true but the inner RHS is NOT an assignment.
if !matches!(context, AssignmentContext::Chain)
&& rhs_is_assignment
&& let ExpressionKind::AssignmentExpression(inner) = &assign.right.kind
{
let inner_rhs_is_assignment =
matches!(inner.right.kind, ExpressionKind::AssignmentExpression(_));
if !inner_rhs_is_assignment {
return self.build_assignment_layout(
left_doc,
assign.operator.doc_with_leading_space(d),
assign.right,
left,
rhs_info(),
);
}
}
// Use unified assignment layout for simple (non-chain) cases.
// build_assignment_layout builds right_doc internally and handles rhs_comments.
//
// A DESTRUCTURING target is not special here. Prettier's `printAssignment` chooses
// the layout from the VALUE — a non-inlining binaryish RHS breaks after the
// operator whatever the left is — so `({ a } = x ?? y ?? z)` hangs its value like
// every other assignment host, and the value's own chain then indents (or not) by
// the mark above. Excluding a pattern target here sends it to the assignment-chain
// fall-through at the bottom of this function instead, which is keyed on an
// assignment RHS and answers nothing about a value's own width.
if !matches!(context, AssignmentContext::Chain) && !rhs_is_assignment {
// A block run the author broke AFTER (`w = /* c */⏎<value>`): the shared
// `=` broke-after arm ([`Printer::broke_after_operator_rhs_doc`] — the
// two-half rule, its declines, and what falls through live there),
// applied to the expression statement's operator. Site-specific gates:
// a frozen RHS keeps the layout below (its bypass owns the directive's
// placement). A class field takes the same arm at its own seam
// (`build_property_assignment_layout`); an object property value does NOT —
// prettier relocates there and tsv preserves the authored position
// (conformance_prettier_ts_comments.md §Comment relocation, the
// `value_block_comment_break` family).
// A LINE comment in the gap: route to the shared `=`→value partition rather
// than letting `RhsCommentInfo::has_line_comment` hang the whole run under
// the operator. That hang made the assignment expression the lone dissenter
// among its own siblings — the declarator and the class field both trail a
// comment the author glued to the operator (`a = // c⏎expr`), and prettier
// trails at all three — so it was tsv disagreeing with itself, not a
// divergence tsv had chosen. Only the line arm routes here: the block arms
// already agree with prettier through the assignment layout below
// (`operator_value_glued_multiline_block_comment`).
// The value under either `=`-comment arm, both of which have already broken
// after the operator and indented ([`Printer::build_hung_value_doc`]).
let build_hung_value = || {
self.build_hung_value_doc(assign.right, effective_rhs_start, || {
self.build_value_with_outermost_owned_comment(assign.right, || {
self.build_value_doc_keeping_relational_sibling_pair(
assign.right,
assign_span.end,
)
})
})
};
if rhs_frozen.is_none()
&& let Some(op_pos) = op_pos
&& self.has_line_comments_between(effective_rhs_start, rhs_comment_end)
// The partition anchors on the operator's LAST character, since that is
// the line a comment shares to count as trailing the operator — one char
// back from its end, and not the same thing for a compound operator
// (`+=`) as for `=`.
&& let Some(rhs) = self.build_eq_comment_break_rhs(
op_pos + assign.operator.as_str().len() as u32 - 1,
rhs_comment_end,
assign.operator.as_str_with_leading_space(),
// Both `=`-comment arms print their run outside the value, so a
// multi-line comment the value owns prints outside the value's own group
// too — the declarator's rule ([`Printer::build_value_with_outermost_owned_comment`]).
build_hung_value,
)
{
return d.concat(&[left_doc, rhs]);
}
if rhs_frozen.is_none()
&& rhs_comments.is_some()
&& let Some(rhs_doc) = self.broke_after_operator_rhs_doc(
effective_rhs_start,
rhs_comment_end,
build_hung_value,
)
{
return d.concat(&[left_doc, assign.operator.doc_with_leading_space(d), rhs_doc]);
}
return self.build_assignment_layout(
left_doc,
assign.operator.doc_with_leading_space(d),
assign.right,
left,
rhs_info(),
);
}
// TODO: the line-comment partition above stops at the 2-segment chain. A 3+
// segment chain's innermost `=` (`a = b = c = // c1⏎expr`) still hangs the comment
// on its own line where the shallower spellings — and prettier — trail it. The
// predicate is not what differs: this tail's layout is owned by `build_chain_doc`,
// which decides the breaks and indents across every segment, so routing the
// partition here is a layout change rather than the gap fix, and no corpus file
// reaches the shape. Take it with the chain layout, not as a patch.
// Build right doc for paths that handle layout directly
let right_doc = if let Some(frozen) = rhs_frozen {
// The same shell the unfrozen arm two lines down builds, over the slice — the
// RHS's grouping `)` sits past the slice's end, so its interior comment has no
// other emitter (`Printer::build_frozen_value_shell_doc`).
self.build_frozen_value_shell_doc(assign.right, frozen, assign_span.end, false)
} else if let ExpressionKind::AssignmentExpression(rhs_assign) = &assign.right.kind {
self.build_assignment_doc_with_context(
rhs_assign,
assign.right.span,
AssignmentContext::Chain,
)
} else {
self.build_value_doc_keeping_relational_sibling_pair(assign.right, assign_span.end)
};
// Prepend inline comments to right doc if present
let right_doc = if let Some(comments_doc) = rhs_comments {
d.concat(&[comments_doc, right_doc])
} else {
right_doc
};
if matches!(context, AssignmentContext::Chain) {
// Chain formatting - parent is an assignment
let segment = if rhs_is_assignment {
ChainSegment::Middle
} else if is_nested_arrow_function(assign.right) {
ChainSegment::ArrowChainTail
} else {
ChainSegment::Tail
};
build_chain_doc(
d,
left_doc,
assign.operator.doc_with_leading_space(d),
right_doc,
segment,
)
} else {
// RHS is a chain - group + indent (chain formatting from recursive call)
d.group(d.concat(&[
left_doc,
assign.operator.doc_with_leading_space(d),
d.indent_line(right_doc),
]))
}
}
/// A compound assignment's object or array LITERAL left — and, recursively, a nested
/// literal or an `=` value inside one — printed as prettier prints the pattern its
/// parser makes of it ([`LiteralRole::CompoundTarget`]). Any other expression is its
/// ordinary doc.
///
/// ecma262 reparses only an `=` left as a pattern (§13.15.1), so the parser keeps a
/// compound left the literal it was written as, methods and accessors included; only
/// the layout follows the pattern. The one entry the role has: the compound
/// assignment's left, and the literal's own builders as they recurse.
pub(in crate::printer) fn build_compound_target_doc(&self, expr: &Expression<'_>) -> DocId {
let doc = match &expr.kind {
ExpressionKind::ObjectExpression(obj) => {
self.build_object_doc_as(obj, expr.span, LiteralRole::CompoundTarget)
}
ExpressionKind::ArrayExpression(arr) => {
self.build_array_doc_as(arr, expr.span, LiteralRole::CompoundTarget)
}
ExpressionKind::AssignmentExpression(assign)
if assign.operator == AssignmentOperator::Assign =>
{
let pattern = ast::AssignmentPattern {
left: assign.left,
right: assign.right,
decorators: None,
};
self.build_assignment_pattern_doc(&pattern, expr.span)
}
_ => return self.build_expression_doc(expr),
};
self.prepend_owned_leading_comment(expr, doc)
}
/// Where a destructuring pattern's `{…}` / `[…]` body ENDS — the offset just past the
/// closing delimiter.
///
/// The bound every comment scan over the pattern takes. The pattern's own span runs on
/// over a `?` marker and a `: T` annotation, so with either present the closer has to be
/// **scanned for** rather than read off the node: `last_content_end` is the last
/// element's span end (just inside the opening delimiter when the pattern is empty), and
/// everything between it and the closer is whitespace, comments, and at most one comma —
/// so the first `closing` byte outside a comment is the delimiter itself.
///
/// Naming the annotation's start instead is the same-shaped near-miss the identifier
/// head made: it reads as "the body's end" and is one *token* too far, which hands the
/// pattern's three head gaps (`}`→`?`, `?`→`:`, `}`→`:`) to the interior scans. They then
/// print a comment written OUTSIDE the brackets inside them — re-associating it with the
/// last element — while the empty-pattern emitter's `span_end - 1` (its contract is "one
/// past the closer") lands inside the comment and drops it outright.
fn pattern_body_end(
&self,
span: Span,
optional: bool,
type_annotation: Option<&ast::TSTypeAnnotation<'_>>,
last_content_end: u32,
closing: u8,
) -> u32 {
// No `?` and no `: T`: the span already ends at the closing delimiter.
if !optional && type_annotation.is_none() {
return span.end;
}
find_char_skipping_comments(
self.source.as_bytes(),
last_content_end as usize,
span.end as usize,
closing,
)
.map_or_else(
// Unreachable for a parsed pattern — a tail implies a closer before it. Falling
// back to the pre-scan reading keeps a hypothetical miss at today's behavior
// rather than claiming the annotation's own comments.
|| type_annotation.map_or(span.end, |t| t.span.start),
|pos| pos as u32 + 1,
)
}
/// The object pattern's [`PatternTail`] — [`Self::pattern_body_end`] in its brace
/// spelling, paired with the tail it bounds.
fn object_pattern_tail<'t>(
&self,
obj: &'t ast::ObjectPattern<'_>,
span: Span,
) -> PatternTail<'t> {
let optional = obj.optional;
let type_annotation = obj.type_annotation.as_ref();
let last_content_end = obj
.properties
.last()
.map_or(span.start + 1, |p| p.span().end);
PatternTail {
body_end: self.pattern_body_end(
span,
optional,
type_annotation,
last_content_end,
b'}',
),
optional,
type_annotation,
hugged: false,
}
}
/// The array pattern's [`PatternTail`]. A trailing hole carries no span, so the scan
/// starts at the last REAL element — the commas past it are structure.
fn array_pattern_tail<'t>(
&self,
arr: &'t ast::ArrayPattern<'_>,
span: Span,
) -> PatternTail<'t> {
let optional = arr.optional;
let type_annotation = arr.type_annotation.as_ref();
let last_content_end = arr
.elements
.iter()
.rev()
.flatten()
.next()
.map_or(span.start + 1, |e| e.span().end);
PatternTail {
body_end: self.pattern_body_end(
span,
optional,
type_annotation,
last_content_end,
b']',
),
optional,
type_annotation,
hugged: false,
}
}
/// Build the doc for a destructuring pattern's [`PatternTail`] — the optional `?` marker
/// (parameter position only), then a `: Type` annotation. Either may be absent: `[]`,
/// `[]?`, `[]: T`, `[]?: T`. Shared by every object/array pattern path so the `?` lands
/// between the brackets and the type uniformly.
///
/// The tail is **position-keyed** rather than position-free because the two head gaps
/// behind it are nobody else's: respelling the marker as a bare `?` and calling
/// [`Self::build_type_annotation_doc`] skips both landings that own them
/// ([`Self::push_modifier_marker_doc`], [`Self::build_binding_type_annotation_doc`]) —
/// `docs/comments.md` hazard 4 in its head variant, and the same defect the function-type
/// hug had. Routing them is also what makes the pattern head agree with the identifier
/// head it is a spelling of: one rule for `a /* c */?: T` and `{ a } /* c */?: T` alike.
fn build_pattern_tail_doc(&self, tail: PatternTail<'_>) -> Option<DocId> {
let d = self.d();
let mut parts = DocBuf::new();
let marker_end = if tail.optional {
self.push_modifier_marker_doc(&mut parts, tail.body_end, b'?')
} else {
tail.body_end
};
if let Some(ta) = tail.type_annotation {
// A hugged pattern's object-type annotation is group-less like the pattern
// (prettier's `printObject`, the same arm a hugged identifier's literal takes).
let literal = if tail.hugged {
self.hugged_param_annotation_literal(ta)
} else {
None
};
parts.push(match literal {
Some(literal) => {
self.build_binding_type_annotation_doc_with(marker_end, ta, || {
self.build_hugged_literal_annotation_doc(ta, literal, true)
})
}
None => self.build_binding_type_annotation_doc(marker_end, ta, false),
});
}
(!parts.is_empty()).then(|| d.concat(&parts))
}
/// Build a Doc for an object pattern
///
/// Prettier expands object patterns when:
/// 1. Any property has a nested pattern value (always expand)
/// 2. The pattern exceeds print width (width-based expansion)
pub(super) fn build_object_pattern_doc(
&self,
obj: &ast::ObjectPattern<'_>,
span: Span,
) -> DocId {
self.build_object_pattern_doc_with_context(obj, span, PatternContext::Standalone)
}
/// Build object pattern doc with explicit context
pub(super) fn build_object_pattern_doc_with_context(
&self,
obj: &ast::ObjectPattern<'_>,
obj_span: Span,
context: PatternContext,
) -> DocId {
self.build_object_pattern_doc_grouped(obj, obj_span, context, true)
}
/// The object pattern a sole parameter HUGS ([`Printer::hugs_sole_parameter`]): the
/// width-decided layout with **no group of its own**. Prettier's `printObject`
/// returns bare content for an undecorated `ObjectPattern` whose function hugs its
/// only parameter ("so that the object breaks before the return type"): the
/// pattern's lines belong to the enclosing signature group, so once the signature
/// overflows the properties expand and a breakable return type (`string |
/// undefined`, `Promise<B | null>`) stays inline. The object-type literal twin is
/// [`Printer::build_hugged_literal_param_doc`]. An empty pattern and a pattern that
/// expands on its own (nesting, comments, blank lines) keep their layout.
pub(in crate::printer) fn build_hugged_object_pattern_doc(
&self,
obj: &ast::ObjectPattern<'_>,
obj_span: Span,
) -> DocId {
self.build_object_pattern_doc_grouped(
obj,
obj_span,
PatternContext::FunctionParameter,
false,
)
}
/// [`Self::build_object_pattern_doc_with_context`]'s body; `own_group` is `false` only
/// for the hugged sole parameter ([`Self::build_hugged_object_pattern_doc`]).
fn build_object_pattern_doc_grouped(
&self,
obj: &ast::ObjectPattern<'_>,
obj_span: Span,
context: PatternContext,
own_group: bool,
) -> DocId {
let d = self.d();
if obj.properties.is_empty() {
self.build_empty_object_pattern_doc(obj, obj_span)
} else {
// Expand if: nested patterns, line comments, blank lines BETWEEN
// properties, or own-line block comments between/around properties.
// One whole-span comment pre-check gates both comment-scanning hints
// (blank lines are comment-independent, so `formatting_hints` still runs
// to detect them). The blank half is deliberately inter-property only —
// a blank after the `{` is not a reason to expand, and acting on one made
// the expanded output its own second fixed point (`collection_formatting_hints`).
let should_expand = object_pattern_should_expand(obj, context);
let tail = PatternTail {
hugged: !own_group,
..self.object_pattern_tail(obj, obj_span)
};
let boundary = tail.body_end;
let has_comments = self.has_comments_on_page_between(obj_span.start, boundary);
let (has_line_comments, has_blank_lines) =
self.object_pattern_formatting_hints(obj, obj_span, boundary, has_comments);
let has_own_line_block = has_comments
&& self.object_pattern_has_own_line_block_comments(obj, obj_span, boundary);
// The multi-line half of the own-line question, asked on its own the way the
// array pattern asks it (`has_own_line_block_comments_in_bracket_list` skips
// multi-line comments by design): without it an own-line MULTI-LINE block past
// the last property reached the grouped path, whose closer run prints every
// comment inline, and was pulled onto the property's line — where the array
// pattern, the object literal and prettier keep it own-line.
let has_multiline_block = has_comments
&& self.has_multiline_block_comments_on_page_between(obj_span.start, boundary);
// A rest's stripped-paren interior lies inside its printed span, out of every
// gap scan above — see the array pattern's twin.
let rest_interior_expands = has_comments
&& self.paren_interior_forces_expansion(
obj.properties
.last()
.and_then(ObjectPatternProperty::paren_interior),
);
if should_expand
|| has_line_comments
|| has_blank_lines
|| has_own_line_block
|| has_multiline_block
|| rest_interior_expands
{
self.build_expanded_object_pattern_doc(obj, obj_span, tail)
} else {
// Use group with line breaks for width-based expansion
// Include type annotation in the group so its width is considered
let mut parts = d.pooled_docbuf();
// Track previous end for comment detection (start after `{`)
let mut prev_end = obj_span.start + 1;
for (i, prop) in obj.properties.iter().enumerate() {
// Check for leading comments before this property. Gated on the
// object-wide comment flag: with no comment in the pattern span, the
// per-property gap is empty too, so skip the `empty()` leading child
// (render + every fits pass would still walk it). Byte-identical.
let prop_start = prop.span().start;
if has_comments {
let leading_comments = self
.build_inline_comments_between_doc_trailing_space(prev_end, prop_start);
parts.push(leading_comments);
}
parts.push(self.build_object_pattern_property_doc(prop));
// The property's PRINTED end, not its span end — see
// `Printer::element_claim_anchor`, which this path asks unguarded
// because nothing freezes here: every format-ignore spelling carries a
// comment that routes the pattern to the expanded builder instead.
let prop_end = prop.value_end();
let is_last = i == obj.properties.len() - 1;
// Collect trailing comments (stop at next property or type annotation)
let upper_bound = obj
.properties
.get(i + 1)
.map_or(boundary, |next| next.span().start);
let trailing = self.collect_trailing_comments(prop_end, upper_bound, is_last);
// Separator comma between properties; no trailing comma on the last
// property (trailingComma: 'none').
let comma = (!is_last).then(|| d.text(","));
self.push_element_comma_trailing(&mut parts, &trailing, comma);
// Add line break between properties
if !is_last {
parts.push(d.line());
}
prev_end = trailing.end_pos;
}
// Whatever the last property's trailing run left unclaimed, before the
// closing brace — e.g. `{a /*, b*/}`.
if let Some(trailing) =
self.build_object_pattern_trailing_comments(prev_end, boundary)
{
parts.push(trailing);
}
// Build group contents: { + properties + } with bracketSpacing
// boundaries (space when flat `{ a }`, newline when broken).
let mut group_parts: DocBuf = smallvec![
d.text("{"),
d.indent_line(d.concat(&parts)),
d.line(),
d.text("}"),
];
// Include `?` + type annotation in the group for width calculation
if let Some(tail) = self.build_pattern_tail_doc(tail) {
group_parts.push(tail);
}
let content = d.concat(&group_parts);
if own_group { d.group(content) } else { content }
}
}
}
/// Build trailing comments doc for the inline (single-line) object pattern
/// path (between last property and `}`), e.g. `{a /*, b*/}`.
///
/// `prev_end` is the loop's cursor — the last property's trailing-run end
/// (`TrailingComments::end_pos`) — so everything still in `[prev_end, boundary)` is by
/// construction unclaimed and emitted here. Re-deriving "not on the last property's line" instead would ask the gap a
/// second, *different* question than the run that already answered it: the collector
/// follows a multi-line block to its closing line, and a comment sitting on that line
/// is neither on the property's line nor unclaimed. The expanded paths use
/// `build_pattern_trailing_dangling_comments` instead, which puts each comment on its
/// own line.
fn build_object_pattern_trailing_comments(
&self,
prev_end: u32,
boundary: u32,
) -> Option<DocId> {
let d = self.d();
let mut parts = DocBuf::new();
for comment in self.comments_to_emit_between(prev_end, boundary) {
parts.push(d.text(" "));
parts.push(self.build_comment_doc(comment));
}
(!parts.is_empty()).then(|| d.concat(&parts))
}
/// Build dangling comments after the last element of an *expanded* pattern
/// (between the last element and the closing `}`/`]`).
///
/// Each comment goes on its own line, with blank lines preserved. Same-line
/// trailing comments are handled in the per-element loop, so they are
/// skipped here. Mirrors the object/array expression printers' handling of
/// trailing comments before the closing delimiter (`objects.rs`). Without
/// this, expanded array patterns dropped these comments entirely (content
/// loss) and expanded object patterns glued them onto the last property's
/// line.
///
/// ⚠️ `past_elision` says a trailing **hole** sits between `prev_end` and this run, and
/// suppresses the blank line ahead of the FIRST comment. An elision's own line break is
/// structure, not authorship — the rule `Printer::has_blank_line_after_slot` states for
/// the array literal (prettier's `node &&`) — and a raw scan from the last binding's end
/// counts it, so `const [a,⏎,⏎/* c */] = arr` grew a blank the first pass never wrote:
/// not a fixed point. Blanks *between* two dangling comments are unaffected; only the
/// one measured across the hole is.
///
/// ⚠️ The run is measured from the last element's shell end
/// ([`Printer::element_shell_end`]) — the same "structure is not authorship" rule in
/// its *stripped paren* spelling. An array pattern's element span ends INSIDE its
/// shell, so a raw scan from it reads the erased `)`'s line breaks as an author blank
/// and fabricates one. The comment range still opens at `prev_end`: the peel steps over
/// nothing but `)` and whitespace and stops at the first comment, so no interior
/// comment is skipped.
fn build_pattern_trailing_dangling_comments(
&self,
prev_end: u32,
boundary: u32,
past_elision: bool,
) -> DocId {
let d = self.d();
let mut parts = DocBuf::new();
let mut last_pos = self.element_shell_end(prev_end, boundary);
let mut first = true;
for comment in self.comments_to_emit_between(prev_end, boundary) {
let blanks_measurable = !(first && past_elision);
first = false;
if self.is_same_line(last_pos, comment.span.start) {
// Same source line as the preceding content, which here means the
// preceding *comment*: the per-element loop's run already claimed
// everything sharing a line with the element (and follows a multi-line
// block to its closing line), so `prev_end` opens on a fresh line. The
// test is keyed on the running `last_pos` rather than `prev_end` for
// exactly that reason — it keeps two same-line dangling comments
// together (`/* a */ // b`) instead of splitting them onto two lines.
parts.push(d.text(" "));
parts.push(self.build_comment_doc(comment));
} else {
if blanks_measurable && self.has_blank_line_between(last_pos, comment.span.start) {
parts.push(d.literalline());
}
parts.push(d.hardline());
parts.push(self.build_comment_doc(comment));
}
last_pos = comment.span.end;
}
d.concat(&parts)
}
/// Generic helper: Check for line comments and blank lines in a collection
///
/// Returns (has_line_comments, has_blank_lines) in a single pass.
/// Works for any collection with elements that have spans.
///
/// `has_comments` is the caller's whole-span comment pre-check: when false,
/// the per-element comment scans are skipped (no comment can be in any gap),
/// leaving only the comment-independent blank-line detection.
///
/// The blank-line half is an **inter-element** question only: the leading gap
/// between the opening delimiter and the first element never counts (see the
/// loop's note). The comment half spans every gap, leading one included.
fn collection_formatting_hints<T>(
&self,
collection_start: u32,
collection_end: u32,
elements: &[T],
has_comments: bool,
get_span: impl Fn(&T) -> Span,
get_printed_end: impl Fn(&T) -> u32,
) -> (bool, bool) {
let mut has_line_comments = false;
let mut has_blank_lines = false;
let mut prev_end = collection_start + 1; // After opening bracket/brace
let mut prev_span_end = prev_end;
for (i, elem) in elements.iter().enumerate() {
let span = get_span(elem);
let elem_start = span.start;
// Blank-line detection is comment-independent; when the collection has
// no comments the first-comment lookup is a no-op (check_pos == elem_start).
// **in source**: bounds a raw blank-line scan (see `blank_scan_end`).
let check_pos = if has_comments {
self.comments_in_source_between(prev_end, elem_start)
.next()
.map_or(elem_start, |c| c.span.start)
} else {
elem_start
};
// ⚠️ The LEADING gap — the opening `{`/`[` to the first element — is skipped:
// it is an INTER-element rule, and a blank there is not an authorship signal
// a pattern can act on. Prettier guards its whole newline-after-`{` preserve
// on `node.type !== "ObjectPattern"` (`print/object.js`), so a pattern never
// opens on one; and the expanded form prints no leading blank, so measuring
// it made tsv's own output a SECOND fixed point — `const {⏎⏎a} = o` expanded
// on pass 1 and collapsed on pass 2, in every pattern position alike
// (declarator, parameter, catch binding, assignment pattern, nested).
// A blank-line rule cannot be conditioned on a trigger its own output erases.
//
// ⚠️ The blank scan keeps the SPAN end while the comment scan takes the printed
// one: they are opposite questions about the same stripped-paren shell
// (`docs/comments.md` §The element-comma seam). Measuring the distance from
// inside the shell reads its own line breaks as an author blank and expands a
// pattern prettier keeps inline (`{a = (1⏎⏎), b}`).
if i > 0 && self.has_blank_line_between(prev_span_end, check_pos) {
has_blank_lines = true;
}
if has_comments {
// Check for line comments
for comment in self.comments_to_emit_between(prev_end, elem_start) {
if !comment.is_block {
has_line_comments = true;
break;
}
}
// Early exit if both found
if has_line_comments && has_blank_lines {
return (true, true);
}
}
prev_end = get_printed_end(elem);
prev_span_end = span.end;
}
// Check comments after last element
if has_comments {
for comment in self.comments_to_emit_between(prev_end, collection_end) {
if !comment.is_block {
return (true, has_blank_lines);
}
}
}
(has_line_comments, has_blank_lines)
}
/// Check if object pattern has line comments or blank lines between properties
///
/// Returns (has_line_comments, has_blank_lines) in a single pass.
fn object_pattern_formatting_hints(
&self,
obj: &ast::ObjectPattern<'_>,
obj_span: Span,
boundary: u32,
has_comments: bool,
) -> (bool, bool) {
self.collection_formatting_hints(
obj_span.start,
boundary,
obj.properties,
has_comments,
ObjectPatternProperty::span,
ObjectPatternProperty::value_end,
)
}
/// Check if object pattern has any own-line single-line block comments
///
/// Mirrors the array pattern's own-line gate
/// ([`Self::has_own_line_block_comments_in_bracket_list`], asked inline in
/// [`Self::build_array_pattern_doc`]): an own-line block comment (between, before, or
/// after properties) forces expansion, matching prettier.
fn object_pattern_has_own_line_block_comments(
&self,
obj: &ast::ObjectPattern<'_>,
obj_span: Span,
boundary: u32,
) -> bool {
let span = Span::new(obj_span.start, boundary);
self.has_own_line_block_comments_in_bracket_list(span, obj.properties, |prop| {
// The property's PRINTED span — see the array pattern's twin.
Span::new(prop.span().start, prop.value_end())
})
}
/// Build doc for empty object pattern: `{}` with optional `?` + type annotation
fn build_empty_object_pattern_doc(
&self,
obj: &ast::ObjectPattern<'_>,
obj_span: Span,
) -> DocId {
// Bound the comment scan to the braces (before any `?`/`: Type`), mirroring
// `build_empty_array_pattern_doc`. Scanning the full span would pull a
// comment out of the type annotation into the empty `{}` and duplicate it.
let tail = self.object_pattern_tail(obj, obj_span);
let body_doc = self
.build_empty_braces_inline_with_comments_doc(Span::new(obj_span.start, tail.body_end));
self.append_opt(body_doc, self.build_pattern_tail_doc(tail))
}
/// Build expanded doc for object pattern with hardlines (always multiline)
fn build_expanded_object_pattern_doc(
&self,
obj: &ast::ObjectPattern<'_>,
span: Span,
tail: PatternTail<'_>,
) -> DocId {
let d = self.d();
let boundary = tail.body_end;
// Zero-comment fast gate: one binary search over the whole object-pattern
// window (up to any `: Type` annotation) decides whether the per-property
// loop needs any comment work. This branch is reached on nesting depth or a
// blank line alone (see `build_object_pattern_doc_with_context`), so a
// deeply-nested but comment-free pattern lands here with nothing to scan —
// the `{`-line prefix pull, the per-property leading-comment scan, the
// format-ignore lookup, the between-property comment scan, and the trailing
// dangling scan are all skipped. The gate is a general comment check (block
// comments too, not just line comments): a same-line block comment can
// reach this branch via nesting with no line comment, and the loop must
// still emit it. The expansion decision itself runs earlier and is
// unaffected. Canonical reference: build_params_doc_with_comments.
let has_comments = self.has_comments_to_emit_between(span.start, boundary);
// A comment trailing the opening `{` on its own line is kept on the `{`
// line when the pattern expands (divergence from prettier, which relocates
// it to its own line as the first property's leading comment). See
// conformance_prettier_ts_comments.md §Comment relocation (Object destructuring `{`).
let first_prop_start = obj.properties[0].span().start;
let (brace_line_prefix, brace_pull_pos) = if has_comments {
self.delimiter_line_comment_prefix(span.start, first_prop_start)
} else {
(None, None)
};
// Track previous end for comment detection (start after `{`)
let mut prev_end = span.start + 1;
let mut prop_parts = DocBuf::new();
for (i, prop) in obj.properties.iter().enumerate() {
// Handle leading comments before this property (with blank line preservation)
let prop_start = prop.span().start;
let leading_comments: CommentVec<'_> = if has_comments {
self.collect_item_leading_comments(
prev_end,
prop_start,
(i == 0).then_some(brace_pull_pos).flatten(),
)
} else {
CommentVec::new()
};
self.push_leading_comments_before(&mut prop_parts, &leading_comments, prop_start);
// A preceding format-ignore directive keeps the property's source verbatim
// (trailing comment/comma handled normally)
let frozen = has_comments
.then(|| self.gap_frozen_span(prev_end, prop.span()))
.flatten();
match frozen {
Some(slice) => prop_parts.push(self.raw_source_doc(slice)),
None => prop_parts.push(self.build_object_pattern_property_doc(prop)),
}
let prop_end = Self::element_claim_anchor(frozen, prop.value_end());
let is_last = i == obj.properties.len() - 1;
// Collect trailing comments (stop at next property or type annotation)
let upper_bound = obj
.properties
.get(i + 1)
.map_or(boundary, |next| next.span().start);
// A rest's stripped-paren interior (a frozen one printed it verbatim): the run
// past its `)` follows the pattern's share of it.
let rest_interior = prop.paren_interior().filter(|_| frozen.is_none());
let trailing = self.collect_element_trailing_comments(
prop_end,
upper_bound,
is_last,
rest_interior,
);
// Separator comma between properties; no trailing comma on the last
// property under `trailingComma: 'none'` (a rest element never takes one
// either — it is a syntax error there).
let comma = (!is_last).then(|| d.text(","));
self.push_element_comma_trailing(&mut prop_parts, &trailing, comma);
if !is_last {
// Blank line before the next property, on prettier's `isNextLineEmpty` —
// the same question the object LITERAL asks, through the same helper,
// because `printObject` prints `ObjectExpression` and `ObjectPattern`
// through one path. A range-based predicate cannot
// express it: counting the newlines in the gap, a blank *after* a
// comma the author pushed onto its own line (`a1⏎,⏎⏎b1`) reads as an author
// blank and was preserved, where prettier collapses it.
let next_start = obj.properties[i + 1].span().start;
self.push_item_blank_separator(&mut prop_parts, trailing.end_pos, next_start);
}
prev_end = trailing.end_pos;
}
// Check for dangling comments after the last property (before `}`)
if has_comments {
// An object pattern has no elisions, so the blank scan is never across one.
prop_parts
.push(self.build_pattern_trailing_dangling_comments(prev_end, boundary, false));
}
// Structure: { + brace-line prefix + indent(hardline + props) + hardline + } + type_annotation
let mut result_parts: DocBuf = smallvec![d.text("{")];
if let Some(prefix) = brace_line_prefix {
result_parts.push(prefix);
}
result_parts.push(d.indent_hardline(d.concat(&prop_parts)));
result_parts.push(d.hardline());
result_parts.push(d.text("}"));
if let Some(tail) = self.build_pattern_tail_doc(tail) {
result_parts.push(tail);
}
d.concat(&result_parts)
}
/// Build a Doc for an object pattern property
///
/// String keys that are valid identifiers are normalized to unquoted form:
/// `{"key": value}` → `{key: value}`
fn build_object_pattern_property_doc(&self, prop: &ObjectPatternProperty<'_>) -> DocId {
// A property is a list element: a run its default deferred out of a stripped pair
// flushes at its comma.
self.build_list_element_doc(prop.span().start, || {
self.build_object_pattern_property_doc_uncounted(prop)
})
}
fn build_object_pattern_property_doc_uncounted(
&self,
prop: &ObjectPatternProperty<'_>,
) -> DocId {
let d = self.d();
match prop {
ObjectPatternProperty::Property(p) => {
if p.shorthand {
// A shorthand default (`{ k = 1 }`) parses as
// `Property{shorthand, key: k, value: AssignmentPattern{left: k,
// right: 1}}`, so the *value* is already the whole `k = 1` binding
// and prints through `build_assignment_pattern_doc` — the same
// builder every other binding default uses (array element,
// non-shorthand property, standalone parameter). Delegating rather
// than re-deriving `key = default` here is load-bearing: an arm
// reaching into `pat.right` directly never acquires
// that builder's `=`→value freeze (`value_head_frozen_span`) or its
// `ParenContext::DefaultValue` wrap, making the shorthand the one
// position where an own-line `prettier-ignore` was ignored and an
// `AssignmentExpression` default lost its required parens
// (`{ k = (a = 1) }`).
//
// `p.value` can never be a bare `AssignmentExpression` here:
// `ObjectPatternProperty::Property` is built at exactly one site
// (`object_property_to_pattern`), which routes the value through
// `refine_assignable`, and that converts every `AssignmentExpression`
// into an `AssignmentPattern`.
debug_assert!(
!matches!(p.value.kind, ExpressionKind::AssignmentExpression(_)),
"a pattern property's value is converted to an AssignmentPattern"
);
match &p.value.kind {
// Simple shorthand: `{k}` — the value repeats the key.
ExpressionKind::Identifier(_) => self.build_expression_doc(p.key),
_ => self.build_expression_doc(p.value),
}
} else {
// Handle computed keys: {[key]: value}
// For regular keys, use property_key_doc to normalize string keys to identifiers
let key_region_end;
let key_doc = if p.computed {
// Cannot-hang gap: a leading cast in the `[`→key gap reflows
// (`Printer::with_jsdoc_cast_cannot_hang_gap`), as at the
// object-literal/class spelling (`build_computed_key_expr_doc`).
let (doc, end) = self.build_computed_key_bracket_doc(
p.span.start,
p.key,
Some(ParenContext::ComputedPropertyKey),
|| {
self.with_jsdoc_cast_cannot_hang_gap(p.key, || {
self.build_expression_doc(p.key)
})
},
);
key_region_end = end;
doc
} else {
key_region_end = p.key.span().end;
self.build_property_key_doc(p.key)
};
// Comments between key and value, split at `:`
// e.g., `{[x] /* c1 */: /* c2 */ a}` → before `:` and after `:`
//
// The `: ` is static text, so the `:` byte scan exists only to split
// the two comment ranges — a comment-free `{a: b}` neither scans nor
// pushes an empty child.
let value_start = p.value.span().start;
let mut parts: DocBuf = smallvec![key_doc];
let mut tail: DocBuf = DocBuf::new();
// Set when a pre-`:` comment hangs what follows: the whole
// `: local` tail then drops to a continuation line.
let mut hang_range: Option<(u32, u32)> = None;
// Set when the after-`:` gap holds comments: the shared value-gap
// seam emits them and hangs the local when one forces a break.
let mut value_gap: Option<(u32, u32)> = None;
if self.has_comments_to_emit_between(key_region_end, value_start) {
#[expect(clippy::expect_used)]
// Parser guarantees `:` exists in destructuring property
let colon_pos = find_char_skipping_comments(
self.source.as_bytes(),
key_region_end as usize,
value_start as usize,
b':',
)
.expect(": not found in destructuring property")
as u32;
// Comment(s) before `:`: a glued block stays inline
// (`a /* c */: b`); a line comment — or a multiline block the
// author broke after — trails the key and drops `: local` to a
// continuation line indented one level (uniform
// forced-continuation indent; without the break a `//` would
// swallow `: b } = o` — content loss). tsv preserves the
// authored position — prettier hoists the comment to lead the
// whole property.
hang_range =
self.route_pre_separator_gap(&mut parts, key_region_end, colon_pos);
tail.push(d.text(": "));
// Comments after `:`: a glued block inlines onto the local; a
// line comment — or an own-line multiline block — hangs it,
// the local indented one level so it reads as this property's
// value, not a sibling (the same rule as the `=`→value gaps
// above). The shared seam owns that choice — the caller only
// says the gap holds comments.
if self.has_comments_to_emit_between(colon_pos + 1, value_start) {
value_gap = Some((colon_pos + 1, value_start));
}
} else {
tail.push(d.text(": "));
}
let value_doc = self.build_expression_doc(p.value);
match value_gap {
Some((start, end)) => {
tail.push(self.build_pattern_value_gap_doc(start, end, value_doc));
}
None => tail.push(value_doc),
}
let tail_doc = d.concat(&tail);
match hang_range {
Some((start, end)) => {
parts.push(self.build_continuation_indent(start, end, tail_doc));
}
None => parts.push(tail_doc),
}
d.concat(&parts)
}
}
ObjectPatternProperty::RestElement(r) => self.build_rest_element_doc(r),
}
}
/// Build a Doc for an array pattern
pub(super) fn build_array_pattern_doc(
&self,
arr: &ast::ArrayPattern<'_>,
arr_span: Span,
) -> DocId {
if arr.elements.is_empty() {
return self.build_empty_array_pattern_doc(arr, arr_span);
}
// Expand if line comments, multi-line block comments, or own-line block comments.
// One whole-span comment pre-check gates the scans; array patterns don't
// force-expand on blank lines, so a comment-free pattern skips the element scan
// entirely.
//
// The three sub-questions must match the array EXPRESSION's gate
// (`arrays.rs`'s `has_expanding_comments`) — this asked only two, and a comment
// shape that expands `[a, b /* x⏎y */]` but not `const [a, b /* x⏎y */] = arr` is
// a bug every time: only the expanded path emits the dangling comments
// after the last element, so a multi-line one landing on the grouped path would be
// DROPPED outright (fixture `array_own_line_multiline_comment_expand`).
// `has_own_line_block_comments_in_bracket_list` deliberately skips multi-line
// comments — the multi-line question is this separate predicate's, not its.
let tail = self.array_pattern_tail(arr, arr_span);
let boundary = tail.body_end;
let has_comments = self.has_comments_on_page_between(arr_span.start, boundary);
let (has_line_comments, has_multiline_block, has_own_line_block) = if has_comments {
// Flatten once (skip holes) and share across the scans.
let non_null: SmallVec<[_; 8]> = arr.elements.iter().flatten().collect();
// Only the line-comment half: an array pattern does NOT force-expand on a
// blank line (the object pattern does), so `.1` is deliberately discarded
// rather than forgotten — the shared scan just answers one question more
// than this caller asks.
let has_line_comments = self
.collection_formatting_hints(
arr_span.start,
boundary,
&non_null,
true,
|elem| elem.span(),
|elem| elem.printed_end(),
)
.0;
let has_multiline_block =
self.has_multiline_block_comments_on_page_between(arr_span.start, boundary);
let has_own_line_block = self.has_own_line_block_comments_in_bracket_list(
Span::new(arr_span.start, boundary),
&non_null,
// The element's PRINTED span: a comment in a stripped shell's interior is
// NOT "inside the element" for this gate, because the element's doc does
// not print it — the same widening the trailing seam takes.
|elem| Span::new(elem.span().start, elem.printed_end()),
);
(has_line_comments, has_multiline_block, has_own_line_block)
} else {
(false, false, false)
};
// A rest element's stripped-paren interior lies inside its printed span, so no gap
// scan above sees it — the argument list's entry-gate problem
// (`Printer::any_paren_interior_forces_expansion`), in pattern form.
let rest_interior_expands = has_comments
&& self.paren_interior_forces_expansion(
arr.elements
.last()
.and_then(|e| e.as_ref()?.paren_interior()),
);
if has_line_comments || has_multiline_block || has_own_line_block || rest_interior_expands {
self.build_expanded_array_pattern_doc(arr, arr_span, tail)
} else {
self.build_grouped_array_pattern_doc(arr, arr_span, tail, has_comments)
}
}
/// Build doc for empty array pattern: `[]` with optional type annotation
fn build_empty_array_pattern_doc(&self, arr: &ast::ArrayPattern<'_>, span: Span) -> DocId {
// For array patterns with a `?` / type annotation, the body ends at the `]`
let tail = self.array_pattern_tail(arr, span);
let body_doc =
self.build_empty_brackets_inline_with_comments_doc_range(span.start, tail.body_end);
self.append_opt(body_doc, self.build_pattern_tail_doc(tail))
}
/// Build grouped array pattern doc (width-based expansion). `has_comments` is the
/// caller's whole-pattern comment pre-check (threaded to avoid re-scanning): when
/// false, the per-element leading gap is empty, so the `empty()` leading child is
/// skipped (render + fits would still walk it). Byte-identical.
fn build_grouped_array_pattern_doc(
&self,
arr: &ast::ArrayPattern<'_>,
span: Span,
tail: PatternTail<'_>,
has_comments: bool,
) -> DocId {
let d = self.d();
let boundary = tail.body_end;
let mut parts = d.pooled_docbuf();
let mut prev_end = span.start + 1;
for (i, elem) in arr.elements.iter().enumerate() {
let is_last = i == arr.elements.len() - 1;
if let Some(e) = elem {
// Check for leading comments before this element
let elem_start = e.span().start;
if has_comments {
let leading_comments =
self.build_inline_comments_between_doc_trailing_space(prev_end, elem_start);
parts.push(leading_comments);
}
parts.push(
self.build_list_element_doc(e.span().start, || self.build_expression_doc(e)),
);
// Unguarded for the same reason as the object pattern's grouped twin: a
// freeze needs a directive comment, which routes the pattern to the
// expanded builder before this path is reached.
let elem_end = e.printed_end();
// Collect trailing comments, bounded by the next REAL element or (past the
// last) the body's end — `array_pattern_gap_end` carries both halves.
let upper_bound = array_pattern_gap_end(arr, i, boundary);
let trailing = self.collect_trailing_comments(elem_end, upper_bound, is_last);
// Block comments around the comma (line comments force the expanded
// path, so `trailing.line` is empty here). Separator comma between
// elements; no trailing comma on the last (trailingComma: 'none').
let comma = (!is_last).then(|| d.text(","));
self.push_element_comma_trailing(&mut parts, &trailing, comma);
if !is_last {
parts.push(d.line());
}
prev_end = trailing.end_pos;
} else {
// A hole's comma is syntactically significant and is ALWAYS emitted,
// the last slot included: a trailing elision is a destructuring slot,
// and dropping its comma turns `[a, ,]` into `[a, ]` — one binding
// fewer, so the iterator advances one step fewer. The expanded twin
// already emits it unconditionally; only this one asked `is_last`, and
// the result was not even a fixed point (each pass ate another hole).
parts.push(d.text(","));
if !is_last {
parts.push(d.line());
}
}
}
// The trailing-elision region: a comment past the last binding has no next element
// to lead, so it slides to the pattern's own trailing position — the array
// literal's rule (`build_array_group_doc`'s `has_trailing_elision` arm), and this
// is its only emitter, since no hole slot prints an element. `prev_end` is the last
// binding's claim end (just inside `[` when there is no binding at all), so the two
// sides partition the gap. A line comment routes to the expanded path, so only
// blocks reach here.
if has_comments && arr.elements.last().is_some_and(Option::is_none) {
self.push_trailing_elision_blocks(
&mut parts,
self.comments_to_emit_between(prev_end, boundary),
);
}
// Build group for the array pattern brackets only
// Type annotation is OUTSIDE the group so it breaks independently.
// This ensures `[a, b]: [long_tuple]` breaks the tuple type, not the pattern.
let group_parts: DocBuf = smallvec![
d.text("["),
d.indent_softline(d.concat(&parts)),
d.softline(),
d.text("]"),
];
let group_doc = d.group(d.concat(&group_parts));
self.append_opt(group_doc, self.build_pattern_tail_doc(tail))
}
/// Build expanded array pattern doc (always multiline)
fn build_expanded_array_pattern_doc(
&self,
arr: &ast::ArrayPattern<'_>,
arr_span: Span,
tail: PatternTail<'_>,
) -> DocId {
let d = self.d();
let boundary = tail.body_end;
let mut parts = DocBuf::new();
let mut prev_end = arr_span.start + 1;
// A comment trailing the opening `[` on its own line is kept on the `[`
// line when the pattern expands (divergence from prettier, which relocates
// it to its own line as the first element's leading comment). See
// conformance_prettier_ts_comments.md §Comment relocation (Array destructuring `[`).
// Only applies when the first element is present (a leading hole has no
// span to anchor the range); otherwise the existing path handles comments.
let (bracket_line_prefix, bracket_pull_pos) =
match arr.elements.first().and_then(|opt| opt.as_ref()) {
Some(first) => {
self.delimiter_line_comment_prefix(arr_span.start, first.span().start)
}
None => (None, None),
};
for (i, elem) in arr.elements.iter().enumerate() {
let is_last = i == arr.elements.len() - 1;
if let Some(e) = elem {
// Check for leading comments before this element (with blank line preservation)
let elem_start = e.span().start;
let leading_comments = self.collect_item_leading_comments(
prev_end,
elem_start,
(i == 0).then_some(bracket_pull_pos).flatten(),
);
// The element's leading run and the element form one group — see
// `build_list_element_group` for why (prettier routes `ArrayPattern`
// through the same `printArray` as an array literal). An own-line
// format-ignore directive in the element's gap freezes it verbatim
// (Rule A) — the array literal's expanding printer does the same, and
// either spelling of an own-line comment routes the pattern here.
let frozen_span = self.element_frozen_span(arr_span.start + 1, arr.elements, i);
let element_doc = match frozen_span {
Some(frozen) => self.build_frozen_arg_doc(e, frozen),
None => {
self.build_list_element_doc(e.span().start, || self.build_expression_doc(e))
}
};
parts.push(self.build_list_element_group_from_comments(
leading_comments.iter().copied(),
elem_start,
element_doc,
));
let elem_end = Self::element_claim_anchor(frozen_span, e.printed_end());
// Collect trailing comments, bounded by the next REAL element or (past the
// last) the body's end — `array_pattern_gap_end` carries both halves.
let upper_bound = array_pattern_gap_end(arr, i, boundary);
// A rest's stripped-paren interior (a frozen one printed it verbatim): the
// run past its `)` follows the pattern's share of it.
let rest_interior = e.paren_interior().filter(|_| frozen_span.is_none());
let trailing = self.collect_element_trailing_comments(
elem_end,
upper_bound,
is_last,
rest_interior,
);
// Separator comma between elements; no trailing comma on the last
// element under `trailingComma: 'none'` (a rest element never takes one
// either — it is a syntax error there).
let comma = (!is_last).then(|| d.text(","));
self.push_element_comma_trailing(&mut parts, &trailing, comma);
if !is_last {
// Check for a blank line before the next REAL binding (or its leading
// comment). A hole between them opens no gap of its own, so this is
// still the gap after THIS element — the same walk the trailing run
// took ([`next_real_element_start`]); asking only slot `i + 1` skipped
// the check whenever a hole followed and DROPPED the author's blank.
if let Some(next_start) = next_real_element_start(arr.elements, i) {
// The same range every other family's separator reads
// ([`Printer::item_gap_blank_scan`]): from the previous element's
// DISTANCE anchor (its shell end, not its claim anchor) to the first
// comment in source.
//
// The predicate is the ARRAY family's ([`BlankRule::AfterComma`],
// prettier's `isLineAfterElementEmpty`), the one the array literal
// and the tuple take: advance to the comma, then measure. A raw
// newline COUNT over the whole gap cannot tell the author's blank
// from the line break they put around the comma, so `[a⏎,⏎b]` and
// `[a⏎⏎,⏎b]` — one line break each side, no blank line anywhere —
// both FABRICATED one, in the one family whose siblings do not.
// Stable once printed, so only a `compare` finds it.
//
// `bound` bounds the SEPARATOR search, `check_pos` the blank scan:
// the comma may sit below the next element's leading comment, where
// the content bound cannot see it
// ([`Printer::has_blank_line_after_comma`]). When slot `i + 1` is a
// HOLE, the gap's next printed content is that hole's own comma and
// BOTH bounds move there ([`Printer::hole_slot_comma`], the array
// literal's rule): the separator this gap is measured from is this
// element's own comma, which always precedes it, so nothing is left
// to find past it — while a search allowed to run to the next real
// binding finds the HOLE's comma instead and measures the blank on
// the far side of it, where a hole never carries one.
let bound = if matches!(arr.elements.get(i + 1), Some(None)) {
self.hole_slot_comma(elem_end, next_start)
} else {
next_start
};
let (from, check_pos) = self.item_gap_blank_scan(trailing.end_pos, bound);
if self.has_blank_line_after_comma(from, check_pos, bound) {
parts.push(d.literalline());
}
}
parts.push(d.hardline());
}
prev_end = trailing.end_pos;
} else {
// Hole in array pattern
parts.push(d.text(","));
if !is_last {
parts.push(d.hardline());
}
}
}
// Check for dangling comments after the last element (before `]`)
let past_elision = arr.elements.last().is_some_and(Option::is_none);
parts.push(self.build_pattern_trailing_dangling_comments(prev_end, boundary, past_elision));
// Structure: [ + bracket-line prefix + indent(hardline + elements) + hardline + ] + type_annotation
let mut result_parts: DocBuf = smallvec![d.text("[")];
if let Some(prefix) = bracket_line_prefix {
result_parts.push(prefix);
}
result_parts.push(d.indent_hardline(d.concat(&parts)));
result_parts.push(d.hardline());
result_parts.push(d.text("]"));
if let Some(tail) = self.build_pattern_tail_doc(tail) {
result_parts.push(tail);
}
d.concat(&result_parts)
}
/// Build a Doc for an assignment pattern
pub(super) fn build_assignment_pattern_doc(
&self,
pattern: &ast::AssignmentPattern<'_>,
span: Span,
) -> DocId {
let d = self.d();
// A type-assertion target keeps its required parens (`{ a: (b as T) =
// 1 }` — bare `b as T = 1` is a TS parse error); non-null `b!` stays
// bare. Same rule as an assignment expression's left, through the same
// seam, which also emits the `^`→left gap — the authored `(`'s interior.
// The object-pattern arm is asked first: it never takes the pair
// (`needs_parens` matches only the three type assertions), and its
// reassembled doc keeps its own owned-comment claim below.
let left_doc = if let ExpressionKind::ObjectPattern(obj) = &pattern.left.kind {
// A destructuring default's left object pattern does NOT expand on nesting
// — prettier's shouldBreak excludes an AssignmentPattern parent (object.js),
// so `{ a: { b } = {} }` (and deeper) stays inline. Width-based breaking
// still applies. (Array-pattern lefts don't expand on nesting.)
//
// This arm REASSEMBLES the left instead of routing it through
// `build_expression_doc`, so it must claim the comment the left OWNS itself —
// `docs/comments.md` hazard 1. The `AssignmentPattern` above it hands the claim
// down (`left_spine_child`), and with nothing catching it here
// `[/* c */ {a} = 1]` DROPS the comment outright.
let obj_doc = self.build_object_pattern_doc_with_context(
obj,
pattern.left.span,
PatternContext::AssignmentDefault,
);
self.prepend_owned_leading_comment_at(pattern.left.span.start, obj_doc)
} else {
self.build_shell_operand_doc(
span.start,
pattern.left,
ParenContext::AssignmentTarget {
operator: AssignmentOperator::Assign,
},
)
};
let left_end = pattern.left.span().end;
let rhs_start = pattern.right.span().start;
// Zero-comment fast gate: one binary search over the whole `left = right`
// gap. On the common comment-free default the `=`-locating byte scan and
// both per-side comment probes are skipped and it renders as `left = right`
// (this builder is also the function-param default path `f(a = 1)`, so the
// gate fires broadly). Canonical reference: build_params_doc_with_comments.
let gap_has_comments = self.has_comments_on_page_between(left_end, rhs_start);
let eq_pos = if gap_has_comments {
self.find_equals_position(left_end, rhs_start)
} else {
rhs_start
};
// Comment(s) before `=` (e.g., `{a /* c */ = 1}`): a glued block stays inline;
// a line comment — or a multiline block the author broke after — trails the
// left and drops `= value` to a continuation line indented one level (uniform
// forced-continuation indent), so the `=` can't be swallowed (`a // c⏎= 1`).
// tsv preserves the authored position — prettier relocates the comment to
// trail the whole binding. The shorthand default's key→`=` gap is the same
// rule on the same seams (`build_object_pattern_property_doc`).
let mut head: DocBuf = smallvec![left_doc];
let mut tail: DocBuf = DocBuf::new();
let pre_eq_hang = gap_has_comments
.then(|| self.route_pre_separator_gap(&mut head, left_end, eq_pos))
.flatten();
// The `=`→value head: an own-line directive there freezes the whole default.
// The directive also keeps its own line, unlike the ordinary own-line comment
// below — the trailing placement that emitter produces is inert under the
// placement floor, so following it would lose the freeze on the second pass.
// Same rule, same reason, as at the declaration headers.
//
// Rides the zero-comment gate above: a directive is a comment, so a comment-free
// gap provably holds none — and `eq_pos` is a real `=` position only there.
// A binding default's `=` is a value gap, and object, array and parameter defaults
// all land here (`mark_jsdoc_cast_value_gap`).
self.mark_jsdoc_cast_value_gap(pattern.right);
let frozen = gap_has_comments
.then(|| self.value_head_frozen_span(eq_pos + 1, pattern.right.span()))
.flatten();
if let Some(frozen) = frozen {
let frozen_doc =
self.build_frozen_value_doc(pattern.right, frozen, ParenContext::DefaultValue);
let rhs = match self.build_rhs_comments_opt(eq_pos + 1, rhs_start) {
Some(comments_doc) => d.concat(&[comments_doc, frozen_doc]),
None => frozen_doc,
};
tail.push(d.text(if pre_eq_hang.is_some() { "=" } else { " =" }));
tail.push(hang_after_operator(d, rhs));
} else {
// A block comment after `=` inlines onto the value (`a = /* c */ b`),
// collapsing an own-line authoring; a line comment forces the value onto
// the next line (it can't share the `//` line). Prettier instead moves a
// block before `=` (`a /* c */ = b`) or floats a line past the value
// (`a = b // c`) — see param_default_*_comment_prettier_divergence.
// The ordinary dispatch, so a binary default takes the continuation-indent
// default: `AssignmentPattern` is deliberately absent from prettier's
// `shouldIndentIfInlining`, so this is NOT an assignment value and must not be
// marked as one (`Printer::mark_assignment_value`).
// A comment the value OWNS travels inside its doc, and a multi-line block's
// reprinted body force-breaks every group around it — so the default's binary
// chain, ternary and member chain all exploded under one
// (`param_default_multiline_block_comment_flat`). The hoist pulls that run out
// to the seam ([`Printer::hoist_owned_value_gap_run`]); it fires only where the
// whole gap run is glued through, which is exactly where the two emit-axis arms
// below cannot also print it — a `//` or an own-line block never glues, so
// `hoisted_run` and those arms are mutually exclusive by construction.
//
// Behind `gap_has_comments` because the whole gap is provably empty there —
// `eq_pos` is the sentinel `rhs_start`, not a real `=` position — so the scan
// is skipped rather than run over the inverted range the sentinel makes.
let position_parens = self.needs_parens(pattern.right, ParenContext::DefaultValue);
let hoisted_run = gap_has_comments
.then(|| {
self.hoisted_owned_value_gap_run_opt(eq_pos + 1, pattern.right, position_parens)
})
.flatten();
// Where the hoist declines (a run the author broke after, `a = /* c1 */⏎/* c2⏎*/ v`)
// the value claims its owned multi-line comment itself, beneath the parens this
// position adds — `build_gap_value_doc` would prepend it outside the pair.
let rhs_doc = self.build_value_under_hoist(hoisted_run, pattern.right, || {
let rhs_doc = self.build_expression_doc_claiming_outermost(pattern.right);
if position_parens {
d.parens(rhs_doc)
} else {
rhs_doc
}
});
let value_doc = if let Some(run) = hoisted_run {
// The hoisted run REPLACES the gap's own emitter, never joins it — it is
// the same gap read on the wider (on-page) axis, so concatenating the two
// prints every un-owned comment in the gap twice.
d.concat(&[run, rhs_doc])
} else if gap_has_comments && self.has_comments_to_emit_between(eq_pos + 1, rhs_start) {
self.build_pattern_value_gap_doc(eq_pos + 1, rhs_start, rhs_doc)
} else if self.left_spine_shell_has_own_line_comment(pattern.right) {
// The same line comment one shell deeper — inside the grouping parens
// of the value's LEFTMOST node (`x = (⏎// c⏎a as any).b`): the value's
// printer hoists that run to the head of its doc, so the doc opens
// with the run and a hardline, and the gap emitter's continuation
// indent is what this seam owes it (`x = // c⏎↹↹value`, the form the
// gap's own `//` takes). Without it the value rendered at the
// parameter's level and the reparse — reading the comment as the
// `=`→value gap's — indented it, a second fixed point one pass away.
d.indent(rhs_doc)
} else {
rhs_doc
};
// A comment the value OWNS is glued to its first token and travels inside its
// doc, so neither probe above can see it (`docs/comments.md` hazard 2) — and an
// own-line cast's still decides this layout, which a default builds itself rather
// than routing through `build_assignment_layout`. The two halves must agree: the
// cast prints a hardline between its comment and its `(` on exactly that shape,
// and a hardline without the hang leaves the `(` at the binding's own indent — a
// form the next pass collapses, so the authoring had no fixed point at all
// (`jsdoc_type_cast_binding_default_own_line`). The NARROW test, deliberately:
// see [`Printer::is_own_line_jsdoc_cast`].
if self.is_own_line_jsdoc_cast(pattern.right) {
tail.push(d.text(if pre_eq_hang.is_some() { "=" } else { " =" }));
tail.push(hang_after_operator(d, value_doc));
} else {
tail.push(d.text(if pre_eq_hang.is_some() { "= " } else { " = " }));
tail.push(value_doc);
}
}
let tail_doc = d.concat(&tail);
// A hanging pre-`=` comment left `= value` on its own line; the shared
// continuation seam emits the comment run and indents the tail so it reads as
// this binding's continuation, not a sibling element.
match pre_eq_hang {
Some((start, end)) => head.push(self.build_continuation_indent(start, end, tail_doc)),
None => head.push(tail_doc),
}
d.concat(&head)
}
/// Build a Doc for a rest element
pub(super) fn build_rest_element_doc(&self, rest: &ast::RestElement<'_>) -> DocId {
let d = self.d();
// Comments between `...` and the argument (e.g., `.../* c */ args`). Skipped on
// the comment-free path — rest/spread elements are everywhere (`f(...xs)`,
// `{a, ...rest}`, `(...args) =>`) and a comment inside the `...` gap is not.
let dots_end = rest.span.start + "...".len() as u32;
let arg_start = rest.argument.span().start;
let mut parts: DocBuf = smallvec![d.text("...")];
if let Some(comments_doc) =
self.build_inline_comments_between_doc_trailing_space_opt(dots_end, arg_start)
{
parts.push(comments_doc);
}
parts.push(self.build_expression_doc(rest.argument));
// The rest's share of its stripped-paren interior (`[...(a /* c */)] = x`): what
// shares the argument's line. The own-line rest is the enclosing pattern's
// (`Printer::push_element_share_and_run`), which anchors its own
// trailing scan past the `)` — the spread's partition, one emitter per share.
self.append_paren_interior_trailing_comments(&mut parts, rest.paren_interior());
// The optional `?` marker and the `: type` annotation, with the comment landings
// both of their gaps need — `wrap` is false to match the value-side identifier
// path this mirrors (`build_identifier_doc_inner`). See
// `push_rest_element_tail_doc`, shared with the type-side rest arm
// (`types/function_types.rs`).
self.push_rest_element_tail_doc(&mut parts, rest, false);
d.concat(&parts)
}
}
/// A Svelte **block binding pattern** (`{#each … as P}`, `{#await … then P}`, `{:then P}`,
/// `{:catch P}`) — the host's two seams into the destructuring layout above.
///
/// The rule the host states: a block pattern lays out like its TypeScript twin wherever a
/// COMMENT breaks it, and stays inline everywhere else — at any width, and whatever it nests
/// (prettier-plugin-svelte keeps these heads on one line, and the declaration twin's
/// nested-pattern break is a declarator's rule). So the gate here is the comment half of the
/// object and array patterns' expansion gates only, asked of the whole binding, and the doc is
/// the expanded builder those gates select.
impl Printer<'_> {
/// Whether a comment in the destructuring pattern `expr` takes the declaration twin's
/// layout — any comment in an EMPTY pattern, which only the twin's empty-brace builder
/// prints the twin's way, and otherwise a comment that forces the pattern open the way the
/// twin opens it: a `//` or a multi-line block anywhere in its body (the
/// twin's break propagates out of any sub-node that holds one), or an own-line block in
/// the gaps of this pattern or of a pattern nested in it (each expands its own list, and
/// that break propagates too), or an own-line comment in a rest's stripped parens. The
/// `: T` tail is not the pattern's — the twin's annotation breaks on its own — and neither
/// is a blank line, which only a comment-broken pattern keeps.
pub(crate) fn block_pattern_comment_breaks(&self, expr: &Expression<'_>) -> bool {
let (span, body_end) = match &expr.kind {
ExpressionKind::ObjectPattern(obj) => {
(expr.span, self.object_pattern_tail(obj, expr.span).body_end)
}
ExpressionKind::ArrayPattern(arr) => {
(expr.span, self.array_pattern_tail(arr, expr.span).body_end)
}
_ => return false,
};
if !self.has_comments_on_page_between(span.start, body_end) {
return false;
}
// An empty pattern's only content is its dangling run, which the twin's empty-brace
// builder prints whatever it holds — spaced braces around a block (`{ /* c */ }`),
// tight brackets (`[/* c */]`), and opened around a `//`.
let is_empty = match &expr.kind {
ExpressionKind::ObjectPattern(obj) => obj.properties.is_empty(),
ExpressionKind::ArrayPattern(arr) => arr.elements.is_empty(),
_ => false,
};
is_empty
|| self.has_line_comments_between(span.start, body_end)
|| self.has_multiline_block_comments_on_page_between(span.start, body_end)
|| self.pattern_own_line_block_breaks(expr)
}
/// The own-line half of [`Self::block_pattern_comment_breaks`], recursing into every
/// pattern nested in `expr` — the nested pattern's own expansion is what the twin
/// propagates out.
fn pattern_own_line_block_breaks(&self, expr: &Expression<'_>) -> bool {
match &expr.kind {
ExpressionKind::ObjectPattern(obj) if !obj.properties.is_empty() => {
let boundary = self.object_pattern_tail(obj, expr.span).body_end;
self.object_pattern_has_own_line_block_comments(obj, expr.span, boundary)
|| self.paren_interior_forces_expansion(
obj.properties
.last()
.and_then(ObjectPatternProperty::paren_interior),
)
|| obj.properties.iter().any(|p| match p {
ObjectPatternProperty::Property(p) => {
self.pattern_own_line_block_breaks(p.value)
}
ObjectPatternProperty::RestElement(r) => {
self.pattern_own_line_block_breaks(r.argument)
}
})
}
ExpressionKind::ArrayPattern(arr) if !arr.elements.is_empty() => {
let boundary = self.array_pattern_tail(arr, expr.span).body_end;
let non_null: SmallVec<[_; 8]> = arr.elements.iter().flatten().collect();
self.has_own_line_block_comments_in_bracket_list(
Span::new(expr.span.start, boundary),
&non_null,
|elem| Span::new(elem.span().start, elem.printed_end()),
) || self.paren_interior_forces_expansion(
arr.elements
.last()
.and_then(|e| e.as_ref()?.paren_interior()),
) || non_null
.iter()
.any(|e| self.pattern_own_line_block_breaks(e))
}
// A default value is no list of this pattern's, but an own-line block inside it
// opens the value itself (an object's or array's elements, a call's arguments, an
// arrow's body), and that break propagates out through the pattern in the twin.
ExpressionKind::AssignmentPattern(ap) => {
self.pattern_own_line_block_breaks(ap.left)
|| self.default_value_comment_breaks(ap.right)
}
ExpressionKind::AssignmentExpression(ae) => {
self.pattern_own_line_block_breaks(ae.left)
|| self.default_value_comment_breaks(ae.right)
}
ExpressionKind::RestElement(r) => self.pattern_own_line_block_breaks(r.argument),
_ => false,
}
}
/// Whether a comment-bearing default value `value` breaks — asked of the value's own doc,
/// since what opens a value is each value kind's own rule (an own-line block among an
/// object's or array's elements, in a call's arguments, in an arrow's body).
///
/// Keyed on the value holding ANY comment, not only an own-line one, because the verdict
/// must survive its own output. An own-line block at an object's `key:` gap opens the
/// object while collapsing onto the key's line (`{ s:⏎/* c */⏎1 }` → `{⏎→s: /* c */ 1⏎}`),
/// and that object then stays open on its authored first-line break with the comment no
/// longer on its own line — so an own-line key read on the second pass returned the
/// pattern to one line. A value that breaks with no comment in it (an arrow's block body,
/// an object the author opened) is still the rule a block pattern does not take.
fn default_value_comment_breaks(&self, value: &Expression<'_>) -> bool {
let span = value.span();
self.has_comments_on_page_between(span.start, span.end)
&& self.d().will_break(self.build_expression_doc(value))
}
/// The comment-broken layout of the block pattern `expr` — the declaration twin's
/// expanded object or array pattern, `: T` tail included, with the owned comment glued
/// ahead of its opening delimiter claimed here, since this reaches past
/// `build_expression_doc`. Asked only once [`Self::block_pattern_comment_breaks`] holds.
pub(crate) fn build_block_pattern_broken_doc(&self, expr: &Expression<'_>) -> DocId {
let doc = match &expr.kind {
// An empty pattern holds only a dangling run, which the twin's empty-brace builder
// opens around a `//` itself.
ExpressionKind::ObjectPattern(obj) if obj.properties.is_empty() => {
self.build_empty_object_pattern_doc(obj, expr.span)
}
ExpressionKind::ArrayPattern(arr) if arr.elements.is_empty() => {
self.build_empty_array_pattern_doc(arr, expr.span)
}
ExpressionKind::ObjectPattern(obj) => {
let tail = self.object_pattern_tail(obj, expr.span);
self.build_expanded_object_pattern_doc(obj, expr.span, tail)
}
ExpressionKind::ArrayPattern(arr) => {
let tail = self.array_pattern_tail(arr, expr.span);
self.build_expanded_array_pattern_doc(arr, expr.span, tail)
}
_ => return self.build_expression_doc(expr),
};
self.prepend_owned_leading_comment(expr, doc)
}
}