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
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
// Comment handling for TypeScript printer
//
// This module handles all comment-related operations:
// - Building Doc representations for comments
// - Printing comments directly to buffer
// - Finding and filtering comments in ranges
// - Handling leading/trailing/inline comments
//
// ## Module Organization
//
// - **mod.rs** (this file): The `CommentSpacing` / `CommentFilter` enums and the
// generic comment-emission primitives every other module builds on.
// - **render.rs**: Single-comment text-layout leaves (block-comment framing,
// indentable / preserved block comments, trailing line/block comment docs).
// - **paren.rs**: Stripped-grouping-paren comment handling (promotion across `=`
// / operators, trailing-paren comment preservation, removed-paren prepends).
// - **owned.rs**: The comment/paren binding seam — a comment glued to the token
// after it is printed by the node that token begins, so a synthesized paren
// can't land between the two (`Comment::owned_by_node`).
// - **scan.rs**: Pure source span-math helpers (comma/angle/blank-line scanning).
// - **declarations.rs**: Member-keyword / modifier-marker / marker→colon /
// heritage / keyword→name comment emitters.
// - **lists.rs**: List- and body-level comment emitters (leading/trailing body
// comments, delimiter-line prefixes, empty-container comments, comma emission).
// - **member_body.rs**: The `{ … }` member-body walk shared by the class body, the
// interface body and both type-literal force-multiline walks — the member analog
// of `build_statement_list_docs_into`.
// - **element_comma.rs**: The single source of the `trailingComma: 'none'`
// comment-position contract for inline element lists (block-before / comma /
// block-after-on-last / line-suffix), shared by the object/array pattern and
// object-literal builders.
mod declarations;
mod element_comma;
mod lists;
mod member_body;
mod owned;
pub(crate) use owned::ValueGap;
mod paren;
mod render;
mod scan;
pub(in crate::printer) use scan::ends_no_trivia;
pub(crate) use declarations::{ClassMemberModifiers, ContinuationValue, HeritageKeyword};
pub(super) use element_comma::{block_is_before_comma, next_real_element_start, run_defers_line};
pub(crate) use lists::{BlankRule, MemberGap, StandaloneGlue};
pub(crate) use member_body::{MemberBlankScan, MemberBody, MemberFloor, MemberFreeze, MemberSeam};
pub(in crate::printer) use paren::printed_left_spine_step;
pub(crate) use paren::{
AsiOperandShell, LeftSpineClaim, ParenLeadingValue, next_significant_byte,
paren_pair_keeps_leading_run, paren_shell_close_after,
};
pub(crate) use paren::{LeftSpineStep, leading_regex_start, left_spine_shell_is_comment_free};
// Re-export for submodules to use `super::X` instead of `super::super::X`.
pub(super) use super::{Printer, calls};
use smallvec::SmallVec;
use tsv_lang::Comment;
use tsv_lang::doc::DocBuf;
use tsv_lang::doc::arena::DocId;
use tsv_lang::printing;
use tsv_lang::source_scan::has_newline_after_position;
/// Small stack-allocated vector of comment references. Inline capacity 8 keeps
/// the common comment gaps off the heap: 0–2 comments are the bulk, and a
/// stacked `//` block (3–8 lines, common in documented code) still fits inline;
/// comment-dense corpora put the p99 statement-gap run at 7 (`cargo run -p
/// tsv_debug --features _internal_buffer_stats buffer_sizes` — the histogram source for
/// this `N`). A larger run spills to a single heap alloc — exactly what a
/// `Vec` would do.
pub(crate) type CommentVec<'a> = SmallVec<[&'a Comment; 8]>;
/// Spacing style for comments in doc building
#[derive(Debug, Clone, Copy)]
pub(crate) enum CommentSpacing {
/// Space before comment: ` /* c */`
Leading,
/// Space after comment: `/* c */ `
Trailing,
/// No spacing around the run: `/* c */`.
///
/// ⚠️ This governs the run's **outer** edges only — comments *within* the run are
/// still separated, or a multi-comment run fuses into `/* a *//* b */`. The caller
/// picks `None` because it has already placed the anchor's space itself.
None,
}
impl CommentSpacing {
/// `Trailing` when followed by type params (`/* c */ <T>`),
/// `Leading` when followed by parens (` /* c */()`).
pub(crate) fn for_type_params(has_type_params: bool) -> Self {
if has_type_params {
Self::Trailing
} else {
Self::Leading
}
}
}
/// Filter for which comment types to include
#[derive(Debug, Clone, Copy)]
pub(crate) enum CommentFilter {
/// Include all comments (block and line)
All,
/// Only include block comments (/* */)
BlockOnly,
}
/// Whether an author blank line **above a comment run's first comment** survives.
///
/// The Trailing-vs-Leading question, at the one emitter two kinds of run share
/// ([`Printer::push_anchored_trailing_run`]). A **trailing** run's blank sits between the
/// value and the comment and prettier's `printTrailingComment` emits it
/// (`isPreviousLineEmpty`); a **leading** run's sits between the introducer — an operator,
/// a keyword, a delimiter — and the comment, where `printLeadingComment` has no emitter
/// for it at all, so it is dropped. tsv drops it at every other leading gap (a value's
/// `=`, an object `:`, a call argument, an array element, a `return` operand); the binary
/// operator→operand gap kept it only because it borrows the trailing emitter for its
/// anchor rule.
///
/// A blank *between* two comments is a different question, never this one: it separates
/// two distinct remarks and survives in both kinds of run.
///
/// The same split the control-flow gaps name as `GapCommentRun::blank_seed`
/// (`Some`/`None`) — one concept, stated per emitter because the two emitters take
/// different shapes of it.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum RunLeadingBlank {
/// A trailing run: the blank is the author's, between the value and its comment.
Keep,
/// A leading run: the blank sits above the run, where prettier cannot emit one.
Drop,
}
/// Who emits a retained paren SHELL's leading **line**-comment run — the one axis
/// [`Printer::push_paren_shell_leading_run`] and its callers part on.
///
/// An ownership question, so it is a named axis rather than a bare `bool`: a region two
/// emitters each believe the other owns is a DROP or a DOUBLE-PRINT, and neither is
/// visible to the print-once ledger when the two spellings sit in different modules
/// ([`comments.md`](../../../../docs/comments.md) hazards 1 and 3). One type means a call
/// site states which emitter owns the run in the same vocabulary the emitter reads it in.
///
/// The **block** arm is not on this axis: a block comment renders inline inside the shell
/// wherever the run is emitted, so every value emits it and only the `//` is claimed. What
/// the block arm reads instead is [`ShellPair`] — whether the parens it would render
/// inside survive at all.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum ShellLeadingRun {
/// An upstream emitter already placed the run — the union's own member path
/// ([`Printer::build_union_type_doc_with_line_comments`]) and the conditional check /
/// `extends` callers (the stripped-shell relocation). Emitting here would double-print
/// it.
///
/// ⚠️ **Scope**: the axis reaches the parenthesized-**union** rendering only. The
/// required pair around every other operand kind opens over its own leading run
/// without consulting it, because the licence is granted on an upstream emitter
/// EXISTING — true of an intersection's FIRST member and of a redundant member, false
/// of every later retained one, where reading the flag left the run welded to the `(`
/// ([`Printer::build_type_doc_maybe_parens_impl`]).
Upstream,
/// This site is the run's only emitter, so it renders inside the pair. True of an
/// optional tuple element, which has no upstream at all: its run was simply DROPPED.
Here,
}
/// Whether the paren pair a shell's leading run ends up inside SURVIVES in the output.
///
/// One fact, read at the run's two decision points, which is why it is one type:
///
/// - the **emitter** ([`Printer::push_paren_shell_leading_run`]) asks it of the pair it is
/// printing into — a block the author isolated on its own line keeps that line inside a
/// pair that stays (prettier keeps it there too), and takes the space inside one that is
/// stripped, where the line is not the shell's to keep;
/// - the **claim** ([`Printer::keyword_value_stripped_paren_hang_with_pair`]) asks it of
/// the POSITION, which is the only thing that knows whether it re-mints a pair around
/// the stripped inner. Where it does, the run stays inside that pair; where it does not,
/// the enclosing gap owns the run — and the indent, which is the enclosing construct's
/// fact and never the shell's.
///
/// Not a bare `bool`: the two readings are claims about whose rule applies, and the wrong
/// one is an F1 break rather than a visible mistake. ⚠️ Claiming inside a pair that SURVIVES
/// is the F1 break in question — a surviving pair's OPEN form puts a run the author glued to
/// the `(` on its own line, so pass 2 reads its own output as the isolated authoring and
/// hoists what pass 1 kept. Own-line-ness is the one break-forcing property a LAYOUT can
/// create, so a rule keyed on it holds only where its own output reproduces its trigger.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum ShellPair {
/// The parens are printed around this run — an authored shell the trailing-run rule
/// retains, a pair the position requires or re-mints, or the parenthesized-union /
/// aligned-object renderings that print their own `(`.
Kept,
/// The parens are dropped and the run lands in the enclosing gap, which either claims
/// it or lays the value out by its own rule — and several of those gaps deliberately
/// COLLAPSE an isolated own-line block (the annotation `:` for a non-composite type,
/// the prefix operators, a conditional's `extends`), so this arm takes the space at
/// every single-line block. A broke-after run holding a MULTI-LINE block — glued tail
/// and all — is the one run it does not space: every separator the author broke takes
/// the soft `line` ([`Printer::broke_after_run_holds_multiline_block`]), which the
/// multi-line body's own break opens in the enclosing gap's group, so the value drops
/// below the run as the shell-free authoring's does. Only that half of
/// [`Printer::block_run_forces_break`]: the whole function is the CLAIM's reading, and
/// an isolated own-line block is claimed precisely so the gap that collapses it does
/// the collapsing.
///
/// ⚠️ **The MEMBER gaps are not among them, and that list is the whole of the set.** A
/// union or intersection member's stripped shell puts its run at the member gap, which
/// keeps the author's line — the seams `Printer::intersection_member_shell_run` and
/// `Printer::member_shell_own_line_run_region` own, both emitting through
/// [`Printer::push_leading_comment_run`]. So this arm is reached for a member only with
/// a run that does NOT keep a line of its own, where the collapse is the right answer
/// by construction. Read as the universal stripped answer, it collapsed an own-line
/// block after the `&` and a union's `| ` alike.
Stripped,
}
/// How a leading-comment run decides whether a *block* comment hugs the token
/// that follows it (a trailing space, `/* c */ X`) rather than dropping to its
/// own line. The rest of the run is identical across sites — one
/// `build_comment_doc` per comment, and a `line`/`hardline` toward the next
/// comment (or the terminal) for every comment that doesn't hug — so only this
/// glue test varies, and [`push_leading_comment_run`](Printer::push_leading_comment_run)
/// takes it as a mode.
#[derive(Debug, Clone, Copy)]
pub(crate) enum LeadingGlue {
/// A block hugs when it shares a source line with whatever follows it — the
/// *next* comment, or the terminal for the last one. Prettier's rule: its
/// `printLeadingComment` reads only the source right after the comment's `*/`
/// (`hasNewline(text, locEnd(comment))`), never where the terminal starts, so
/// a run the author glued together stays glued (`/* a */ /* b */⏎X` keeps the
/// pair on one line and breaks before `X`).
Adjacent,
/// `Adjacent`, plus a single-line block glued to the operator hugs the value
/// across a source newline — prettier's assignment/call pull-up
/// ([`build_rhs_comments_glued_opt`](Printer::build_rhs_comments_glued_opt)).
AdjacentGlued,
/// `Adjacent`, plus a block whose only company on its line is a grouping paren the
/// printer STRIPPED hugs across the newline that paren left behind — the nested
/// JSDoc-cast shape `/** @type {A} */ (⏎/** @type {B} */ (expr))`, where the author
/// glued the comment to a `(` that is not in the output, so
/// [`Printer::comment_hugs_next`] alone reads the pair as broken and splits it.
/// The call-argument leading run's mode, since paren stripping is what puts two casts
/// in one gap.
AdjacentStrippedParen,
/// `Adjacent`, but an author **blank** line after a glued block's `*/` does not
/// force the comment onto its own line — it yields with the soft `line` like a
/// plain newline. The **value-gap** mode: the gap between a head (`=`, `:`, `as`,
/// a keyword) and the value it introduces.
///
/// The distinction is which break the blank belongs to. A glued block does not run
/// to end-of-line, so nothing forces the value down and the break after `*/` is the
/// author's — which tsv reflows at every value position
/// ([conformance_prettier.md](../../../../docs/conformance_prettier.md) §Authored
/// breaks in value position). A blank line is a property of a line break: collapse
/// the break and there are no longer two lines for it to separate, so the blank
/// yields with it. `Adjacent` keeps the opposite rule because *its* blank separates
/// two list items, which is ordinary authoring tsv preserves (the
/// `arrays/end_of_line_block_comment` divergence fixture pins it).
///
/// Without this split the two families disagreed on whitespace *quantity*: at a
/// value gap one newline collapsed and two hung, while ~20 peer gaps on
/// `AdjacentGlued` collapsed both. An author who wants the blank kept writes the
/// comment on its own line, where the break IS forced and it survives.
AdjacentValueGap,
/// `Adjacent`, but the run's **first** comment is emitted on the anchor's line by
/// construction (the caller pushes a bare `" "` ahead of it, never a `line`), so the
/// newline the author wrote *before* it is erased by this emitter's own output.
/// Reading `is_own_line_comment` there is
/// [rule-conditioned-on-what-its-own-output-destroys][docs]: it forces a `hardline`
/// for a lone own-line block (`if (a)⏎/* c */⏎;`) that the very next pass collapses,
/// because by then the comment sits on the `)` line — **non-idempotent**, and prettier
/// needs two passes for exactly that reason. So the first comment answers "own-line"
/// as **false** and takes the soft `line`, landing on prettier's fixed point in one
/// pass; every later comment keeps the source reading, since the separator that put it
/// on its own line reproduces it.
///
/// The **empty-statement body** gap's mode (`) ;` / `else ;`), the one site that pins
/// its run to the anchor's line.
///
/// [docs]: ../../../../../docs/comments.md
AdjacentAnchorLine,
/// `Adjacent`, but the run's **first** comment opens a line of its own by
/// construction (the caller pushes a hard break ahead of it): a chain member gap's
/// own-line run, which the gap's trail scan sends there past a `//` that ended the
/// line even where the author wrote it behind a `)` (`a // c⏎) /* d */⏎.h`). Read from
/// the source, `is_own_line_comment` calls it glued and takes the soft `line`, so what
/// follows pulls up whenever the enclosing group stays flat — onto a line the next
/// pass reads as the comment's own and glued. So the first comment answers
/// "own-line" as **true**; every later comment keeps the source reading. The mirror of
/// [`Self::AdjacentAnchorLine`].
AdjacentOwnLine,
}
impl LeadingGlue {
/// Whether an author blank line after a glued block's `*/` forces it onto its own
/// line (preserving the blank) rather than yielding with the soft `line`.
fn blank_forces_own_line(self) -> bool {
!matches!(self, Self::AdjacentValueGap)
}
/// Whether the run's FIRST comment shares the anchor's line in the output, which
/// erases the newline the author wrote before it — see [`Self::AdjacentAnchorLine`].
fn first_shares_anchor_line(self) -> bool {
matches!(self, Self::AdjacentAnchorLine)
}
/// Whether the run's FIRST comment opens its own line in the output — see
/// [`Self::AdjacentOwnLine`].
fn first_opens_own_line(self) -> bool {
matches!(self, Self::AdjacentOwnLine)
}
}
/// The separator a leading-comment run prints after one comment — prettier's
/// `printLeadingComment` three ([`Printer::push_leading_comment_run`]).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LeadingSeparator {
/// A space: the comment is glued to what follows (`/* c */ X`).
Space,
/// The soft `line`: a newline after the `*/` but none before the `/*`.
Line,
/// A blank-preserving `hardline`: a line comment, or an own-line block.
Hard,
}
/// How a broke-after run's separators render
/// ([`Printer::broke_after_value_leading_run`]) — the one thing its two halves disagree on.
/// Which separators exist is the source's answer either way (prettier's
/// `printLeadingComment`, asked of each comment's own neighbour): a pair the author glued
/// keeps its space in both.
#[derive(Debug, Clone, Copy)]
pub(crate) enum BrokeAfterBreak {
/// The run is forced open — by the value's own hard break, or by a multi-line comment in
/// the run ([`Printer::broke_after_run_break_is_forced`]) — so every separator the author
/// broke renders as the break prettier's `propagateBreaks` makes of its `line`: a
/// blank-preserving `hardline` ([`Printer::push_leading_run_separator`]).
Forced,
/// The run rides a group that may still render flat: a broken separator is a soft
/// `line`, and an author blank yields with it ([`LeadingGlue::AdjacentValueGap`]).
Soft,
}
/// Where a conditional branch's value sits relative to the comment run in its gap, in the
/// layout a hanging comment has already forced open — the answer the four conditional gaps
/// (the `?:` expression's two and the `extends ? :` type's two) take from their one emitter,
/// [`Printer::push_conditional_branch_gap_run`], which reads it from one producer,
/// [`Printer::conditional_branch_placement`], so no gap decides it twice.
///
/// **The LAST comment alone decides it**, by the same forward reading every leading-run
/// separator takes ([`Printer::comment_hugs_next`], prettier's `printLeadingComment`): the
/// author glued the value to that comment, or broke after it. A run-wide reading — "does any
/// comment here own a line" — answers for a comment the value never meets, and splits
/// `// c⏎/* m */ D` where prettier keeps `/* m */ D` on one line. That is the TYPE gap's
/// spelling; at the expression gap the same authoring only reaches the run through a stripped
/// paren shell (`? // c⏎/* m */ (x)`), since a block glued to the value itself is OWNED by it
/// and the run never sees it.
///
/// A named pair rather than a `(bool, bool)`: the two flags have the same type and are read
/// at **four** gaps, so a positional tuple is one transposition away from hanging a value
/// that should trail and keeping a blank that should collapse — a swap the compiler cannot
/// see and every fixture in either file would still pass on one of the gaps.
#[derive(Debug, Clone, Copy)]
pub(in crate::printer) struct ConditionalBranchPlacement {
/// The value drops below the run, because the author broke after the run's last comment
/// (a `//` never hugs) or left a blank line above the value.
pub(in crate::printer) on_own_line: bool,
/// The author left a blank line between the run and the value, which survives when the
/// value takes its own line.
pub(in crate::printer) blank_before: bool,
}
impl ConditionalBranchPlacement {
/// The value trails the run on the operator's own line — the answer for a gap with no
/// comments at all, and the one the non-breaking arm tail passes.
pub(in crate::printer) const INLINE: Self = Self {
on_own_line: false,
blank_before: false,
};
}
/// Whether a deferred own-line comment in a trailing run keeps the author's BLANK line
/// above it (prettier's `isPreviousLineEmpty(locStart(comment))`).
///
/// **The answer belongs to the run's DESTINATION, not to the gap it is read from**, which
/// is why it is the caller's to give. A run that still trails the node it was written after
/// keeps the blank, and prettier keeps it too. A run the enclosing composite re-reads as the
/// NEXT member's LEADING run does not: prettier's `printLeadingComment` asks
/// `isNextLineEmpty` — a blank BELOW — so a blank above the comment has nowhere to land and
/// both formatters drop it.
///
/// The two lifted union / intersection member runs are exactly that split
/// (`Printer::member_hoisted_trailing_shell`). The union's stays trailing: prettier's
/// `handleUnionTypeComments` binds every in-union comment to the PRECEDING member, so the
/// blank survives at both formatters. The intersection's does not — no such handler fires
/// there, the comment attaches to the following member, and its `&` has been pulled back
/// onto the previous member's line, so an emitted blank would sit *after* the operator where
/// the author wrote it before: the fabrication
/// `Printer::build_intersection_type_doc_with_line_comments` already refuses for its own gap
/// run.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::printer) enum TrailingBlank {
/// Keep an author blank directly above an own-line comment in the run.
Keep,
/// Drop it — the run lands where the reparse reads it as a leading run, which has no
/// blank-above rule to reproduce it with.
Drop,
}
/// Where an element carrying a stripped-paren interior sits, for the share-and-run emitter
/// ([`Printer::push_element_share_and_run`]).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ShareSite {
/// Nothing but the list's closer follows: the share and the run trail the element,
/// and the next pass reads them as a trailing run, which keeps a pair the author glued
/// on one line.
Closer,
/// Another element follows: the next pass reads everything past the comma as that
/// element's LEADING run, where only a comment on a line of its own holds the list
/// open — so each share comment takes its own line, and so does a run of blocks.
NextElement,
}
/// Where a **block** comment the author gave its own line lands in a trailing gap's run
/// ([`Printer::push_trailing_comments_in_range_with`]) when no `//` ahead of it has
/// already deferred the run — the one question the gap's DESTINATION decides, so the
/// caller names it.
///
/// A `//` is deferred by construction and an own-line comment behind one keeps its line
/// either way; this policy is asked only of the run a block OPENS.
/// What [`Printer::push_trailing_comments_in_range_with`] emitted — the two facts a caller
/// can need, read off the one walk that produced the run.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::printer) struct TrailingRun {
/// The run held a `//`, so the caller must break: the answer every other spelling of
/// the emitter returns bare.
pub has_line_comment: bool,
/// The run DEFERRED with a break of its own — a `//`, or an own-line block under
/// [`OwnLineBlock::Defer`] — so it lands at the next line end the renderer takes, and a
/// construct that may otherwise stay flat and glue its closer must supply that end
/// INSIDE itself (the mapped shell's question).
pub deferred: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::printer) enum OwnLineBlock {
/// Inline, before the gap's closer — the single-line-block collapse
/// ([docs/conformance_prettier.md](../../../../docs/conformance_prettier.md)
/// §Comment Position Philosophy): the run stays inside the construct it was written
/// in (a shell's `)`, an index's `]`, a signature head's gap), where deferring would
/// carry it PAST that closer, and an unforced own-line break collapses.
Inline,
/// Deferred on its own line — prettier's `printTrailingComment` own-line arm. For a
/// gap that spans the construct's own TERMINATOR and closes inside its braces (the
/// mapped member's `V;`→`}`), where the block trails the member the way a type
/// literal's own-line comment after its last member does, and inlining it would pull
/// it across the `;` (`V;⏎/* c */` → `V /* c */;`).
Defer,
}
/// A trailing run's **line reference** — the last source position whose PRINTED image is
/// still on the run's line — stepped one comment at a time.
///
/// A trailing run asks about the OUTPUT line, not the author's, and three things put a
/// comment on it that a bare `is_same_line(anchor, comment)` cannot see. They are one rule:
/// the reference tracks what has been printed, so it advances over anything whose image
/// lands on this line.
///
/// - A **multi-line block comment** the run took carries it to that comment's closing line.
/// - The statement's own **`;`**, when the author detached it (`a()⏎; // c`), prints back up
/// on the content's line, so the reference jumps to it.
/// - A **dropped `EmptyStatement`** (`a();⏎; // c`) prints nothing at all, so it cannot begin
/// a new output line either, and the same jump reaches past it.
///
/// The last two are one value, the statement's [`Printer::printed_tail`]; [`u32::MAX`] where
/// the run's line is the author's line throughout, which is every caller but the statement
/// trailing runs.
///
/// ⚠️ **A `//` the run takes CLOSES it.** It owns the rest of the output line, so nothing
/// joins the run behind it. Without a jump the rule is inert — no comment can follow a `//`
/// on one SOURCE line — and with one it is what stops `a() // c1⏎; /* c2 */` welding the two
/// into a single comment.
///
/// Both trailing walks step this and neither spells the rule, because they are one predicate
/// over two axes (`docs/comments.md` §the three axes):
/// [`Printer::trailing_same_line_comments_through`] over what this emitter owes,
/// [`Printer::find_end_with_trailing_comments`] over the bytes physically there — and the
/// cursor the second returns has to land exactly past the run the first emits. A re-spelled
/// copy is how two walks drift (see [`Printer::push_member_trailing_run`], whose own doc
/// records the last time it happened).
pub(in crate::printer) struct TrailingLineRef {
line_ref: u32,
printed_tail: u32,
closed: bool,
}
impl TrailingLineRef {
pub(in crate::printer) fn new(after_pos: u32, printed_tail: u32) -> Self {
Self {
line_ref: after_pos,
printed_tail,
closed: false,
}
}
/// Whether `comment` is on the run's line — advancing the reference over it when it is.
/// `false` ends the run.
pub(in crate::printer) fn take(&mut self, printer: &Printer<'_>, comment: &Comment) -> bool {
if self.closed {
return false;
}
if comment.span.start >= self.printed_tail {
self.line_ref = self.line_ref.max(self.printed_tail);
}
if !printer.is_same_line(self.line_ref, comment.span.start) {
return false;
}
if comment.is_block {
if !printer.is_same_line(comment.span.start, comment.span.end) {
self.line_ref = comment.span.end;
}
} else {
self.closed = true;
}
true
}
}
impl<'a> Printer<'a> {
/// Where a conditional branch's value sits below the comment `run` in its gap — the one
/// producer of a [`ConditionalBranchPlacement`], for both conditional gap emitters
/// ([`Self::push_conditional_branch_gap_run`], the four gaps' one emitter).
/// `run` is the gap's emitted comments and `value_start` the branch's span start.
///
/// **The run's LAST comment decides it**, by the forward reading every leading-run
/// separator takes ([`Self::comment_hugs_next`], prettier's `printLeadingComment`): the
/// value keeps that comment's line where the author glued it, and drops below the run
/// where the author broke after it — a `//` never hugs, so a run ending in one always
/// hangs the value — or where they left a blank line above the value, which then survives
/// below the run.
///
/// The blank scan is the STRICT one ([`Self::has_blank_line_between_strict`]), the same
/// question [`Self::push_blank_preserving_hardline`] answers for the run's own separators
/// and so the same spelling, never the table-only newline count: `value_start` is a span
/// start, and at the EXPRESSION gaps the parser discards a grouping paren, so for a
/// parenthesized branch it lies INSIDE the stripped shell and a counting scan reads the
/// `(`'s two line breaks as an author blank (`a ? /* c */⏎(⏎b⏎) : c` grew one, and since
/// the blank feeds `on_own_line` too, the whole conditional came open). At the type gaps
/// a `TSParenthesizedType` keeps its `(` and its span starts there, so a shelled branch
/// reaches this producer only once the branch claim has widened `value_start` past it.
///
/// ⚠️ And it takes the in-source CEILING ([`Self::blank_scan_end_after`]), the spelling
/// every leading-run blank scan takes. That half is inert at the type gap — no comment in
/// a type branch's gap is owned, since ownership binds at an expression's start, so the
/// run's last comment is also the physically last one there — and
/// load-bearing at the expression gap, where a block the author glued to the branch is
/// OWNED: the run skips it, and a raw scan to `value_start` would read its interior
/// newlines as an author blank (`docs/comments.md` §The five hazards, hazard 5).
pub(in crate::printer) fn conditional_branch_placement(
&self,
run: &[&Comment],
value_start: u32,
) -> ConditionalBranchPlacement {
let blank_before = run.last().is_some_and(|c| {
self.has_blank_line_between_strict(
c.span.end,
self.blank_scan_end_after(c, value_start),
)
});
ConditionalBranchPlacement {
on_own_line: run.last().is_some_and(|c| !self.comment_hugs_next(c)) || blank_before,
blank_before,
}
}
/// Whether the FIRST comment of a `?` / `:`→branch gap keeps the line the author gave it
/// rather than trailing the operator: the gap holds a line comment, so it hangs open
/// anyway, and that first comment does not share `op_end`'s line. The keyword→value rule
/// ([`Self::append_keyword_value_line_comments`]) for the one emitter that places the
/// first comment itself, [`Self::push_conditional_branch_gap_run`]. A comment there leads
/// the branch, so own-line-ness is authorship (conformance_prettier.md §Comment Position
/// Philosophy).
fn first_gap_comment_keeps_own_line(&self, comments: &[&Comment], op_end: u32) -> bool {
comments.iter().any(|c| !c.is_block)
&& comments
.first()
.is_some_and(|c| !self.is_same_line(op_end, c.span.start))
}
/// Emit the comment RUN of a conditional's `?` / `:`→branch gap — `[from, to)`, the
/// operator's end to the branch's start — into `parts`, returning where the branch then
/// sits ([`ConditionalBranchPlacement`], from [`Self::conditional_branch_placement`]).
/// The one emitter for the expression ternary's two gaps and the conditional type's two,
/// in their breaking layouts, so the four cannot drift.
///
/// Every separator is the leading-run rule read FORWARD from the comment BEFORE it
/// ([`Self::comment_hugs_next`], prettier's `printLeadingComment`): the first comment
/// trails the operator inline (`? /* c */`), a pair the author glued keeps its space,
/// and a break the author wrote after a comment survives as a blank-preserving
/// `hardline`. The layout this serves has already been forced open by a hanging comment,
/// so every one of prettier's `line`s here renders as that break.
/// [`Self::first_gap_comment_keeps_own_line`] is the one backward question: in a gap
/// holding a line comment, a first comment the author put on its OWN line keeps that
/// line, leaving the operator alone on its own, because there the comment leads the
/// branch and own-line-ness is authorship. An HONORED directive keeps the line the author
/// gave it wherever in the run it sits: sharing a line with the operator (or with the
/// comment before it) is the placement the floor classifies as inert, and the freeze it
/// earns would be lost on the second pass — the rule [`Self::build_header_comment_run`]
/// states at the declaration headers and [`Self::comment_hangs_next`] states for what
/// follows a comment; the emitter never relocates a directive.
///
/// The whole run rides inside ONE `indent` — prettier's `printBranch` indents comments
/// and branch alike — never an indent-width text ahead of each own-line comment: a text
/// indents a comment's first line only, so an indentable block's continuation lines
/// landed one level short of it, in the first slot (a block the author broke after,
/// `? /* y⏎ */⏎// c⏎b`) as in every later one
/// (`ternary/branch_run_indentable_block_comment`, `conditional/…` its type twin).
///
/// The placement is the run's LAST comment alone, read forward, so the value keeps that
/// comment's line where the author glued it and drops below the run where the author
/// broke after it. Reading the RUN instead ("is there a line comment anywhere in here")
/// answers for a comment the value never meets and split the glued pair — at the
/// expression gap only through a stripped paren shell (`? // c⏎/* m */ (x)`, which
/// prettier keeps on one line), since a block glued to the branch itself is OWNED by it
/// and never enters the run; the bare `? // c⏎/* m */ x` is inert there for that reason,
/// and moves only at the type gap, where a type owns nothing. Spending the answer is the
/// caller's value emitter's job (`Printer::push_ternary_branch_value`,
/// `Printer::build_conditional_branch_tail_doc`).
pub(in crate::printer) fn push_conditional_branch_gap_run(
&self,
parts: &mut DocBuf,
from: u32,
to: u32,
) -> ConditionalBranchPlacement {
let d = self.d();
let comments: CommentVec<'_> = self.comments_to_emit_between(from, to).collect();
let first_keeps_own_line = self.first_gap_comment_keeps_own_line(&comments, from);
let mut run = DocBuf::new();
for (i, comment) in comments.iter().enumerate() {
let directive = self.is_honored_directive(comment);
match i.checked_sub(1).map(|p| comments[p]) {
// The run's first comment: the operator's line, unless the backward rule
// above — or the directive floor — gives it its own.
None if directive || first_keeps_own_line => run.push(d.hardline()),
None => run.push(d.text(" ")),
// Glued to the previous comment — keep the line the author wrote them on.
Some(prev) if !directive && self.comment_hugs_next(prev) => {
run.push(d.text(" "));
}
// The author broke after the previous comment (a `//` always has), so this
// one takes its own line, below any blank they left above it.
Some(prev) => {
self.push_blank_preserving_hardline(
&mut run,
prev.span.end,
comment.span.start,
);
}
}
run.push(self.build_comment_doc(comment));
}
if !run.is_empty() {
parts.push(d.indent(d.concat(&run)));
}
self.conditional_branch_placement(&comments, to)
}
/// Push one **block** comment with `spacing` applied to its outer edges — the single
/// definition of what [`CommentSpacing`] means for a comment that does not end its
/// line: ` /* c */` (`Leading`), `/* c */ ` (`Trailing`), or bare (`None`).
///
/// `space_before` lets a `Leading` caller suppress the space when the run already
/// sits at the start of a fresh (indented) line, where a space would render as a
/// stray `\t /* c */`; a caller with nothing to suppress passes `true`.
///
/// Shared by [`Self::format_block_comments`] (the chain gaps' block-only runs) and
/// the block arm of [`Self::build_header_comment_run`]. A **line** comment has no
/// spacing choice to make — it must end its line — so it never reaches here; that
/// asymmetry is the whole content-loss hazard, and the one emitter that hand-rolls
/// both halves ([`Self::build_comments_between_filtered_opt`]) documents it in place.
pub(crate) fn push_block_comment_spaced(
&self,
parts: &mut DocBuf,
comment: &Comment,
spacing: CommentSpacing,
space_before: bool,
) {
debug_assert!(comment.is_block, "line comments have no spacing choice");
let d = self.d();
match spacing {
CommentSpacing::Leading => {
if space_before {
parts.push(d.text(" "));
}
parts.push(self.build_comment_doc(comment));
}
CommentSpacing::Trailing => {
parts.push(self.build_comment_doc(comment));
parts.push(d.text(" "));
}
CommentSpacing::None => parts.push(self.build_comment_doc(comment)),
}
}
/// Whether a comment can't share a line with anything around it — any line comment
/// (it runs to EOL), or a block comment with a newline on **both** sides in source:
/// nothing before it on its line ([`Self::comment_follows_content_on_its_line`]) and
/// nothing glued after it ([`Self::comment_hugs_next`]). An adjacency on either side
/// keeps the comment inline (`a /* c */ b`), matching prettier, which collapses both
/// `a,⏎/* c */ b` and `a /* c */,⏎b` back to the inline form. The shared rule behind
/// the intersection member break gates, the import-attribute gaps, and the first-param
/// leading-run collapse (`build_leading_param_comments`).
///
/// ⚠️ **Both halves read the SOURCE, and that is the whole predicate.** This is
/// prettier's `printLeadingComment` hardline condition transcribed —
/// `hasNewline(text, locEnd(comment)) && hasNewline(text, locStart(comment),
/// {backwards: true})` — so it asks about physical newlines, never about where the
/// neighboring *items* start and end. An item-boundary spelling
/// (`is_same_line(prev, c.start)` / `is_same_line(c.end, next)`) is blind to every
/// byte no item span covers — a list's comma, a stripped paren shell's `)`, an `&`
/// operator, **and another comment in the same gap** — each of which reads as
/// isolation that the author did not write. The last one is why the anchors could not
/// be kept even where the gap holds no structure at all: at the import-attribute
/// `:`→value gap, whose `prev` *is* the `:`, a glued run (`a:⏎/* c1 */ /* c2 */⏎'x'`)
/// still mis-read. The union keeps its own one-sided rule
/// ([`Self::is_own_line_comment`], no glue half) because prettier's union printer
/// genuinely expands a block adjacent to its member where the intersection collapses
/// it — see `union_has_own_line_member_comment`.
///
/// The kind clause is the only thing this adds to
/// [`Self::block_comment_owns_its_line`] (with an item following), which the
/// list-expansion gates call directly because they filter on kind themselves.
pub(crate) fn comment_isolated_on_its_line(&self, c: &Comment) -> bool {
!c.is_block || self.block_comment_owns_its_line(c, true)
}
/// Whether a *block* comment is glued to what follows it (`/* c */ X` — nothing but
/// spaces after its `*/`), so it leads that token inline instead of taking its own
/// line. Prettier's leading-comment rule, asked exactly as `printLeadingComment` asks
/// it: of the **source right after the comment** (`hasNewline(text, locEnd(comment))`),
/// never of where the item it leads happens to start. So a run the author glued
/// together stays glued (`/* a */ /* b */⏎X` → the pair shares a line, `X` starts a new
/// one).
///
/// ⚠️ **The two readings differ by exactly the structure a list re-emits.** With a
/// comma between the comment and its item (`<T⏎/* c */, U // x>`), an
/// `is_same_line(comment.end, item_start)` approximation says "the item is on the next
/// line, so break" and drops the comment onto a line of its own — while the author's
/// own line, and prettier's, ends *after* the comma. The array literal reached the same
/// output by collapsing a soft `line` in its per-element group; the params family has
/// no such group (`docs/comments.md` §Array family vs params family), so the glue test
/// is the whole answer there and the two families disagreed about one authoring.
///
/// The rule's one carve-out is about the comment AFTER this one, so it lives in the
/// run's emitter rather than here: nothing glues onto an honored format-ignore
/// directive ([`Self::directive_follows`]).
///
/// The single statement of the rule. [`push_leading_comment_run`](Self::push_leading_comment_run)
/// is the emitter for the sites whose surrounding loop is the shared one; a site
/// whose separator policy genuinely differs (the union member's own-line run,
/// which brackets the `| ` separator and preserves blanks in different positions)
/// calls this directly rather than re-deriving it.
pub(crate) fn comment_hugs_next(&self, comment: &Comment) -> bool {
comment.is_block && !has_newline_after_position(self.source, comment.span.end)
}
/// Whether a **block** comment OWNS its line — the classification every list-EXPANSION
/// gate applies, stated once. Four families spell the surrounding scan differently
/// (what bounds the gap, what counts as an item, whether a multi-line block is in
/// scope), and the one thing they must not spell differently is this.
///
/// Both halves read the SOURCE, per comment: nothing before the `/*` on its line
/// ([`Self::comment_follows_content_on_its_line`]) and nothing after the `*/` there
/// ([`Self::comment_hugs_next`]) — prettier's `printLeadingComment` hardline condition,
/// which is the only separator of its three that ends the line unconditionally. Every
/// list here flattens when it fits, so the author's line break *around* the comma is
/// layout, not own-line-ness — an item-boundary reading of either half calls a
/// comma-glued comment own-line and reaches a third fixed point neither the bare
/// authoring nor prettier produces (`docs/comments.md` §Own-line-ness is a SOURCE
/// question).
///
/// ⚠️ **A glued RUN given its own line does not own it.** In `<A,⏎/* c1 */ /* c2 */⏎B>`
/// only `c1` has a newline before it and only `c2` has one after, so neither takes the
/// hardline: `c1` glues to `c2` and `c2` takes the **soft `line`** that collapses when
/// the list fits and breaks when it doesn't. Asking the RUN instead — "does the object
/// the author glued end the line?" — is the wrong reading: it
/// forces open a list prettier keeps flat at every one of these families (params, type
/// params, type args, function-type params, call args, tuples, intersections, all
/// measured). The soft `line` is what makes the two authorings one fixed point, and it
/// lives in the emitters ([`Self::push_leading_comment_run`]); a gate that pre-empts it
/// with a forced break is answering a question the emitter already answers better.
///
/// `item_follows` is the caller's fact about the range it scanned, not a default: with
/// no item left to lead, the comment is DANGLING and the container's closer sharing its
/// line is not glue (`{ a: 1⏎/* c */ }`, `[a,⏎/* c */ ]` — prettier expands both). A
/// gap bounded by the next item passes `true`, and the families whose trailing position
/// belongs to a separate predicate ([`Self::has_own_line_block_comment_before_closer`])
/// always do.
///
/// The caller filters on kind first: a **line** comment forces every one of these lists
/// open on its own, and each family says so in its own clause rather than here.
/// [`Self::comment_isolated_on_its_line`] is this same question with that clause folded
/// in, for the callers that don't pre-filter.
pub(crate) fn block_comment_owns_its_line(
&self,
comment: &Comment,
item_follows: bool,
) -> bool {
!self.comment_follows_content_on_its_line(comment)
&& (!item_follows || !self.comment_hugs_next(comment))
}
/// Split a comment run into the ones that stay in the RUN and the ones
/// **glued** to what follows ([`Self::comment_hugs_next`]) — for a site whose run and
/// glued suffix take different separators, so it cannot hand the whole run to
/// [`Self::push_leading_comment_run`] (which decides per comment) and must emit the two
/// halves itself.
///
/// The glued suffix leads the following token inline (`/* c */ for (…)`), where the
/// author put it; the rest each take a line. The split is invisible wherever the
/// following token heads an **expression** — `bind_leading_comment` is the only general
/// binder, so the comment is then owned by that node, rides inside its doc and never
/// reaches the gap — leaving only the **keyword** heads (`for` / `if` / `{`, a `for`
/// clause's own head; a statement keyword binds nothing) and the positions where a
/// freeze replaced the doc with a verbatim slice starting past the comment
/// (docs/comments.md hazard 1).
///
/// The caller owns the **break** before the suffix — this only says which comments it
/// holds. A glued comment is still authored somewhere, and gluing it to what follows
/// must not also move it to another line: at a header→body gap
/// ([`Printer::push_header_to_body_gap`]) everything reaching the suffix sat below the
/// anchor, so it takes a `hardline` even when the run before it is empty.
///
/// ⚠️ **The glued set is a SUFFIX**, taken by walking back from the end while each
/// comment still hugs. Per-comment it is not: `/* a1 */ /* a2 */⏎x` has a1 hugging a2
/// and a2 hugging nothing, so a per-comment test puts a1 in the glued half and a2 in
/// the run — two buckets emitted run-first, which prints the authored pair REVERSED.
/// A comment glued to one that takes its own line takes that line with it.
pub(crate) fn split_glued_comments(
&self,
comments: impl IntoIterator<Item = &'a Comment>,
) -> (CommentVec<'a>, CommentVec<'a>) {
let mut run: CommentVec<'a> = comments.into_iter().collect();
let mut split = run.len();
while split > 0 && self.comment_hugs_next(run[split - 1]) {
split -= 1;
}
let glued: CommentVec<'a> = run.drain(split..).collect();
(run, glued)
}
/// Emit the glued suffix [`Self::split_glued_comments`] held back, immediately before
/// the doc the caller pushes next: each comment plus the space that keeps it glued.
pub(crate) fn push_glued_comment_run(&self, parts: &mut DocBuf, glued: &[&'a Comment]) {
let d = self.d();
for comment in glued {
parts.push(self.build_comment_doc(comment));
parts.push(d.text(" "));
}
}
/// The separator after one comment in a per-line comment list, where an author
/// blank line between `comment_end` and `next` (the following own-line comment, or
/// the element the comments lead) **forces a `hardline`** and carries the blank as a
/// leading `literalline`; without one the caller's `sep` stands.
///
/// The single statement of "does an author blank survive here?" — always asked with
/// [`Self::has_blank_line_between_strict`], never the table-only
/// [`Self::has_blank_line_between`], whose newline count can be met by a delimiter
/// the printer emits between two spans (see that method's rustdoc: reading one as a
/// blank is a one-shot non-idempotency). A site that re-derives this pays that bug.
pub(crate) fn push_blank_preserving_separator(
&self,
parts: &mut DocBuf,
comment_end: u32,
next: u32,
sep: DocId,
) {
let d = self.d();
if self.has_blank_line_between_strict(comment_end, next) {
parts.push(d.literalline());
parts.push(d.hardline());
} else {
parts.push(sep);
}
}
/// [`Self::push_blank_preserving_separator`] with the `hardline` separator — the
/// blank-preserving counterpart to a bare `hardline`, for a run whose comments each
/// take their own line unconditionally. The common case; a run whose non-blank
/// separator is collapsible (a conditional branch's soft `line`) calls the general
/// form.
pub(crate) fn push_blank_preserving_hardline(
&self,
parts: &mut DocBuf,
comment_end: u32,
next: u32,
) {
self.push_blank_preserving_separator(parts, comment_end, next, self.d().hardline());
}
/// The **list-element** sibling of [`Self::push_blank_preserving_hardline`]: same
/// emission (`literalline` + `hardline` for an author blank, a bare `hardline`
/// otherwise), different blank question.
///
/// Here it is prettier's `isNextLineEmpty` ([`Self::is_next_line_empty`]), measured
/// from the **element's own end** — which is what makes the *separator* and the
/// element's same-line trailing comments trivia. Two consequences the range-based
/// question cannot express, and both are authored shapes: a blank the author left
/// before a comma they pushed onto its own line still counts (`a: 1⏎⏎// c⏎, b`), and
/// one *after* such a comma does not (`a: 1⏎,⏎⏎b` — the rule
/// `property_own_line_comma_blank` pins). Callers pass `content_start` = where this
/// element's printed content begins (its first leading comment, else the element), so
/// a blank ahead of that content is inside the measured range.
///
/// The function-parameter list calls it directly, deriving its own `content_start`
/// (decorators, the `(`-line pull); every other list goes through
/// [`Self::push_item_blank_separator`], which derives that position once. A further
/// list that needs the same rule should call one of the two rather than re-derive the
/// two-line emission around a bare `is_next_line_empty`.
///
/// The literal and the pattern are one entry, not two: prettier prints
/// `ObjectExpression` and `ObjectPattern` through the same `printObject`, so a site
/// that answers this question differently for one of them is a divergence by
/// omission. Arrays are the deliberate exception — array literals and array patterns
/// take prettier's *other* helper (`isLineAfterElementEmpty`, [`Self::has_blank_line_after_comma`]),
/// which advances to the comma before measuring; see [`Self::is_next_line_empty`] for
/// the table of where the two disagree.
pub(crate) fn push_next_line_empty_hardline(
&self,
parts: &mut DocBuf,
elem_end: u32,
content_start: u32,
) {
let d = self.d();
if self.is_next_line_empty(elem_end, content_start) {
parts.push(d.literalline());
}
parts.push(d.hardline());
}
/// [`Self::push_next_line_empty_hardline`] with the `content_start` derived here: the
/// separator between two list items, asked once for every list that shares the
/// element-comma contract (object literal, object pattern, import/export specifiers,
/// enum members).
///
/// `prev_end` is the previous item's trailing-run end, `item_start` this item's node
/// start; the content between them is this item's leading comment run, so the scan
/// stops at the first comment.
///
/// ⚠️ **in source** — the first comment *physically* in the gap, not the first one
/// this caller will emit. A blank-line scan reads bytes, so an OWNED comment (glued
/// to the item's first token and printed from inside its doc) bounds it just the
/// same: the author's blank sits *before* that comment, and a bound past it would put
/// the blank outside the measured range. Deriving this per site is how the literal
/// and the pattern came to ask it on two different axes — the one thing `printObject`
/// printing both through one path says they must not do.
///
/// ⚠️ The scan starts at the previous item's SHELL end ([`Self::element_shell_end`]),
/// not at `prev_end` itself. This is the seam's **distance** half: `prev_end` is a
/// CLAIM anchor and stops where the item's doc stops printing, which for an item whose
/// span was extended over a stripped paren is *inside* the erased shell — and a
/// distance measured from there reads the shell's own line breaks as an author blank
/// (`{ k: (1⏎⏎), b }` grew one, in the literal and the pattern alike). The peel steps
/// over `)` and whitespace only and **stops at the first comment**, so a blank the
/// author wrote ahead of a comment in that shell is still measured — the two answers
/// this function must give differ by exactly that, and one anchor gives both.
pub(crate) fn push_item_blank_separator(
&self,
parts: &mut DocBuf,
prev_end: u32,
item_start: u32,
) {
let (from, bound) = self.item_gap_blank_scan(prev_end, item_start);
self.push_next_line_empty_hardline(parts, from, bound);
}
/// Whether the author left a blank line in an item gap — the predicate half of
/// [`Self::push_item_blank_separator`], for a caller that must *decide* on the blank
/// before it can pick a separator at all.
///
/// A soft `line` cannot carry a blank, so a list whose separator is one has to know:
/// the blank is authorship the list preserves, and preserving it means taking the
/// hardline separator (which forces the break) rather than the `line`. Both halves read
/// the same range, so the decision and the emission cannot disagree.
pub(crate) fn item_gap_has_blank_line(&self, prev_end: u32, item_start: u32) -> bool {
let (from, bound) = self.item_gap_blank_scan(prev_end, item_start);
self.is_next_line_empty(from, bound)
}
/// The range an item gap's blank-line scan reads: from the previous item's SHELL end
/// to the first comment **in source** (else the item itself). Both ends in one place
/// because both halves above need both — deriving either separately is how a predicate
/// and its emitter drift into disagreeing about the same gap.
///
/// See [`Self::push_item_blank_separator`] for why the start is peeled and why the
/// bound is physical.
pub(in crate::printer) fn item_gap_blank_scan(
&self,
prev_end: u32,
item_start: u32,
) -> (u32, u32) {
let from = self.element_shell_end(prev_end, item_start);
let bound = self
.comments_in_source_between(from, item_start)
.next()
.map_or(item_start, |c| c.span.start);
(from, bound)
}
/// Whether `[from, next)` holds a **truly blank line** — two newlines with nothing
/// but horizontal whitespace between them.
///
/// [`Self::has_blank_line_between`] reads the line-break table and counts newlines
/// without looking at what sits between them, and either endpoint of this gap can be
/// a node span that excludes a delimiter the printer still emits — a grouping paren
/// the value's span starts inside (`const y =⏎// c⏎(⏎ a = b // c2⏎);`). The newline
/// before that `(` and the one after it then read as an author blank line, one is
/// emitted, and the next pass reads it back as real: a one-shot non-idempotency.
///
/// The table lookup is kept as the fast reject — fewer than two newlines is
/// conclusive — so only the rare positive pays the byte scan
/// ([`printing::has_blank_line_between_strict`], the shared statement of the
/// intervening-line rule), over a gap that is whitespace and at most a delimiter or
/// two.
///
/// ⚠️ **A stripped paren's HEAD in front of content is not part of the gap.** When `next`
/// is where content starts rather than a comment, the `(` run directly ahead of it is a
/// shell the printer erases (`/* c */⏎(⏎⏎v)`), and a blank inside that run is shell
/// structure — prettier's `printLeadingComment` measures forward from the comment and
/// stops at the `(`. So the scan ends at the run's first `(` ([`Self::shell_head_start`]).
/// A blank the author wrote there ahead of a **comment** (`/* c1 */⏎(⏎⏎/* c2 */`) is
/// authorship instead and keeps the whole gap: the sanctioned shell-head divergence
/// (`conformance_prettier_ts_comments.md` §Comment relocation), the line the element-comma
/// seam draws with [`Self::element_shell_end`]. Reading the paren's blank moved layout at
/// every leading run a stripped paren can follow — a call opened around a lone argument
/// (`fn(/* c */⏎(⏎⏎b))`), a ternary broke — each form its own fixed point.
pub(in crate::printer) fn has_blank_line_between_strict(&self, from: u32, next: u32) -> bool {
self.has_blank_line_between(from, next)
&& printing::has_blank_line_between_strict(
self.source,
from,
self.shell_head_start(from, next),
)
}
/// Where the stripped paren shell in front of `next` begins: the first `(` of the run of
/// `(` and whitespace that ends at `next`, or `next` itself when that run holds no `(`,
/// when `next` starts a comment (see [`Self::has_blank_line_between_strict`]), or where
/// the run is cut short at `from`.
///
/// The whitespace class is ASCII only, deliberately: any other byte ends the run, which
/// falls back to measuring the whole gap — the reading this refines.
fn shell_head_start(&self, from: u32, next: u32) -> u32 {
let bytes = self.source.as_bytes();
let end = (next as usize).min(bytes.len());
if bytes[end..].starts_with(b"/*") || bytes[end..].starts_with(b"//") {
return next;
}
let mut head = next;
let mut i = end;
while i > from as usize {
match bytes[i - 1] {
b'(' => head = (i - 1) as u32,
b' ' | b'\t' | b'\n' | b'\r' => {}
_ => break,
}
i -= 1;
}
head
}
/// Emit the separator after one comment in a leading run, toward the **physical**
/// next comment rather than `emit_next` (the start of the next *emitted* comment,
/// or the value/argument when this is the last). An owned comment — glued to the
/// token after it, printed by that token's node — is skipped by every emit
/// iterator yet still occupies the source gap, so both decisions here must anchor
/// past it: [`blank_scan_end`](Self::blank_scan_end) finds the first physical
/// comment in `(comment.end, emit_next)`, then a same-line block hugs it with a
/// space ([`comment_hugs_next`](Self::comment_hugs_next)) and everything else takes
/// the blank-preserving hardline. The single statement of that rule for the two
/// hand-rolled leading-run emitters whose surrounding loop can't route through
/// [`push_leading_comment_run`](Self::push_leading_comment_run)
/// (`build_operator_line_comment_hang`, `append_keyword_value_line_comments`) — so a run
/// the author glued stays glued and a multiline owned comment's own newline is never
/// read as an author blank line.
///
/// ⚠️ **Two states, so it belongs only where the break is already FORCED.** Both
/// hand-rolled callers sit past a `//` that has taken the line, inside an
/// `indent_hardline` / forced continuation, where prettier's third separator — the soft
/// `line` — would render as the hardline anyway; so does the forced half of a broke-after
/// value run ([`Self::push_leading_run_before_breaking_value`]), whose forced break — the
/// value's own, or a multi-line comment's — is what prettier's `propagateBreaks` turns every
/// `line` in that run into. A site
/// whose enclosing group can still be flat needs
/// [`push_leading_comment_run`](Self::push_leading_comment_run) instead: reaching for
/// this one there forces the group open around a run prettier keeps inline, which is
/// exactly what the call family's leading emitter did before it was converged.
pub(crate) fn push_leading_run_separator(
&self,
parts: &mut DocBuf,
comment: &Comment,
emit_next: u32,
) {
let next = self.blank_scan_end_after(comment, emit_next);
if self.comment_hugs_next(comment) {
parts.push(self.d().text(" "));
} else {
self.push_blank_preserving_hardline(parts, comment.span.end, next);
}
}
/// The separator after a comment in a **single-slot gap** — a keyword/operator and
/// the one token that follows it (`if /* c */ (`, `new /* c */ (`, a template
/// interpolation's `${ /* c */ expr`, a type-literal member's leading run): a block
/// glues with a space, a line comment takes a hardline because a `//` would
/// otherwise swallow that token.
///
/// ⚠️ **Keyed on the comment's KIND, not on the source** — that is the whole
/// distinction from [`push_leading_run_separator`](Self::push_leading_run_separator),
/// which asks whether the *author* glued the pair. Both are correct where they sit:
/// a single-slot gap collapses a block comment to the inline form whenever it can do
/// so losslessly, so the authored break carries no signal to preserve
/// (`docs/conformance_prettier.md` §Comment Position Philosophy, the keyword→value
/// rule), while a leading run inside a list keeps the lines the author gave it. Pick
/// by which of those two a new site is, never by which is nearer.
///
/// ⚠️ One caller deliberately does NOT route through here: the expression-statement
/// leading gap additionally forces the hardline for an **honored directive**
/// (`printer/expressions/mod.rs`), since a `prettier-ignore` must start its frozen
/// node on a fresh line. Whether the other single-slot gaps owe that carve-out is
/// open — none of them freezes a node, so no fixture reaches it today.
pub(crate) fn push_comment_kind_separator(&self, parts: &mut DocBuf, comment: &Comment) {
let d = self.d();
if comment.is_block {
parts.push(d.text(" "));
} else {
parts.push(d.hardline());
}
}
/// Whether the comment a TRAILING run is about to emit keeps the previous one's line,
/// because the author GLUED the pair (`/* c1 */ /* c2 */`, `/* c */ // t`).
///
/// The single statement of that question — the trailing-side counterpart of
/// [`Self::comment_hugs_next`](Self::comment_hugs_next): every end-of-container run asks
/// it, and a run that answered differently would be the drifted copy `docs/comments.md`
/// §Trailing and dangling runs is about. `None` — the run's first comment — is never
/// glued: it has the anchor behind it, not a comment.
///
/// ⚠️ **Asked BETWEEN the two comments, never of the source right after the `*/`** —
/// and the two spellings are not equivalent, which is the whole reason this is not
/// `comment_hugs_next(prev)`. They part on exactly the byte these runs exist for: the
/// list's own **comma**, deleted under `trailingComma: 'none'`. Asking what follows the
/// `*/` stops at that comma, reports "no newline", and WELDS a comment the author gave
/// its own line onto the previous one (`[A /* c1 */,⏎// c2]` → `A /* c1 */ // c2`, with
/// the `//` then swallowing anything printed behind it). Asking whether the author put a
/// newline *between* the two reads past the comma, because a deleted separator is not a
/// line. The other trailing-run sites never saw the difference: everywhere else the
/// comma coincides with a predecessor an earlier emitter already claimed, so `prev` is
/// `None` there — only the one walk that covers a whole gap in a single pass
/// ([`Self::build_trailing_gap_comments`]) reaches it.
///
/// The `is_block` half is belt-and-braces rather than load-bearing: a `//` runs to end of
/// line, so a following comment always has a newline between.
///
/// The space it licenses is not the weld the separator-before rule exists to prevent
/// ([`Self::push_trailing_body_comments`]): both comments stay distinct, and a line
/// comment never hugs, so nothing can land behind a `//`. A **dangling** run — the
/// container's only content — does not ask it at all, because prettier splits a glued
/// pair there ([`Self::push_dangling_comment_run`]).
pub(crate) fn trailing_run_hugs_previous(
&self,
prev: Option<&Comment>,
next_start: u32,
) -> bool {
prev.is_some_and(|prev| {
prev.is_block && !self.has_newline_between(prev.span.end, next_start)
})
}
/// Emit the separator before one comment in a TRAILING run: a space when the author
/// glued it to `prev` ([`Self::trailing_run_hugs_previous`]), otherwise the
/// blank-preserving `hardline` that gives it its own line.
///
/// The trailing-side counterpart of
/// [`push_leading_run_separator`](Self::push_leading_run_separator), for the runs whose
/// own-line arm is that unconditional break — the last-item→closer walk
/// ([`Self::build_trailing_gap_comments`]), the array literal's end-of-array scan,
/// and the call family's dangling emitter. A run whose non-glue separator is the
/// caller's ([`Self::push_trailing_closer_comments`], whose container may still
/// collapse) asks the predicate directly and keeps its own arm.
///
/// ⚠️ **The blank question is `isPreviousLineEmpty`, asked of the COMMENT** — the
/// own-line arm is [`Self::push_adjacent_blank_hardline`], which carries that rule and
/// its `blank_scan_start` floor. Both are the recurring bug here: a gap-wide reading
/// preserves a blank prettier has no way to emit, and an unfloored scan reads a
/// multi-line block's own newlines as one (`[1 /* x⏎⏎y */⏎// c]`). Either way the
/// fabricated form is a fixed point both formatters then agree on — so F1, the ledger
/// and the census are all blind to it, and only `fabrication:audit` on the pristine
/// seed, or a prettier `compare`, shows it.
pub(crate) fn push_trailing_run_separator(
&self,
parts: &mut DocBuf,
prev: Option<&Comment>,
scan_from: u32,
next_start: u32,
) {
if self.trailing_run_hugs_previous(prev, next_start) {
parts.push(self.d().text(" "));
} else {
self.push_adjacent_blank_hardline(parts, scan_from, next_start);
}
}
/// The own-line arm of a TRAILING run's separator: the comment's own `hardline`,
/// preceded by a `literalline` where the author left a blank line **directly above
/// it**.
///
/// ⚠️ `isPreviousLineEmpty`, asked of the COMMENT ([`Self::previous_line_is_empty`])
/// — not "does this gap hold a blank". Prettier's `printTrailingComment` emits its
/// extra `hardline` from `isPreviousLineEmpty(locStart(comment))`, so the blank has to
/// be adjacent to the comment; the gap-wide scan
/// ([`Self::push_blank_preserving_hardline`], which is prettier's *leading* reading)
/// additionally fires when re-emitted structure separates the two. Two places that
/// bites: the specifier list's own comma (`import {a⏎⏎,⏎// c⏎b}` put the author's blank
/// above the COMMA, which `printModuleSpecifiers` — a bare `join([",", line], …)` — has
/// no way to emit), and a stripped grouping paren in a condition's trailing gap
/// (`if (⏎(⏎x⏎⏎)⏎/* c */⏎)`, where the blank is the shell's and is erased with it).
///
/// ⚠️ **The scan is still an IN-SOURCE question, and `blank_scan_start` is its FLOOR**
/// rather than `scan_from` itself: a comment physically in the gap that this run did
/// not emit — one an earlier emitter claimed, or an OWNED one printed from inside a
/// node's doc — still occupies those bytes, and the backward walk must not read its
/// newlines as the author's.
///
/// The single statement of that arm, for the trailing-run separator above and for the
/// control-flow gap builder's trailing run
/// (`Printer::build_comments_between_parts`), whose glue policy differs per gap but
/// whose blank question is this same one.
pub(crate) fn push_adjacent_blank_hardline(
&self,
parts: &mut DocBuf,
scan_from: u32,
next_start: u32,
) {
let from = self.blank_scan_start(scan_from, next_start);
if self.previous_line_is_empty(from, next_start) {
parts.push(self.d().literalline());
}
parts.push(self.d().hardline());
}
/// Emit the whole trailing comment run in `[anchor, end)` for a gap whose layout is
/// **already vertical** — the run's first comment trails `anchor` on its line if the
/// author put it there, every later one takes
/// [`push_trailing_run_separator`](Self::push_trailing_run_separator). Returns the
/// end of the last comment emitted (`anchor` when the gap holds none), which is the
/// cursor a caller needs for whatever it emits next.
///
/// The anchor question is asked **once**, of the first comment only: past it the
/// thing behind a comment is another comment, so the separator's own glue test
/// answers it. A loop that re-asks the anchor per comment reads the second half of a
/// pair the author glued *across the first* and splits it — the bug this shape
/// exists to prevent, which is why the four gaps that share it (a binary chain's
/// operand→operator and operator→operand gaps, a unary operand's stripped-paren
/// trailing gap, a retained non-null paren shell's operand→`)!` gap) call this
/// rather than each carrying the loop.
///
/// For a gap that may still collapse — where the run has to DEFER past a closer
/// instead of taking real breaks — the emitter is
/// [`push_trailing_comments_in_range`](Self::push_trailing_comments_in_range) or, at
/// a stripped operand paren, `append_trailing_paren_comments`. See
/// [docs/comments.md](../../../../docs/comments.md) §Trailing and dangling runs.
///
/// ⚠️ `leading_blank` is the one thing the three gaps do NOT share: whether an author
/// blank ABOVE the run's first comment survives ([`RunLeadingBlank`]). Two of them are
/// genuinely trailing runs and keep it; the operator→operand gap is a **leading** run
/// of the right operand that borrows this emitter for its anchor rule, and prettier's
/// `printLeadingComment` has no emitter for such a blank at all.
///
/// ⚠️ **Caller invariant: an unconditional break must IMMEDIATELY follow the run.**
/// The own-line arms emit each comment as REAL text, so a run-final `//` leaves its
/// line open until the caller's next break — and a later gap's `line_suffix` flushing
/// at that break lands welded onto it, invisible to the render-time run separator
/// (`doc/arena_render_suffix.rs`), which compares suffix against suffix, never
/// suffix against emitted text. Every current caller closes the line at once (a
/// `hardline`, or a `group_break`-forced `softline`) with nothing between that could
/// queue a suffix; a new call site that cannot guarantee that must defer the run
/// through the suffix machinery instead (the clause-body statement tail does —
/// `GapDeferral::LineSuffix` in `Printer::push_gap_comments`).
pub(crate) fn push_anchored_trailing_run(
&self,
parts: &mut DocBuf,
anchor: u32,
end: u32,
leading_blank: RunLeadingBlank,
) -> u32 {
let mut pos = anchor;
let mut prev_comment: Option<&Comment> = None;
for comment in self.comments_to_emit_between(anchor, end) {
if prev_comment.is_none() && !self.comment_has_newline_between(pos, comment.span.start)
{
// On the anchor's own line (`x /* c */`, `a && // c`): it trails there —
// a line comment via `line_suffix` (zero width, so a long trailing
// comment never forces the preceding group to break, matching prettier's
// `lineSuffix`), a block inline with its width counted.
parts.push(self.build_trailing_comment_doc(comment));
} else if prev_comment.is_none() && leading_blank == RunLeadingBlank::Drop {
// The run's first comment in a gap that drops the blank above it: the
// separator is the bare break, with no scan to make.
parts.push(self.d().hardline());
parts.push(self.build_comment_doc(comment));
} else {
// On its own line — unless the author GLUED it to the previous comment,
// which keeps that line; otherwise an author blank above it is preserved.
self.push_trailing_run_separator(parts, prev_comment, pos, comment.span.start);
parts.push(self.build_comment_doc(comment));
}
pos = comment.span.end;
prev_comment = Some(comment);
}
pos
}
/// Emit the whole gap between two comma-separated items when the gap contains a
/// **line** comment (the forced-break case): the comma, the comments, and the
/// break to the next item, leaving `parts` positioned to emit that item.
///
/// The gap decomposes at the comma. Block comments before the first line comment
/// trail the previous item inline (`= 0 /* c */`) and the comma is placed *before*
/// the first line comment — a line comment runs to EOL, so a comma after it would
/// be commented out. The first line comment then trails the comma iff it was
/// authored on the comma's line (`comma_pos` → no intervening newline). Everything
/// from there is the next item's **leading run**, emitted by the shared
/// [`push_leading_comment_run`](Self::push_leading_comment_run) toward
/// `next_start`, which also owns the final break: a block glued to the next item
/// hugs it (`/* c */ b`), anything else drops to its own line. The break between
/// the comma's line and the leading run is a bare `hardline` — prettier drops an
/// author blank line there (it belongs to the item join, not to the run).
///
/// Every break here takes its continuation indent from the doc tree — each caller's
/// run sits inside the list's own `indent()`. Shared by the variable-declarator,
/// for-init, and heritage inter-item sites.
///
/// Callers gate on the gap holding a line comment (`has_line_comments_between`) —
/// a block-only gap has no forced break and belongs to their own path.
pub(crate) fn push_inter_item_line_comment_gap(
&self,
parts: &mut DocBuf,
prev_end: u32,
comma_pos: u32,
next_start: u32,
) {
let d = self.d();
let comments: CommentVec<'_> = self
.comments_to_emit_between(prev_end, next_start)
.collect();
// Everything before the first line comment trails the previous item, and the
// comma is placed there rather than at its authored offset — a `//` runs to
// EOL, so a comma after it would be commented out, and any block between the
// two rides left with it (`a, /* c */ // x` → `a /* c */, // x`, matching
// prettier). With no line comment (the callers' gate makes that unreachable)
// this is 0 and the whole run simply leads the next item.
let first_line_idx = comments.iter().position(|c| !c.is_block).unwrap_or(0);
for comment in &comments[..first_line_idx] {
parts.push(d.text(" "));
parts.push(self.build_comment_doc(comment));
}
parts.push(d.text(","));
// The first line comment trails the comma when authored on the comma's line
// (no newline between); an own-line one starts the leading run below. The
// `is_block` test keeps this honest without leaning on the callers' gate:
// only a *line* comment can trail the comma, since a block there would be
// the caller's block-only path.
//
// The comma was just emitted AHEAD of this comment rather than at its authored
// offset, so a comment written before `comma_pos` still trails the printed comma
// — the side `comment_on_comma_line` deliberately answers `true` for.
let trails_comma = comments
.get(first_line_idx)
.is_some_and(|c| !c.is_block && self.comment_on_comma_line(comma_pos, c));
let run_start = if trails_comma {
parts.push(self.build_trailing_comment_doc(comments[first_line_idx]));
first_line_idx + 1
} else {
first_line_idx
};
parts.push(d.hardline());
self.push_leading_comment_run(
parts,
comments[run_start..].iter().copied(),
next_start,
LeadingGlue::Adjacent,
);
}
/// Whether a gap comment sits on the **comma's** line — the anchor every question about
/// that gap reads, since the comma is re-emitted structure that the printer pulls back
/// onto the previous item's line whatever the author did with it.
///
/// Deliberately not the *item*'s line: an author who pushed the comma onto a line of
/// its own (`a⏎, /* c */⏎ b`) wrote the comment against the comma, and an item-anchored
/// reading calls it own-line and hands it to the next item's leading run — re-binding it
/// to an item it was never written against.
///
/// ⚠️ **A comment-classification read, so it takes the COMMENT line-break table**
/// ([`Printer::comment_has_newline_between`], not the layout one). It decides whether a
/// `//` is emitted as a trailing `line_suffix` or falls to the next item's leading run,
/// and that role must stay real in the canonical reprint — where `layout_line_breaks`
/// is empty and every comment in the gap would read as on the comma's line, re-binding
/// an own-line comment the author wrote against the NEXT item onto the comma. Outside
/// [`Printer::set_canonical`] the two tables are the same slice, so the normal path is
/// byte-identical either way.
///
/// ⚠️ **A comment BEFORE `comma_pos` reads as on the comma's line**, and that is the
/// answer rather than a short-circuit to guard against: a caller in that position emits
/// the comma *ahead* of the comment instead of at its authored offset, so the comment
/// does trail the printed comma ([`Self::push_inter_item_line_comment_gap`] asks exactly
/// that). A caller that instead needs the **side of the authored comma** must test
/// `span.start >= comma_pos` itself — the parameter list does, at both ends of its
/// partition, because an own-line comment before the comma there belongs to a different
/// emitter entirely.
pub(crate) fn comment_on_comma_line(&self, comma_pos: u32, comment: &Comment) -> bool {
!self.comment_has_newline_between(comma_pos, comment.span.start)
}
/// Whether the gap opening at `gap_start` has already put a `//` on the output line by
/// the time it reaches `pos` — the point at which a claim past the separator must stop.
///
/// A `//` ends its output line whichever way the gap emitted it (an anchor-line one
/// leaves as a `line_suffix`, an own-line one as a deferred hardline run), so a second
/// comment claimed onto that line welds onto the first — the second delimiter becoming
/// text, the comment ceasing to exist and the code behind it swallowed — and an inline
/// block claimed after one comes out REORDERED ahead of it. Everything past that point
/// falls to the next item's leading run, on its own line, which is where prettier puts
/// it too.
///
/// ⚠️ **The question is the comment's KIND, never its anchor line.** Asking it with an
/// anchor reading misses an own-line `//` (`a /* x⏎y */ // c1⏎, // c2⏎ b`, where the
/// block's `*/` pushes `// c1` off the anchor's line) and welds the pair.
///
/// One spelling for both emitters that claim past a comma —
/// [`Printer::emit_multiline_comma_with_comments`] and
/// [`Printer::param_trailing_line_comment`] — since a drift between them is a weld at
/// whichever one drifted, and the weld is lossless-looking to every structural guard.
pub(crate) fn gap_emitted_line_comment_before(&self, gap_start: u32, pos: u32) -> bool {
self.comments_to_emit_between(gap_start, pos)
.any(|c| !c.is_block)
}
/// A block comment after the comma that sits on the comma's own line (no
/// newline between the comma and the comment) while a newline separates it
/// from the next item — a **stranded** comment. It trails the comma,
/// preserving the author's placement, rather than dropping to its own line;
/// prettier relocates it *before* the comma. Mirrors the call-argument
/// stranded rule (`calls/arg_comments.rs`). See conformance_prettier_ts_comments.md
/// §Comment relocation. (A block that instead *hugs* the next item — no
/// newline before it — leads that item and matches prettier, so it is not
/// stranded.)
pub(crate) fn is_stranded_after_comma_block(
&self,
comment: &Comment,
comma_pos: u32,
next_start: u32,
) -> bool {
comment.is_block
&& self.comment_on_comma_line(comma_pos, comment)
&& !self.is_same_line(comment.span.end, next_start)
}
/// Emit the **before-comma** block comments in `[start, comma_pos)` trailing
/// the preceding item (` /* c */`), preserving the author's side of the comma.
/// The caller pushes the comma after this. Shared by the variable-declarator,
/// for-init, and heritage inter-item sites; the after-comma counterparts are
/// [`Self::push_stranded_after_comma_blocks`] (stranded, trails the comma) and
/// the site's leading run (a block hugging the next item leads it).
pub(crate) fn push_before_comma_blocks(&self, parts: &mut DocBuf, start: u32, comma_pos: u32) {
let d = self.d();
for comment in self.comments_to_emit_between(start, comma_pos) {
parts.push(d.text(" "));
parts.push(self.build_comment_doc(comment));
}
}
/// Emit every comment in `[start, end)` trailing what precedes it
/// ([`Self::build_trailing_comment_doc`] — a block inline, a line comment via
/// `line_suffix` so it flushes at end of the rendered line rather than swallowing
/// the following token). For a gap whose comments all keep their position and need
/// no per-comment routing; a caller that must split the run by position (around a
/// separator, by own-line-ness) iterates itself.
///
/// Returns whether the run held a **line** comment — i.e. whether the caller must
/// break. That is the same walk, so asking it here spares the caller a second
/// binary search over the range it just scanned, and keeps the break question
/// keyed on what was actually EMITTED (an owned comment is skipped by both).
/// Callers whose following token can't be swallowed ignore it.
///
/// ⚠️ **The separator goes BEFORE each comment, and it is asked of the SOURCE.**
/// A comment the author left on its own line takes
/// [`Self::build_trailing_comment_doc_own_line`] instead, so a run of them stays a
/// run. Emitting them back to back with nothing between welds the run into a single
/// comment — the second `//` becomes text of the first (`// c1 // c2`), so the
/// second comment stops existing — and a block following a line comment additionally
/// **reorders** ahead of it, the inline form jumping the deferred one. Both are
/// stable, lossless-looking outputs that reparse, which is why the ledger, F1, the
/// round-trip and the fuzzer are all blind to them; only the census and a prettier
/// `compare` see it. This is the trailing-gap face of the rule in
/// [`Self::push_trailing_body_comments`] / [`Self::push_dangling_comment_run`]
/// — see [docs/comments.md](../../../../docs/comments.md) §Trailing and dangling
/// runs.
///
/// Asking the source ("did the author give this comment its own line?") rather than
/// the previous comment's kind is what keeps the two glued shapes intact: a run the
/// author wrote on one line (`/* c1 */ /* c2 */`) stays on one line, because a block
/// comment can share it. The kind test is the trap that formulation avoids.
pub(crate) fn push_trailing_comments_in_range(
&self,
parts: &mut DocBuf,
start: u32,
end: u32,
) -> bool {
self.push_trailing_comments_in_range_blank(parts, start, end, TrailingBlank::Drop)
}
/// [`Self::push_trailing_comments_in_range`] with the author-BLANK policy named — see
/// [`TrailingBlank`] for whose question it is. The plain spelling answers `Drop`, which
/// is every gap whose run keeps landing where it was written.
pub(in crate::printer) fn push_trailing_comments_in_range_blank(
&self,
parts: &mut DocBuf,
start: u32,
end: u32,
blank: TrailingBlank,
) -> bool {
self.push_trailing_comments_in_range_with(parts, start, end, blank, OwnLineBlock::Inline)
.has_line_comment
}
/// [`Self::push_trailing_comments_in_range_blank`] with the own-line BLOCK policy named
/// too — see [`OwnLineBlock`] for whose question it is — and both facts of the run
/// returned ([`TrailingRun`]). Every other spelling answers `Inline`, the collapse a
/// gap inside a construct wants; only the mapped member's terminator-spanning gap asks
/// for `Defer`.
pub(in crate::printer) fn push_trailing_comments_in_range_with(
&self,
parts: &mut DocBuf,
start: u32,
end: u32,
blank: TrailingBlank,
own_line_block: OwnLineBlock,
) -> TrailingRun {
let mut has_line_comment = false;
// Whether any comment of this run has been deferred yet — a `//`, or an own-line
// block under `Defer`. Distinct from `has_line_comment`, the caller's break
// question: a run a block opened under `Defer` holds no `//` and forces nothing.
let mut run_deferred = false;
// Cursor over what physically precedes each comment — an **in-source** question
// (docs/comments.md §the three axes), so it advances over every comment emitted
// here, not just the ones that broke.
let mut prev_end = start;
for comment in self.comments_to_emit_between(start, end) {
// The *comment* line-break table, never the layout one: this decides whether
// a `//` is followed by a break, so it must stay real under the canonical
// reprint, where an erased read would re-weld the run.
let own_line = self.comment_has_newline_between(prev_end, comment.span.start);
let is_line = !comment.is_block;
// A **line** comment is deferred by construction (it runs to EOL). A **block**
// one is deferred to stay BEHIND a comment already deferred in this run —
// deferring is what carries a comment out past the construct's closer, so a
// block that could simply sit inline must, or it leaves the parens it was
// written inside — or, under [`OwnLineBlock::Defer`], because the author gave
// it its own line. Once the run is deferred, every later comment rides the
// suffix too, own-line or glued (prettier's `printTrailingComment` threading
// `previousComment.hasLineSuffix`): an inline block would render *before* the
// deferred text and the pair would come out reordered (`V /* c7 */; // c5⏎/*
// c6 */` for `V; // c5⏎/* c6 */ /* c7 */`).
let deferred =
is_line || run_deferred || (own_line && own_line_block == OwnLineBlock::Defer);
parts.push(if deferred && own_line {
// The author BLANK directly above the comment, where the caller's
// destination keeps it: `isPreviousLineEmpty(locStart(comment))`, the same
// question every non-deferred trailing run asks
// ([`Self::push_adjacent_blank_hardline`]). `blank_scan_start` floors the
// backward walk so a comment already emitted in this gap cannot pass its
// own newlines off as the author's.
let blank_above = blank == TrailingBlank::Keep && {
let floor = self.blank_scan_start(prev_end, comment.span.start);
self.previous_line_is_empty(floor, comment.span.start)
};
self.build_trailing_comment_doc_own_line_blank(comment, blank_above)
} else if deferred {
// The kind-agnostic deferred spelling: a `//` by construction, and a block
// glued behind an already-deferred run.
self.build_trailing_line_comment_doc(comment)
} else {
self.build_trailing_comment_doc(comment)
});
has_line_comment |= is_line;
run_deferred |= deferred;
prev_end = comment.span.end;
}
TrailingRun {
has_line_comment,
deferred: run_deferred,
}
}
/// The break a comment-bearing construct owes at a seam of its own: a `hardline` when a
/// `//` makes that break an OBLIGATION, and `ordinary` — whatever break point the seam
/// would otherwise take — when it does not.
///
/// **One question, one emitter.** The askers sit at two kinds of seam and the ordinary
/// spelling differs between them, which is exactly why it is a parameter rather than a
/// constant here: a SHELL edge (the unary comment-holder's `(`…`)`, the computed/mapped
/// bracket's `[`…`]`, a template interpolation's `${`…`}`) takes a `softline`, and the
/// SEPARATOR between list elements (the sequence expression's `,`) takes a `line`. The
/// obligated answer is the same at both, and that is the half worth having one spelling
/// of.
///
/// Every asker sits inside a group that is already breaking — via `group_break`, or via
/// a `break_parent` the enclosing group's `fits` walk reads — so the two spellings
/// render identically *here*, and the difference only shows under a caller that
/// statically flattens the doc: [`tsv_lang::doc::arena::DocArena::remove_lines`], which
/// the Svelte whitespace-sensitive block head applies so a `<pre>` head cannot
/// width-wrap. That transform keeps hard lines but drops `BreakParent` and flattens soft
/// lines **inside a group whose break flag is already set** — so whichever way the
/// obligation was encoded as a *flag*, it survives with nothing left to break at, and
/// the construct renders welded. A break the content REQUIRES has to be a break
/// *point*, never a group flag.
///
/// Both edges of a shell ask it, for reasons that mirror each other, and so do both
/// PLACEMENTS of the comment:
///
/// - the OPENING edge, for a `//` in the delimiter→body gap. Glued to the delimiter the
/// comment rides its line and this break is what ENDS that line — without it the body
/// welds onto the comment (`{#if typeof ( // cast) === 'o'}`, `{#if a[b // c]}`:
/// output tsv's own parser rejects). Left OWN-LINE above the body it is what KEEPS the
/// comment's line — flattened, the run slides onto the delimiter, which the next pass
/// reads as glued and re-spaces, a two-pass document. Own-line-ness is a SOURCE
/// question ([`comments.md`](../../../../docs/comments.md)), so that collapse is a
/// relocation rather than a layout choice. One range covers both, since a glued
/// comment is still physically in the gap it left.
/// - the CLOSING edge, for a `//` in the body→closer gap. That run is deferred through a
/// `line_suffix`, so flattened it carries past the closer entirely — the run escaping
/// the construct it was written in, which is content loss rather than a layout choice.
///
/// A list SEPARATOR is the closing edge's question asked between elements: the previous
/// element's deferred trailing run flushes at this break, so flattening it carries that
/// run past every remaining element and out of the construct
/// (`{#each (a // c⏎, b) as x}` → the `//` landing after `</pre>`, where it is page text).
///
/// ⚠️ **The LAST trailing gap of a construct is a different question, and this is the
/// wrong instrument for it.** Every asker above has content on both sides of the break;
/// a `//` written before the construct's own closer has none, can never render inside it,
/// and so needs no break *here* — forcing one produces a layout the next pass will not
/// reproduce, the comment having left the construct by then. What may still be owed is
/// the ENCLOSING construct's break, so the suffix flushes inside it instead of escaping,
/// and that is [`tsv_lang::doc::arena::DocArena::flush_break`]: emitted after the run, it
/// arms only a line opportunity reached past it, leaving this construct's own lines to
/// width. See [`comments.md`](../../../../docs/comments.md) §A STATIC FLATTENER.
#[inline]
pub(crate) fn obligated_break(&self, obligated: bool, ordinary: DocId) -> DocId {
if obligated {
self.d().hardline()
} else {
ordinary
}
}
/// Split an OPENING DELIMITER's leading run at the author's GLUE: a **line** comment
/// written on the delimiter's own line is returned separately, for the caller to emit
/// right after it (`( // c`), and the rest of the run resumes below, inside the
/// construct. Returns `(glued, resume)`, where `resume` is where the remaining run
/// starts — `start` unchanged when nothing was glued.
///
/// This is the opening-delimiter rule, in one place: what the author put on the
/// `(`/`[`/`{`/`<` line stays there, and what they put on its own line keeps its own
/// line, so both authorings are fixed points
/// (`Printer::push_leading_comment_run`'s glue test, asked here of the delimiter
/// rather than of the item). Only a `//` can claim the line and only one can — it runs
/// to end of line — and only a comment the author actually glued; a **block** is
/// untouched, since it already renders inline inside the construct.
///
/// Its callers include the retained type paren shells, every **statement header's** `(`
/// (`if` / `while` / do-while / `with` / `switch` / `catch` and all three `for`
/// spellings) and the comment-bound pair (a JSDoc cast's or a glued block's,
/// `Self::build_comment_bound_pair_doc`). Prettier is no coherent oracle for the
/// position: it un-glues at `if`-like heads and the C-style `for`, **glues** at for-in /
/// for-of (emitting the rest of the head unindented at column zero), and relocates the
/// comment out of the parens entirely at do-while. See
/// [conformance_prettier_ts_comments.md §Comment relocation](../../../../docs/conformance_prettier_ts_comments.md#comment-relocation).
///
/// The leading `" "` is the PRINTER's and rides in the returned doc, never the
/// author's — `fn(// c` and `fn( // c` both normalize to one space
/// ([`comments.md`](../../../../docs/comments.md) §The delimiter-line question). A
/// caller therefore emits its bare delimiter text and concatenates this; writing
/// `"( "` itself is the rule spelled a second time, which is how a shell owning a
/// copy of this function comes to disagree with it.
///
/// The author blank BELOW the pulled comment rides with it, for every caller — see
/// [`Self::push_delimiter_glued_blank`], which this pushes and the list / call families
/// push for their own pull.
pub(crate) fn split_open_delimiter_glued_run(
&self,
start: u32,
end: u32,
) -> (Option<DocId>, u32) {
let d = self.d();
let Some(comment) = self.comments_to_emit_between(start, end).next() else {
return (None, start);
};
// ⚠️ The COMMENT line-break table, never the layout one: this is a comment-adjacency
// classification, so it must stay real under `Printer::set_canonical` — read against
// the emptied layout table it calls every own-line comment glued and relocates it
// onto the delimiter's line in the canonical reprint.
if comment.is_block || self.comment_has_newline_between(start, comment.span.start) {
return (None, start);
}
let mut parts = DocBuf::new();
parts.push(d.text(" "));
parts.push(self.build_comment_doc(comment));
// The run emitter below never sees this blank, because the comment left its range.
self.push_delimiter_glued_blank(&mut parts, comment.span.end, end);
(Some(d.concat(&parts)), comment.span.end)
}
/// The author blank BELOW the comment(s) a pull moved onto an opening delimiter's line
/// — one `hardline` on top of the caller's own break, which is the blank itself.
///
/// **One rule for the whole family**, and the second half of the opening-delimiter rule
/// ([`comments.md`](../../../../docs/comments.md) §The delimiter-line question): a blank
/// between a comment and what follows it is authorship, and the pull moves the comment's
/// LINE, not its membership. Every leading-run emitter in the printer already keeps this
/// blank ([`Self::push_leading_comment_run`],
/// [`Self::push_paren_shell_leading_run`]) and prettier keeps it at every construct —
/// call, `new`, `[`, `{`, `if`, `return (`, `yield` — from its own un-glued placement.
///
/// ⚠️ It is NOT a two-valued axis (a per-family `DelimiterGluedBlank`) with the list and
/// call families dropping the blank on the reading that a blank directly under a container's
/// opening line is that container's LEADING-GAP blank. Two things sink that reading. The
/// null control it rests on (`fn(⏎⏎a)`, no comment) measures a different gap — delimiter→
/// content, which both formatters clear here too — while the blank in question is
/// comment→content, which tsv itself keeps from the un-glued authoring of the same
/// comment. And the premise is true only *after* tsv's own pull puts the comment on that
/// line, so the blank's fate would turn on whether the author had glued the comment to the
/// delimiter: `fn(⏎// c⏎⏎a)` kept it and `fn( // c⏎⏎a)` did not, one authored blank with
/// two fates.
///
/// A blank between the delimiter and the FIRST comment is not this question and stays
/// erased: it sits against the delimiter, where tsv and prettier drop it at every
/// bracket alike.
///
/// `pulled_end` is the end of the last comment the pull emitted; `gap_end` the first
/// item's start. The scan stops at the next comment, so a blank further down the run
/// belongs to that comment's own leading rule rather than to the pull.
pub(crate) fn push_delimiter_glued_blank(
&self,
parts: &mut DocBuf,
pulled_end: u32,
gap_end: u32,
) {
let next_start = self
.comments_to_emit_between(pulled_end, gap_end)
.next()
.map_or(gap_end, |c| c.span.start);
// ⚠️ The scan measures a DISTANCE, so it must not span a delimiter glyph the
// author put on a line of its own. At most sites the delimiter precedes the
// comment (`fn( // c`), but the unary comment-holder's gap opens at the OPERATOR
// (`!! // c⏎(⏎x`), so the `(`'s own line break lands inside the range and the two
// newlines read as an author blank — a fabricated blank line, which is its own
// fixed point and reparses, so only a prettier `compare` or the fabrication audit
// over real code finds it (`docs/comments.md` hazard 5). Ending the scan at the
// glyph also gives the right answer for a blank BELOW it: that one sits against
// the delimiter, where every bracket in both formatters drops it.
// `blank_scan_end` first, because the scan must not span comment BYTES either —
// an owned comment in this gap is skipped by the emit axis but still occupies its
// interior newlines (hazard 5's other half). It also makes the byte scan below
// comment-free by construction.
let scan_end = self.blank_scan_end(pulled_end, next_start);
let scan_end = self.first_open_delimiter_in(pulled_end, scan_end);
if self.has_blank_line_between(pulled_end, scan_end) {
parts.push(self.d().hardline());
}
}
/// The first `(` / `[` / `{` physically in `[start, end)`, or `end` — the bound a
/// blank-line scan across a delimiter-glued gap takes
/// ([`Self::push_delimiter_glued_blank`]).
///
/// A raw byte scan is sound because the caller hands a comment-free range
/// ([`Self::blank_scan_end`]), so no `(` inside a comment's text can be reached.
fn first_open_delimiter_in(&self, start: u32, end: u32) -> u32 {
self.source.as_bytes()[start as usize..end as usize]
.iter()
.position(|b| matches!(b, b'(' | b'[' | b'{'))
.map_or(end, |i| start + i as u32)
}
/// [`Self::split_open_delimiter_glued_run`] for a shell whose `(` is **located** by a
/// scan rather than known from the AST — the restricted-production hang
/// ([`Printer::build_restricted_production_paren_doc`]) and the ASI-gap operand shell
/// ([`Self::build_asi_operand_shell_doc`]). A gap with no `(` in it has no delimiter
/// line for a comment to claim, so the whole gap stays the leading run's.
///
/// The guard is all this adds: the rule itself is the one function above, so the two
/// shells cannot answer differently about the same authoring — and no longer differ
/// from every other delimiter either, which they did on all three of the space, the
/// blank and the line-break table.
///
/// ⚠️ `gap_start` is the gap's own start, NOT `open_paren + 1`, and it is both the scan
/// start and the glue anchor. The two coincide, but for different reasons per caller and
/// neither is a free choice:
/// - Scanning from `gap_start` is required. The restricted-production gap opens at the
/// KEYWORD, so a comment before the `(` (`return /* c */ (x)`) is in it and has no
/// other emitter; starting the scan at the `(` would drop it, and gluing past it would
/// reorder it.
/// - Measuring from `gap_start` is safe. It differs from `open_paren + 1` only if a
/// newline sits between the two, and all three restricted productions forbid one
/// (`return` / `throw` / `yield` + a line terminator is ASI, so the argument is gone);
/// the ASI shell's `gap_start` IS `open + 1`. A multi-line *block* in that region
/// cannot reach the measurement either — it is the gap's first comment, and a block
/// declines the glue outright.
pub(crate) fn split_located_paren_glued_run(
&self,
gap_start: u32,
open_paren: Option<u32>,
value_start: u32,
) -> (Option<DocId>, u32) {
if open_paren.is_none() {
return (None, gap_start);
}
self.split_open_delimiter_glued_run(gap_start, value_start)
}
/// Whether a block-comment run in `[start, end)` forces a break of its own — a block
/// the author isolated on its own line, or a run the author BROKE AFTER
/// ([`Self::broke_after_value_leading_run`]) holding a multi-line block
/// ([`Self::run_holds_multiline_block`]). The second half is prettier's
/// `propagateBreaks` reading: the multi-line body cannot print flat, so every
/// separator in the run it belongs to opens, whichever comment carries the break.
/// It is the pair [`Self::breaking_value_leading_run`] takes, minus that pair's
/// `will_break` half — which needs a value doc, and no shell site has one.
///
/// ⚠️ **The break is the RUN's, not one comment's.** A per-comment reading
/// (`multi-line && !hugs_next`) answered no for `/* a⏎b */ /* c */⏎Y` — the author
/// glued the multi-line block ahead of a single-line one and broke after THAT — and
/// the run then welded onto the value's line at every seam it reaches
/// (`head_paren_shell_glued_run_broke_after_multiline_block_comment`).
///
/// ⚠️ **Three call sites take the WHOLE function, and everyone else takes the
/// broke-after HALF** ([`Self::broke_after_run_holds_multiline_block`]) — the split is
/// not incidental, it is the two sides of one seam. The whole function answers the
/// block half of the leading-edge **ownership** question (`EdgeRun::Breaking`, read by
/// `stripped_paren_hang_has_leading_run` in `types/mod.rs` — such a run in a stripped
/// shell is the ENCLOSING gap's to lay out, at that gap's continuation indent, since a
/// break the shell opens at its own indent is one the reparse re-lays), the conditional
/// BRANCH's twin of that claim (`branch_claim_and_start` in `types/composite.rs` — a
/// redundant shell that IS the `?`/`:` branch, holding such a run and no `//`, is
/// claimed whole so the branch gap hangs it at the branch's own indent) and the mapped
/// value's hang gate (`build_mapped_value_tail_doc`, also in `types/composite.rs` — a
/// run that forces no break keeps the inline `: /* c */ V`). The CLAIM must take the
/// isolated own-line block precisely because the enclosing gap COLLAPSES it: the
/// collapse has to be the gap's own, or the shell
/// prints a line the gap never asked for. The `//` half is each caller's, where it is
/// already asked; a run holding one declines the broke-after HALF, which takes blocks
/// alone — the isolated arm is per-comment, so a `//` beside an own-line block still
/// fires, and the mapped gate, which reads the whole with no `//` guard of its own,
/// hangs such a run as its own `//` arm would.
///
/// The stripped-shell run **emitter** ([`Self::push_paren_shell_leading_run`]) and the
/// keyword→value **HANG GATES** read the half instead, and for one reason between
/// them: neither may open a line the enclosing gap would close. The emitter prints
/// what no gap claims, so a soft `line` it opens for an isolated own-line block is one
/// pass 2 — reading the bare bytes, the shell gone — collapses. The gates answer
/// whether the gap hangs the value at all, a question each seam asks for itself, and
/// several ask it with [`Self::comments_force_own_line_between`] ALONE — which answers
/// no for exactly the run the broke-after half answers yes for, so a seam where the
/// claim fires and the gate does not prints the shell's soft `line` on pass 1 and
/// re-lays it on pass 2: the F1 shape docs/comments.md names. A seam whose gate reads
/// `comments_force_own_line_between` must therefore read a disjunct of the half over
/// the same window — the conditional `extends` at BOTH its arms
/// (`build_conditional_type_extends_doc`), the predicate `is`, the indexed access's
/// index (which needed a stripped-shell window built for it first, its gap having
/// none), and a type parameter's `extends` / `=` at both of that seam's gates
/// (`push_hang_or_inline_value` and `append_keyword_value` in `types/type_params.rs`).
/// The angle-bracket type ASSERTION's `<`→type gap
/// (`build_ts_type_assertion_doc` in `expressions/mod.rs`) reads the half as a ROUTER
/// rather than a disjunct: its `//` is already answered above it, so the half alone
/// decides whether that gap hands its run to
/// [`Self::build_trailing_comments_hang_next`] or to the per-comment
/// [`CommentSpacing::Trailing`] spacing, which welded the type onto the run's closing
/// line. `hang_annotation_union_doc` (`types/type_annotation.rs`) spells the half
/// inline rather than calling it, because it needs the RUN itself to hand the forced
/// emitter, not the boolean.
///
/// `single_type_arg_run_breaks_after` (`types/type_arguments.rs`) is deliberately NOT a
/// reader: its gap is a LIST's, so it takes the wider rule — every block the author
/// broke after, single-line included — and the two must not be unified.
pub(crate) fn block_run_forces_break(&self, start: u32, end: u32) -> bool {
self.block_comment_isolated_own_line_between(start, end)
|| self.broke_after_run_holds_multiline_block(start, end)
}
/// The **broke-after** half of [`Self::block_run_forces_break`], alone: a block run in
/// `[start, end)` the author BROKE AFTER ([`Self::broke_after_value_leading_run`])
/// holding a multi-line block ([`Self::run_holds_multiline_block`]). Prettier's
/// `propagateBreaks` reading — the multi-line body cannot print flat, so every
/// separator in the run opens, whichever comment carries it.
///
/// This is the half a keyword→value **hang gate** adds to
/// [`Self::comments_force_own_line_between`], the half the stripped-shell run emitter
/// ([`Self::push_paren_shell_leading_run`]) opens its soft `line` for, and the half the
/// type-construct gap emitter ([`Self::build_trailing_comments_hang_next`]) reads
/// beside its own per-comment [`Self::comment_hangs_next`], so every gap that emitter
/// serves drops the following token below a run whose break the run itself forces.
/// The sole type argument's own shell loop (`build_type_doc_for_type_arg`'s
/// `Parenthesized` arm) reads it for one narrower thing: whether an author BLANK below
/// a comment survives, which it does only where the `line` under it is always open. This
/// half is deliberately NOT the whole function: the isolated own-line block is one those gaps
/// DELIBERATELY COLLAPSE (`x is⏎/* a */⏎T` prints `x is /* a */ T`, pinned by
/// `predicate_is_own_line_block_comment`), so a gate that hung it — or an emitter that
/// opened a line for it — would print what the gap's own rule then takes back on the
/// next pass. The broke-after run is the half where the two disagreed: the shell's
/// claim takes it, so the gap must lay it out or pass 2 re-lays what pass 1 printed.
pub(crate) fn broke_after_run_holds_multiline_block(&self, start: u32, end: u32) -> bool {
self.broke_after_value_leading_run(start, end)
.is_some_and(|run| self.run_holds_multiline_block(&run))
}
/// Emit a paren SHELL's leading run — the comments in `[start, end)` between the
/// shell's `(` and the type it wraps — each followed by its own separator: a
/// `hardline` for a line comment, which must end its line or it would swallow the type
/// after it, and for a block the answer `shell` gives ([`ShellPair`]).
/// Returns whether the run forced a line, i.e. whether the shell must break.
///
/// Inside a pair that SURVIVES, the block half is prettier's `printLeadingComment` read
/// straight: a newline both before and after a block makes its separator a `hardline`,
/// one on neither side or on the leading side alone a space. tsv has no
/// `TSParenthesizedType` in prettier's AST to disagree about, and prettier keeps such a
/// comment inside the parens too — so a shell that glued it was the one place an
/// authored line went missing for standing inside parens.
///
/// ⚠️ A pair that is **stripped** takes the space at every single-line block instead
/// ([`ShellPair::Stripped`]); only a broke-after run holding a MULTI-LINE block
/// ([`Self::broke_after_run_holds_multiline_block`]) takes prettier's soft `line`, at
/// every separator the author broke — and that is not a second opinion about the comment: with
/// the parens gone the layout is the ENCLOSING gap's, which either claims the run
/// ([`Printer::keyword_value_stripped_paren_hang`], the seam that also knows the
/// continuation indent) or lays the value out by its own rule — and several of those
/// gaps deliberately COLLAPSE an isolated own-line block (the annotation `:`, the
/// prefix operators, a conditional's `extends`). A hardline emitted here at the shell's
/// own indent is neither answer: it is the placement the bare authoring never produces,
/// so the reparse re-laid it and the format stopped being idempotent. The MEMBER gaps
/// left that set and claim instead — see the ⚠️ on [`ShellPair::Stripped`], which is
/// why this emitter now sees only runs the collapse is right for.
///
/// `line_comments` names who owns them ([`ShellLeadingRun`]):
/// [`ShellLeadingRun::Upstream`] where an emitter above already placed them (the
/// later-member paren-union arms of `build_union_type_doc_with_line_comments`), so
/// emitting here would double-print them; the block arm always emits.
///
/// ⚠️ Still not [`Self::push_leading_comment_run`], the canonical leading emitter —
/// a shell's separator is the delimiter's question, not the item's — but no longer
/// keyed on the comment's own KIND alone, which was the whole of the difference. Both
/// of that emitter's what-follows rules now hold here too: the **glue** test (asked
/// one step earlier, by [`Self::split_open_delimiter_glued_run`], since a glued
/// comment belongs to the `(` line the caller owns) and **blank-line preservation**
/// below. The shells were the last leading-run sites deciding a separator without
/// asking what follows, and each half was a real difference from every other opening
/// delimiter: `fn(`, `[`, `{` and `<` keep both the glue and the blank, and prettier
/// keeps the blank at all of them too.
pub(crate) fn push_paren_shell_leading_run(
&self,
parts: &mut DocBuf,
start: u32,
end: u32,
line_comments: ShellLeadingRun,
shell: ShellPair,
) -> bool {
let d = self.d();
let mut forced_line = false;
let run: CommentVec<'_> = self.comments_to_emit_between(start, end).collect();
// Asked ONCE, of the whole run, and of the broke-after HALF alone
// ([`Self::broke_after_run_holds_multiline_block`]) — not of
// [`Self::block_run_forces_break`], whose other half, the isolated own-line block,
// is one the enclosing gaps deliberately COLLAPSE. Opening a line here for a run
// the gap will close is the F1 shape from the other side: pass 1 prints the
// shell's soft `line`, pass 2 reads the bare bytes and collapses it. The CLAIM
// reads the whole function on purpose — it must own an isolated block so the gap
// that collapses it is the one doing the collapsing — while this emitter must
// never open a line the gap would not.
let run_forces_break =
shell == ShellPair::Stripped && self.broke_after_run_holds_multiline_block(start, end);
for (i, comment) in run.iter().enumerate() {
// What follows this comment: the next one, or the type the shell wraps.
let next_start = run.get(i + 1).map_or(end, |c| c.span.start);
let ends_its_line = if comment.is_block {
shell == ShellPair::Kept
&& self.block_comment_isolated_own_line(comment, next_start)
} else if line_comments == ShellLeadingRun::Here {
true
} else {
continue;
};
parts.push(self.build_comment_doc(comment));
if !ends_its_line {
// A broke-after run holding a MULTI-LINE block, in a pair that strips,
// takes prettier's `printLeadingComment` soft `line` at every separator
// the AUTHOR broke ([`Self::comment_hugs_next`]) rather than a space: the
// multi-line body cannot print flat, so the enclosing gap's group opens
// the run and the value drops below it exactly as the shell-free authoring
// lays it out. The break belongs to the run, not to one comment — a
// multi-line block the author glued ahead of a single-line one they broke
// after (`/* a⏎b */ /* c */⏎Y`) forces the break from `c`'s separator,
// which a per-comment reading answered with a space and welded.
//
// The space stays inside the run wherever the author glued (their own
// line), at every SINGLE-line block — every value gap pulls one the author
// merely broke after up onto the operator's line
// (`LeadingGlue::AdjacentGlued`), and an isolated own-line block is one
// those gaps COLLAPSE, so the shell must land where the bare authoring
// does — and inside a pair that SURVIVES, where the delimiter owns the
// line ([`ShellPair`]). Keyed on the gaps the author wrote, so every
// layout this emits reproduces its own trigger.
if run_forces_break && !self.comment_hugs_next(comment) {
parts.push(d.line());
// The author BLANK below the comment survives here too, exactly as it
// does at the own-line arm below and at every other opening delimiter.
// `printLeadingComment` pushes its blank `hardline` after whichever
// separator it chose — the soft `line` included — and the run's break
// is forced, so the `line` this blank sits on always opens. Dropping
// it made the shelled authoring of a blanked run disagree with the
// bare twin, which keeps it (tuple element, union first member, sole
// type argument, intersection later member, tuple rest).
if self.has_blank_line_between(comment.span.end, next_start) {
parts.push(d.hardline());
}
} else {
parts.push(d.text(" "));
}
continue;
}
parts.push(d.hardline());
forced_line = true;
// An author blank between this comment and what follows it is authorship, not
// shell structure, and every other opening delimiter keeps it (`fn(`, `{`, `[`,
// where prettier keeps it too). Dropping it here made the shells the one
// place tsv erased a blank line both its own siblings and prettier
// preserve. A blank between the `(` and the FIRST comment is a different
// question and stays erased: it sits against the delimiter, where every
// formatter — tsv and prettier alike, at every bracket — drops it.
if self.has_blank_line_between(comment.span.end, next_start) {
parts.push(d.hardline());
}
}
forced_line
}
/// Emit the comment run in `[anchor, end)` PRESERVING each comment's own-line-ness —
/// the emission a routed format-ignore directive needs, since the placement that
/// earned the freeze is exactly what an ordinary hang emitter would relocate.
/// Returns `(trailing, own_line)`: a comment still on `anchor`'s line trails it, and
/// every own-line comment keeps its own line, each preceded by a `hardline`. Both
/// sinks are the caller's to place — the conditional's `?` arm trails the extends
/// line (a separate buffer) while its `:` arm and the function type's pre-arrow gap
/// trail into the run they are already building.
///
/// A gap's same-line comments are exactly the prefix of its run (position and line
/// number are both monotonic), so splitting the run across two sinks preserves
/// source order at every call site.
///
/// Deliberately NOT [`Self::push_leading_comment_run`]: that seam measures each
/// separator against the run's terminal, but what follows this run in the OUTPUT is
/// an operator (`?` / `:` / `=>`), not the node the comments lead — so its glue test
/// would hug a block comment whose `*/` merely shares a line with that node's start,
/// pulling a directive off the own line that earned the freeze. The same-line test
/// here is a ROUTING question (trail the previous line vs. sit above the operator's
/// line), not a separator question.
pub(crate) fn build_own_line_preserving_run(&self, anchor: u32, end: u32) -> (DocId, DocBuf) {
let d = self.d();
let mut trailing_parts = DocBuf::new();
let mut own_line_parts = DocBuf::new();
for comment in self.comments_to_emit_between(anchor, end) {
if self.is_same_line(anchor, comment.span.start) {
trailing_parts.push(self.build_trailing_comment_doc(comment));
} else {
own_line_parts.push(d.hardline());
own_line_parts.push(self.build_comment_doc(comment));
}
}
(d.concat(&trailing_parts), own_line_parts)
}
/// Emit the **stranded** after-comma block comments in `[comma_pos, next_start)`
/// trailing the comma (` /* c */`), preserving the author's placement. The
/// caller pushes the comma before this and handles the remaining (non-stranded)
/// after-comma comments as leading comments on the next item. Shared by the
/// variable-declarator, for-init, and heritage inter-item sites; see
/// [`Self::is_stranded_after_comma_block`].
///
/// Returns `Some(end)` — where the next item's **leading** run resumes, past the
/// stranded prefix — or `None` when nothing was stranded, leaving the caller's own
/// anchor standing. The stranded set is a contiguous prefix of the gap (each member
/// sits on the comma's line, and the first that doesn't ends it), so the two halves
/// partition it: a caller that emits the run and then resumes before it DOUBLE-PRINTS
/// the stranded blocks. Returning the anchor is what keeps the two from being written
/// apart — the same coupling [`Self::push_item_trailing_run`] states at the other end
/// of the gap. A caller with no leading emitter of its own ignores it.
///
/// ⚠️ **`None` is not `comma_pos`.** The gap opens at the previous item's claimed
/// trailing run, which ends BEFORE the comma, so a caller handed `comma_pos` on the
/// empty case resumes past `[run_end, comma)` — and a comment the author put on its own
/// line there (`x: T⏎/* c */, y: U`) is claimed by neither half and DROPPED. Measured,
/// by `gaps:audit`, on the change that introduced it.
pub(crate) fn push_stranded_after_comma_blocks(
&self,
parts: &mut DocBuf,
comma_pos: u32,
next_start: u32,
) -> Option<u32> {
let d = self.d();
let mut resume = None;
for comment in self.comments_to_emit_between(comma_pos, next_start) {
if !self.is_stranded_after_comma_block(comment, comma_pos, next_start) {
break;
}
parts.push(d.text(" "));
parts.push(self.build_comment_doc(comment));
resume = Some(comment.span.end);
}
resume
}
/// Build a Doc for inline comments between two positions with specified spacing and filter
///
/// Returns a Doc containing all comments in the range with the specified spacing.
/// Returns empty concat if no comments found.
///
/// Uses binary search to find starting point: O(log n + k)
pub(crate) fn build_comments_between(
&self,
start: u32,
end: u32,
spacing: CommentSpacing,
) -> DocId {
self.build_comments_between_filtered(start, end, spacing, CommentFilter::All)
}
/// Build a Doc for inline comments with filtering
// `track_caller` (ledger builds only): the `BlockOnly` skip annotation below records
// the caller that held the filter licence, and this wrapper must be transparent to it.
#[cfg_attr(feature = "_internal_comment_check", track_caller)]
pub(crate) fn build_comments_between_filtered(
&self,
start: u32,
end: u32,
spacing: CommentSpacing,
filter: CommentFilter,
) -> DocId {
self.build_comments_between_filtered_opt(start, end, spacing, filter)
.unwrap_or_else(|| self.d().empty())
}
/// Build a Doc for inline comments with filtering, returning None if no comments.
///
/// This is more efficient than `has_comments_to_emit_between` + `build_comments_between`
/// because it uses a single binary search instead of two.
///
/// ⚠️ **`Leading` emits no separator AFTER the run — the caller owns what follows.**
/// The separators here sit *between* comments, so a run whose last comment is a `//`
/// ends mid-line and the caller's next token is **swallowed** by it. `Trailing` has no
/// such hole (its separator is emitted after each comment, `hardline` for a line one),
/// which is why an identical gap can be correct under one spacing and lossy under the
/// other — `call // c⏎<T>()` versus the callee→`(` gap that produced `call // c();`,
/// eating the parens and the `;` (fixture
/// `calls/callee_line_comment_empty_args_prettier_divergence`).
///
/// A `Leading` caller must therefore answer "what follows this run?" itself: gate on
/// [`Self::has_line_comments_between`] and route to
/// [`Self::build_continuation_indent`] (or push its own break) before falling through
/// to this builder. `build_empty_args_parens_doc` and `build_dot_gap_doc` are the worked examples.
/// The swallowed form parses and is a fixed point, so idempotency, round-trip, and the
/// print-once ledger are all blind to it; `swallow_audit` sees only shapes some fixture
/// already carries.
#[cfg_attr(feature = "_internal_comment_check", track_caller)]
pub(crate) fn build_comments_between_filtered_opt(
&self,
start: u32,
end: u32,
spacing: CommentSpacing,
filter: CommentFilter,
) -> Option<DocId> {
// The `BlockOnly` licence, checked (ledger builds only): record every line comment
// this filter passes over, with the caller that held the licence (`track_caller`).
// The licence is a promise that a gate routed line comments to the expansion
// builder first; nothing else enforces that the gate is the exact complement of
// what this filtered builder can express. Annotation only — the ledger's drain
// surfaces the site solely on a comment that ends the format DROPPED; a skip whose
// comment another emitter prints (the routed expansion path, a winning
// `conditional_group` sibling) stays invisible. See
// `comment_ledger::record_filtered_skip`.
#[cfg(feature = "_internal_comment_check")]
if matches!(filter, CommentFilter::BlockOnly)
&& tsv_lang::comment_ledger::comment_check_enabled()
{
let site = std::panic::Location::caller();
for c in self.comments_to_emit_between(start, end) {
if !c.is_block {
tsv_lang::comment_ledger::record_filtered_skip(self.source, c.span, site);
}
}
}
let d = self.d();
// Check if any comments exist in range (considering filter)
let has_comments = self
.comments_to_emit_between(start, end)
.any(|c| !matches!(filter, CommentFilter::BlockOnly) || c.is_block);
if !has_comments {
return None;
}
// Build docs for matching comments.
//
// A line comment ends its line, so the next comment in the run must start a new
// one — else two line comments merge onto one (`// c1 // c2` reparses as a single
// comment: boundary loss). So a `hardline`, not the spacing separator, sits across
// any line-comment boundary *within* the run. The run's far edge is the caller's
// (see the ⚠️ on this function): `Leading` adds nothing after the last comment,
// `Trailing` hardlines. A block
// comment keeps the inline spacing.
let mut parts = DocBuf::new();
let mut prev_was_line = false;
let mut prev_end: Option<u32> = None;
let mut first = true;
for comment in self.comments_to_emit_between(start, end) {
// Apply filter
if matches!(filter, CommentFilter::BlockOnly) && !comment.is_block {
continue;
}
// An authored blank line between two comments that each occupy their own
// line separates two distinct remarks, exactly as a blank between two
// statements does, so it survives (`conformance_prettier_ts_comments.md` §"No
// blank above a body block's `{`"). Only meaningful where the separator is a `hardline`:
// an inline run has no lines to separate.
let blank_before = prev_was_line
&& prev_end
.is_some_and(|p| self.has_blank_line_between_strict(p, comment.span.start));
match spacing {
CommentSpacing::Leading => {
// Separator before this comment: the surrounding-indent `hardline`
// after a line comment (no leading space — it starts the line),
// else the inline leading space.
if !first && prev_was_line {
if blank_before {
parts.push(d.literalline());
}
parts.push(d.hardline());
} else {
parts.push(d.text(" "));
}
parts.push(self.build_comment_doc(comment));
}
CommentSpacing::Trailing => {
parts.push(self.build_comment_doc(comment));
self.push_comment_kind_separator(&mut parts, comment);
}
CommentSpacing::None => {
if !first {
if prev_was_line {
if blank_before {
parts.push(d.literalline());
}
parts.push(d.hardline());
} else {
// A block comment doesn't end its line, so the next comment
// still needs an explicit separator — without one the run
// fuses into `/* a *//* b */`. `None` suppresses the
// *leading* space before the run, not the separators inside
// it.
parts.push(d.text(" "));
}
}
parts.push(self.build_comment_doc(comment));
}
}
prev_was_line = !comment.is_block;
prev_end = Some(comment.span.end);
first = false;
}
Some(d.concat(&parts))
}
/// Build a Doc for inline comments between two positions (leading space)
#[inline]
pub(crate) fn build_inline_comments_between_doc(&self, start: u32, end: u32) -> DocId {
self.build_comments_between(start, end, CommentSpacing::Leading)
}
/// Build a Doc for trailing comments where a comment that forces the following
/// content onto its own line gets a hardline after it, and every other comment
/// collapses inline (a space, as `build_comments_between(_, _, Trailing)` does).
///
/// The separator is [`Printer::comment_hangs_next`] — the same per-comment
/// rule as the gate that selects this builder
/// ([`Printer::comments_force_own_line_between`]), so a gate and its emitter can't
/// answer differently. Two shapes hang: a **line** comment (a `//` would swallow
/// the following content) and an **own-line multiline** block (inlining it would
/// reflow the author's break). A single-line block in any position, and a glued
/// multiline block, collapse.
///
/// Use across a gap whose following token must not be swallowed or reflowed — the
/// type-construct delimiter/keyword gaps (`: // leading\nT`,
/// an indexed access's `[`→index, a template-literal type's `${`→type).
pub(crate) fn build_trailing_comments_hang_next(&self, start: u32, end: u32) -> DocId {
let d = self.d();
let mut parts = DocBuf::new();
// Asked ONCE, of the whole run, through the same function the widened hang GATES
// read ([`Self::broke_after_run_holds_multiline_block`]): a run holding a
// multi-line block the author broke after cannot print flat, so every separator
// the AUTHOR broke opens, whichever comment of the run carries the block. The
// per-comment `comment_hangs_next` below answers no for both halves of
// `/* a⏎b */ /* c */⏎v` — the first is glued, the second single-line — so without
// this the gate selected the hang and the emitter welded the run back onto one
// line (docs/comments.md §Own-line-ness is a SOURCE question; the stripped-shell
// emitter states the same rule for its own run).
let run_forces_break = self.broke_after_run_holds_multiline_block(start, end);
let mut comments = self.comments_to_emit_between(start, end).peekable();
while let Some(comment) = comments.next() {
parts.push(self.build_comment_doc(comment));
let emit_next = comments.peek().map_or(end, |n| n.span.start);
// **in source**: both the hang question and the blank measure anchor on the
// physically next comment, not the next one *this* builder emits, so an
// owned comment sitting between two emitted ones can't desync them — and it
// is the same anchor the selecting gate uses
// (`comments_force_own_line_between` walks `comments_in_source_range`), so
// gate and emitter answer the one question identically.
let next = self.blank_scan_end_after(comment, emit_next);
// An honored directive owns its line on BOTH sides. The after-side is
// [`Self::comment_hangs_next`], which this asks of every comment; the
// before-side is this separator read one comment earlier — a single-line block
// ahead of a directive (`await /* c1 */⏎// prettier-ignore⏎v`) hangs nothing on
// its own, so without this the run renders `/* c1 */ // prettier-ignore` and the
// directive lands in the placement the floor calls INERT.
let next_is_directive = comments
.peek()
.is_some_and(|n| self.is_honored_directive(n));
if next_is_directive
|| self.comment_hangs_next(comment, next)
|| (run_forces_break && !self.comment_hugs_next(comment))
{
// An authored blank line survives wherever the break it separates
// survives. Here the break is FORCED — a `//` runs to end-of-line, an
// own-line multiline block would reflow — so the blank is authoring
// intent, not layout this continuation gets to collapse. Same answer as
// the value side of these very constructs
// (`build_comments_between_filtered_opt`'s blank arm); the two emitters
// must answer it the same way. See conformance_prettier.md §Authored
// breaks in value position. Only meaningful across a `hardline`: an
// inline run has no lines to separate.
self.push_blank_preserving_hardline(&mut parts, comment.span.end, next);
} else {
parts.push(d.text(" "));
}
}
// `concat` short-circuits the no-comments-in-range case to `empty()`.
d.concat(&parts)
}
/// Leading comment run for a conditional branch arm (`?`/`:` → branch value):
/// each comment takes a space when the next content shares its closing line
/// (`? /* c */ v` stays glued), else `soft_sep` — the caller's collapsible
/// line, so an authored break after the comment holds when the conditional is
/// broken and yields when it is flat. This is prettier's `printLeadingComment`
/// separator, except its own-line `hardline` case is deliberately not
/// mirrored: tsv re-glues an own-line comment to the operator, so a hardline
/// keyed on the authored newline *before* the comment would collapse on the
/// second pass (prettier itself is non-idempotent there), and the
/// §Authored-breaks-in-value-position rule collapses the fitting form anyway.
/// Separator anchors ride the physical next comment
/// ([`Self::blank_scan_end`]) so an owned comment glued to the value can't
/// desync them.
///
/// Line comments never reach this run — both conditional printers route them
/// to their breaking layouts — and a line comment's collapsible separator
/// would swallow the branch, so that routing is load-bearing.
///
/// Returns `None` when the gap has no comments to emit.
pub(crate) fn build_branch_comment_run(
&self,
start: u32,
end: u32,
soft_sep: DocId,
) -> Option<DocId> {
let d = self.d();
let mut parts = DocBuf::new();
let mut comments = self.comments_to_emit_between(start, end).peekable();
while let Some(comment) = comments.next() {
debug_assert!(
comment.is_block,
"line comments belong to the breaking layout"
);
parts.push(self.build_comment_doc(comment));
let emit_next = comments.peek().map_or(end, |n| n.span.start);
let next = self.blank_scan_end_after(comment, emit_next);
if self.comment_hugs_next(comment) {
parts.push(d.text(" "));
} else {
// An author blank after the comment is itself a break trigger
// (prettier breaks the conditional on it too), so the break is
// forced and the blank survives — the conditional-branch
// carve-out in conformance_prettier.md §Authored breaks in
// value position. The expression printer routes blank gaps to
// its breaking layout before building a run
// (`comment_followed_by_blank`), so that arm serves the
// conditional-type branches. Without a blank the caller's
// collapsible `soft_sep` stands — the one thing that makes this
// run differ from `push_leading_run_separator`.
self.push_blank_preserving_separator(&mut parts, comment.span.end, next, soft_sep);
}
}
if parts.is_empty() {
None
} else {
Some(d.concat(&parts))
}
}
/// Prepend an optional leading doc (a comment run) to `doc`; `None` passes
/// `doc` through untouched, keeping the comment-free path allocation-free.
pub(crate) fn prepend_opt(&self, lead: Option<DocId>, doc: DocId) -> DocId {
match lead {
Some(lead) => self.d().concat(&[lead, doc]),
None => doc,
}
}
/// Append an optional trailing doc to `doc` — [`Self::prepend_opt`]'s other side;
/// `None` passes `doc` through untouched.
pub(crate) fn append_opt(&self, doc: DocId, tail: Option<DocId>) -> DocId {
match tail {
Some(tail) => self.d().concat(&[doc, tail]),
None => doc,
}
}
/// Build a Doc for inline comments, returning None if no comments.
///
/// Use this instead of `has_comments_to_emit_between` + `build_inline_comments_between_doc`
/// to avoid redundant binary searches.
#[inline]
pub(crate) fn build_inline_comments_between_doc_opt(
&self,
start: u32,
end: u32,
) -> Option<DocId> {
self.build_comments_between_filtered_opt(
start,
end,
CommentSpacing::Leading,
CommentFilter::All,
)
}
/// Build a Doc for inline comments between two positions (no spaces)
#[inline]
pub(crate) fn build_inline_comments_between_doc_no_leading_space(
&self,
start: u32,
end: u32,
) -> DocId {
self.build_comments_between(start, end, CommentSpacing::None)
}
/// Build a Doc for inline comments between two positions (trailing space)
///
/// Used when comments appear before an element and need a space after.
/// Example: `{a, /* comment */ b}` - the comment needs a space after it.
#[inline]
pub(crate) fn build_inline_comments_between_doc_trailing_space(
&self,
start: u32,
end: u32,
) -> DocId {
self.build_comments_between(start, end, CommentSpacing::Trailing)
}
/// Build a Doc for inline comments (trailing space), returning `None` if no comments.
///
/// The `_opt` sibling of `build_inline_comments_between_doc_trailing_space`, matching
/// the ones the other two spacings already have. Callers that push into a parts
/// buffer want this rather than the `DocId` form: the `empty()` it would otherwise
/// return is not free — `concat` keeps it as a child slot for the renderer and every
/// `fits` pass to walk.
#[inline]
pub(crate) fn build_inline_comments_between_doc_trailing_space_opt(
&self,
start: u32,
end: u32,
) -> Option<DocId> {
self.build_comments_between_filtered_opt(
start,
end,
CommentSpacing::Trailing,
CommentFilter::All,
)
}
/// Build inline comments between two positions with line-comment-safe trailing spacing.
///
/// A block comment keeps the following value (or next comment) on its `*/`
/// line when the source did (`/* comment */ expr`), and stays on its own line
/// when the source broke (`/* comment */\nexpr`) — the author's layout is
/// preserved. Line comments always get a hardline (`// comment\nexpr`) so they
/// can't absorb the value as comment text.
/// Use for any position where a comment appears before an expression (RHS of `=`,
/// after keywords like `return`/`await`, after operators like `!`/`...`, etc.).
pub(crate) fn build_rhs_comments_opt(&self, start: u32, end: u32) -> Option<DocId> {
self.build_leading_comment_run_opt(start, end, LeadingGlue::Adjacent)
}
/// Like `build_rhs_comments_opt`, but an author blank line after a glued block's
/// `*/` yields with the soft `line` instead of forcing the comment onto its own
/// line — [`LeadingGlue::AdjacentValueGap`], the head→value gap rule. Use at a
/// value gap (`=`, `:`, `as`, a keyword); a list gap stays on
/// [`build_rhs_comments_opt`](Self::build_rhs_comments_opt), where a blank
/// separates two items and is preserved.
pub(crate) fn build_value_gap_comments_opt(&self, start: u32, end: u32) -> Option<DocId> {
self.build_leading_comment_run_opt(start, end, LeadingGlue::AdjacentValueGap)
}
/// [`Self::build_value_gap_comments_opt`] on the **on page** axis — the run INCLUDING
/// a comment the value owns.
///
/// The one caller is [`Self::hoist_owned_value_gap_run`], which pairs it with the
/// suppression that keeps the value from printing the same comment again; read that
/// function for why the seam hoists at all. Nothing else may call this: on every other
/// gap the emit axis is the correct one, and an on-page reading there double-prints.
pub(crate) fn build_hoisted_value_gap_comments_opt(
&self,
start: u32,
end: u32,
) -> Option<DocId> {
let mut parts = DocBuf::new();
self.push_leading_comment_run(
&mut parts,
self.comments_on_page_between(start, end),
end,
LeadingGlue::AdjacentValueGap,
);
(!parts.is_empty()).then(|| self.d().concat(&parts))
}
/// Like `build_rhs_comments_opt`, but a single-line block comment glued to the
/// operator (not on its own line) hugs the value with a space even when the
/// value follows on the next source line — prettier pulls the value up in the
/// assignment/call layout (`= /* c */⏎v` → `= /* c */ v`). Positions that keep
/// the author's line break for a glued block stay on the non-gluing
/// `build_rhs_comments_opt` — a decorator is the clear case (`@dec /* c */⏎class`),
/// since its following declaration owns its own line regardless.
///
/// ⚠️ Don't grow an example list here without probing each entry: `await`
/// operands and object property values look like they keep the break, and
/// **both actually collapse** (`await /* c */ x`, `k: /* c */ 1`).
/// The gluing/non-gluing split is a property of each call site, so the call sites
/// are the source of truth, not a list here.
///
/// `return`/`throw` arguments pull up here too, but for a stronger reason than
/// layout: they are restricted productions, so keeping the break would be ASI and
/// would change the program. See `build_keyword_argument_doc`.
///
/// ⚠️ **The pull-up is prettier's soft `line` collapsing, so it holds only while the run
/// can print flat.** A run that pushes a hardline of its own — a comment after the glued
/// one owning its line (`= /* c1 */⏎/* c2 */⏎v`), or a `//` — breaks the group that
/// `line` rides, and prettier's `propagateBreaks` breaks it too
/// (`=⏎/* c1 */⏎/* c2 */⏎v`, an author blank after `c1` kept). Such a run is rebuilt
/// in the plain [`LeadingGlue::Adjacent`] mode, whose `line` breaks with the hardline
/// beside it in the same concat. Welding it instead merged the pair on one pass
/// (`= /* c1 */ /* c2 */⏎v`) and collapsed the value onto it on the next.
pub(crate) fn build_rhs_comments_glued_opt(&self, start: u32, end: u32) -> Option<DocId> {
let glue = self.rhs_comments_glued_mode(start, end)?;
self.build_leading_comment_run_opt(start, end, glue)
}
/// The glue [`build_rhs_comments_glued_opt`](Self::build_rhs_comments_glued_opt) builds its
/// run in: the glued mode, or the plain one when the glued run would push a `hardline`.
/// `None` when the range holds no comment to emit.
fn rhs_comments_glued_mode(&self, start: u32, end: u32) -> Option<LeadingGlue> {
let (_, forces_break) = self.leading_run_tail_separator(
self.comments_to_emit_between(start, end),
end,
LeadingGlue::AdjacentGlued,
)?;
Some(if forces_break {
LeadingGlue::Adjacent
} else {
LeadingGlue::AdjacentGlued
})
}
/// The separator [`build_rhs_comments_glued_opt`](Self::build_rhs_comments_glued_opt)
/// prints after its run's LAST comment — read in that builder's own mode
/// ([`Self::rhs_comments_glued_mode`]) through [`Self::leading_run_tail_separator`],
/// without building the run. `None` when the range holds no comment to emit.
pub(crate) fn rhs_comments_glued_tail_separator(
&self,
start: u32,
end: u32,
) -> Option<LeadingSeparator> {
let glue = self.rhs_comments_glued_mode(start, end)?;
self.leading_run_tail_separator(self.comments_to_emit_between(start, end), end, glue)
.map(|(separator, _)| separator)
}
/// Emit a run of leading comments before `terminal_pos` — the value, member,
/// item, or body the comments lead. Each comment is emitted with
/// `build_comment_doc`, followed by one of three separators — prettier's
/// `printLeadingComment` (`src/main/comments/print.js`), which reads only the
/// source around *this* comment, never where `terminal_pos` is:
///
/// - **space** — no newline after the `*/` (per `glue`): the comment is glued to
/// what follows, so it leads it inline (`/* c */ X`). A run the author glued
/// together therefore stays glued (`/* a */ /* b */⏎X`).
/// - **`line`** — a newline after the `*/` but none before the `/*`: soft, so what
/// follows pulls up onto the comment's line when the enclosing group fits and
/// drops below when it breaks.
/// - **`hardline`** — a newline on *both* sides (an own-line comment), or any line
/// comment (it must break, or it would absorb what follows). Blank-preserving:
/// an author blank line before the value / next comment is kept, matching
/// prettier everywhere in this "comment before expression" position (RHS of
/// `=`/`:`, call args, `return`/`await`, unary operands, …).
///
/// `continuation` is emitted after each break, for a site whose run is not already
/// inside a `d.indent()` and so must carry explicit `INDENT` text (the
/// variable-declarator gap); every other site passes `None`, and nothing is pushed —
/// an `empty()` pushed here would ride the enclosing concat through every later walk.
///
/// **Returns whether the run pushed a `hardline`** — i.e. whether it ends a line
/// unconditionally. Only the third separator does; the space and the soft `line`
/// both leave the enclosing group free to stay flat. A site that must open its
/// own shell around a forced break needs that answer and cannot re-derive it:
/// tsv has no `propagateBreaks`, so a hardline buried in this run is invisible to
/// the group that would have to break for it ([`Printer::build_paren_leading_value_doc`]
/// is the caller, and the import parens are the shell). Re-deriving it caller-side
/// is the same three-way rule spelled a second time, which is what
/// `docs/comments.md` §Leading comments exists to prevent — so the emitter reports
/// on its own output instead. Callers with no shell to open ignore it.
///
/// The single leading-comment emitter: every site that puts comments before an
/// item routes here, so the rule lives once. Behind
/// [`build_rhs_comments_opt`](Self::build_rhs_comments_opt),
/// [`build_rhs_comments_glued_opt`](Self::build_rhs_comments_glued_opt), the
/// arrow-body run, the member-leading sites (interface / intersection members),
/// the comma-separated inter-item gaps (declarators, for-init, heritage,
/// switch cases), the breaking chain gap's leading run
/// (`chain::push_gap_comments_and_break`), the forced-multiline lists via
/// [`build_leading_comments_multiline`](Self::build_leading_comments_multiline)
/// (tuples, type params/args, function-type params, the union's first member, the
/// bracket-break shell, the broken `<T>` cast), the array literal / array pattern
/// element runs, the **non-block** header→body gap across `if` / `while` / the
/// C-style `for` / for-in / for-of / `do`
/// ([`Printer::push_indented_header_to_body_gap`]), the body/member runs via
/// [`push_leading_comments_before`](Self::push_leading_comments_before) (class,
/// interface and enum members, statement lists, type literals, expanded object
/// patterns), the broken for-in/for-of header's `(`→binding gap, and — for all but
/// its last comment —
/// [`push_orphaned_comment_run`](Self::push_orphaned_comment_run).
///
/// ⚠️ **This list is a WORK-LIST, not a summary** — an unlisted loop is a bug report,
/// and both halves of that have already been paid here (the for-clause leading gap
/// converged onto this emitter and stayed listed as a hand-roller; the labeled
/// statement's run was a hand-roller and was never listed). **Four** loops still emit a
/// leading run themselves, because their surrounding separator policy genuinely
/// differs:
///
/// - the **import/export specifier list** (`specifier_list.rs`);
/// - the **union's inter-member run** (`types/union_intersection.rs`
/// `push_union_member_leading_run` / `build_member_leading_block_comments`), which
/// brackets the `| ` separator and preserves blanks in different positions;
/// - the **control-flow gap builder's** `GapCommentRun::Leading` arm
/// (`Printer::build_comments_between_parts`, the **block**-body header→body gaps and
/// the `else` gap) — a between-parts shape whose separators go BEFORE each comment
/// and whose caller owns the tail. Its **non-block** twin
/// (`Printer::push_indented_header_to_body_gap`) is not among them: that gap is the
/// body's leading run outright and routes here;
/// - the **labeled statement's relocated name→`:` run** (`try_jump.rs`
/// `build_labeled_statement_doc`), whose last comment must break whatever the source
/// says — the run has been relocated off the label it would otherwise hug.
///
/// Each calls [`comment_hugs_next`](Self::comment_hugs_next) rather than re-deriving
/// the rule, so what differs there is the loop, never the decision.
pub(crate) fn push_leading_comment_run<'c>(
&self,
parts: &mut DocBuf,
comments: impl Iterator<Item = &'c Comment>,
terminal_pos: u32,
glue: LeadingGlue,
) -> bool {
let d = self.d();
let mut forces_break = false;
let mut is_first = true;
let mut comments = comments.peekable();
while let Some(comment) = comments.next() {
parts.push(self.build_comment_doc(comment));
let (separator, next) = self.leading_comment_separator(
comment,
comments.peek().copied(),
terminal_pos,
glue,
is_first,
);
is_first = false;
match separator {
LeadingSeparator::Space => parts.push(d.text(" ")),
LeadingSeparator::Line => parts.push(d.line()),
LeadingSeparator::Hard => {
self.push_blank_preserving_hardline(parts, comment.span.end, next);
forces_break = true;
}
}
}
forces_break
}
/// What [`push_leading_comment_run`](Self::push_leading_comment_run) would print after
/// the run's LAST comment, and whether any separator in the run is a `hardline` — the
/// emitter's own decision ([`Self::leading_comment_separator`]), read without building
/// the run. `None` for an empty run.
///
/// For the layout question a seam must answer BEFORE it picks the builder that emits
/// the run: whether the item the run leads still shares the last comment's line in the
/// output — the reparse's reading of that line. A sole multiline template the call hug
/// prints behind a comment run is the case (`sole_template_on_its_own_line`, in
/// `calls::arg_wrapping`).
pub(crate) fn leading_run_tail_separator<'c>(
&self,
comments: impl Iterator<Item = &'c Comment>,
terminal_pos: u32,
glue: LeadingGlue,
) -> Option<(LeadingSeparator, bool)> {
let mut is_first = true;
let mut any_hard = false;
let mut last = None;
let mut comments = comments.peekable();
while let Some(comment) = comments.next() {
let (separator, _) = self.leading_comment_separator(
comment,
comments.peek().copied(),
terminal_pos,
glue,
is_first,
);
is_first = false;
any_hard |= separator == LeadingSeparator::Hard;
last = Some(separator);
}
last.map(|separator| (separator, any_hard))
}
/// The separator [`push_leading_comment_run`](Self::push_leading_comment_run) prints
/// after `comment` — the one statement of the run's three-way rule, read by the emitter
/// and by [`Self::leading_run_tail_separator`] — plus the blank-scan end its `hardline`
/// preserves an author blank up to. `peek` is the run's next comment.
fn leading_comment_separator(
&self,
comment: &Comment,
peek: Option<&Comment>,
terminal_pos: u32,
glue: LeadingGlue,
is_first: bool,
) -> (LeadingSeparator, u32) {
// See `LeadingGlue::AdjacentAnchorLine`: only the run's first comment can have
// its authored newline-before erased, and only at that one site.
let first_shares_anchor_line = is_first && glue.first_shares_anchor_line();
let first_opens_own_line = is_first && glue.first_opens_own_line();
// The next thing after this comment — the following comment, or the
// terminal (value/member/item/body) for the last one. Anchored on the
// PHYSICAL next comment, not just the emitted one: an owned comment (glued
// to the value, so printed by the value's node and skipped by the emit
// iterator) still occupies the gap here, and both the glue test and the
// blank-line scan below are physical questions. Anchoring past it would
// unglue a run the author wrote glued (`/* a */ /* b⏎*/ v` → `/* a */` on
// its own line) and, worse, read the owned comment's own newline as an
// author blank line — inserting one on the next pass (non-idempotent).
// Owned comments are always the glued suffix of a leading run, so this
// only ever differs at the last emitted comment; bounding `blank_scan_end`
// at the emit-next keeps it from over-reaching a caller's filtered set.
let next = self.blank_scan_end_after(comment, peek.map_or(terminal_pos, |c| c.span.start));
// An honored format-ignore directive next in the run keeps its line whatever
// the source around THIS comment says (`Self::directive_follows`).
let directive_next = self.directive_follows(peek);
let hugs = !directive_next
&& match glue {
// `AdjacentValueGap` differs from `Adjacent` only in the blank-line
// rule below, not in the hug test — the soft `line` is the point at a
// value gap (it lets a value too long for the comment's line break
// below it), so it must not become an unconditional space.
LeadingGlue::Adjacent
| LeadingGlue::AdjacentValueGap
| LeadingGlue::AdjacentAnchorLine
| LeadingGlue::AdjacentOwnLine => self.comment_hugs_next(comment),
// A glued (not own-line) single-line block hugs across a source
// newline; the hugs-what-follows case still hugs as in `Adjacent`.
// ⚠️ That first half asks the comment's own neighbours, never the
// distance to `next`: for the LAST comment `next` is the value's span
// start, which sits INSIDE any grouping paren the author wrote, so a
// break they put after the `(` read as a break after the comment and
// un-glued a run prettier keeps glued (`= /* c */ (⏎v)`).
LeadingGlue::AdjacentGlued => {
comment.is_block
&& (self.comment_hugs_next(comment)
|| !self.comment_cannot_glue_to_operator(comment))
}
// `Adjacent`, plus the stripped grouping paren the author glued the
// comment to (`/* c */ (⏎…`) — invisible in the output, so the newline it
// left behind must not un-glue the pair.
LeadingGlue::AdjacentStrippedParen => {
self.comment_hugs_next(comment)
|| (comment.is_block
&& calls::has_stripped_paren_gap(self.source, comment.span.end, next))
}
};
let separator = if hugs {
// Value (or next comment) shares the `*/` line — keep it glued.
LeadingSeparator::Space
} else if !directive_next
&& comment.is_block
// `first_shares_anchor_line ||` reads as "this comment HAS no own line to
// keep, so don't ask" — and short-circuits the source scan when so.
&& (first_shares_anchor_line
|| !(first_opens_own_line || self.is_own_line_comment(comment)))
&& !(glue.blank_forces_own_line()
&& self.has_blank_line_between_strict(comment.span.end, next))
{
// A block with a newline *after* its `*/` but none before its `/*`:
// prettier's `printLeadingComment` emits a soft `line` here, so what
// follows pulls up onto the comment's line when the enclosing group
// fits and drops below when it breaks. An own-line block (newline on
// both sides) takes the `hardline` branch instead.
//
// Whether a **blank** line after the `*/` overrides that and forces the
// hardline is per-site (`LeadingGlue::blank_forces_own_line`): it does
// in a list, where a blank between items is ordinary authoring tsv
// preserves, and does not in a value gap, where the blank sits inside a
// break already judged unforced.
LeadingSeparator::Line
} else {
// Line comment, or an own-line block: keep them on separate lines
// (preserve the author's layout; a line comment must break so it
// can't absorb the value).
LeadingSeparator::Hard
};
(separator, next)
}
/// Push a leading block-comment run the author broke after
/// ([`Self::broke_after_value_leading_run`]: `/* c */⏎<value>`, or
/// `/* c1 */⏎/* c2 */ <value>`) whose break is **forced** — the value's doc will break,
/// or a comment in the run is multi-line — the break-materialized form of prettier's
/// `printLeadingComment` newline-after soft `line`. Prettier gets the materialization free
/// from `propagateBreaks` (a hard break marks every enclosing group broken, so each `line`
/// renders as a break); tsv has no `propagateBreaks`, so the caller asks
/// [`Self::broke_after_run_break_is_forced`] and routes here only when it holds — every
/// separator the author broke is a (blank-preserving) `hardline` because the break
/// is already forced, and forced is what lets it carry an author blank. The
/// width-driven half of the same rule — nothing forced, the seam itself
/// deciding — is [`hang_after_operator_run_doc`](Self::hang_after_operator_run_doc),
/// whose soft `line` rides the seam's own hang group and absorbs the blank
/// with the unforced break. A value that FITS behind single-line comments collapses the
/// `line` to a space in both formatters, so the caller's glued path already renders that
/// case.
///
/// Every separator is asked of the comment's own neighbour
/// ([`Self::push_leading_run_separator`]), never of the value: a pair the author
/// glued keeps its space, and a last comment glued to the value — only an earlier one
/// broke — keeps the value on its line. The caller supplies the value doc after this
/// returns.
pub(crate) fn push_leading_run_before_breaking_value(
&self,
parts: &mut DocBuf,
comments: &[&'a Comment],
value_start: u32,
) {
if let Some(last) =
self.push_breaking_value_run_body(parts, comments, BrokeAfterBreak::Forced)
{
self.push_leading_run_separator(parts, last, value_start);
}
}
/// Push a broke-after run's interior and its last comment, leaving the final
/// separator toward the value to the caller — the shared body of
/// [`push_leading_run_before_breaking_value`](Self::push_leading_run_before_breaking_value)
/// (the forced separators), and
/// [`hang_after_operator_run_doc`](Self::hang_after_operator_run_doc) and
/// [`Self::push_leading_run_with_soft_line`] (the soft ones, via
/// [`push_leading_comment_run`](Self::push_leading_comment_run)). Returns the last
/// comment so the caller can anchor that separator.
///
/// ⚠️ **The interior is not glue-only.** A run whose LAST comment is glued to the value
/// is a broke-after run too (`= /* c1 */⏎/* c2 */ v`), and its break lives between two
/// comments; reading the interior as a space welds `/* c1 */ /* c2 */` onto the head's
/// line whatever the value does.
fn push_breaking_value_run_body(
&self,
parts: &mut DocBuf,
comments: &[&'a Comment],
brk: BrokeAfterBreak,
) -> Option<&'a Comment> {
let (last, interior) = comments.split_last()?;
match brk {
BrokeAfterBreak::Forced => {
for (i, comment) in interior.iter().enumerate() {
parts.push(self.build_comment_doc(comment));
self.push_leading_run_separator(parts, comment, comments[i + 1].span.start);
}
}
BrokeAfterBreak::Soft => {
self.push_leading_comment_run(
parts,
interior.iter().copied(),
last.span.start,
LeadingGlue::AdjacentValueGap,
);
}
}
parts.push(self.build_comment_doc(last));
Some(last)
}
/// The soft half's separator after a broke-after run's LAST comment: the glued space
/// when the author glued it to the value (an earlier comment broke), else the soft
/// `line` that breaks with the enclosing group.
fn push_soft_run_tail(&self, parts: &mut DocBuf, last: &Comment) {
let d = self.d();
parts.push(if self.comment_hugs_next(last) {
d.text(" ")
} else {
d.line()
});
}
/// The `=`-seam emission for a broke-after run before a value that **will
/// break** — prettier's break-after-operator body, everything after the
/// operator in one indent: a hardline, the run
/// ([`Self::push_leading_run_before_breaking_value`]), then the value. The
/// type-alias, variable-declarator, and assignment-expression `=` seams share
/// it; the `:` seam emits the run un-indented on the caller's own line
/// instead and does not.
pub(crate) fn break_after_operator_run_doc(
&self,
run: &[&Comment],
value_start: u32,
value_doc: DocId,
) -> DocId {
let d = self.d();
let mut indent_content = DocBuf::new();
indent_content.push(d.hardline());
self.push_leading_run_before_breaking_value(&mut indent_content, run, value_start);
indent_content.push(value_doc);
d.indent(d.concat(&indent_content))
}
/// The width-keyed sibling of [`Self::break_after_operator_run_doc`], for a
/// broke-after run whose break is NOT forced ([`Self::broke_after_run_break_is_forced`]:
/// no hard break in the value, no multi-line comment in the run): the run,
/// its newline-after soft `line`, and the value inside ONE
/// `hang_after_operator` group, so the separator materializes exactly when
/// the operator seam itself breaks — prettier's `printLeadingComment` `line`,
/// which rides the enclosing assignment group and breaks with it (its
/// hard-break half comes free from `propagateBreaks`, and that half is
/// [`Self::break_after_operator_run_doc`]'s job here). Flat, the group
/// renders the glued bytes (`= /* c */ value`); broken, the run holds its
/// own line and the value — its own group — re-fits below
/// (`=⏎\t/* c */⏎\tvalue`), exploding further only when that line also
/// overflows. An author blank after the run yields with the unforced break
/// ([`LeadingGlue::AdjacentValueGap`]'s rule), unlike the hard sibling,
/// whose forced break carries it.
fn hang_after_operator_run_doc(&self, run: &[&'a Comment], value_doc: DocId) -> DocId {
let d = self.d();
let mut content = DocBuf::new();
if let Some(last) =
self.push_breaking_value_run_body(&mut content, run, BrokeAfterBreak::Soft)
{
self.push_soft_run_tail(&mut content, last);
}
content.push(value_doc);
tsv_lang::doc::after_operator::hang_after_operator(d, d.concat(&content))
}
/// Push a broke-after run and its newline-after soft `line` — the
/// group-riding form of the same rule
/// ([`push_leading_run_before_breaking_value`](Self::push_leading_run_before_breaking_value)
/// is the hard-break form, [`hang_after_operator_run_doc`](Self::hang_after_operator_run_doc)
/// the seam-grouped one). The caller places the run inside an existing group
/// (the arrow's ternary-body parens), so the `line` materializes exactly when that
/// group breaks — the value's own hard break, or width — and collapses to the glued
/// bytes when it stays flat. That is prettier's `printLeadingComment` `line` riding
/// the enclosing group, which its `propagateBreaks` marks broken for either reason.
pub(crate) fn push_leading_run_with_soft_line(&self, parts: &mut DocBuf, run: &[&'a Comment]) {
if let Some(last) = self.push_breaking_value_run_body(parts, run, BrokeAfterBreak::Soft) {
self.push_soft_run_tail(parts, last);
}
}
/// Does no comment in a broke-after run OWN its line — a newline on both sides of it,
/// the one case prettier's `printLeadingComment` answers with a `hardline` rather than a
/// `line` or a space? The soft-separator gate of the value gaps
/// ([`Self::value_gap_soft_broke_after_run`], [`Self::broke_after_operator_rhs_doc`]).
///
/// ⚠️ **Not "every comment on the opener's line".** That reading declines a glued run the
/// author gave its own line (`=⏎/* c1 */ /* c2 */⏎v` — `c1` hugs, `c2` has content before
/// it), and the caller's glue path then welds the run to a breaking value
/// (`/* c1 */ /* c2 */ {`), a form the next pass reads differently from this one. It
/// declines the `(shell)` spelling too (`= /* c1 */⏎/* c2 */ ({…})`): `c2` is glued to a
/// paren the printer strips, so nothing owns it, and it sits in the run on a line of its
/// own.
fn no_comment_owns_its_line(&self, run: &[&'a Comment]) -> bool {
run.iter()
.all(|c| self.comment_follows_content_on_its_line(c) || self.comment_hugs_next(c))
}
/// [`Self::broke_after_value_leading_run`] restricted to a run that may ride a value
/// gap's soft separator ([`Self::no_comment_owns_its_line`]) — the arrow body's soft
/// arms, which build the soft half without a hard sibling of their own.
pub(crate) fn value_gap_soft_broke_after_run(
&self,
opener: u32,
value_start: u32,
) -> Option<CommentVec<'a>> {
let run = self.broke_after_value_leading_run(opener, value_start)?;
self.no_comment_owns_its_line(&run).then_some(run)
}
/// [`Self::break_after_operator_run_doc`] or
/// [`Self::hang_after_operator_run_doc`], chosen by whether the run's break is forced
/// ([`Self::broke_after_run_break_is_forced`]) — the two-half dispatch the operator seams
/// share (the type-alias,
/// declarator, and assignment `=`). The arrow's hug arm asks the hard half
/// alone: its glued hug already renders the width case.
pub(crate) fn break_or_hang_after_operator_run_doc(
&self,
run: &[&'a Comment],
value_start: u32,
value_doc: DocId,
) -> DocId {
if self.broke_after_run_break_is_forced(run, value_doc) {
self.break_after_operator_run_doc(run, value_start, value_doc)
} else {
self.hang_after_operator_run_doc(run, value_doc)
}
}
/// The whole declarator / assignment-expression `=` broke-after arm —
/// prettier's break-after-operator (`chooseLayout`'s `hasLeadingOwnLineComment`
/// arm) in the type-alias seam's two-half form: the geometry gate
/// ([`Self::broke_after_value_leading_run`]), the value built through the
/// caller's own builder, then `Some(rhs_doc)` when either half applies —
/// a value carrying a hard break takes the blank-preserving hardline emitter
/// (any run authoring: the own-line emission is the preserved own-line shape), as does a
/// run holding a multi-line comment ([`Self::broke_after_run_break_is_forced`]);
/// anything else one hang group whose soft `line` breaks exactly when the
/// operator seam does, restricted to a run that may ride a soft separator
/// ([`Self::no_comment_owns_its_line`]: no comment in it owns its line).
/// A value that FITS behind single-line comments collapses to the glued bytes in both
/// formatters; a
/// width-broken one holds the run on its own line with the value re-fitting
/// below instead of stranding it mid-line (`⏎\t/* c */ aaa +`, a form
/// prettier never emits). An author blank yields with the soft half's break. Declines
/// — a line comment (its mandatory-break path owns it), a run holding an own-line
/// comment before a value with no hard break — keep the caller's layouts, which
/// preserve those breaks; the value doc built for a declined probe is discarded, as
/// the combined gate's is.
/// The returned doc begins after the operator; the caller emits the LHS and
/// the operator text before it.
pub(crate) fn broke_after_operator_rhs_doc(
&self,
gap_start: u32,
value_start: u32,
build_value: impl FnOnce() -> DocId,
) -> Option<DocId> {
let run = self.broke_after_value_leading_run(gap_start, value_start)?;
let value_doc = build_value();
(self.d().will_break(value_doc) || self.no_comment_owns_its_line(&run))
.then(|| self.break_or_hang_after_operator_run_doc(&run, value_start, value_doc))
}
/// The geometry half of the broke-after value gate: `Some(run)` when the
/// `[gap_start, value_start)` gap holds a block-only leading run the author broke
/// after — its last comment broke after (`/* c */⏎v`), or its last is a single-line
/// block glued to the value and some comment before it broke (`/* c1 */⏎/* c2 */ v`).
/// Where the value's node OWNS that glued tail (an expression) the returned run is
/// `[c1]` and the value's doc prints `c2`; where nothing owns it (a type) the run
/// carries it, and its separator is the glued space. Anything else — an empty gap, a
/// line comment (those routes own their own mandatory-break layouts), a run glued
/// through to the value — declines, and the caller keeps its glued path, whose flat
/// render is the same bytes prettier's collapsed `line` produces. Asks nothing of the
/// value doc: the caller pairs a hit with
/// [`Self::break_after_operator_run_doc`] when the value `will_break` (the
/// hard materialization) and with [`Self::hang_after_operator_run_doc`]
/// otherwise (the width-keyed one); a caller whose other layouts already
/// render the width-driven case takes the combined
/// [`Self::breaking_value_leading_run`] instead.
///
/// ⚠️ The broke-after question is asked against the **physical** next thing in
/// the gap, not the emit-set's view of it: an OWNED comment glued to the value
/// (`= /* c */ /* m⏎ */ v`) is printed by the value's doc and absent from the
/// emit run, but it still occupies the gap — measured against `value_start`,
/// the emitted run's tail reads as broken-after when the author glued the whole
/// run. So the blank scan's in-source ceiling stops at the owned comment, and the
/// owned tail must be glued through to the value
/// ([`Self::owned_tail_glued_to_value`]): it is then the second half of the run, which
/// broke after only if an earlier comment did — `c` glued to `m` above did not, so that
/// gap declines. Declining a real one welded it (`= /* c1 */⏎/* c2 */ v`): the glued path
/// puts a space after every comment, so `c1`'s break vanished whatever the value did.
///
/// ⚠️ **A MULTI-line owned tail is taken too, and every consumer owes the claim.** Its
/// doc answers `will_break`, and that answer is right: prettier breaks `c1`'s separator
/// around a multi-line comment of either kind, indentable or preserved, and keeps an
/// author blank there. What is wrong is the comment breaking the VALUE's own group,
/// which it does from the innermost node that owns it (`= /* c1 */⏎/* c2⏎*/ a ? b : c`
/// exploded the ternary) — so each consumer builds its value with the comment claimed
/// outside that group ([`Self::build_value_with_outermost_owned_comment`]).
///
/// ⚠️ **The glue test asks the comment's own NEIGHBOURS, never the distance to the
/// value** ([`Self::comment_hugs_next`], prettier's `hasNewline(text,
/// locEnd(comment))` — spaces and tabs skipped, nothing else). The distance
/// reading (`is_same_line(last.end, value_start)`) is blind to anything the printer
/// erases or the value's own shell contributes: a **paren** standing between the run
/// and the value took the author's break INSIDE those parens (`= /* c */ (⏎v)`) and
/// reported it as a break after the comment, so the seam broke a value prettier
/// hugs. It reached three seams off this one predicate — the arrow `=>`, the
/// declarator/assignment `=`, and `await` — because the paren need not even be
/// stripped: an object arrow body keeps its required parens and the inner node's
/// span still starts past the `(`
/// (`docs/comments.md`, and the standing rule that a bound derived from a node span
/// is not the printed edge).
///
/// Nearly every value gap holds no comment — no run, so no break after one — and that is
/// answered inline at the caller ([`Self::gap_known_comment_free`]); only a gap a search
/// must answer reaches the outlined collect.
#[inline]
pub(crate) fn broke_after_value_leading_run(
&self,
gap_start: u32,
value_start: u32,
) -> Option<CommentVec<'a>> {
if self.gap_known_comment_free(gap_start, value_start) {
return None;
}
self.broke_after_value_leading_run_wide(gap_start, value_start)
}
/// The search half of [`Self::broke_after_value_leading_run`] — one outlined copy: the
/// collect and its classification.
#[inline(never)]
fn broke_after_value_leading_run_wide(
&self,
gap_start: u32,
value_start: u32,
) -> Option<CommentVec<'a>> {
let comments: CommentVec<'a> = self
.comments_to_emit_between(gap_start, value_start)
.collect();
let last = comments.last()?;
if !comments.iter().all(|c| c.is_block) {
return None;
}
let tail_start = self.blank_scan_end_after(last, value_start);
if tail_start == value_start && !self.comment_hugs_next(last) {
return Some(comments);
}
// The run ends in a comment glued to the value, so it broke after only if an earlier
// separator did. That comment is OWNED when the value's node prints it (an expression:
// the emit run stops short of it) and un-owned otherwise (a type position, or a
// comment glued to a grouping paren whose strip discards the node that would own it).
let glued_tail = if tail_start == value_start {
self.comment_glued_through_open_parens(last, value_start)
} else {
self.owned_tail_glued_to_value(tail_start, value_start)
};
(glued_tail && comments.iter().any(|c| !self.comment_hugs_next(c))).then_some(comments)
}
/// Whether the in-source remainder `[tail_start, value_start)` of a value gap is exactly
/// one OWNED block comment glued through to the value
/// ([`Self::comment_glued_through_open_parens`]) — the glued second half of a broke-after
/// run ([`Self::broke_after_value_leading_run`]), single- or multi-line.
fn owned_tail_glued_to_value(&self, tail_start: u32, value_start: u32) -> bool {
let mut tail = self.comments_in_source_between(tail_start, value_start);
let Some(owned) = tail.next() else {
return false;
};
owned.owned_by_node
&& tail.next().is_none()
&& self.comment_glued_through_open_parens(owned, value_start)
}
/// Whether `comment` is glued to the value at `value_start`, possibly through grouping
/// parens the author wrote around it (`/* c */ (v)`, stripped, or an arrow's object body
/// `/* c */ ({…})`, kept): nothing but spaces and tabs after its `*/`
/// ([`Self::comment_hugs_next`]), then nothing but those parens and the whitespace
/// inside them.
///
/// ⚠️ **A break INSIDE the parens is the parens' own layout, not a break after the
/// comment** — the glue is asked of the comment's neighbour, as at every other gap.
/// Reading `/* c2 */ (⏎{…})` as unglued welded a run whose earlier comment broke
/// (`= /* c1 */⏎/* c2 */ (⏎{…})` → `= /* c1 */ /* c2 */ {`) where its bare twin and
/// prettier break. It does not make `= /* c */ (⏎v)` a broke-after run: no comment there
/// broke, which is the caller's other conjunct. Without the parens allowed at all, the
/// shell authoring welded where its bare twin broke — two fixed points for one layout.
fn comment_glued_through_open_parens(&self, comment: &Comment, value_start: u32) -> bool {
self.comment_hugs_next(comment)
&& self.source.as_bytes()[comment.span.end as usize..value_start as usize]
.iter()
.all(|b| matches!(b, b' ' | b'\t' | b'\n' | b'('))
}
/// [`Self::broke_after_value_leading_run`] plus the forced-break question:
/// `Some((run, value_doc))` only when the run's break is forced
/// ([`Self::broke_after_run_break_is_forced`] — the value doc, built here via the
/// caller's builder, **will break**, or the run holds a multi-line comment) — the gate for
/// [`push_leading_run_before_breaking_value`](Self::push_leading_run_before_breaking_value)
/// at the seams whose remaining layouts already render the width-driven case
/// (the simple `:` annotation, the spread's dots→argument gap), and the gate by which a
/// keyword→value seam hangs a forced run under its keyword
/// ([`Self::keyword_value_hang_doc`]). The arrow's hug arm spells the same pair out itself
/// — its geometry half is [`Printer::arrow_gap_broke_after_run`], which the
/// enclosing call must ask too. A value that fits declines,
/// and the caller keeps its glued path. The declarator and
/// assignment-expression `=` instead split the gate — geometry here, the two
/// break halves via
/// [`break_or_hang_after_operator_run_doc`](Self::break_or_hang_after_operator_run_doc)
/// — because their width half is real but restricted to a run in which no
/// comment owns its line ([`Self::no_comment_owns_its_line`]).
pub(crate) fn breaking_value_leading_run(
&self,
gap_start: u32,
value_start: u32,
build_value: impl FnOnce() -> DocId,
) -> Option<(CommentVec<'a>, DocId)> {
let comments = self.broke_after_value_leading_run(gap_start, value_start)?;
let value_doc = build_value();
self.broke_after_run_break_is_forced(&comments, value_doc)
.then_some((comments, value_doc))
}
/// Whether a broke-after run's break is FORCED — by a hard break of the value's own
/// (`will_break`), or by a multi-line block in the run
/// ([`Self::run_holds_multiline_block`]). The one question every dispatcher between the run's
/// forced emitter ([`Self::push_leading_run_before_breaking_value`]) and its soft half asks:
/// forced is what lets an author blank after the run survive, where the soft separator
/// yields it with an unforced break.
pub(crate) fn broke_after_run_break_is_forced(
&self,
run: &[&Comment],
value_doc: DocId,
) -> bool {
self.d().will_break(value_doc) || self.run_holds_multiline_block(run)
}
/// The forced broke-after run and the value it leads, as one doc —
/// [`Self::push_leading_run_before_breaking_value`] for a caller with no parts of its own
/// to push into.
pub(crate) fn leading_run_before_breaking_value_doc(
&self,
run: &[&'a Comment],
value_start: u32,
value_doc: DocId,
) -> DocId {
let mut parts = DocBuf::new();
self.push_leading_run_before_breaking_value(&mut parts, run, value_start);
parts.push(value_doc);
self.d().concat(&parts)
}
/// Whether a broke-after run ([`Self::broke_after_value_leading_run`]) holds a MULTI-line
/// block — a comment that cannot print flat, so the run carries a hard break of its own
/// whatever the value does. Prettier breaks the run's separators around such a comment and
/// keeps an author blank after it. The dispatchers ask it through
/// [`Self::broke_after_run_break_is_forced`]; the annotation union's hang asks it alone,
/// since a hard break in the union's own doc is that hang's to render.
///
/// ⚠️ **Any comment in the run, not only a tail glued to the value.** The glued-tail reading
/// (`: /* x */⏎/* y⏎*/ B`) left the commonest shape on the soft half — a lone multi-line
/// comment the author broke after (`= /* x⏎y */⏎⏎b`) — whose unforced separator dropped the
/// blank at the declarator and assignment `=`, and at the annotation `:` glued the type onto
/// the comment's closing line (`let v: /* x⏎y */ B;`), while the arrow body and the type
/// alias already kept both. The frame's own rule (`conformance_prettier.md` §Authored breaks
/// in value position): the blank survives wherever the break does, and here the comment
/// forces the break.
///
/// Only an UN-owned comment reaches it — every comment of an expression run the value's
/// node does not print, a type position's whole run, a comment glued to a stripped paren —
/// since it rides in the run. An OWNED tail is printed by the value's doc, whose `will_break`
/// already answers for it.
pub(crate) fn run_holds_multiline_block(&self, run: &[&Comment]) -> bool {
run.iter().any(|c| c.multiline)
}
/// Build a leading-comment run over `[start, end)` into a fresh `DocBuf`,
/// returning `None` when the range holds no comments. The `Option`-returning
/// form of [`push_leading_comment_run`](Self::push_leading_comment_run) that
/// the RHS-comment wrappers use.
fn build_leading_comment_run_opt(
&self,
start: u32,
end: u32,
glue: LeadingGlue,
) -> Option<DocId> {
self.build_leading_comment_run_with_break(start, end, glue, None)
.map(|(doc, _)| doc)
}
/// [`build_leading_comment_run_opt`](Self::build_leading_comment_run_opt) plus the
/// run's own forced-break report — for the caller that must open a **shell** around
/// it ([`Self::build_paren_leading_value_doc`], the `import(…)` parens).
///
/// The two share one body because the shell question is not separable from the run:
/// re-deriving "did this break" caller-side is
/// [`push_leading_comment_run`](Self::push_leading_comment_run)'s three-way rule
/// spelled a second time, and a *range*-shaped approximation of it is the bug the
/// import gap had. Callers with no shell take the `_opt` form and the report is
/// dropped here rather than at each of them.
/// `skip_delim` drops the comments an opening delimiter already pulled onto its own
/// line ([`Printer::comment_on_delimiter_line`]) — the exclusion every consumer of
/// [`Printer::delimiter_line_comment_prefix`] owes, without which the pulled run
/// prints twice.
fn build_leading_comment_run_with_break(
&self,
start: u32,
end: u32,
glue: LeadingGlue,
skip_delim: Option<u32>,
) -> Option<(DocId, bool)> {
let mut parts = DocBuf::new();
let forces_break = self.push_leading_comment_run(
&mut parts,
self.comments_to_emit_between(start, end)
.filter(|c| !skip_delim.is_some_and(|pos| self.comment_on_delimiter_line(pos, c))),
end,
glue,
);
(!parts.is_empty()).then(|| (self.d().concat(&parts), forces_break))
}
/// Prepend optional RHS leading comments — block comments in the gap between an
/// `=`/`:` and the value — to an already-built `value_doc`, returning `value_doc`
/// unchanged when the gap carries none. Centralizes the `match { Some(c) =>
/// concat([c, v]), None => v }` idiom shared by the initializer/property value
/// sites (variable declarators, class properties, enum members, object property
/// values, import-attribute values).
///
/// Every caller is a head→value gap, so the run is built in the value-gap mode
/// ([`LeadingGlue::AdjacentValueGap`]) — an author blank line after a glued block
/// yields with the break rather than forcing the comment onto its own line. A
/// *list* gap must not route here; it wants
/// [`build_rhs_comments_opt`](Self::build_rhs_comments_opt).
pub(crate) fn prepend_rhs_comments(
&self,
value_doc: DocId,
start: u32,
value_start: u32,
) -> DocId {
match self.build_value_gap_comments_opt(start, value_start) {
Some(comments_doc) => self.d().concat(&[comments_doc, value_doc]),
None => value_doc,
}
}
/// The tail after an operator (`=`, a property's `:`) whose gap to the value holds a
/// LINE comment, which forces the break: the run is partitioned — a comment on the
/// operator's line trails it, the rest lead the value on their own lines (author
/// blank lines kept) — and the value hangs one level in. The caller pushes the
/// operator itself first; `op_pos` is its position.
///
/// The partition is what keeps both authorings of the comment stable: one on the
/// operator's line is never pushed onto a line of its own, and an own-line one is never
/// pulled up. Shared by [`Self::build_eq_comment_break_rhs`] (declarator, for-loop init,
/// enum member, assignment expression), the object property's `:` and the import
/// attribute's `:`.
pub(crate) fn build_operator_line_comment_hang(
&self,
op_pos: u32,
value_start: u32,
value_doc: DocId,
) -> DocId {
let d = self.d();
let run: CommentVec<'_> = self
.comments_to_emit_between(op_pos + 1, value_start)
.collect();
let mut trailing = DocBuf::new();
let mut leading = DocBuf::new();
for (ci, comment) in run.iter().enumerate() {
if self.is_same_line(op_pos, comment.span.start) {
trailing.push(d.text(" "));
trailing.push(self.build_comment_doc(comment));
// A BLOCK on the operator's line whose break is forced (a `//` below it)
// keeps an author blank after it, as prettier does and as the keyword→value
// seam does (`append_keyword_value_line_comments`). After a same-line `//`
// the blank is dropped instead — prettier's answer at the `=`, pinned by
// `multi_block_comment_after_eq`. Only the LAST same-line comment can have
// one: anything after a blank is on a later line.
if comment.is_block {
let next = self.blank_scan_end_after(
comment,
run.get(ci + 1).map_or(value_start, |c| c.span.start),
);
// The blank line goes AHEAD of the hang's own hardline: inside the indent
// it would carry the indent as trailing whitespace and drop the next line
// to column 0.
if self.has_blank_line_between_strict(comment.span.end, next) {
debug_assert!(
leading.is_empty(),
"no own-line comment precedes a same-line one"
);
trailing.push(d.literalline());
}
}
} else {
leading.push(self.build_comment_doc(comment));
self.push_leading_run_separator(
&mut leading,
comment,
run.get(ci + 1).map_or(value_start, |c| c.span.start),
);
}
}
d.concat(&[
d.concat(&trailing),
d.indent_hardline(d.concat(&[d.concat(&leading), value_doc])),
])
}
/// Build the `= value` RHS for an initializer whose `=`→value gap
/// (`eq_pos + 1 .. value_start`) holds a comment that forces break handling,
/// or `None` when the caller should emit its normal inline `= value` form (no
/// comment, or a single inline block that glues to the value). The returned doc
/// begins at `operator` (`" ="`, or an assignment expression's `" +="` and
/// friends); the caller emits the LHS (name/pattern) before it.
/// `build_value` is called only when a break is forced, so a comment-free
/// initializer never pays to build the value doc here.
///
/// Shared by variable declarators, for-loop init clauses, enum members, and the
/// assignment expression's line-comment arm so they all place a comment after the
/// operator identically. That sharing is the point: the enum
/// member emitted its own positional run instead, and drifted twice over — it
/// preserved a break the others reflow, and relocated an own-line comment onto the
/// `=` line, which is not idempotent (the moved comment reads as glued next pass).
/// A new `=`→value gap should route here rather than re-derive the layout:
///
/// - **Line comment** after `=`: mandatory break after `=`, placed by
/// [`Self::build_operator_line_comment_hang`]. A comment on the `=`'s line trails it
/// inline; a comment on its own line leads the value on its own line (author blank
/// lines preserved — after a same-line block too, but not after a same-line `//`,
/// where prettier drops it and so does tsv). Diverges from prettier, which
/// relocates the line comment to trail the whole statement — tsv preserves the
/// author's placement (see [`conformance_prettier_ts_comments.md` §Comment relocation]).
/// - **Own-line block, or multiline block the author broke after**: break-after-
/// operator hang, the comment on its own line (matches prettier's
/// `hasLeadingOwnLineComment`; the broke-after multiline case is tsv's own
/// authored-break rule — conformance_prettier_ts_comments.md §Pre-separator
/// multiline block).
/// - **Inline block (or glued run, multiline members included) reaching the
/// value**, or no comment: `None` — the caller keeps the value on the `=` line
/// (`= /* c */ value`, `= /* c */ /* x⏎y */ value`).
pub(crate) fn build_eq_comment_break_rhs(
&self,
eq_pos: u32,
value_start: u32,
operator: &'static str,
build_value: impl FnOnce() -> DocId,
) -> Option<DocId> {
let d = self.d();
if !self.has_comments_to_emit_between(eq_pos + 1, value_start) {
return None;
}
if self.has_line_comments_between(eq_pos + 1, value_start) {
Some(d.concat(&[
d.text(operator),
self.build_operator_line_comment_hang(eq_pos, value_start, build_value()),
]))
} else if self.comment_hangs_value_after_operator(eq_pos + 1, value_start) {
// The operator→value rule — both conjuncts, and the reasoning for each,
// live on [`Self::comment_hangs_value_after_operator`]. A glued single-line
// block trailing the `=` (`= /* c */⏎v`) stays with the `=` (the `None`
// arm below), and a run whose glue chain reaches the value hangs nothing.
// Own-line / multiline block → break-after-operator hang.
let comments_doc = self
.build_rhs_comments_opt(eq_pos + 1, value_start)
.unwrap_or_else(|| d.empty());
Some(d.concat(&[
d.text(operator),
tsv_lang::doc::after_operator::hang_after_operator(
d,
d.concat(&[comments_doc, build_value()]),
),
]))
} else {
// Only an inline block glued to `=`: caller emits `= /* c */ value`.
None
}
}
}