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
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
// Operator expression printing for TypeScript
//
// Handles printing of unary and binary expressions with:
// - Operator precedence and parenthesization
// - Clarity-based parens (mixing logical operators, etc.)
use crate::ast::{self, BinaryOperator, Expression, ExpressionKind};
use crate::printer::comments::{AsiOperandShell, CommentSpacing, left_spine_shell_is_comment_free};
use crate::printer::ignore::FrozenOperandPair;
use crate::printer::statements::restricted_production::ArgumentHang;
use crate::printer::{
CommentVec, ParenContext, Printer, RunLeadingBlank, class_expr_has_decorators,
flattened_left_operand,
};
use smallvec::{SmallVec, smallvec};
use tsv_lang::Span;
use tsv_lang::doc::{
DocBuf,
arena::{DocId, LeadingBreak},
};
/// Holds information about an operand in a binary expression chain
/// Used to track position information for comment placement
struct ChainOperand {
doc: DocId,
span: Span,
}
/// How a binary chain's LAST operand prints ([`Printer::build_chain_end_operand`]).
#[derive(Clone, Copy)]
enum ChainEndShape {
/// Through the ordinary operand builder, reporting the operand's own span.
Bare,
/// Inside the author's pair kept for the value's end, a same-line block run inline.
Inline { open: u32, close: u32 },
/// Inside a required or kept pair opened around a run that would defer.
Opened { open: u32, close: u32 },
}
/// Stack buffers for a flattened binary chain's operands / operators, collected
/// once per binary expression. The common 2–3 operand chain stays inline
/// (`ChainOperand` is 12 bytes, `BinaryOperator` 1 byte); longer chains spill.
type OperandBuf = SmallVec<[ChainOperand; 8]>;
pub(super) type OperatorBuf = SmallVec<[ChainOperator; 8]>;
/// One operator of a flattened binary chain, with the one layout fact that is the
/// operator's own rather than the chain's.
#[derive(Clone, Copy)]
pub(super) struct ChainOperator {
pub(super) op: BinaryOperator,
/// The operator LEADS its line when the chain breaks (`aaa⏎> bbb`) rather than ending
/// the one before — a `>` that may close a type-argument region an earlier `<`
/// opened (or a `>>` / `>>>` behind as many nested ones), which a line break past it
/// would commit (`BinaryExpression::may_close_type_arguments`). Every chain emitter
/// reads it, since a `>` any one of them lets end a line is the same syntax error.
pub(super) leads_line: bool,
}
impl ChainOperator {
pub(super) fn of(expr: &ast::BinaryExpression<'_>) -> Self {
Self {
op: expr.operator,
leads_line: expr.may_close_type_arguments,
}
}
pub(super) fn as_str(self) -> &'static str {
self.op.as_str()
}
}
/// Where [`Printer::push_operand_operator_gap`] left a chain's operator relative to the
/// line break between its two operands. Anything but [`Self::EndsLine`] means the operator
/// leads the next operand's line, so that operand hugs it with a space.
#[derive(Clone, Copy, PartialEq, Eq)]
enum OperatorGap {
/// `a +⏎b` — emitted, closing the operand's line; the breakable `line` follows it.
EndsLine,
/// `a // c⏎+ b` — emitted below a line comment that would otherwise swallow it.
ForcedDown,
/// `a⏎> b` — NOT emitted: the caller places it behind a breakable `line`
/// ([`ChainOperator::leads_line`]).
LeadsLine,
}
/// Style for building binary expression chain docs.
///
/// One variant per answer prettier's `printBinaryishExpression` can give
/// (`shouldNotIndent`, `print/binaryish.js`). **Every variant is named at a call site** — there is no
/// fall-through style, which is the whole point of the split: prettier's own rule is a
/// fall-through *to indent*, so the only safe default is [`Self::ContinuationIndent`]
/// (`Printer::build_binary_chain_doc`), and a position that wants anything else says so.
#[derive(Clone, Copy)]
enum BinaryChainStyle {
/// `group(parts)` — continuation lines land at the first operand's own column.
/// Prettier's `shouldNotIndent` answer; the positions are listed on
/// [`Printer::build_flat_chain_expression_doc`].
///
/// ⚠️ This is an OPT-OUT, never a default. The flat answer was tsv's fall-through
/// once, and every position that inherited it silently was wrong.
Flat,
/// The assignment-value layout — prettier's `shouldIndentIfInlining` set (a
/// declarator's initializer, an assignment's RHS, an object property's value, a
/// class property's initializer), which the printer identifies by the
/// `assignment_value_target` mark rather than by a parent pointer.
///
/// Resolves to [`Self::Flat`], except for an inlining chain with earlier operators —
/// prettier's `samePrecedenceSubExpression` arm — which takes
/// [`Self::ContinuationIndent`]. Resolved in `build_binary_chain_doc_core`, so no
/// layout builder ever sees this variant.
AssignmentValue,
/// No group wrapper, flat structure (for contexts where parent controls breaking)
Ungrouped,
/// Like Ungrouped, but also suppresses shouldGroup for logical operators.
/// Used only for condition parentheses (if/while/for/do-while/switch) where
/// Prettier's `isInsideParenthesis` is true. In these contexts, logical chain
/// breaks must be controlled by the parent condition group, not a sub-group.
UngroupedCondition,
/// First operand at base indent, continuation lines indented — every position where
/// prettier's `shouldNotIndent` chain yields false, which is to say **the default**
/// ([`Printer::build_binary_chain_doc`]). No enumeration belongs here: the positions
/// that do NOT take it are the ones worth naming, and they are named on
/// [`Printer::build_flat_chain_expression_doc`].
ContinuationIndent,
/// `ContinuationIndent` without the outer group wrapper, for contexts where
/// the caller controls grouping (the comment-aware twin for inline-paren
/// chains).
ContinuationIndentUngrouped,
}
/// How a `SequenceExpression`'s own grouping parens are emitted. One 3-valued mode
/// rather than two independent bools — the fourth combination (bare *and*
/// keep-trailing-inside) is impossible (a bare sequence has no `)` to keep a comment
/// before), so an enum makes it unrepresentable.
#[derive(Clone, Copy)]
enum SeqParens {
/// Self-parenthesize; the last operand's trailing comment floats OUT after `)`
/// (statement / throw / call-argument position).
FloatOut,
/// Self-parenthesize; the last operand's trailing comment stays INSIDE before `)`
/// (value position — return arg / var init / assignment RHS; prettier #19263).
KeepInside,
/// No parens — bare comma-joined operands; an enclosing construct supplies the
/// grouping and owns the edge-comment gaps (a leading-comment-forced hanging paren).
Bare,
}
/// Where a `SequenceExpression`'s operands sit once the list breaks — prettier's
/// `printSequenceExpression` (`print/sequence-expression.js`), whose three arms are keyed on
/// the sequence's PARENT.
///
/// Orthogonal to [`SeqParens`]: that names what the parens do with an edge comment, this names
/// the geometry between them. tsv has no parent pointer, so each caller names the layout its
/// position takes — the same way it already names the paren mode.
#[derive(Clone, Copy)]
pub(in crate::printer) enum SeqLayout {
/// `group(join([",", line]))` — every operand at the caller's own indent. Prettier's
/// default arm, and so the common one: a call argument, a variable init, an assignment
/// RHS, a `yield` argument, a nested operand.
Aligned,
/// `group([first, ",", indent([line, rest…])])` — operands after the first take one
/// indent level, prettier's `ExpressionStatement` / `ForStatement` arm (the two positions
/// where a sequence appears unparenthesized in source). ⚠️ The indent starts AFTER the
/// first operand and not around the whole run: an operand that breaks internally keeps
/// its own lines at the base column.
Indented,
/// `group(indent(softline + join) + softline)` — the whole list hangs inside the parens,
/// `)` on its own line. Prettier's `shouldIndentSequenceExpression`: a `return`/`throw`
/// argument and an arrow-function body. (Prettier writes it as an `ifBreak` whose flat arm
/// is the bare join; the softlines already collapse flat, so the arms coincide.)
Hanging,
}
/// The sequence's OPERAND RUN — `[first operand start, last operand end)`.
///
/// The window every comment question in this family is measured against, and **neither end is
/// `seq.span`'s**. Ahead of the run sits the grouping `(` the parser stripped from the first
/// operand, whose comments some *other* emitter owns in every paren mode
/// ([`Printer::build_sequence_doc_inner`] carries that argument); past it, in the value
/// positions, sits a trailing gap that runs beyond `seq.span.end` to the caller's
/// `trailing_end`, because the grouping parens are not part of the node.
///
/// Both ends were re-derived at three sites apiece, in two parallel builders and their shared
/// envelope. That is the shape such a pair drifts through, so the ends are named here once.
pub(super) fn sequence_operand_run(seq: &ast::SequenceExpression<'_>) -> (u32, u32) {
let operands = &seq.expressions;
(
operands[0].span().start,
operands[operands.len() - 1].span().end,
)
}
/// Operator position in source, used for comment splitting
#[derive(Clone, Copy)]
struct OperatorPosition {
/// Start position of operator in source
start: u32,
/// End position of operator in source (start + operator length)
end: u32,
}
impl<'a> Printer<'a> {
/// Build a Doc for an update expression
pub(in crate::printer) fn build_update_doc(
&self,
update: &ast::UpdateExpression<'_>,
update_span: Span,
) -> DocId {
let d = self.d();
let operator_doc = d.text(update.operator.as_str());
let operator_len = update.operator.as_str().len() as u32;
// Postfix `x++` / `x--`: the operand→operator gap is ASI-sensitive — `a // c⏎++`
// parses as `a;` then `++;`, so the operator may not start a line.
//
// That does not make the gap block-only, though. A grouping paren shell holds the
// line terminator off the gap, so `(a // c⏎)++` really is a postfix update
// carrying a `//` — and inlining it swallowed the `++;` into the comment. Such a
// comment keeps the shell (`asi_gap_needs_parens`), which also supplies the
// `(a as T)++` parens, so nothing below adds a second pair.
//
// Asked BEFORE the operand's doc is built: the shell builds its own operand doc,
// so building one here first would be discarded — and `build_expression_doc` is
// not side-effect-free (it consumes the expression-statement paren target), which
// makes a discarded build more than wasted work.
let shell_run = if update.prefix {
None
} else {
match self.build_asi_operand_shell_doc(
update_span.start,
update.argument,
update_span.end - operator_len,
ParenContext::UpdateArgument { postfix: true },
) {
AsiOperandShell::Shell(shell) => return d.concat(&[shell, operator_doc]),
AsiOperandShell::Bare(run) => run,
}
};
let inner_doc = self.build_expression_doc(update.argument);
// The stripped shell's glued block leads the operand inside whatever pair the
// operand keeps — where the reparse, which owns it, prints it too.
let inner_doc = match shell_run {
Some(run) => d.concat(&[run, inner_doc]),
None => inner_doc,
};
// One `needs_parens` call for both spellings — the rule is
// `ParenContext::UpdateArgument`'s: every operand looser than a member access
// keeps its pair, and `postfix` decides the instantiation case alone (the ASI
// shell above takes the ctx on its postfix-only branch).
let argument_doc = if self.needs_parens(
update.argument,
ParenContext::UpdateArgument {
postfix: !update.prefix,
},
) {
d.parens(inner_doc)
} else {
inner_doc
};
if update.prefix {
// Prefix: ++x, --x. The operator→operand gap has no other emitter, so a
// comment authored there is dropped unless this builder claims it. Emit-axis,
// so a comment the operand OWNS (a glued block, an annotation, a JSDoc cast)
// returns `None` and keeps printing from the operand's own doc — claiming it
// here too would double-print it.
//
// `Adjacent`, not the `AdjacentGlued` of the assignment/call pull-up: an
// update operand is not a value gap, so prettier does not reflow the author's
// break after a glued block — `++/* c */⏎x` keeps the break. Gluing pulled the
// operand up to `++/* c */ x` instead.
let operator_end = update_span.start + operator_len;
let mut parts: DocBuf = smallvec![operator_doc];
if let Some(comments) =
self.build_rhs_comments_opt(operator_end, update.argument.span().start)
{
parts.push(comments);
}
// The operand→`)` gap is the PREFIX form's alone, and it RETAINS its shell —
// the `await`/`yield` answer, reached for the same reason: a prefix update
// **spans its own `)`**, so the enclosing terminator gap starts past it and
// cannot see inside. Unclaimed the comment was simply DROPPED
// (`++(a as T /* c */);` → `++(a as T);`, `++(a⏎// c⏎);` → `++a;`), and
// relocating it outside instead has no fixed point: on reparse it lands in
// the terminator gap and moves again, past the `;`. The POSTFIX twin has no
// such gap — its span ends past the operator, so the region before it is
// inside the node and its own emitter covers it.
parts.push(
self.build_paren_operand_comment_doc(
update.argument.span().end,
update_span.end,
inner_doc,
")",
)
.unwrap_or(argument_doc),
);
d.concat(&parts)
} else {
// Postfix, no shell needed (see above): the same unclaimed gap on the other
// side, emitted **inline** rather than hanging the operator — a break would
// rewrite the program. Only a single-line block reaches here (`x /* c */++`);
// anything spanning lines took the shell.
let operator_start = update_span.end - operator_len;
match self
.build_inline_comments_between_doc_opt(update.argument.span().end, operator_start)
{
Some(comments) => d.concat(&[argument_doc, comments, operator_doc]),
None => d.concat(&[argument_doc, operator_doc]),
}
}
}
/// Build a Doc for a unary expression.
///
/// A hoist site like the value heads: a comment in a shell below a pair the operand's
/// left spine prints is claimed ([`Printer::left_spine_run_claim`]) and printed in the
/// operator→operand gap, which runs to the region's end — inside the comment-holder
/// pair the comment earns, ahead of the operand's own pairs, where the reparse reads
/// it (`!((/* d */⏎ a.b) + 1).c` prints `!(/* d */ (a.b + 1).c)`).
pub(in crate::printer) fn build_unary_doc(&self, unary: &ast::UnaryExpression<'_>) -> DocId {
let operator_end = unary.span.start + unary.operator.as_str().len() as u32;
let argument_start = unary.argument.span().start;
let claim = self.left_spine_run_claim(unary.span, operator_end, unary.argument, || true);
let gap_end = claim.map_or(argument_start, |claim| claim.region.end.max(argument_start));
self.with_left_spine_claim(claim, || self.build_unary_doc_gap(unary, gap_end))
}
/// [`Self::build_unary_doc`] under its claim, the operator→operand gap ending at
/// `argument_start`.
fn build_unary_doc_gap(&self, unary: &ast::UnaryExpression<'_>, argument_start: u32) -> DocId {
let d = self.d();
// Check for comments between operator and argument.
// When grouping parens containing a JSDoc comment are stripped by the parser,
// the comment ends up in the gap between operator and argument span.
// Re-add parens to preserve the comment: `!(/** @type {T} */ expr.prop)`
let operator_end = unary.span.start + unary.operator.as_str().len() as u32;
let argument_end = unary.argument.span().end;
// Unary argument — an `ancestorNameMap` value position. A BARE ternary operand
// records nothing (prettier's `child === node` guard), which is why `!(c ? a : b)`
// keeps the hanging form while `!((c ? a : b) as T)` expands.
self.mark_ternary_extra_indent(unary.argument);
// The shared leading-comment emitter, in its plain `Adjacent` mode — prettier's
// `printLeadingComment` exactly. Its third separator is what makes the shell below
// a width decision rather than a comment one: a single-line block glued to the
// operator (`!/* c */⏎x`) takes the soft `line`, so the operand pulls up to
// `!(/* c */ x)` while it fits and drops below the comment once it doesn't. The
// gluing variant (`build_rhs_comments_glued_opt`) spends that `line` on an
// unconditional space and can only ever reach the first of those two forms.
// The opening-delimiter rule at the comment-holder `(`: a `//` the author glued to
// it keeps that line ([`Printer::split_open_delimiter_glued_run`]), as at `return (`
// / `throw (`, a retained type paren shell, every bracket and every statement
// header. This shell was the value family's last holdout — prettier un-glues here
// and tsv followed, which is the one-position-agreeing shape that froze the rule
// everywhere else it was found (`docs/comments.md` §The delimiter-line question).
//
// The gap opens at the OPERATOR's end because the parser strips the source `(` — so
// the region spans it, and "no newline before the comment" is exactly "the author
// wrote it on the line this shell's re-added `(` will sit on".
let (paren_glued, leading_run_start) =
self.split_open_delimiter_glued_run(operator_end, argument_start);
let leading_comments_opt = self.build_rhs_comments_opt(leading_run_start, argument_start);
// Whether a leading comment is *present* — the gate for re-adding the parens — as
// opposed to whether this emitter has to print it. A **forward-binding** comment (a
// bundler annotation, a JSDoc cast) is `owned_by_node`, so the operand's own doc
// prints it and `leading_comments_opt` is `None` — but the parens are still wanted,
// for the same reason an ordinary gap comment wants them: bare,
// `!/* @__PURE__ */ f()` reads as annotating the *operator* rather than the operand.
// Counting the owned comment here is what keeps the wrap, and it is the ONLY thing
// that does — `needs_parens` deliberately doesn't (it would double-wrap).
//
// This is the `has_comments_on_page_in_range` / `has_comments_to_emit_in_range` split: an
// emit-decision skips owned comments, a layout/semantic gate counts them.
//
// An operand that already prints its own value-position pair is the exception:
// that pair encloses the owned comment on the plain needs-parens path below
// (`!(/** @type {A} */ (x), y)`, `!(/* c */ x = y)`, `!(/* c */ cond ? b : c)`),
// so counting the owned comment here would drive the comment-holder wrap and
// double the parens (`!((/* c */ x = y))`). Skipping it routes the operand to that
// plain path, where its own single pair encloses the owned comment (prepended by
// `build_expression_doc`). That's any `needs_parens` operand — assignment, a
// conditional, an arrow, await/yield, a type assertion — plus a sequence, whose
// own printer supplies the pair (`needs_parens` reports false for it). Binary is
// deliberately NOT here: its plain path renders through
// `build_binary_chain_doc_ungrouped`, which does not prepend the owned comment, so
// binary must take the comment-holder path (where `needs_paren_wrap` excludes it —
// one pair either way). A *trailing* comment on such an operand still keeps both
// pairs (the comment-holder path via `has_trailing_comments`) — that is the
// deliberate `!((x = y) /* c */)` form, pinned by `operand_paren_comment`.
// A dividend tail is not one of them: its pair is this shell's when a comment leads
// it (`needs_paren_wrap` below), so its owned comment is counted here like any other
// operand's.
// Asked three times below (the exception here, the comment-holder inner wrap,
// and the plain path), always with the same operand + context — compute once.
let arg_needs_parens = self.needs_parens_at_operand_end(
unary.argument,
ParenContext::UnaryArgument {
parent_op: unary.operator,
},
);
let operand_encloses_owned_comment =
matches!(unary.argument.kind, ExpressionKind::SequenceExpression(_))
|| (arg_needs_parens
&& !self.is_dividend_tail(unary.argument)
&& !matches!(unary.argument.kind, ExpressionKind::BinaryExpression(_)));
// Anchored at `leading_run_start`, not `operator_end`: the glued `//` above already
// left the run, and counting it here would make the name a lie (it is never owned —
// `owned ⇒ is_block`). Inert for the wrap, which ORs the two, but this flag is the
// reason the wrap survives a run the emitter prints nothing for, so it has to mean
// what it says.
//
// An OWNED comment is counted up to the operand's printed leaf, not its span start: a
// block glued inside the stripped shell at the head of the operand's chain
// (`typeof ((/* c */x)[y])`) sits inside the span, yet its leaf prints it first, so it
// leads the operand's doc exactly as a comment in this gap does — and the reparse
// finds it here. The walk stops at the first pair the printer re-emits
// ([`Printer::stripped_left_spine_leaf`]), which holds an owned comment inside it
// (`typeof (/* c */ a + b).m()` keeps one pair). A layout question only: the
// operand's own doc prints the comment, inside this shell, so no emitter widens.
let owned_leading_comment = leading_comments_opt.is_none()
&& !operand_encloses_owned_comment
&& (self.has_comments_on_page_between(leading_run_start, argument_start)
|| self.owned_comment_leads_operand(unary.argument, argument_start));
let has_leading_comments =
paren_glued.is_some() || leading_comments_opt.is_some() || owned_leading_comment;
// Check for trailing comments after the argument but inside the original parens.
// When the parser strips grouping parens from `!(x /* c */)`, the comment
// between argument end and unary span end is lost if we don't re-add parens.
let has_trailing_comments = self.has_comments_to_emit_between(argument_end, unary.span.end);
// The operator→operand value head ([`Printer::value_head_frozen_span`]): an own-line
// directive in the gap freezes the whole operand. The comment-holder shell below is
// the PRINTER's pair, so it stays outside the slice exactly as at every other value
// head — and its own trailing-run emitter still answers the slice→`)` gap, so nothing
// here open-codes a second pair.
//
// Unlike `await` / `new` this gap needs no placement rule of its own: the shell's
// leading run already gives an own-line comment its own line, which is where the
// floor reads the directive, so the frozen and unfrozen layouts agree.
let frozen = self.value_head_frozen_span(operator_end, unary.argument.span());
// A resolved freeze reaches the comment-holder arm and no other: the directive is a
// LINE comment alone on its line, so it can neither be owned nor be claimed by the
// glued split, and `leading_comments_opt` therefore holds it. Every other arm emits
// no run at all, which would drop the directive along with the freeze.
debug_assert!(
frozen.is_none() || has_leading_comments,
"a unary→operand freeze must reach the comment-holder shell"
);
let argument_doc = if has_leading_comments || has_trailing_comments {
// Comments inside grouping parens — must wrap in parens to preserve them.
// `build_flat_chain_expression_doc`: a unary argument is `shouldNotIndent`
// (`key === "argument" && parent.type === "UnaryExpression"`, `shouldNotIndent`
// — the term that covers the COMMENTED operand, the uncommented one taking the
// early return the sibling arm below spells). The shell's own `indent` supplies
// the one level, so the chain inside it stays flush.
// A MULTI-LINE block the operand OWNS prints just inside this shell's `(`,
// outside the operand's own group
// ([`Printer::build_value_with_outermost_owned_comment`]). The shell itself
// still breaks — the comment's body forces it, as prettier's does — but the
// operand inside it stays flat. The claim wraps the BUILD, not the finished
// doc: prepending without suppressing double-prints.
let inner =
self.build_value_with_outermost_owned_comment(unary.argument, || match frozen {
Some(frozen) => self.build_frozen_expression_doc(unary.argument, frozen),
None => self.build_flat_chain_expression_doc(unary.argument),
});
// The outer comment-holder parens already group the operand, so the inner
// needs_parens layer is redundant for a binary/logical operand — prettier
// strips it (`!(x + y /* c */)`). Assignment/ternary operands keep their
// parens for clarity in both formatters, so leave those untouched.
// Nor does a dividend tail's pair nest inside it: that pair exists to put a `)`
// between the body's `}` and the `/`, which the shell's own does.
let needs_paren_wrap = arg_needs_parens
&& !matches!(unary.argument.kind, ExpressionKind::BinaryExpression(_))
&& !self.is_dividend_tail(unary.argument);
let inner = if needs_paren_wrap {
d.parens(inner)
} else {
inner
};
// Prettier's `UnaryExpression` shell (inline in `print/estree.js`) verbatim
// (`print/estree.js`: `group(["(", indent([softline, argumentDoc]), softline,
// ")"])`), so the two questions stay separate: the GROUP decides flat vs
// broken on width, and the comment runs inside it decide their own
// separators. Nothing here asks whether a comment "owns its line" — a gate
// that did (`comment_cannot_glue_to_operator` over the leading gap) answered
// a width question with a comment answer, and read the FIRST comment of a run
// the author glued onto one line as owning it, which it does not
// (`docs/comments.md` §Own-line-ness is a SOURCE question).
//
// Everything that genuinely must break still does, through the run's own
// hardline and `DocArena::will_break`: an own-line comment, a `//`, a
// multiline block. Those reach the group as a forced break instead of as a
// pre-empted layout, which is the whole difference.
// The shell's OPENING edge, through the shared obligation seam
// ([`Printer::obligated_break`], which carries the rule): a `//` anywhere in
// the operator→operand gap makes this break required rather than chosen, and
// one range covers both its placements — the glued comment `paren_glued` took
// is still physically in the gap it left.
let leading_line_comment = self.has_line_comments_between(operator_end, argument_start);
let mut parts: DocBuf =
smallvec![self.obligated_break(leading_line_comment, d.softline())];
if let Some(leading) = leading_comments_opt {
parts.push(leading);
}
parts.push(inner);
// The trailing run, one separator BEFORE each comment — the shared
// anchored-run emitter, so this gap answers the glue question the way every
// other trailing run does. A fixed `argument_end` anchor re-asked per comment
// read the *second* half of a pair the author glued (`x⏎/* c1 */ /* c2 */`)
// as own-line and split it, and asking the comment's KIND instead welded an
// own-line `//` onto whatever preceded it (`docs/comments.md` §Trailing and
// dangling runs).
self.push_anchored_trailing_run(
&mut parts,
argument_end,
unary.span.end,
RunLeadingBlank::Keep,
);
// The one break the group cannot see for itself. A trailing **line** comment
// is deferred through a `line_suffix` (`build_trailing_line_comment_doc`), so
// `will_break` reports nothing and a flat shell would carry the `//` out past
// its own `)` and `;` — the deferred run escaping the construct it was written
// in, which is content loss rather than a layout choice
// (`docs/comments.md` §Trailing and dangling runs). Every other break-forcing
// comment reaches the group as a real hardline: an own-line trailing comment
// through `push_trailing_run_separator`, a `//` or own-line block in the
// LEADING run through `push_leading_comment_run`'s third separator.
let trailing_line_comment =
self.has_line_comments_between(argument_end, unary.span.end);
let mut shell_parts: DocBuf = smallvec![d.text("(")];
// The glued `//` rides on the `(`'s own line, ahead of the `indent` — no break
// precedes it, so the indent would have nothing to act on anyway.
shell_parts.extend(paren_glued);
shell_parts.push(d.indent(d.concat(&parts)));
// The shell's CLOSING edge, the same seam: a deferred `//` needs this
// separator to end its line, or a flattening lets the run escape past the `)`.
shell_parts.push(self.obligated_break(trailing_line_comment, d.softline()));
shell_parts.push(d.text(")"));
let shell = d.concat(&shell_parts);
// The second break the group cannot see for itself. A `//` in the leading run
// reaches it as a real `hardline` through `push_leading_comment_run` — but the
// glued one has LEFT that run, so nothing inside the group forces it open any
// more, and a flat shell would put `x)` on the `//`'s own line and swallow it.
// Claiming the delimiter's line is therefore a break obligation, not just a
// placement: the shell rendered the same way before, the comment merely sat one
// line lower.
if paren_glued.is_some() || trailing_line_comment {
d.group_break(shell)
} else {
d.group(shell)
}
} else if arg_needs_parens || self.region_keeps_shell(unary.argument) {
// The author's shell a comma-list region keeps takes the pair the position's own
// answer would: a non-binary operand's ([`Printer::mark_region_source_pairs`]).
// The comment-holder arm above needs no such test, its shell being the pair.
//
// Binary expressions need parens - grouping lets the parens expand when the arg is long
if let ExpressionKind::BinaryExpression(binary) = &unary.argument.kind {
// Wrap any binaryish arg (logical or not) in a single paren group.
// Matches Prettier's `parent.type === "UnaryExpression"` path
// (`printBinaryishExpression`, `print/binaryish.js`): `group([indent([softline, ...parts]), softline])`,
// the same early return the call and `new` callees take — one seam for all
// three ([`Printer::build_expanding_parens_doc`]).
// The chain's shouldGroup is computed normally: 2-operand chains
// get a sub-group (can stay flat at inner indent when paren group
// breaks), 3+ chained operands break together with the paren group.
self.build_expanding_parens_doc(
self.build_binary_chain_doc_ungrouped(binary, unary.argument.span),
)
} else {
// Non-binary that needs parens (e.g., ternary or assignment in unary/assertion)
self.build_operand_pair_doc(
unary.argument,
self.build_expression_doc(unary.argument),
)
}
} else {
// No binary reaches here: `needs_parens_unary_arg_common` matches every
// `BinaryExpression`, so a binaryish operand always takes one of the two arms
// above — which is why neither of prettier's binaryish answers is spelled here.
self.build_expression_doc(unary.argument)
};
// Keyword operators need a space before the operand
if unary.operator.is_keyword_operator() {
d.concat(&[d.text(unary.operator.as_str()), d.text(" "), argument_doc])
} else {
d.concat(&[d.text(unary.operator.as_str()), argument_doc])
}
}
/// Whether a comment the operand's printed leaf OWNS sits inside the operand's span,
/// ahead of that leaf — in a stripped grouping shell at the head of its left spine — so
/// the operand's doc opens with it ([`Self::build_unary_doc_gap`]'s holder pair).
fn owned_comment_leads_operand(&self, argument: &Expression<'_>, argument_start: u32) -> bool {
// No shell opens the operand, so nothing sits ahead of its leaf: the common case, one
// byte ([`left_spine_has_no_shell`]'s test).
if self.source.as_bytes().get(argument_start as usize) != Some(&b'(')
|| !self.has_comments_on_page_between(argument_start, argument.span().end)
{
return false;
}
// The printed leaf: past every erased shell the printer does not re-emit — a pair a
// comma-list region keeps for the author is re-emitted too
// ([`Printer::region_keeps_shell`]), and holds the comment inside it — and at a
// sequence, whose own pair prints first (its envelope is the caller's to notice).
let mut leaf = argument;
while let Some(child) = self.stripped_left_side_child(leaf)
&& !self.region_keeps_shell(child)
&& !matches!(child.kind, ExpressionKind::SequenceExpression(_))
{
leaf = child;
}
let leaf_start = leaf.span().start;
leaf_start > argument_start
&& self
.comments_on_page_between(argument_start, leaf_start)
.any(|c| c.owned_by_node)
}
/// Build a doc for a chain of binary operators with line wrapping support — **the
/// default layout**, what a `BinaryExpression` gets from the generic
/// `build_expression_doc` dispatch in every context that has not named a style.
///
/// The default is prettier's: **continuation indent**. Prettier's rule
/// (`shouldNotIndent`, `print/binaryish.js`) is a *fall-through* — a parent indents unless it is named
/// in `shouldNotIndent` or `shouldIndentIfInlining` — so a position nobody thought
/// about must land on indent, or it is wrong by omission. It landed on flat here
/// until the default was inverted, and eleven positions were silently wrong for
/// exactly that reason (`yield`/`yield*` argument, `case` test, `for…of`/`for…in`
/// right, `class extends`, a bare expression statement, a labeled body, `export
/// default`, `export =`, a default parameter value — each first fixed as a point hunk
/// that named the style, and each now taking it from this default instead).
///
/// The two exempt buckets are the ones that now opt OUT, each at its call site:
///
/// - `shouldNotIndent` → [`Self::build_binary_chain_doc_flat`] (or, where the parent
/// owns the group, [`Self::build_binary_chain_doc_ungrouped`] /
/// [`Self::build_binary_chain_doc_ungrouped_condition`]).
/// - `shouldIndentIfInlining` → no call-site change: those four parents are the ones
/// `Printer::mark_assignment_value` marks, so this entry point reads the mark and
/// answers [`BinaryChainStyle::AssignmentValue`] for them. tsv has no parent
/// pointer, and the mark already existed for the inlining arm.
///
/// Positions that take the expanding-paren shape prettier returns *before*
/// `shouldNotIndent` is even computed — a call/`new` callee, a non-computed member
/// object, an uncommented unary argument — build their own parens and reach the
/// ungrouped builders, not this one. Both callee spellings say so through one seam
/// (`super::super::calls::CalleeParens::Binary`), which shares
/// [`Printer::build_expanding_parens_doc`] with the unary argument; the member object
/// takes the same layout from the other side — its parens stay bare outside the group
/// ([`Printer::build_expanding_parens_body_doc`]) so the chain's `conditional_group`
/// drives the break.
///
/// ⚠️ One position reaches the same *layout* through a different builder: a call/`new`
/// argument and an array element take `Printer::build_binary_chain_doc_indented`,
/// which indents the continuation exactly as this default does but puts it directly in
/// the outer group rather than in a sub-group. That is not an opt-in to the indent —
/// it is a break-propagation difference, and collapsing the two was measured and
/// refused; that builder's call site carries the evidence.
///
/// When the chain exceeds print width, breaks after operators:
/// ```text
/// a +
/// b +
/// c
/// ```
///
/// Implements prettier's "add parens for clarity" behavior where mixing certain
/// operators requires parentheses for readability (`a && b || c` →
/// `(a && b) || c`, chained equality, right-side same-precedence parens).
/// Flattens same-precedence operators that can be chained (e.g., a + b + c)
/// but preserves parentheses where needed for clarity (e.g., a * b / c).
///
/// Handles comments between operands (Prettier 3.7 #17723):
/// - Line comments force a line break
/// - Block comments are printed inline
pub(in crate::printer) fn build_binary_chain_doc(
&self,
binary: &ast::BinaryExpression<'_>,
span: Span,
) -> DocId {
// Read the marks BEFORE any operand is built: an operand's own marking positions
// (an object literal's property values) re-mark as they build, so a later read
// would answer for the last one instead of for this chain.
let style = if self.flat_chain_target.get() == Some(span) {
BinaryChainStyle::Flat
} else if self.assignment_value_target.get() == Some(span) {
BinaryChainStyle::AssignmentValue
} else {
BinaryChainStyle::ContinuationIndent
};
self.build_binary_chain_doc_core(binary, span, style)
}
/// Record `expr` as sitting at one of prettier's `shouldNotIndent` positions
/// (`shouldNotIndent`, `print/binaryish.js`), so a binary chain there takes [`BinaryChainStyle::Flat`].
///
/// The `shouldNotIndent` twin of [`Printer::mark_assignment_value`], and reserved for
/// the same shape of caller: a position that hands its value to a **generic value
/// builder** and so cannot name the chain style at the build site — a ternary branch
/// (its value goes through the paren shell, which owns the gap between the value and
/// the `:` / terminator and must keep owning it), a C-style `for` header clause
/// (whose element builder belongs to its caller), and a `Boolean()` coercion's
/// argument (whose builder, [`Printer::build_arg_expression_doc`], is shared by every
/// call layout and marked by both call-argument printers). Every other
/// `shouldNotIndent` position builds its own value and says so directly; the list of
/// them all lives on [`Printer::build_flat_chain_expression_doc`].
///
/// Keyed by span and not consumed, like `mark_assignment_value`. Mark immediately
/// before building the value: the cell holds one span, so anything built in between
/// that marks its own value would clear this one.
pub(in crate::printer) fn mark_flat_chain(&self, expr: &Expression<'_>) {
self.flat_chain_target.set(Some(expr.span()));
}
/// Build a binary chain doc with **no** continuation indent — prettier's
/// `shouldNotIndent` answer, `group(parts)`.
///
/// ⚠️ An OPT-OUT of [`Self::build_binary_chain_doc`], not a default, and not a call
/// site's entry point: reach it through [`Printer::build_flat_chain_expression_doc`],
/// which lists the positions and owns the owned-comment obligation this builder does
/// not discharge. (return/throw and the statement-head conditions are also
/// `shouldNotIndent` / `isInsideParenthesis`, but their parent owns the group, so they
/// take the ungrouped twins below.)
pub(in crate::printer) fn build_binary_chain_doc_flat(
&self,
binary: &ast::BinaryExpression<'_>,
span: Span,
) -> DocId {
self.build_binary_chain_doc_core(binary, span, BinaryChainStyle::Flat)
}
/// Build a binary chain doc WITHOUT the outer group wrapper
///
/// Use in contexts where the parent group should control breaking
/// (e.g., !!(), new expression callee, return/throw).
/// The line() elements will break with the parent group, but shouldGroup
/// is computed normally — 2-operand chains get a sub-group.
pub(in crate::printer) fn build_binary_chain_doc_ungrouped(
&self,
binary: &ast::BinaryExpression<'_>,
span: Span,
) -> DocId {
self.build_binary_chain_doc_core(binary, span, BinaryChainStyle::Ungrouped)
}
/// Build a binary chain doc for condition parentheses (if/while/for/do-while/switch)
///
/// Like ungrouped, but also suppresses shouldGroup for logical operators so that
/// logical chain breaks are controlled by the parent condition group.
/// Matches Prettier's `isInsideParenthesis` behavior (`printBinaryishExpressions`, `print/binaryish.js`).
pub(in crate::printer) fn build_binary_chain_doc_ungrouped_condition(
&self,
binary: &ast::BinaryExpression<'_>,
span: Span,
) -> DocId {
self.build_binary_chain_doc_core(binary, span, BinaryChainStyle::UngroupedCondition)
}
/// Build a binary chain doc with continuation indent
///
/// When the chain breaks, continuation lines are indented relative to the first:
/// ```text
/// first &&
/// second &&
/// third
/// ```
///
/// The style every position where prettier's `shouldNotIndent` chain yields false
/// takes — which is to say the DEFAULT, reached through
/// [`Self::build_binary_chain_doc`]. The one caller that names it instead is the
/// Embedded expression root (`build_root_expression_doc`), because there it must
/// override an assignment-value mark the host may have set.
pub(in crate::printer) fn build_binary_chain_doc_with_continuation_indent(
&self,
binary: &ast::BinaryExpression<'_>,
span: Span,
) -> DocId {
self.build_binary_chain_doc_core(binary, span, BinaryChainStyle::ContinuationIndent)
}
/// Build binary chain with continuation indent WITHOUT the outer group wrapper
///
/// Use this when the caller controls grouping: the one caller is
/// `build_binary_chain_parts_indented`'s comment-aware twin hand-off
/// (`prepare_binary_chain_layout`, inline-paren contexts).
pub(in crate::printer) fn build_binary_chain_parts_with_continuation_indent(
&self,
binary: &ast::BinaryExpression<'_>,
span: Span,
) -> DocId {
self.build_binary_chain_doc_core(
binary,
span,
BinaryChainStyle::ContinuationIndentUngrouped,
)
}
/// Core implementation for binary chain doc building
///
/// One body for every `BinaryChainStyle` (the variants document themselves):
/// the flat styles route through `build_binary_chain_flat`, the
/// continuation-indent pair through
/// `build_binary_chain_continuation_indent{,_parts}`.
fn build_binary_chain_doc_core(
&self,
binary: &ast::BinaryExpression<'_>,
span: Span,
style: BinaryChainStyle,
) -> DocId {
// A comment in a shell below a pair this chain's left spine prints leads the whole
// chain: the claim hands it to `wrap_chain_with_paren_comments` here
// (`Printer::left_spine_run_claim`).
let claim = self.left_spine_run_claim(span, span.start, binary.left, || {
self.needs_parens(
binary.left,
ParenContext::BinaryLeft {
parent_op: binary.operator,
},
)
});
self.with_left_spine_claim(claim, || {
self.build_binary_chain_doc_claimed(binary, span, style)
})
}
/// [`Self::build_binary_chain_doc_core`] under its left-spine claim.
fn build_binary_chain_doc_claimed(
&self,
binary: &ast::BinaryExpression<'_>,
span: Span,
style: BinaryChainStyle,
) -> DocId {
// Collect all operands (with spans) and operators in the chain
let mut operands: OperandBuf = OperandBuf::new();
let mut operators: OperatorBuf = OperatorBuf::new();
self.collect_binary_chain_with_spans(
binary,
&mut operands,
&mut operators,
span.start,
true,
);
if operands.len() <= 1 {
// Single operand, shouldn't happen but handle gracefully
return self.build_expression_doc(binary.left);
}
// shouldInlineLogicalExpression: when the outermost logical has a non-empty
// object/array on the right, keep operator and RHS on the same line.
// Prettier ref: `printBinaryishExpressions` + `shouldInlineLogicalExpression` (`print/binaryish.js`)
let should_inline_last = super::assignment::should_inline_logical_expression(binary);
// Resolve the assignment-value layout, the one style keyed on a *property of the
// chain* rather than on the position alone. An inlining chain there is flat: the
// assignment withholds its break-after-operator for it, and prettier's binaryish
// printer answers `shouldIndentIfInlining` with a bare `group(parts)` — UNLESS the
// chain has earlier operators whose lines need a home, prettier's
// `samePrecedenceSubExpression` arm, `group([head, indent(rest)])`:
// ```text
// const a = b ??
// c ?? [1];
// ```
// Prettier keys that arm on `node.left` being a same-precedence chain; tsv keys it
// on the FLATTENED operand count. Both halves of the rule read the same structure:
// prettier's parse postprocess rebalances `a ?? (b ?? [1])` into `(a ?? b) ?? [1]`
// before the printer runs, so its `node.left` is a chain exactly when tsv's chain
// has 3+ operands, and its `node.right` is the last operand — which is what
// `should_inline_logical_expression` reads (`BinaryExpression::rebalanced_right`). A
// two-operand inlining chain has no line for either answer to act on.
//
// The non-inlining half needs no arm of its own: `shouldIndentIfInlining` with
// `!shouldInlineLogicalExpression` is prettier's flat answer, which is what the
// variant falls through to.
// Fixtures: `logical/inline_chain_long`, `logical/inline_chain_paren_nested_long`.
//
// Resolved in the dispatch below rather than by reassigning `style`, so the whole
// enum is answered in ONE exhaustive match: a `_` arm there would render a seventh
// variant flat-and-ungrouped by omission, which is the exact shape this rule exists
// to retire.
// Compute shouldGroup from the original binary expression.
// This matches Prettier's shouldGroup in printBinaryishExpressions:
// the continuation gets its own group only when both operand types
// differ from the current node type (BinaryExpression vs LogicalExpression).
//
// In UngroupedCondition mode (if/while/for/do-while/switch conditions),
// logical operators (&&, ||, ??) must NOT get a sub-group — the parent
// condition group controls their breaking. This matches Prettier's
// `isInsideParenthesis` suppression (`printBinaryishExpressions`, `print/binaryish.js`).
// Without this, `while (a < b && c === d)` keeps the chain flat when
// the condition group breaks, because the sub-group evaluates fit
// independently.
//
// In plain Ungrouped mode (!!(), new, return/throw), shouldGroup is
// computed normally — 2-operand chains get a sub-group so they can
// stay flat when the parent's paren group breaks.
let should_group = if matches!(style, BinaryChainStyle::UngroupedCondition)
&& binary.operator.is_logical()
{
false
} else {
Self::should_group_binary_continuation(binary)
};
// The continuation-indent styles separate the first operand from the rest; the
// flat ones build one parts list and differ only in whether the group is theirs.
let indent_continuation = |p: &Self| {
p.build_binary_chain_continuation_indent(
&operands,
&operators,
should_inline_last,
should_group,
)
};
let flat = |p: &Self, grouped: bool| {
p.build_binary_chain_flat(
&operands,
&operators,
grouped,
should_group,
should_inline_last,
)
};
let chain = match style {
BinaryChainStyle::AssignmentValue if should_inline_last && operands.len() > 2 => {
indent_continuation(self)
}
BinaryChainStyle::AssignmentValue | BinaryChainStyle::Flat => flat(self, true),
BinaryChainStyle::Ungrouped | BinaryChainStyle::UngroupedCondition => flat(self, false),
BinaryChainStyle::ContinuationIndent => indent_continuation(self),
BinaryChainStyle::ContinuationIndentUngrouped => self
.build_binary_chain_continuation_indent_parts(
&operands,
&operators,
should_inline_last,
should_group,
),
};
self.wrap_chain_with_paren_comments(span, &operands, chain)
}
/// Wrap a binary chain doc with comments from stripped grouping parens.
///
/// When the parser strips parens like `(/* l */ a + b /* t */)`, the
/// comments are orphaned in the gaps between `span` and the outer
/// operand spans. Without this, those comments are silently dropped — a
/// SAFETY violation.
///
/// Leading comments (between `span.start` and the leftmost operand)
/// are prepended via `prepend_removed_paren_comments`. Trailing comments
/// (between the rightmost operand and `span.end`) emit inline for
/// same-line blocks (` /* t */`) and via `line_suffix` for line/own-line
/// comments (so they defer past any enclosing semicolon).
fn wrap_chain_with_paren_comments(
&self,
span: Span,
operands: &[ChainOperand],
chain: DocId,
) -> DocId {
let Some(leftmost_start) = operands.first().map(|o| o.span.start) else {
return chain;
};
let Some(rightmost_end) = operands.last().map(|o| o.span.end) else {
return chain;
};
let with_leading =
self.prepend_removed_paren_comments(span, span.start, leftmost_start, chain);
if rightmost_end >= span.end {
return with_leading;
}
// A member gap that opens at the chain base's printed end prints this region
// itself, after the base's `)` ([`Printer::member_gap_start`]).
if let Some(claim) = self.claimed_right_spine_run.get()
&& claim.start <= rightmost_end
&& span.end <= claim.end
{
return with_leading;
}
debug_assert!(
self.claimed_right_spine_run
.get()
.is_none_or(|claim| span.end <= claim.start || claim.end <= rightmost_end),
"a claimed right-spine region covers a binary's whole trailing region or none of it"
);
let mut parts = smallvec![with_leading];
self.append_trailing_paren_comments(&mut parts, rightmost_end, span.end);
// `concat` short-circuits the no-trailing-comment case (`[with_leading]`).
self.d().concat(&parts)
}
/// Check if the binary continuation should be wrapped in its own group.
///
/// Matches Prettier's `shouldGroup` in `printBinaryishExpressions`:
/// - Returns true when both left and right operands are a different AST type
/// category than the current node (BinaryExpression vs LogicalExpression).
/// - In ESTree, `+`, `*`, etc. are BinaryExpression while `&&`, `||`, `??`
/// are LogicalExpression. We use `is_logical()` to distinguish these categories.
///
/// When shouldGroup is true, the continuation gets its own group, allowing it
/// to independently evaluate whether it fits on the current line when the outer
/// group breaks (e.g., due to a multi-line parenthesized left operand).
///
/// Unlike `should_inline_logical_expression` this needs no rebalanced view: a right
/// operand prettier would rebalance away is same-category by definition, and the
/// rebalanced tree's LEFT is then same-category too — so both trees answer false.
pub(in crate::printer) fn should_group_binary_continuation(
binary: &ast::BinaryExpression<'_>,
) -> bool {
let current_is_logical = binary.operator.is_logical();
// Check if left operand is same AST type category
let left_is_same_category = matches!(
&binary.left.kind,
ExpressionKind::BinaryExpression(inner) if inner.operator.is_logical() == current_is_logical
);
// Check if right operand is same AST type category
let right_is_same_category = matches!(
&binary.right.kind,
ExpressionKind::BinaryExpression(inner) if inner.operator.is_logical() == current_is_logical
);
// shouldGroup when NEITHER operand is the same category
!left_is_same_category && !right_is_same_category
}
/// Common logic for building binary chain (shared by flat and continuation indent styles)
///
/// Returns (head_parts, continuation_parts) where head includes first operand + operator.
///
/// When `should_inline_last` is true (shouldInlineLogicalExpression), the last operand
/// uses a space instead of `line()`, keeping operator and RHS on the same line so the
/// object/array can self-expand. Prettier ref: `printBinaryishExpressions` +
/// `shouldInlineLogicalExpression` (`print/binaryish.js`)
fn build_binary_chain_parts(
&self,
operands: &[ChainOperand],
operators: &[ChainOperator],
should_inline_last: bool,
) -> (DocBuf, DocBuf) {
if operands.is_empty() || operands.len() == 1 {
// Edge cases handled by callers
return (DocBuf::new(), DocBuf::new());
}
// Whole-chain comment presence gate (idiom 8): one on-page lookup over the chain's
// whole span lets the per-gap emitters below skip their per-gap comment scans for the
// ~all chains that hold no comment anywhere. A *presence* flag (on-page counts owned),
// so it fails open — it can only add work on a commented chain, never suppress a
// comment (the hazard any fast-path flag must avoid). Every operand→operator / operator→operand gap the
// emitters scan lies within `[first operand start, last operand end]`.
let chain_has_comments = self.has_comments_on_page_between(
operands[0].span.start,
operands[operands.len() - 1].span.end,
);
// First operand + first operator (stays at base indent)
let mut head_parts: DocBuf = smallvec![operands[0].doc];
let first_op_str = operators[0].as_str();
let first_op_pos =
self.find_operator_position(operands[0].span.end, operands[1].span.start, first_op_str);
// Build continuation parts
let mut continuation_parts: DocBuf = DocBuf::new();
// shouldInlineLogicalExpression: the last operand (non-empty object/array) uses a
// space instead of line(), keeping operator and RHS on the same line.
let inlined = |i: usize| i == operands.len() - 1 && should_inline_last;
// Whether the operator ahead of `operands[i]` leads its line rather than ending the
// one before ([`ChainOperator::leads_line`]). It needs a `line` of its own to lead
// — an inlined operand has none — and it yields to a comment that already forces
// the operand below the operator: that authoring is a syntax error to tsc as
// written, and the only repair is a comment relocation.
let leads_line = |i: usize, op_end: u32| {
operators[i - 1].leads_line
&& !inlined(i)
&& !(chain_has_comments
&& self.comment_hangs_binary_operand(op_end, operands[i].span.start))
};
// An operator that leads its line opens the CONTINUATION, wherever the gap before
// it sat — for the first operator that is the indented half rather than the head.
let push_leading_operator = |continuation_parts: &mut DocBuf, op_str: &'static str| {
continuation_parts.push(self.d().line());
continuation_parts.push(self.d().text(op_str));
};
// operand[0] → first operator. A line comment in this gap would swallow the
// operator if emitted inline; the helper keeps it trailing the operand and
// reports where the operator went.
let mut gap = self.push_operand_operator_gap(
&mut head_parts,
operands[0].span.end,
first_op_pos.start,
first_op_str,
chain_has_comments,
leads_line(1, first_op_pos.end),
);
if gap == OperatorGap::LeadsLine {
push_leading_operator(&mut continuation_parts, first_op_str);
}
// The operand[i-1]→operand[i] operator gap is located once and carried across
// iterations: iteration i's leading gap is either the first gap (i == 1) or the
// trailing gap the previous iteration already scanned, so it is never re-scanned.
let mut op_pos = first_op_pos;
for i in 1..operands.len() {
let operand = &operands[i];
// When the operator already leads this operand's line — forced down by a line
// comment, or leading by rule — the operand hugs it with a space (not a line).
self.append_post_operator_parts(
&mut continuation_parts,
op_pos.end,
operand,
!inlined(i),
gap != OperatorGap::EndsLine,
chain_has_comments,
);
// operand[i] → next operator (if not last operand)
if i < operands.len() - 1 {
let next_op_str = operators[i].as_str();
let next_op_pos = self.find_operator_position(
operand.span.end,
operands[i + 1].span.start,
next_op_str,
);
gap = self.push_operand_operator_gap(
&mut continuation_parts,
operand.span.end,
next_op_pos.start,
next_op_str,
chain_has_comments,
leads_line(i + 1, next_op_pos.end),
);
if gap == OperatorGap::LeadsLine {
push_leading_operator(&mut continuation_parts, next_op_str);
}
// Carry this trailing gap forward as the next iteration's leading gap.
op_pos = next_op_pos;
}
}
(head_parts, continuation_parts)
}
/// Build a flat binary chain — `grouped` says whether the group is THIS builder's
/// (prettier's `shouldNotIndent` answer, `group(parts)`) or the caller's (the two
/// ungrouped styles, where a parent controls the breaking).
///
/// Matches Prettier's binaryish.js structure:
/// - First operand + first operator at base indent (head)
/// - Continuation (line + remaining operands) optionally in a sub-group
///
/// When `should_group` is true (operand types differ from current node,
/// e.g., `(LogicalExpr) + d`), the continuation gets its own group so it
/// can independently evaluate fit when the outer group breaks due to a
/// multi-line left operand. When false (same category, e.g., `(a+b)*c`),
/// continuation breaks with the outer group.
fn build_binary_chain_flat(
&self,
operands: &[ChainOperand],
operators: &[ChainOperator],
grouped: bool,
should_group: bool,
should_inline_last: bool,
) -> DocId {
let d = self.d();
if operands.is_empty() {
return d.empty();
}
if operands.len() == 1 {
return operands[0].doc;
}
let (mut head_parts, continuation_parts) =
self.build_binary_chain_parts(operands, operators, should_inline_last);
if !continuation_parts.is_empty() {
if should_group {
// Sub-group: continuation evaluates fit independently
head_parts.push(d.group(d.concat(&continuation_parts)));
} else {
// No sub-group: continuation breaks with outer group
head_parts.extend(continuation_parts);
}
}
if grouped {
d.group(d.concat(&head_parts))
} else {
d.concat(&head_parts)
}
}
/// Build a binary chain with continuation indent
///
/// When flat: "first && second && third"
/// When broken:
/// "first &&
/// second &&
/// third"
fn build_binary_chain_continuation_indent(
&self,
operands: &[ChainOperand],
operators: &[ChainOperator],
should_inline_last: bool,
should_group: bool,
) -> DocId {
let d = self.d();
d.group(self.build_binary_chain_continuation_indent_parts(
operands,
operators,
should_inline_last,
should_group,
))
}
/// Build binary chain continuation indent parts WITHOUT group wrapper.
///
/// Returns the concat of first_parts + indent(continuation_parts) without
/// wrapping in a group. Used in Svelte template expressions and when the
/// caller controls grouping.
///
/// When `should_group` is true, wraps the continuation in a sub-group so it
/// can independently evaluate fit (bypassing the renderer's `will_break` check
/// on the outer group).
fn build_binary_chain_continuation_indent_parts(
&self,
operands: &[ChainOperand],
operators: &[ChainOperator],
should_inline_last: bool,
should_group: bool,
) -> DocId {
let d = self.d();
let (first_parts, continuation_parts) =
self.build_binary_chain_parts(operands, operators, should_inline_last);
// When should_group is true, wrap the continuation in its own group so it
// can independently evaluate fit. Without this, the renderer's will_break()
// check on the outer group sees hardlines in the left operand (e.g., a
// multi-line call expression) and forces the entire group to Break mode,
// even when the continuation (e.g., `?? 'text'`) fits on the closing line.
//
// An inlining chain (`should_inline_last`: the last operand hugs its operator with
// a space in place of a line) is flat when that hug is ALL the continuation holds
// — prettier's `shouldInline && !samePrecedence` arm, a bare `group(parts)` — so
// the hugged object's body sits one level past the chain, not two
// (`svelte/expressions/logical_object_attr`). With earlier operators in the
// chain the continuation takes the indent, hug included: prettier's
// `samePrecedenceSubExpression` arm, `indent(rest)`, at every position this style
// serves — a nested operand, a cast operand, a return-ternary's test, the Svelte
// expression root. Keyed on the flattened operand count rather than on `left`
// for the reason `build_binary_chain_doc_core` gives.
let indent_continuation = !should_inline_last || operands.len() > 2;
let continuation_doc = if indent_continuation {
d.indent(d.concat(&continuation_parts))
} else {
d.concat(&continuation_parts)
};
let continuation_doc = if should_group {
d.group(continuation_doc)
} else {
continuation_doc
};
d.concat(&[d.concat(&first_parts), continuation_doc])
}
/// Emit a binary chain's operand→operator gap, reporting where the operator went
/// ([`OperatorGap`]).
///
/// Without a line comment the gap renders inline as it always has
/// (`operand <inline block comments> operator`). With a line comment, emitting it
/// inline would run to end-of-line and **swallow the operator**
/// (`1 // c⏎+ 2` → `1 // c + 2`, the `+ 2` absorbed into the comment — content
/// loss). Instead the comment is kept trailing the operand where the author wrote
/// it — the first, on the operand's own line, via `line_suffix` (zero width); any
/// later ones on their own line — and a hardline then forces the operator down to
/// hug its right operand (`1 // c⏎+ 2`, [`OperatorGap::ForcedDown`], where a breakable
/// line after it would over-break to `1 // c⏎+⏎2`). Prettier instead relocates the
/// comment past the operator; see conformance_prettier_ts_comments.md §Comment
/// relocation.
///
/// `leads_line` asks for the third layout ([`ChainOperator::leads_line`]): the gap's
/// comments stay with the operand and the operator is NOT emitted — the caller puts it
/// behind a breakable `line` of its own ([`OperatorGap::LeadsLine`]). A line comment in
/// the gap already forces the operator down, which is the same place.
fn push_operand_operator_gap(
&self,
parts: &mut DocBuf,
operand_end: u32,
op_start: u32,
op_str: &'static str,
chain_has_comments: bool,
leads_line: bool,
) -> OperatorGap {
let d = self.d();
let has_comments =
chain_has_comments && self.has_comments_to_emit_between(operand_end, op_start);
let has_line_comments =
has_comments && self.has_line_comments_between(operand_end, op_start);
if leads_line && !has_line_comments {
if has_comments {
parts.push(self.build_inline_comments_between_doc(operand_end, op_start));
}
return OperatorGap::LeadsLine;
}
// Zero-comment fast path: the operand→operator gap holds no comment (the
// ubiquitous case), so emit just the operator — no empty comment node in the
// parts concat, and no per-gap comment scan at all. Byte-identical: the gap is
// comment-free, so the general path below would build `empty()` here (renders to
// nothing). The gap ⊆ the binary span, so this can only skip work, never a comment.
// The whole-chain gate short-circuits the per-gap scan when the chain is
// comment-free (`chain_has_comments` false ⇒ this gap holds none to emit either).
if !has_comments {
parts.push(d.text(" "));
parts.push(d.text(op_str));
return OperatorGap::EndsLine;
}
if !has_line_comments {
// No line comment — inline gap (block comments stay inline, as before).
parts.push(self.build_inline_comments_between_doc(operand_end, op_start));
parts.push(d.text(" "));
parts.push(d.text(op_str));
return OperatorGap::EndsLine;
}
// Keep each comment where the author wrote it, then break before the operator —
// the shared anchored-run emitter ([`Printer::push_anchored_trailing_run`]).
self.push_anchored_trailing_run(parts, operand_end, op_start, RunLeadingBlank::Keep);
parts.push(d.hardline());
parts.push(d.text(op_str));
OperatorGap::ForcedDown
}
/// Append post-operator parts (comments and line breaks) to a parts vector
///
/// Handles line comments vs block comments appropriately.
/// When `allow_breaks` is true, uses `line()` (space when flat, newline when broken).
/// When `lead_with_space` is true, the leading separator is a hard space instead of a
/// breakable line — used when the previous operand→operator gap forced a break, so the
/// operator now leads this operand on the same line (`1 // c⏎+ 2`, not `1 // c⏎+⏎2`).
///
/// Handles multiple consecutive comments by preserving their line structure:
/// - `a && // comment1\n// comment2\nb` keeps each comment on its own line
///
/// The inline arm's separators are the leading-run rule read FORWARD, per comment,
/// from the source around that comment alone ([`Printer::comment_hugs_next`],
/// prettier's `printLeadingComment`): the author's glue keeps a space and a break the
/// author wrote after a comment survives as a break. It reaches that rule through the
/// shared broke-after pair — [`Printer::breaking_value_leading_run`] for the geometry
/// plus the forced-break question, then
/// [`Printer::push_leading_run_before_breaking_value`] — exactly as the annotation `:`
/// seam does, so the operator→operand gap cannot answer a run differently from every
/// other value gap. A run whose break is not forced keeps the kind-keyed inline
/// emitter, whose flat render is what prettier's collapsed `line` produces.
fn append_post_operator_parts(
&self,
parts: &mut DocBuf,
op_end: u32,
operand: &ChainOperand,
allow_breaks: bool,
lead_with_space: bool,
chain_has_comments: bool,
) {
let d = self.d();
// Zero-comment fast path, the same shape as the operand→operator gap's: the
// whole-chain gate short-circuits the per-gap scan for the ~all chains with no
// comment (`chain_has_comments` false ⇒ this gap, ⊆ the chain span, holds none to
// emit). A *presence* flag, so a false gate proves this gap emits nothing.
if !chain_has_comments || !self.has_comments_to_emit_between(op_end, operand.span.start) {
// No comments - simple case
if allow_breaks && !lead_with_space {
parts.push(d.line());
} else {
parts.push(d.text(" "));
}
parts.push(operand.doc);
return;
}
// Whether the run forces the operand onto its own (broken) line — a **line**
// comment, or a block the author gave a line of its own. Anything else is
// inline-leading and lets the chain's group decide on width.
let forces_own_line = self.comment_hangs_binary_operand(op_end, operand.span.start);
if !forces_own_line {
// Only inline-leading block comments - place as leading on RHS operand.
// In flat mode: `a || /* comment */ b` (space from line(), comment+trailing space, operand)
// In break mode: `a ||\n<indent>/* comment */ b` (comment leads continuation line)
let lead = if allow_breaks && !lead_with_space {
d.line()
} else {
d.text(" ")
};
// A block run the author broke AFTER whose break is already FORCED — a
// multi-line comment in the run, or an operand that breaks on its own — takes
// the shared forced emitter ([`Printer::breaking_value_leading_run`] +
// [`Printer::push_leading_run_before_breaking_value`]): every separator the
// author broke is a blank-preserving hardline, so the operand leads the
// continuation line below the run's closing `*/` exactly as the shell-free
// authoring lays it, and an author blank after the run survives. The
// kind-keyed emitter below spells a space at every comment→next seam, which
// glued the operand onto a multi-line block's closing line and erased that
// blank — the class `docs/comments.md` §Leading comments carries. A run that
// FITS behind single-line comments declines here and keeps the glued path,
// whose flat render is the bytes prettier's collapsed `line` produces.
if let Some((run, operand_doc)) =
self.breaking_value_leading_run(op_end, operand.span.start, || operand.doc)
{
parts.push(lead);
self.push_leading_run_before_breaking_value(parts, &run, operand.span.start);
parts.push(operand_doc);
return;
}
let comments_doc =
self.build_comments_between(op_end, operand.span.start, CommentSpacing::Trailing);
parts.push(lead);
parts.push(comments_doc);
parts.push(operand.doc);
return;
}
// An own-line (or line) comment forces the chain to break. Each comment keeps
// its line; authored blank lines are preserved. A trailing comment glued to the
// operator (no newline after it) stays inline-leading it. The shared
// anchored-run emitter ([`Printer::push_anchored_trailing_run`]), which hands
// back the cursor the operand separator below reads.
let pos = self.push_anchored_trailing_run(
parts,
op_end,
operand.span.start,
RunLeadingBlank::Drop,
);
// Operand: on its own line when the last comment has a newline after it
// (preserving an author blank line), else glued inline (`/* c */ operand`).
// Comment-adjacency read (real even in canonical mode): a line comment always
// has a source newline before the operand, and gluing the operand after its
// `line_suffix` would swallow it at flush (inside `${…}` this even makes the
// output unparseable).
if self.comment_has_newline_between(pos, operand.span.start) {
// ⚠️ The blank scan takes the in-source ceiling: the operand is an EXPRESSION
// start, so a block comment the author glued to it is OWNED — physically in
// this gap, printed by the operand's own doc, and skipped by the run above.
// Scanning across it reads its interior newlines as an author blank, which
// this gap already keeps for real (`RunLeadingBlank::Keep`, the sanctioned
// `expressions/binary/operator_trailing_comment_blank`) — so the fabrication
// wears the sanction's own output and nothing tells them apart.
self.push_blank_preserving_hardline(
parts,
pos,
self.blank_scan_end(pos, operand.span.start),
);
} else {
parts.push(d.text(" "));
}
parts.push(operand.doc);
}
/// Find operator position between two operands in source
///
/// Returns the start and end positions of the operator string in the source,
/// which is used to correctly split comments before/after the operator.
/// Skips over comments to avoid matching operators inside them.
fn find_operator_position(
&self,
prev_span_end: u32,
next_span_start: u32,
op_str: &str,
) -> OperatorPosition {
let range_start = prev_span_end as usize;
let range_end = next_span_start as usize;
let bytes = self.source.as_bytes();
let op_bytes = op_str.as_bytes();
let op_len = op_bytes.len();
let mut i = range_start;
while i + op_len <= range_end {
// Skip comments
if let Some(new_i) = tsv_lang::source_scan::skip_comment(bytes, i, range_end) {
i = new_i;
continue;
}
// Check for operator match
if &bytes[i..i + op_len] == op_bytes {
return OperatorPosition {
start: i as u32,
end: (i + op_len) as u32,
};
}
i += 1;
}
// Fallback (shouldn't happen in valid code)
OperatorPosition {
start: prev_span_end,
end: prev_span_end + op_str.len() as u32,
}
}
/// Collect all operands (with spans) and operators from a chain of binary expressions
///
/// Uses `should_flatten()` to determine which operators can be chained together.
/// Flattens both left and right sides when operators are compatible (e.g., `&&`, `||`).
///
/// `ends_chain` says whether `expr`'s right operand is the chain's LAST operand — true
/// at the root, carried through a rebalanced right side, false down a flattened left
/// one — which is the one operand whose pair can hold a run nothing after it in the
/// chain can end ([`Self::build_chain_end_operand`]).
fn collect_binary_chain_with_spans(
&self,
expr: &ast::BinaryExpression<'_>,
operands: &mut OperandBuf,
operators: &mut OperatorBuf,
chain_start: u32,
ends_chain: bool,
) {
// The flattened left spine, walked down without recursing — the chain's length is
// bounded only by `SPINE_LIMIT`, so a recursion per link would spend a frame per
// operator: each binary's own marks in spine order, outermost first, then the
// leftmost operand, then each binary's operator and right side, innermost first —
// the order a recursion down the left visits them in.
let mut spine: SmallVec<[&ast::BinaryExpression<'_>; 8]> = smallvec![expr];
let mut node = expr;
loop {
self.mark_dividend_tail(node);
self.withhold_clarity_pairs_behind_region_close(node);
let Some(left_binary) = self.flattenable_left(node) else {
break;
};
spine.push(left_binary);
node = left_binary;
}
let left = node.left;
// The LEFTMOST operand — the first one the collector pushes — takes the erased-paren
// freeze over the chain's leading region, which the chain itself emits
// ([`Self::wrap_chain_with_paren_comments`]). A later operand's shell keeps the
// ordinary builder: its erased-paren region belongs to the operator gap before it,
// which is a separate question.
let doc = self.build_left_spine_operand_doc_if(
operands.is_empty(),
chain_start,
left,
FrozenOperandPair::position(ParenContext::BinaryLeft {
parent_op: node.operator,
}),
|| self.build_binary_operand_doc(left, node.operator, false),
);
operands.push(ChainOperand {
doc,
span: left.span(),
});
for (depth, &node) in spine.iter().enumerate().rev() {
self.collect_binary_chain_link(
node,
operands,
operators,
chain_start,
ends_chain && depth == 0,
);
}
}
/// One link of [`Self::collect_binary_chain_with_spans`]'s chain, its left side
/// collected: its operator, then its right operand — flattened in turn where prettier
/// rebalances it. `ends_chain` as there.
fn collect_binary_chain_link(
&self,
expr: &ast::BinaryExpression<'_>,
operands: &mut OperandBuf,
operators: &mut OperatorBuf,
chain_start: u32,
ends_chain: bool,
) {
// Add current operator
operators.push(ChainOperator::of(expr));
// Also flatten the right side where prettier rebalances it ([`Self::rebalanced_right`]).
if let Some(right_binary) = self.rebalanced_right(expr, ends_chain) {
self.collect_binary_chain_with_spans(
right_binary,
operands,
operators,
chain_start,
ends_chain,
);
return;
}
// Right operand can't be flattened - add as-is
operands.push(if ends_chain {
self.build_chain_end_operand(expr.right, expr.operator)
} else {
ChainOperand {
doc: self.build_binary_operand_doc(expr.right, expr.operator, true),
span: expr.right.span(),
}
});
}
/// The chain's LAST operand, when the pair its position requires holds a run that
/// would defer: a `//`, a multi-line block, or a comment the author gave its own line,
/// between the operand and the pair's `)` (`x - (y - z // c⏎)`).
///
/// Every other operand's pair is followed by an operator, and the operand→operator gap
/// emitter ends the line there. The last one's is followed by nothing the chain owns:
/// left to [`Self::wrap_chain_with_paren_comments`], the run defers past the pair's
/// `)` to the end of the ENCLOSING line — flushed inside an enclosing list, but past a
/// statement's `;` (merging with a comment already there, in prettier) and past a
/// Svelte tag's `}` into rendered page text. A deferred run must not leave the
/// construct it was written in (`docs/comments.md` §Trailing and dangling runs), and
/// this pair is still in the output to hold it, so the pair keeps the run and opens
/// around it: the operand one indent in, the run behind it in authored order, the `)`
/// back out on its own line — the expanded shell every required pair in the family
/// takes (the unary comment-holder, the non-null operand, the return and export
/// operands).
///
/// The opened pair owns its `(`→operand gap too, so the returned span covers the
/// whole pair: the chain's operator→operand gap ends at the `(`, and its own
/// trailing-paren emitter starts after the `)`, neither printing a run a second time. A
/// run of same-line single-line blocks prints inline and cannot escape, so it keeps the
/// ordinary path.
fn build_chain_end_operand(
&self,
operand: &Expression<'_>,
parent_op: BinaryOperator,
) -> ChainOperand {
let span = operand.span();
match self.chain_end_operand_shape(operand, parent_op) {
ChainEndShape::Bare => ChainOperand {
doc: self.build_binary_operand_doc(operand, parent_op, true),
span,
},
ChainEndShape::Inline { open, close } => {
// A run of same-line blocks stays inline, inside the kept pair.
let d = self.d();
let mut body: DocBuf = smallvec![d.text("(")];
body.push(self.build_value_with_outermost_owned_comment(operand, || {
self.build_chain_aware_operand_doc(operand)
}));
self.push_anchored_trailing_run(&mut body, span.end, close, RunLeadingBlank::Keep);
body.push(d.text(")"));
ChainOperand {
doc: d.concat(&body),
span: Span::new(open, close),
}
}
ChainEndShape::Opened { open, close } => {
// The opened pair owns BOTH its gaps, as every required pair in the family
// does: a `//` the author glued to the `(` keeps the `(` line (the
// opening-delimiter rule), and the rest of the `(`→operand run leads the
// operand inside the pair. Left to the operator→operand gap, that run printed
// AHEAD of the `(` (`x - // c⏎(⏎y - z …`), so the span reported to the chain
// opens at the `(` — the operator gap ends there.
let (paren_trailing, run_start) =
self.split_open_delimiter_glued_run(open + 1, span.start);
let mut body = DocBuf::new();
if let Some(run) = self.build_rhs_comments_opt(run_start, span.start) {
body.push(run);
}
body.push(self.build_value_with_outermost_owned_comment(operand, || {
self.build_chain_aware_operand_doc(operand)
}));
self.push_anchored_trailing_run(&mut body, span.end, close, RunLeadingBlank::Keep);
ChainOperand {
doc: self.compose_expanded_shell_doc(paren_trailing, &body, ")"),
span: Span::new(open, close),
}
}
}
}
/// How [`Self::build_chain_end_operand`] prints the chain's last operand, decided
/// apart from the build so [`Self::binary_printed_last_operand`] reads the same answer.
fn chain_end_operand_shape(
&self,
operand: &Expression<'_>,
parent_op: BinaryOperator,
) -> ChainEndShape {
let span = operand.span();
let ctx = ParenContext::BinaryRight { parent_op };
// The pair must be the AUTHOR's, around this operand: a `(` right before it and a
// `)` right after. A pair the printer mints for clarity (`p && q || r && s`) has no
// `)` of its own in the source, and the next one belongs to an enclosing construct
// (`void(… r && s // c⏎)`) whose own trailing emitter prints that run.
let kept_for_value_end = self.value_end_kept_pair(operand);
let keeps_for_value_end = kept_for_value_end.is_some();
let Some((open, close)) = kept_for_value_end.or_else(|| {
if !self.needs_parens_at_operand_end(operand, ctx) {
return None;
}
let open = self
.prev_significant_pos(span.start)
.filter(|&at| self.source.as_bytes()[at as usize] == b'(')?;
Some((open, self.paren_shell_close_after(span.end)?))
}) else {
return ChainEndShape::Bare;
};
// A kept pair whose `(`→operand gap holds a comment takes the opened shape, which
// owns that gap too.
let leading_run =
keeps_for_value_end && self.has_comments_to_emit_between(open + 1, span.start);
if !leading_run && !self.pair_trailing_run_defers(span.end, close) {
return if keeps_for_value_end {
ChainEndShape::Inline { open, close }
} else {
ChainEndShape::Bare
};
}
ChainEndShape::Opened { open, close }
}
/// The right side of `expr` as the binary [`Self::collect_binary_chain_link`] flattens
/// into, if it does: prettier REBALANCES it, which removes the redundant parens
/// (`a && (b && c)` becomes `a && b && c`, [`BinaryOperator::rebalances_with`] —
/// logical operators only; arithmetic preserves right-side parens), unless it is the
/// chain's last operand and its pair is kept for the value's end.
fn rebalanced_right<'e>(
&self,
expr: &'e ast::BinaryExpression<'e>,
ends_chain: bool,
) -> Option<&'e ast::BinaryExpression<'e>> {
if let ExpressionKind::BinaryExpression(right_binary) = &expr.right.kind
&& expr.operator.rebalances_with(right_binary.operator)
&& !(ends_chain && self.value_end_kept_pair(expr.right).is_some())
{
return Some(right_binary);
}
None
}
/// `binary`'s last operand as its chain prints it ([`Self::collect_binary_chain_link`]
/// rebalancing into a right side, [`Self::build_chain_end_operand`] deciding its pair):
/// `Ok(operand)` when the operand prints bare, so the stripped shells between its end and
/// `binary`'s are the region [`Self::wrap_chain_with_paren_comments`] emits, or
/// `Err(close)` when a kept pair prints them, ending at its `)`.
pub(in crate::printer) fn binary_printed_last_operand<'e>(
&self,
binary: &'e ast::BinaryExpression<'e>,
) -> Result<&'e Expression<'e>, u32> {
let mut node = binary;
while let Some(right) = self.rebalanced_right(node, true) {
node = right;
}
match self.chain_end_operand_shape(node.right, node.operator) {
ChainEndShape::Bare => Ok(node.right),
ChainEndShape::Inline { close, .. } | ChainEndShape::Opened { close, .. } => Err(close),
}
}
/// Whether the run in `[start, end)` holds a comment the trailing emitters DEFER — a
/// `//`, a multi-line block, or one the author gave a line of its own — as opposed to
/// a run of same-line single-line blocks, which prints inline where it stands.
fn pair_trailing_run_defers(&self, start: u32, end: u32) -> bool {
let mut prev_end = start;
self.comments_to_emit_between(start, end).any(|comment| {
let defers = !comment.is_block
|| comment.multiline
|| self.comment_has_newline_between(prev_end, comment.span.start);
prev_end = comment.span.end;
defers
})
}
/// The author's pair around `operand` — a chain's last operand — that must be KEPT
/// because the host rejects a comment at the value's end
/// (`EmbedContext::value_end_takes_no_comment`), as `(open, close)` with `close` past its
/// `)`: the OUTERMOST pair the author wrote around the operand, when a comment sits
/// between the operand and that pair's `)`. Stripped, the comment could end the value
/// (`{#each a || (b // c⏎)}` would print `{#each a || b // c⏎}`, which Svelte rejects);
/// kept, the pair is the one place the comment can stand. Whatever follows the pair —
/// `.m`, `[i]`, a call's `)` — does not matter: the host builds under the field only
/// when the value's ordinary build ends on a comment, and the kept pair is valid either
/// way. Nested redundant pairs (`((b // c1⏎) // c2⏎)`) keep the outermost one, holding
/// every comment the layers held.
fn value_end_kept_pair(&self, operand: &Expression<'_>) -> Option<(u32, u32)> {
if !self.embed.value_end_takes_no_comment {
return None;
}
let bytes = self.source.as_bytes();
let span = operand.span();
let (mut start, mut end) = (span.start, span.end);
let mut pair = None;
while let Some(open) = self
.prev_significant_pos(start)
.filter(|&at| bytes[at as usize] == b'(')
&& let Some(close) = self.paren_shell_close_after(end)
{
pair = Some((open, close));
start = open;
end = close;
}
let (open, close) = pair?;
self.has_comments_to_emit_between(span.end, close)
.then_some((open, close))
}
/// The left operand of `expr` as the binary this chain FLATTENS into, if it does:
/// its operator chains with `expr`'s (`can_flatten_with`) and it takes no pair of
/// its own in this position. The second condition is what keeps a flattened chain
/// honest about the instantiation-tail rule — `(1 + fn<T>) + 2` ends on a `>` the
/// `+` would re-lex, so the left stays a parenthesized operand rather than being
/// printed bare as three operands. Read by both chain collectors, so the two
/// spellings cannot drift, and spelled by [`flattened_left_operand`], which the
/// parser's depth limit prices the walk with.
pub(in crate::printer) fn flattenable_left<'e>(
&self,
expr: &'e ast::BinaryExpression<'e>,
) -> Option<&'e ast::BinaryExpression<'e>> {
flattened_left_operand(expr.operator, expr.left, |left, ctx| {
self.needs_parens(left, ctx)
})
}
/// Record the function or class expression the printed text of a division's left
/// operand ENDS on, so the position that prints it gives it a pair
/// ([`Printer::needs_parens_at_operand_end`]): `!function () {} / 2` prints `!(function
/// () {}) / 2`, `a / async function () {} / 2` prints `a / (async function () {}) / 2`.
///
/// One invariant, of which the dividend's own pair
/// (`needs_parens::needs_parens_binary_operand`) is the direct case: **no function or
/// class body's `}` prints directly before a `/` operator**. acorn, which Svelte parses
/// every `<script>` and template expression with, reads the `/` after such a body as a
/// regex wherever its tokenizer took the keyword for a statement's, and where that is
/// follows the tokens before the keyword rather than the operand's position — an
/// anonymous `async function` and a decorated class read that way after any operator.
/// So the rule is uniform over every function and class expression rather than a model
/// of acorn's contexts.
///
/// Called by both chain collectors on every binary they visit, ahead of any operand's
/// build — a flattened chain's inner division is visited, never built as a node.
#[inline]
pub(in crate::printer) fn mark_dividend_tail(&self, expr: &ast::BinaryExpression<'_>) {
if expr.operator == BinaryOperator::Slash {
self.mark_tail_of_dividend(expr.left);
}
}
/// [`Self::mark_dividend_tail`]'s walk, over a division's left operand.
///
/// It follows the operand's rightmost printed child through the three positions that
/// can print it bare at the end of a `/`'s left operand: a prefix unary's argument, a
/// binary's right operand, an angle-bracket assertion's expression — the positions
/// whose builders ask [`Printer::needs_parens_at_operand_end`]. Every other composite
/// either ends on a token of its own or takes a pair in that position (a conditional,
/// an arrow, an assignment, `await`, `yield`), which ends it on a `)`. The direct left
/// operand is not recorded: its position's own rule gives it the pair.
fn mark_tail_of_dividend(&self, dividend: &Expression<'_>) {
let mut operand = dividend;
let mut ctx = ParenContext::BinaryLeft {
parent_op: BinaryOperator::Slash,
};
loop {
// An operand inside a pair ends on its `)` — the dividend itself included.
if self.needs_parens(operand, ctx) {
return;
}
(operand, ctx) = match &operand.kind {
ExpressionKind::FunctionExpression(_) | ExpressionKind::ClassExpression(_) => {
self.dividend_tail_targets.mark(operand.span());
return;
}
ExpressionKind::UnaryExpression(unary) => (
unary.argument,
ParenContext::UnaryArgument {
parent_op: unary.operator,
},
),
ExpressionKind::BinaryExpression(binary) => (
binary.rebalanced_right(),
ParenContext::BinaryRight {
parent_op: binary.operator,
},
),
ExpressionKind::TSTypeAssertion(assertion) => {
(assertion.expression, ParenContext::AngleBracketAssertion)
}
_ => return,
};
}
}
/// The pair a position puts around `operand`, whose doc is `inner`. A DECORATED class
/// takes it broken open and indented, as at a statement's start and in a heritage
/// clause: the decorator owns a line of its own either way. The pairs a class operand
/// takes are a dividend's (`(@dec class {}) / 2`) and a dividend tail's
/// ([`Self::mark_dividend_tail`]).
pub(in crate::printer) fn build_operand_pair_doc(
&self,
operand: &Expression<'_>,
inner: DocId,
) -> DocId {
let decorated_class = matches!(
&operand.kind,
ExpressionKind::ClassExpression(c) if class_expr_has_decorators(c)
);
if decorated_class {
self.build_break_open_parens(inner)
} else {
self.d().parens(inner)
}
}
/// Build operand with parens if needed for clarity
pub(in crate::printer) fn build_binary_operand_doc(
&self,
operand: &Expression<'_>,
parent_op: BinaryOperator,
is_right: bool,
) -> DocId {
let ctx = if is_right {
ParenContext::BinaryRight { parent_op }
} else {
ParenContext::BinaryLeft { parent_op }
};
// A nested binary sub-expression takes continuation indent, parenthesized or not
// — prettier's `shouldNotIndent` evaluates to false when the parent is a
// BinaryExpression (none of its conditions match), so the inner chain gets
// `indent(rest)` and its continuation lines sit one level in
// (`(first &&\n\t\tsecond)` not `(first &&\n\tsecond)`; likewise
// `0.5 * a(...) * b(...)` inside `... + 1.0`). No arm of its own says so: that IS
// the default `Printer::build_binary_chain_doc` gives every unexempt position.
//
// One shape for every context. No separate `group(parens(parts))` twin (parens
// inside the group so the fit reserves `)`) for the Standalone parenthesized
// arm: the renderer's lookahead fits (`rest_commands`) measures the `)`
// either way, and the two shapes are output-identical over the full
// fixture corpus, so the mode split retired with the depth-blind
// `is_embedded()` keying.
// A MULTI-LINE block the operand OWNS prints just inside the `(`, outside the
// operand's own group ([`Printer::build_value_with_outermost_owned_comment`]).
// Asked only where the pair is KEPT — see that seam's ⚠️.
if self.needs_parens(operand, ctx) || (is_right && self.is_dividend_tail(operand)) {
let inner = self.build_value_with_outermost_owned_comment(operand, || {
self.build_chain_aware_operand_doc(operand)
});
self.build_operand_pair_doc(operand, inner)
} else {
self.build_chain_aware_operand_doc(operand)
}
}
/// Build a binary operand's doc, routing a curried arrow-chain operand
/// (`cond ?? ((a) => (b) => …)`) through the progressive call-arg/binaryish
/// chain layout. Mirrors prettier's `isBinaryish(parent)` reaching
/// `printArrowFunctionSignatures`. Every curried operand is routed; a head triggering
/// `arrow_chain_should_break` gets the break INSIDE that layout rather than being turned
/// away from it. `should_use_arrow_chain_layout` still gates on every comment sitting in
/// a region the chain doc emits, so an operand whose comment lands in a gap that layout
/// has no emitter for falls through to the default path.
fn build_chain_aware_operand_doc(&self, operand: &Expression<'_>) -> DocId {
if crate::printer::is_curried_arrow_chain(operand) {
self.build_with_arrow_chain_context(
crate::printer::ArrowChainContext::CallArgOrBinaryish,
|| self.build_expression_doc(operand),
)
} else {
self.build_expression_doc(operand)
}
}
/// The operand doc of `await` / `yield`, whose own span covers the operand's `)`: ONE
/// pair, from either of two causes.
///
/// - **The position's** (`ctx`): precedence for `await x + y`, clarity for `yield (a = b)`.
/// A MULTI-LINE block the operand OWNS is claimed beneath it, outside the operand's own
/// group but inside the `(` ([`Printer::build_value_with_outermost_owned_comment`]).
/// - **A retained shell**: a comment in the operand→`)` gap keeps the author's parens even
/// where they were redundant, and stays inside them where the author wrote it. The
/// shell is not optional: the keyword's span covers the `)`, so a stripped form hands
/// the comment to the enclosing terminator gap on reparse. A block comment then lands
/// past the `;` on pass 2, and a `//` merges with whatever already trails that line.
/// Prettier relocates it past the `)` and then past the `;`.
///
/// When both hold, the retained shell IS the position's pair. The claim is keyed on the
/// position's pair alone, since the shell's `(` is re-added around this body either way.
/// The keyword→operand gap's run is the caller's and prints outside the pair.
fn build_keyword_operand_pair_doc(
&self,
operand: &Expression<'_>,
ctx: ParenContext,
shell_end: u32,
build_operand: impl FnOnce() -> DocId,
) -> DocId {
let d = self.d();
let needs_parens = self.needs_parens(operand, ctx);
let inner = if needs_parens {
self.build_value_with_outermost_owned_comment(operand, build_operand)
} else {
build_operand()
};
let operand_end = operand.span().end;
let retained = if self.has_comments_to_emit_between(operand_end, shell_end) {
self.build_paren_operand_comment_doc(operand_end, shell_end, inner, ")")
} else {
None
};
match retained {
Some(shell) => shell,
None if needs_parens => d.parens(inner),
None => inner,
}
}
/// Build a Doc for an await expression
pub(in crate::printer) fn build_await_doc(
&self,
await_expr: &ast::AwaitExpression<'_>,
await_expr_span: Span,
) -> DocId {
let d = self.d();
// Preserve comments from stripped grouping parens: `await (/** @type {T} */ expr)`
let keyword_end = await_expr_span.start + "await".len() as u32;
// `await` argument — an `ancestorNameMap` value position.
self.mark_ternary_extra_indent(await_expr.argument);
let argument_start = await_expr.argument.span().start;
// The `await`→operand value head ([`Printer::value_head_frozen_span`]): an own-line
// directive in the gap freezes the whole operand. Resolved ONCE, above the arms, so
// every route to the operand's doc carries it — the grouping parens `await` needs are
// the PRINTER's and stay outside the slice, exactly as at every other value head.
let frozen = self.value_head_frozen_span(keyword_end, await_expr.argument.span());
// The ordinary dispatch, so a binary operand takes the continuation-indent default
// (`AwaitExpression` is in neither of prettier's exempt lists). The grouping parens
// the operand needs are the PRINTER's and stay outside this doc. A FROZEN operand
// emits the author's bytes, where no layout rule applies.
let operand_doc = || {
frozen.map_or_else(
|| self.build_expression_doc(await_expr.argument),
|frozen| self.build_frozen_expression_doc(await_expr.argument, frozen),
)
};
// A comment in a shell below the pair `await` gives its operand leads the whole
// operand from the keyword gap, ahead of that pair, which is where the reparse reads
// it (`Printer::left_spine_run_claim`): the gap runs to the claimed region's end. So
// does a run the operand's doc would OPEN with on a line of its own, pair or no
// pair: printed by the operand, it lands below `await` flush, where the reparse
// reads it as the keyword gap's and hangs the operand (`await ((// c⏎(a) || d).e)`).
let claim =
self.left_spine_run_claim(await_expr_span, keyword_end, await_expr.argument, || {
self.needs_parens(await_expr.argument, ParenContext::AwaitArgument)
|| self.left_spine_shell_has_own_line_comment(await_expr.argument)
});
let gap_end = claim.map_or(argument_start, |claim| claim.region.end.max(argument_start));
self.with_left_spine_claim(claim, || {
// `await` binds tighter than a binary / ternary operand (`await x + y` is
// `(await x) + y`), so those take the precedence pair; a trailing comment retains a
// redundant one. Mirrors `build_spread_doc`.
let argument_doc = self.build_keyword_operand_pair_doc(
await_expr.argument,
ParenContext::AwaitArgument,
await_expr_span.end,
operand_doc,
);
// The keyword→operand gap, shared with `new`→callee. The run is emitted OUTSIDE
// any parens the operand needs — the gap belongs to the keyword, not to a pair
// the printer must emit — so it answers one way whether or not the operand's
// precedence happens to require one. A comment the author wrote INSIDE those
// parens is glued to the operand and therefore owned, so it never reaches this
// axis and keeps its place (the `grouped_operand_comment` divergence).
//
// The argument hangs whole below the run when the gap hangs
// ([`Printer::keyword_value_hang_doc`]) — `await /* c */⏎fn({…})` stays broken, and
// `await` is not a restricted production, so the break is layout, not ASI (contrast
// `yield`). A resolved freeze always hangs, since an honored directive hangs what
// follows it; the inline arm glues the run to the keyword, where a directive would be
// inert and the freeze lost on the next pass.
if let Some(argument_doc) =
self.keyword_value_hang_doc(keyword_end, gap_end, || argument_doc)
{
return self.build_keyword_hang_doc("await", keyword_end, gap_end, argument_doc);
}
debug_assert!(frozen.is_none(), "an await→operand freeze must hang");
match self.build_inline_comments_between_doc_trailing_space_opt(keyword_end, gap_end) {
Some(run) => d.concat(&[d.text("await "), run, argument_doc]),
None => d.concat(&[d.text("await "), argument_doc]),
}
})
}
/// Build a Doc for a yield expression
pub(in crate::printer) fn build_yield_doc(
&self,
yield_expr: &ast::YieldExpression<'_>,
yield_expr_span: Span,
) -> DocId {
let d = self.d();
let keyword = if yield_expr.delegate {
"yield*"
} else {
"yield"
};
let Some(arg) = yield_expr.argument else {
return d.text(keyword);
};
// The gap starts at the end of the bare `yield`, never past a computed `*`:
// `yield*` is one fixed string in the OUTPUT, but in the source the `*` follows
// whatever the author wrote between the two (`yield/* c */* x`), so measuring the
// gap as `+ "yield*".len()` lands inside that comment and both scans below start
// past it — dropping it. A keyword's own bytes can hold no comment, so its end
// bounds the region exactly; the `*` then sits inside the gap, which is harmless
// because both readers look for comments (and `(`) rather than slicing text.
let keyword_end = yield_expr_span.start + "yield".len() as u32;
// `yield` argument — an `ancestorNameMap` value position.
self.mark_ternary_extra_indent(arg);
let argument_start = arg.span().start;
// A comment that forces the break takes the parenthesized form. `yield` is a
// restricted production (`yield [no LineTerminator here] AssignmentExpression`,
// ECMA-262 §15.5), so without the parens ASI ends the `yield` at the newline and
// the operand becomes a separate expression statement — the `yield` silently
// loses its argument. Same gate and same layout as its `return`/`throw` siblings;
// see `build_hanging_paren_doc` for the shared rule, and
// docs/conformance_prettier_ts_comments.md §Comment relocation for why prettier (whose own
// retention is scoped to those two) diverges here.
let hang = self.argument_hang(yield_expr_span.start, arg);
if matches!(hang, ArgumentHang::Always) {
// Shared with `return`/`throw` (`build_comment_paren_doc`), the three restricted
// productions on one path: a same-line-as-`(` `//` comment trails the `(`, a
// sequence operand renders bare, and a comment before the `)` stays inside. The
// boundary is discarded — `yield` is an expression, so its enclosing statement
// (not this doc) appends the `;`, and `yield_expr_span.end` is the `)`.
let (hanging, _boundary) = self.build_restricted_production_paren_doc(
keyword,
keyword_end,
arg,
yield_expr_span.end,
);
return hanging;
}
let mut parts: DocBuf = smallvec![d.text(keyword), d.text(" ")];
// Every remaining comment is glued to the keyword with the operand after it on
// some line, so the operand is pulled up onto the comment's line rather than
// keeping the author's break — the break would be ASI, not layout.
let leading_comments_opt = self.build_rhs_comments_glued_opt(keyword_end, argument_start);
// An assignment operand takes clarity parens (`yield (x ??= y)`); a trailing
// comment retains a redundant pair.
let argument_doc = self.build_keyword_operand_pair_doc(
arg,
ParenContext::YieldArgument,
yield_expr_span.end,
|| self.build_expression_doc(arg),
);
// The keyword→operand run prints OUTSIDE the operand's pair, as at `await`: the gap
// belongs to the keyword, and the author wrote it ahead of any `(` there. Asking the
// pair of the gap's comment is how `yield /* c */ (a = b)` lost its parens.
let tail = self.prepend_opt(leading_comments_opt, argument_doc);
// The built operand is the authority on ASI, as at `return` / `throw`
// (`Printer::argument_leading_break`): a line it ends ahead of its first token in
// every layout takes the hanging pair, and one it ends only when a group breaks
// takes a pair printed with that break. Gated on page, since a comment the operand
// owns is invisible to the gap's emit axis and still prints there (in the gap's own
// stripped `(` or the operand's left-spine shell).
// A member gap the source shows an own-line comment in hangs only when the operand's
// doc prints that comment on a line of its own (`ArgumentHang`).
let edge = if self.argument_gap_hangs(&hang, tail) {
LeadingBreak::Hard
} else if self.has_comments_on_page_between(keyword_end, argument_start)
|| !left_spine_shell_is_comment_free(self.source, argument_start)
{
self.argument_leading_break(tail)
} else {
LeadingBreak::None
};
match edge {
LeadingBreak::Hard => {
let (hanging, _boundary) = self.build_restricted_production_paren_doc(
keyword,
keyword_end,
arg,
yield_expr_span.end,
);
hanging
}
LeadingBreak::Soft => {
if let ExpressionKind::BinaryExpression(binary) = &arg.kind {
// The binary arm's pair, as at `return`
// (`Printer::build_binary_paren_doc`): the ungrouped chain, grouped
// inside the broken pair and flat in the keyword's group.
//
// TODO: a width window here is not F1: `yield ((/* d */⏎a).b ** Z) ** q`
// with `Z` a run of 68–80 `z`s at a two-tab depth (`yield*`: 67–80; the
// window moves with the indentation) breaks the chain on the
// first pass and prints a different form on the second. `return` / `throw`
// close the same window by gluing the comment's line
// (`DocArena::glue_leading_comment_lines`), which measured worse here, so
// the boundary the `left_spine_shell_comment_nested_pair` fixture pins
// (83 `z`s) sits past the window rather than in it.
let expr_doc = self.prepend_opt(
leading_comments_opt,
self.build_binary_chain_doc_ungrouped(binary, arg.span),
);
let broken = d.concat(&[
d.text("("),
d.indent(d.concat(&[d.softline(), d.group(expr_doc)])),
d.softline(),
d.text(")"),
]);
let pair = d.if_break(broken, expr_doc);
let force_break = d.will_break(expr_doc);
if cfg!(debug_assertions) {
self.debug_assert_argument_edge_holds(if force_break {
d.group_break(pair)
} else {
d.group(pair)
});
}
parts.push(pair);
let inner = d.concat(&parts);
return if force_break {
d.group_break(inner)
} else {
d.group(inner)
};
}
let pair = self.build_asi_soft_pair(tail);
self.debug_assert_argument_edge_holds(pair);
parts.push(pair);
d.concat(&parts)
}
LeadingBreak::None => {
self.debug_assert_argument_edge_holds(tail);
parts.push(tail);
d.concat(&parts)
}
}
}
/// Build a Doc for a sequence expression
///
/// Redundantly-parenthesized operand comments anchored to the sequence's
/// outer *edges* float OUT of the sequence parens, matching prettier's fixed
/// point: a leading comment on the first operand (`((/* c */ x), y)`) is
/// emitted before the opening `(` (`/* c */ (x, y)`) and a trailing comment
/// on the last operand (`(x, (y /* c */))`) after the closing `)`
/// (`(x, y) /* c */`). Each floated comment keeps its source line-treatment —
/// own-line (hardline) when a newline separates it from the operand, inline
/// (space) otherwise. Preserving the line-treatment is what makes the float
/// idempotent even when the sequence is nested inside surrounding comments (a
/// naive always-inline float re-collapses on the second pass).
/// See operand_edge_comment_prettier_divergence.
///
/// Interior operand comments (between two operands) stay stripped + inline on
/// the comma-gap path below and match prettier — see operand_comments.
///
/// This is the statement/throw/call-argument default (the comment floats out).
/// Value positions (return / variable init / assignment RHS / arrow body)
/// instead keep the last operand's trailing comment INSIDE the parens — see
/// [`Self::build_sequence_doc_value`].
///
/// `layout` is the orthogonal [`SeqLayout`] axis — the caller's position decides it,
/// since tsv has no parent pointer to read it from.
pub(in crate::printer) fn build_sequence_doc(
&self,
seq: &ast::SequenceExpression<'_>,
span: Span,
layout: SeqLayout,
) -> DocId {
// Float-out path: the last operand's trailing comment is the caller's job
// (it lives in the stripped grouping-paren gap, outside `span`), so the
// in-sequence trailing scan stops at `span.end`.
self.build_sequence_doc_inner(seq, span, span.end, SeqParens::FloatOut, layout)
}
/// Bare variant: the comma-joined operands **without** the sequence's own
/// wrapping parens (and without the paren-gap edge-comment floats). Used where an
/// enclosing construct already supplies the required grouping parens and owns the
/// edge gaps — a restricted-production (`return` / `throw` / `yield` / `yield*`)
/// argument forced into the hanging `kw (⏎ // c⏎ body⏎)` form by a leading own-line
/// comment ([`Self::build_restricted_production_paren_doc`]). Self-parenthesizing
/// there would double the parens (`return (⏎ (a, b)⏎)`); prettier keeps the sequence
/// bare inside the one pair the comment break already needs.
///
/// [`SeqLayout::Aligned`] is not a choice here: the enclosing construct has already
/// emitted the hanging geometry (its `(`, indent and softlines), so the operands only
/// have to join.
///
/// `trailing_end` bounds the LINE-COMMENT gate alone — bare mode emits nothing past the
/// last operand, so it says how far the region this builder must break for reaches. A
/// caller that prints the trailing run BESIDE the operands (the hanging pair's, which
/// glues it to the last operand) hands `seq.span.end` and takes the break; one that
/// prints it BELOW them, on the shell's own line, hands the position its own gap opens
/// at, so a `//` it will print itself does not also split the run
/// ([`Printer::build_kept_paren_shell_doc`] — the operands settle flat on the second
/// pass either way, so counting it here costs the first pass its fixed point).
pub(in crate::printer) fn build_sequence_doc_bare(
&self,
seq: &ast::SequenceExpression<'_>,
span: Span,
trailing_end: u32,
) -> DocId {
self.build_sequence_doc_inner(seq, span, trailing_end, SeqParens::Bare, SeqLayout::Aligned)
}
/// Value-position variant: a trailing comment on the last operand stays
/// **inside** the parens (`return (a, b /* c */)` / `const x = (a, b // c)`)
/// rather than floating out after `)`. Prettier keeps sequence/assignment
/// trailing comments inside the added parens in value positions (return arg,
/// variable init, assignment RHS) — its #19263 — while floating them out in
/// statement / throw / call-argument positions. Callers in value positions use
/// this; everything else uses [`Self::build_sequence_doc`].
///
/// `trailing_end` is where the stripped grouping `)` sits (the comment between
/// the last operand and it must be kept inside) — the caller finds it because
/// it falls *outside* `seq.span` (the grouping parens aren't part of the node).
pub(in crate::printer) fn build_sequence_doc_value(
&self,
seq: &ast::SequenceExpression<'_>,
span: Span,
trailing_end: u32,
layout: SeqLayout,
) -> DocId {
self.build_sequence_doc_inner(seq, span, trailing_end, SeqParens::KeepInside, layout)
}
/// The doc for sequence operand `i` — the freeze-aware twin of the plain
/// `build_expression_doc`, shared by both sequence layouts so neither spells the
/// dispatch (nor the assignment-paren rule) itself.
///
/// **Rule A** over the operand list: an own-line directive in the gap after the
/// previous operand's comma freezes the FOLLOWING operand over its own node span, and
/// the separating `,` stays parent-owned. The FIRST operand's gap is the sequence's own
/// erased-paren region instead ([`Printer::left_spine_operand_frozen_span`]): a directive
/// the author wrote inside grouping parens the parser dropped sits between the sequence's
/// span start and that operand's, which is inside the node and so invisible to the
/// enclosing value head. A directive ahead of the whole node is that head's question
/// ([`Printer::value_head_frozen_span`]).
///
/// The assignment clarity parens are the printer's, not the author's, so they land
/// OUTSIDE the frozen slice, exactly as an argument's do
/// ([`Printer::build_frozen_arg_doc`]); a nested sequence operand's own grouping parens
/// are re-synthesized by [`Printer::build_frozen_expression_doc`], since slicing the
/// node span alone would drop them and change what the operand means.
fn build_sequence_operand_doc(
&self,
seq: &ast::SequenceExpression<'_>,
span: Span,
i: usize,
) -> DocId {
let d = self.d();
let expr = &seq.expressions[i];
let frozen = if i > 0 {
self.gap_frozen_span(seq.expressions[i - 1].span().end, expr.span())
} else {
self.left_spine_operand_frozen_span(span.start, expr)
};
// A later operand's MULTI-LINE owned comment is claimed here, outside the operand's own
// group: the comma gap's run is emitted on the to-emit axis and never sees it, so claimed
// by the innermost node its hard break explodes an operand prettier keeps flat
// (`(a, // x⏎/* y⏎*/ b ? c : d)`). The first operand's is the run builders' claim
// ([`Printer::build_doc_with_outermost_owned_comment_at`]), which the `Hanging` line-comment
// twin declines on purpose.
let core = frozen.map_or_else(
|| {
if i == 0 {
self.build_expression_doc(expr)
} else {
self.build_expression_doc_claiming_outermost(expr)
}
},
|frozen| self.build_frozen_expression_doc(expr, frozen),
);
if (matches!(expr.kind, ExpressionKind::AssignmentExpression(_))
&& !self.region_withholds_pair(expr))
|| self.relational_sibling_pair_is_kept(expr)
{
d.parens(core)
} else {
core
}
}
/// Shape a comma-joined operand run per [`SeqLayout`] — prettier's three arms in one
/// place, shared by the block-comment path and its line-comment twin so the two cannot
/// answer the geometry differently. The parens (when there are any) sit outside the
/// group, as prettier's do: they are added by the paren machinery, not by
/// `printSequenceExpression`.
///
/// `first_end` is where the FIRST operand's docs end in `inner` (the index of its `,`),
/// and only `Indented` reads it — but it is why that arm cannot be spelled
/// `group(indent(run))`. ⚠️ **An `indent` reaches every line inside it, including the
/// ones an operand breaks ITSELF.** Prettier indents per continuation
/// (`[first, ",", indent([line, next]), …]`), so a first operand that breaks internally
/// — an assignment with a ternary RHS, `((a = b ? c : fn()), …)` — keeps its own lines at
/// the base column. Wrapping the whole run instead pushed them one level in, which
/// `prettier/tests/format/js/sequence-break/break.js` is the standing check for.
pub(in crate::printer) fn build_sequence_layout_doc(
&self,
inner: &[DocId],
first_end: usize,
layout: SeqLayout,
) -> DocId {
let d = self.d();
match layout {
SeqLayout::Aligned => d.group(d.concat(inner)),
SeqLayout::Indented => {
let (first, rest) = inner.split_at(first_end.min(inner.len()));
d.group(d.concat(&[d.concat(first), d.indent(d.concat(rest))]))
}
// The expanding-parens body every other paren pair that drops its content onto
// its own lines already uses — the shape, not a copy of it.
SeqLayout::Hanging => self.build_expanding_parens_body_doc(d.concat(inner)),
}
}
/// Wrap a built operand run in the sequence's own paren ENVELOPE: the first operand's
/// leading-edge comments floated out before `(`, the run, `)`, and the last operand's
/// trailing-edge comments floated out after it.
///
/// The two builders differ only in how they assemble the run — the envelope around it is
/// one question with one answer, so they share this rather than each spelling it. That is
/// the standing hazard in a pair of parallel printers: the halves that look incidental are
/// where they drift.
///
/// `SeqParens::Bare` has no envelope at all: the enclosing construct supplies the parens
/// and owns both edge gaps. `KeepInside` keeps the trailing comments inside the run (the
/// caller has already appended them), so only the float-out mode reaches the epilogue.
fn build_sequence_envelope_doc(
&self,
seq: &ast::SequenceExpression<'_>,
span: Span,
body: DocId,
parens: SeqParens,
) -> DocId {
if matches!(parens, SeqParens::Bare) {
return body;
}
let d = self.d();
let (run_start, run_end) = sequence_operand_run(seq);
let mut parts = DocBuf::with_capacity(4);
// Under its own left-spine claim the envelope floats the whole claimed region;
// under an enclosing one it floats only what lies past that region, which the
// enclosing claimer prints.
let (float_start, float_end) = match self.claimed_left_spine_run.get() {
Some(claim) if claim.claimer == span => (span.start, claim.region.end.max(run_start)),
Some(claim) if claim.region.start <= span.start && span.start < claim.region.end => {
(claim.region.end, run_start)
}
_ => (span.start, run_start),
};
if float_start < float_end {
self.append_floated_leading_comments(&mut parts, float_start, float_end);
}
parts.push(d.text("("));
parts.push(body);
parts.push(d.text(")"));
if !matches!(parens, SeqParens::KeepInside) {
// Same-line block comments stay inline (`(x, y) /* c */`); own-line block
// comments defer via `line_suffix` (`append_trailing_paren_comments`) so they
// land past the enclosing comma/semicolon — where they re-parse to, keeping the
// float idempotent.
self.append_trailing_paren_comments(&mut parts, run_end, span.end);
}
d.concat(&parts)
}
fn build_sequence_doc_inner(
&self,
seq: &ast::SequenceExpression<'_>,
seq_span: Span,
trailing_end: u32,
parens: SeqParens,
layout: SeqLayout,
) -> DocId {
// The envelope is a pair the first operand prints inside, so a comment in a shell
// on that operand's left spine leads the whole sequence, floated out ahead of the
// `(` with the edge (`Printer::left_spine_run_claim`). `Bare` prints no envelope:
// the enclosing construct owns the edge.
let claim = match (parens, seq.expressions.first()) {
(SeqParens::Bare, _) | (_, None) => None,
(_, Some(first)) => self.left_spine_run_claim(seq_span, seq_span.start, first, || true),
};
self.with_left_spine_claim(claim, || {
self.build_sequence_doc_claimed(seq, seq_span, trailing_end, parens, layout)
})
}
/// [`Self::build_sequence_doc_inner`] under its left-spine claim.
fn build_sequence_doc_claimed(
&self,
seq: &ast::SequenceExpression<'_>,
seq_span: Span,
trailing_end: u32,
parens: SeqParens,
layout: SeqLayout,
) -> DocId {
self.mark_relational_sibling_authored_pairs(seq.expressions.iter().copied().map(Some));
let keep_trailing_inside = matches!(parens, SeqParens::KeepInside);
// Where this builder's comment scans open: the FIRST OPERAND, never `seq_span.start`.
// The leading edge between the two — the grouping `(` the parser stripped from that
// operand, and any comment inside it — is emitted by someone else in every mode:
// the envelope floats it out ahead of the pair it prints
// (`append_floated_leading_comments`, the self-parenthesizing modes), and in `Bare`
// mode the enclosing construct supplies the pair and emits the edge ahead of the
// operand run (`Printer::build_restricted_production_paren_doc`). So a `//` on that
// edge must not force the operands apart for a comment this builder never prints:
// `(⏎// c⏎a), b;` floated the comment out and still broke the operands, which the
// reparse — the comment now ahead of the statement — printed flat (F1);
// `return (⏎// c⏎a), b` settles on `a, b` the same way.
let (interior_start, last_operand_end) = sequence_operand_run(seq);
// A claimed left-spine region is the envelope's to float — or the enclosing
// claimer's to print — so it no more breaks the operands than the edge does.
let scan_start = match self.claimed_left_spine_run.get() {
Some(claim)
if claim.claimer == seq_span
|| (claim.region.start <= interior_start
&& interior_start < claim.region.end) =>
{
claim.region.end.max(interior_start)
}
_ => interior_start,
};
// Line comments anywhere up to `trailing_end` (incl. the last operand's
// trailing comment, which lives outside `seq_span` in value positions) need
// break handling so the comment isn't swallowed by the following comma/operand
// or the closing `)`.
// Axis-free: the rule looks only at LINE comments, and ownership binds only a block
// comment (`owned ⇒ is_block`), so skipping and counting give the same answer.
// An honored directive routes here too, whatever its spelling: the flat path below
// emits an inter-operand comment inline before its operand, which would glue an
// own-line block directive onto the operand's line — an inert placement, so the
// freeze would be lost on the second pass.
if self
.comments_to_emit_between(scan_start, trailing_end)
.any(|c| !c.is_block || self.is_honored_directive(c))
{
return self.build_sequence_doc_with_line_comments(
seq,
seq_span,
trailing_end,
parens,
layout,
);
}
// A MULTI-LINE block the FIRST operand OWNS prints ahead of the run, outside the
// layout's group — inside the envelope's `(`, where the author wrote it. Left
// inside, its body forces the layout group and the operands break one per line
// where prettier keeps them on one. The **span-keyed** form
// ([`Printer::build_doc_with_outermost_owned_comment_at`]): the pair here is the
// sequence's OWN envelope, so there is no enclosing operand node to key on.
//
// ⚠️ Below the line-comment branch on purpose, and the twin takes the SAME claim over
// its own run ([`Self::build_sequence_doc_with_line_comments`]) — one claim per run
// builder, never one above the branch for both. The two build different runs, and a
// claim hoisted above the branch would suppress the first operand's own print for a
// run this closure never assembles.
let body = self.build_doc_with_outermost_owned_comment_at(
seq_span.start,
seq.expressions.first().copied(),
|| {
self.build_sequence_operand_run_doc(
seq,
seq_span,
interior_start,
last_operand_end,
trailing_end,
keep_trailing_inside,
layout,
)
},
);
self.build_sequence_envelope_doc(seq, seq_span, body, parens)
}
/// The comma-joined operand RUN a sequence's block-comment path builds — the layout's
/// group, without the paren envelope around it ([`Self::build_sequence_envelope_doc`]).
///
/// Extracted so the owned-comment claim at the call site wraps a named call: the claim
/// has to cover the whole run build (its suppression is what stops the first operand
/// printing the comment itself), and inlining the loop into that closure buried it two
/// levels in. The line-comment twin assembles its own run and takes no claim.
#[expect(clippy::too_many_arguments)] // the run's four source bounds travel beside its span
fn build_sequence_operand_run_doc(
&self,
seq: &ast::SequenceExpression<'_>,
span: Span,
interior_start: u32,
last_operand_end: u32,
trailing_end: u32,
keep_trailing_inside: bool,
layout: SeqLayout,
) -> DocId {
let d = self.d();
let n = seq.expressions.len();
// The comma-joined operand run the layout shapes; the parens and the floated edge
// comments around it are the envelope's (`build_sequence_envelope_doc`).
let mut inner = DocBuf::with_capacity(n * 3);
// Whole-sequence comment gate: the inter-operand gaps (after/before each comma)
// all lie within `span`, so with no comment there, every per-operand gap is
// empty. Skip the per-operand comma scans + the `empty()` comment children on the
// comment-free common path. Byte-identical (the line-comment path already branched
// off above, so a present comment here is a block, handled by the full path).
let seq_has_comments = self.has_comments_to_emit_between(interior_start, span.end);
// Where the first operand's docs end — the continuation indent starts here, never
// at the run's own start (see `build_sequence_layout_doc`).
let mut first_end = 0;
for (i, expr) in seq.expressions.iter().enumerate() {
let is_last = i + 1 == n;
let expr_start = expr.span().start;
let expr_end = expr.span().end;
if i > 0 {
if i == 1 {
first_end = inner.len();
}
// `,` + `line`, prettier's `join([",", line], …)`: the operands break one per
// line once they no longer fit, which is what keeps a sequence inside the
// print width. A flat `", "` here made every sequence unbreakable.
inner.push(d.text(","));
inner.push(d.line());
// Leading comments of this operand: the gap after the previous comma.
// Redundant operand parens are stripped, so a comment the user wrote
// inside them (`(/* c */ b)`) is preserved inline before the operand.
if seq_has_comments {
let prev_end = seq.expressions[i - 1].span().end;
if let Some(comma) = self.find_comma_after(prev_end) {
inner.push(self.build_comments_between(
comma + 1,
expr_start,
CommentSpacing::Trailing,
));
}
}
}
inner.push(self.build_sequence_operand_doc(seq, span, i));
// Trailing comments of this operand: the gap before the next comma.
if seq_has_comments
&& !is_last
&& let Some(comma) = self.find_comma_after(expr_end)
{
inner.push(self.build_comments_between(expr_end, comma, CommentSpacing::Leading));
}
}
if keep_trailing_inside {
// Value position: a same-line block comment stays INSIDE before `)`
// (`(a, b /* c */)`). Block-only path, so the comments are blocks. The
// comment lives between the last operand and the grouping `)`
// (`trailing_end`), outside `span`. Inside `inner`, so a hanging layout's
// closing softline lands after it rather than between operand and comment.
// Same emitter as the line-comment builder's arm, not a second spelling of it:
// [`Printer::build_trailing_comment_doc`]'s block branch IS the `" "` + comment
// pair this used to hand-roll, and a hand-rolled copy is how the two arms of one
// rule come to disagree.
for comment in self.comments_to_emit_between(last_operand_end, trailing_end) {
inner.push(self.build_trailing_comment_doc(comment));
}
}
self.build_sequence_layout_doc(&inner, first_end, layout)
}
/// Emit the first operand's leading-edge comments, floated out before the
/// sequence's opening `(`, preserving each comment's source line-treatment:
/// own-line (a newline before the operand) → hardline, inline → space. The
/// spacing follows each comment and is sized by the gap to the next token (the
/// following comment, else the operand at `operand_start`). On re-parse these
/// land in the enclosing context's leading-comment domain, which emits the
/// same own-line/inline treatment — so the float is idempotent.
fn append_floated_leading_comments(&self, parts: &mut DocBuf, start: u32, operand_start: u32) {
let d = self.d();
let comments: CommentVec<'_> = self
.comments_to_emit_between(start, operand_start)
.collect();
for (i, comment) in comments.iter().enumerate() {
parts.push(self.build_comment_doc(comment));
let next = comments.get(i + 1).map_or(operand_start, |c| c.span.start);
// Comment-adjacency read (real even in canonical mode): a line comment
// always has a newline before the next token, and gluing content after
// its inline emission would swallow it.
if self.comment_has_newline_between(comment.span.end, next) {
parts.push(d.hardline());
} else {
parts.push(d.text(" "));
}
}
}
/// Sequence layout used when the sequence contains a line comment, which forces
/// a multiline break so the comment isn't swallowed by the following comma or
/// operand. Mirrors prettier's `group(join([",", line], parts))`: each comma gap
/// is partitioned by line — a comment with no newline before it *trails* the
/// preceding operand (a same-line block stays inline before the comma; a line
/// comment defers past the comma via `line_suffix`, rendering at end-of-line);
/// an own-line comment *leads* the next operand on its own line. A `break_parent`
/// forces the group (and any enclosing call/arg group) to break.
///
/// The outer-edge comments — leading on the first operand, trailing on the last —
/// still float OUT of the parens via the same helpers as the block-comment path
/// (`append_floated_leading_comments` / `append_trailing_paren_comments`).
///
/// The geometry is the caller's [`SeqLayout`], the same axis the block path takes — a
/// forced break doesn't change WHICH layout the position is, only that it happens. Under
/// `Aligned` a deferred trailing `//` rides its `line_suffix` past the `)` and the
/// enclosing `;` (`(aa,⏎bb); // c`); under `Hanging` the closing softline flushes it
/// inside (`return (⏎aa,⏎bb // c⏎);`) — both prettier's, from one rule.
/// Where an operand gap's same-line TRAILING run ENDS: the start of the first comment
/// that begins its own line, or `next_start` when every comment in the gap is glued to
/// what precedes it.
///
/// **One position, read by both sides of the split** — the previous operand's trailing
/// emitter takes `[gap_start, here)` and the next operand's leading emitter takes
/// `[here, next_start)`, so together they PARTITION the gap. The two ran ~40 lines and
/// one loop iteration apart, each walking the same comments and re-deriving the same
/// boundary; that is the shape an unclaimed comment (a DROP) or a doubly-claimed one (a
/// DOUBLE-PRINT) comes out of (`docs/comments.md` §The element-comma seam). Stating it
/// once is what makes the partition structural.
///
/// Own-line-ness is a SOURCE question asked against the PREVIOUS COMMENT's end, never
/// the operand's, so a glued chain (`a /* c1 */ /* c2 */, b`) stays one run. The claim
/// is a prefix by construction: comments are disjoint and ordered, so once one begins
/// its own line every later one in the gap does too.
///
/// Not [`Self::trailing_claim_end`], whose rule looks FORWARD (does this comment's glue
/// chain reach the next item's line?) — the statement-gap seam's question, and a
/// different answer on the same bytes.
fn sequence_gap_trailing_end(&self, gap_start: u32, next_start: u32) -> u32 {
let mut pos = gap_start;
for comment in self.comments_to_emit_between(gap_start, next_start) {
// Comment-adjacency read (real even in canonical mode).
if self.comment_has_newline_between(pos, comment.span.start) {
return comment.span.start;
}
pos = comment.span.end;
}
next_start
}
fn build_sequence_doc_with_line_comments(
&self,
seq: &ast::SequenceExpression<'_>,
span: Span,
trailing_end: u32,
parens: SeqParens,
layout: SeqLayout,
) -> DocId {
// The claim the block path takes, at the twin that shares the question
// ([`Printer::build_doc_with_outermost_owned_comment_at`]). Without it the twin
// leaves a multi-line owned block inside the layout group and breaks the operands,
// while the NEXT pass — the `//` now deferred past the `)`, so the block path runs
// — keeps them flat: an F1 wobble at a fixed point neither formatter has (prettier
// settles flat too). The twin prepends nothing of its own, so the claim has to wrap
// its whole run build exactly as the block path's does.
//
// ⚠️ **Not under [`SeqLayout::Hanging`]**, whose run has no group of its own: the
// layout IS the expanding-parens body ([`Printer::build_expanding_parens_body_doc`]),
// so the claim would lift the comment out of the `(`-softline and weld it to the `(`
// line, where prettier puts it one indent in — and the next pass, reading it from
// there, lays the operands out differently again. The break this claim exists to
// prevent is not reachable there either: with no operand group to force, the body's
// own break is the layout's to make.
let body = if matches!(layout, SeqLayout::Hanging) {
self.build_sequence_line_comment_run_doc(seq, span, trailing_end, parens, layout)
} else {
self.build_doc_with_outermost_owned_comment_at(
span.start,
seq.expressions.first().copied(),
|| {
self.build_sequence_line_comment_run_doc(
seq,
span,
trailing_end,
parens,
layout,
)
},
)
};
self.build_sequence_envelope_doc(seq, span, body, parens)
}
/// The comma-joined operand RUN the line-comment twin builds — the layout's group,
/// without the paren envelope ([`Self::build_sequence_envelope_doc`]).
///
/// Split from its caller for the same reason the block path's run is
/// ([`Self::build_sequence_operand_run_doc`]): the owned-comment claim has to cover the
/// whole build, and the two paths must answer that the same way.
fn build_sequence_line_comment_run_doc(
&self,
seq: &ast::SequenceExpression<'_>,
span: Span,
trailing_end: u32,
parens: SeqParens,
layout: SeqLayout,
) -> DocId {
let keep_trailing_inside = matches!(parens, SeqParens::KeepInside);
let d = self.d();
let n = seq.expressions.len();
// Where the run's own break is an OBLIGATION and where it is only the enclosing
// construct's. A `//` in an operand GAP has to force the operands apart — it flushes
// at that gap's comma, and welding the next operand onto its line would swallow it.
// A `//` in the LAST operand's trailing gap has no such claim: it runs to
// end-of-line, so no layout puts it between two operands, and every line it can
// render on lies past the `)`. Splitting the run for it is a break the reparse cannot
// reproduce (the comment is gone from the parens by then), which is exactly the
// `format ∘ format ≠ format` the flush-scoped break exists to avoid — so that case
// states its break as [`DocArena::flush_break`] below, forcing only the group that
// owns the next line opportunity AFTER the comment. An enclosing construct that must
// break still does (a call argument keeps the comment on its own last line, a
// `Hanging` closer takes it inside the parens); the operand run, with no line left
// before the `)`, stays flat and reaches the shared fixed point in ONE pass.
// Prettier splits here iff its comment ATTACHMENT lands inside the sequence — on the
// last operand where the pair is the statement's own, on the sequence where a token
// of the enclosing expression follows the `)` — a distinction the detached model has
// no read on, so the one answer costs a first-pass match on the bare spelling
// (`conformance_prettier_ts.md` §TypeScript, Sequence trailing line-comment
// convergence). The separators stay `line`s either way, so width still breaks the run.
let (run_start, run_end) = sequence_operand_run(seq);
let trailing_run_only = keep_trailing_inside
&& !self.has_line_comments_between(run_start, run_end)
&& self.has_line_comments_between(run_end, trailing_end);
// Build per-operand docs (own-line leading + core + same-line trailing), joined by
// `,` + line inside a group — forced to break by the flag here, or, in the
// trailing-run-only case, left to the flush below and to width. `first_end` is where
// the first operand's docs end, for the layout's continuation indent.
let mut inner: DocBuf = if trailing_run_only {
smallvec![]
} else {
smallvec![d.break_parent()]
};
let mut first_end = 0;
for (i, expr) in seq.expressions.iter().enumerate() {
let is_last = i + 1 == n;
let expr_start = expr.span().start;
let expr_end = expr.span().end;
let mut od = DocBuf::new();
// The previous comma gap, split at the one position both of its emitters read
// ([`Self::sequence_gap_trailing_end`]): its same-line prefix trailed the
// previous operand and was emitted there, and everything from the first
// own-line comment on leads THIS operand.
let prev_gap = (i > 0).then(|| {
let prev_end = seq.expressions[i - 1].span().end;
(
prev_end,
self.sequence_gap_trailing_end(prev_end, expr_start),
)
});
if let Some((_, own_line_start)) = prev_gap {
for comment in self.comments_to_emit_between(own_line_start, expr_start) {
od.push(self.build_comment_doc(comment));
od.push(d.hardline());
}
}
od.push(self.build_sequence_operand_doc(seq, span, i));
// Same-line comments in the next comma gap trail this operand: a block
// stays inline before the comma; a line comment defers via `line_suffix`
// so it renders after the comma at end-of-line. The rest of that gap leads
// the next operand, which claims it from the same split position.
if !is_last {
let next_start = seq.expressions[i + 1].span().start;
let own_line_start = self.sequence_gap_trailing_end(expr_end, next_start);
for comment in self.comments_to_emit_between(expr_end, own_line_start) {
od.push(self.build_trailing_comment_doc(comment));
}
} else if keep_trailing_inside {
// Value position: the last operand's trailing comment trails the operand — a
// block inline, a line comment via `line_suffix`. Whether the `//` lands
// inside the parens is then the LAYOUT's answer, not this branch's: a
// `Hanging` closing softline flushes it inside (`b // c` then `)` on its own
// line), an `Aligned` one has no break left before `)`, so it rides out past
// the `);` — which is prettier's split too. The comment lives up to the
// grouping `)` (`trailing_end`), outside `span`.
for comment in self.comments_to_emit_between(expr_end, trailing_end) {
od.push(self.build_trailing_comment_doc(comment));
}
// The run's flush-scoped break, standing where the `break_parent` above would
// have (see the gate). It sits AFTER the run so the operand separators are
// measured before it is armed — that ordering is the whole rule: those lines
// stay flat, and only a line opportunity past the comment is forced.
if trailing_run_only {
od.push(d.flush_break());
}
}
if let Some((prev_end, own_line_start)) = prev_gap {
if i == 1 {
first_end = inner.len();
}
inner.push(d.text(","));
// The separator is where the previous operand's deferred trailing run
// FLUSHES, so a `//` in that run makes this break an obligation rather than
// a break point ([`Printer::obligated_break`], which carries the rule). The
// `break_parent` above states this run's geometry as a group FLAG, and
// `remove_lines` drops it — leaving the deferred `//` to ride past every
// remaining operand, the `)`, and the whitespace-sensitive element the head
// sits in (`{#each (a // c⏎, b) as x}` inside a `<pre>`, where the comment
// lands after `</pre>` as page text). The question is asked of the gap this
// separator sits in, over exactly the run that gap's trailing emitter
// claimed — never carried across an iteration from where it was emitted.
let flushes_line_comment = self.has_line_comments_between(prev_end, own_line_start);
inner.push(self.obligated_break(flushes_line_comment, d.line()));
}
inner.push(d.concat(&od));
}
self.build_sequence_layout_doc(&inner, first_end, layout)
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Run `should_group_binary_continuation` on a parsed binary expression.
fn group(src: &str) -> bool {
let arena = bumpalo::Bump::new();
let expr = crate::__private::parse_expression_with_comments(
src,
0,
0,
&arena,
"end of input",
|_| Ok(()),
)
.expect("expression should parse")
.0;
match expr {
Expression {
kind: ExpressionKind::BinaryExpression(b),
..
} => Printer::should_group_binary_continuation(b),
other => panic!("expected a binary expression, got: {other:?}"),
}
}
#[test]
fn should_group_binary_continuation_by_category() {
// A logical operand under an arithmetic parent — categories differ, so the
// continuation gets its own group.
assert!(group("(a && b) + c"));
assert!(group("(a && b) * c"));
// Flattened same-category chains do NOT group (the left is the same category).
assert!(!group("a && b && c"));
assert!(!group("a * b * c"));
}
}