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
// TypeScript printer - converts internal AST back to formatted source code
//
// ## Architecture
//
// This module is organized by concern to support future expansion:
//
// - **mod.rs** (this file): Core Printer struct, constructors, and source/comment utilities
// - **analysis.rs**: Pure AST analysis functions (no Printer state needed)
// - **comments.rs**: Comment handling (printing, doc building, filtering)
// - **program.rs**: Program-level printing orchestration (statements, blank lines, comments)
// - **decorators.rs**: Decorator printing (class-level and class-member)
// - **statements/**: Statement printing (declarations, control flow, modules, etc.)
// - **expressions/**: Expression printing (dispatch, literals, functions, patterns, templates,
// objects, arrays, operators, assignment, conditionals)
// - **types/**: Type annotation printing (TypeScript-specific type syntax)
// - **calls/**: Call and `new` expression formatting (argument wrapping, expand patterns)
// - **chain/**: Member/call chain linearization, grouping, and rendering
// - **class_common.rs**: Shared class-header layout for declaration + expression printers
// - **pre_body.rs**: The head→body `{` seam every braced-body declaration crosses (class,
// interface, enum, namespace, and every value-level function definition)
// - **needs_parens.rs**: Centralized parenthesization logic (`needs_parens(expr, ctx)`)
// - **layout.rs**: Shared hang-indent "break after operator, then indent continuation" doc shapes
//
// ## Design Principles
//
// 1. **Match Prettier**: Output matches prettier for compatibility
// 2. **Preserve Semantics**: Never change TypeScript semantics
// 3. **Modularity**: Each module has single responsibility for future maintainability
mod analysis;
#[cfg(feature = "_internal_buffer_stats")]
pub(crate) mod buffer_stats;
mod calls;
mod chain;
mod class_common;
mod comments;
mod decorators;
mod expressions;
mod ignore;
mod layout;
mod needs_parens;
mod pre_body;
mod program;
mod relational_sibling_pairs;
mod statement_tail;
mod statements;
mod types;
// Layout predicates re-exported from the crate root for embedders (tsv_svelte's
// {@const} assignment layout reuses Prettier's break-after-operator rules).
pub use analysis::conditional_should_break_after_op;
use analysis::{
PatternContext, build_entity_name_doc, is_brace_block_multiline, is_effectively_empty_body,
is_module_path_fluid_call, is_multiline_string_literal, is_multiline_template_expression,
is_string_literal, next_printed_stmt, next_printed_stmt_start, object_pattern_should_expand,
statement_gap_floor, template_literal_has_newlines,
};
pub(crate) use calls::{call_may_print_whole, spine_first_call_within};
pub(crate) use comments::leading_regex_start;
use comments::{
ClassMemberModifiers, CommentFilter, CommentSpacing, CommentVec, ContinuationValue,
HeritageKeyword, LeadingGlue, MemberBlankScan, MemberBody, MemberFloor, MemberFreeze,
MemberGap, MemberSeam, RunLeadingBlank, ShellLeadingRun, ShellPair, StandaloneGlue,
next_significant_byte, paren_shell_close_after,
};
use decorators::class_expr_has_decorators;
pub(crate) use decorators::decorator_takes_pair;
pub use expressions::assignment::should_inline_logical_expression;
use expressions::assignment::{
arrow_chain_should_break, is_simple_value, is_type_assertion_call, should_break_after_operator,
};
pub(crate) use expressions::assignment::{
is_curried_arrow_chain, is_curried_arrow_chain_that_breaks,
};
pub(crate) use expressions::conditional::{ternary_branch_takes_pair, ternary_test_takes_pair};
pub(crate) use needs_parens::{
ParenContext, binary_left_operand_reenters, ends_with_instantiation_close,
flattened_left_operand, operator_operand_takes_no_pair, printer_adds_operand_pair,
printer_parenthesizes_chain_base,
};
use needs_parens::{
SecondTypeArgs, clarity_pairs_behind_region_close, instantiation_keeps_pair_before_type_args,
is_called_function, is_in_binary, needs_parens,
};
use types::unwrap_parenthesized;
use crate::__private::PrinterInputs;
use crate::ast;
use std::cell::{Cell, RefCell};
use tsv_lang::{
CommentFreeWindow, EmbedContext, FxHashSet, OutputBuffer, Span, TAB_WIDTH,
doc::{
self, ShareKey, SharedBuild,
arena::{DocArena, DocId},
},
has_comments_on_page_from, has_comments_to_emit_from, has_line_comments_from,
has_line_spanning_comments_to_emit_from, has_multiline_block_comments_on_page_from,
printing::{self, LineTable},
range_too_narrow_for_a_comment,
source_scan::{
OperandAnchor, PAREN_HOP_NEEDLES, TriviaProfile, has_newline_before_position,
is_hop_needle, skip_regex_literal, skip_trivia,
},
};
/// Which builder a chain-share entry belongs to — the builder half of the share key's tag,
/// sitting ABOVE the two `expandLastArg` state bits it is OR-ed with.
///
/// One node is reached by more than one builder, and their docs are NOT interchangeable: the
/// expand-last body prebuild wraps an object body in the grammar's parens, which the argument
/// builder never does. Keying them apart is what lets both cache instead of one of them
/// poisoning the other.
///
/// ⚠️ A new variant takes the next multiple of four — a value whose two low bits, the
/// [`ShareTag::SKIP_ARROW_CHAIN_BIT`] / [`ShareTag::FLAT_PARAMS_BIT`] states, are clear. A
/// collision there silently merges a builder with a state, which is a cache hit that is not
/// byte-identical to a rebuild. Two builders sharing a value cannot compile (an enum's
/// explicit discriminants must be distinct), and neither can a value past a `u8`: the
/// `#[repr(u8)]` types each discriminant, so the 64th variant's `64 * 4` overflows at
/// compile time instead of truncating through `as u8`. Written as a product rather than a
/// shift, because `64 << 2` drops its high bit to 0 unchecked.
#[derive(Clone, Copy)]
#[repr(u8)]
pub(crate) enum ShareTag {
/// [`Printer::build_arg_expression_doc`] — a call argument.
#[expect(
clippy::identity_op,
reason = "every builder is written as its multiple of four"
)]
ArgExpression = 1 * 4,
/// `calls::prebuild_expand_last_obj_array_body` — an arrow's object/array terminal body,
/// parens included, as the arrow's own body build would produce it.
ExpandLastBody = 2 * 4,
/// `calls::AssumedParamHug::new` — a block-bodied callback argument's body, built once
/// for both of the callback's printings.
AssumedHugBody = 3 * 4,
/// `calls::AssumedParamHug::build` — the callback argument printed with its sole
/// parameter's object type assumed expanded.
AssumedHugArg = 4 * 4,
/// `calls::AssumedParamHug::build` — a `function` callback argument printed with its
/// return type's `{` assumed broken, the line its hug state's gate measures.
AssumedHugProbe = 5 * 4,
/// `calls::prebuild_expand_last_break_body` — an expand-last arrow's call or ternary
/// body, as the arrow's own body build would produce it.
ExpandLastBreakBody = 6 * 4,
/// `chain::print_node_inner` — a chain's unparenthesized base, which every layout
/// candidate of the chain prints.
ChainBase = 7 * 4,
/// `chain::builder`'s breaking-object state — the last call's single argument, built to
/// ask whether it will break. A chain nested in that argument asks the same of its own,
/// so unshared each level rebuilt every chain below it.
ChainBreakProbe = 8 * 4,
/// `calls::chain_args::chain_call_head_doc` — a chain call's type arguments, which every
/// layout candidate of the chain prints in the call's head.
ChainTypeArgs = 9 * 4,
/// `expressions::functions::function_signature_refuses_hug` — a `function` callback's
/// parameter list, built only to ask whether it will break, under the asking state
/// `Printer::ask_signature_params` sets.
SignatureParamsProbe = 10 * 4,
/// `chain::print_node_inner` — a chain's parenthesized base with its pair, which every
/// layout candidate of the chain prints.
ChainParenBase = 11 * 4,
/// [`ShareTag::ChainParenBase`] for a base whose left-spine ternary takes the expanded
/// pair — a verdict one candidate's build can change for the next, so it keys apart.
ChainExpandedParenBase = 12 * 4,
}
impl ShareTag {
/// `skip_arrow_chain` was set for this build (prettier's `expandLastArg`, chain half).
pub(crate) const SKIP_ARROW_CHAIN_BIT: u8 = 0b0000_0001;
/// `expand_last_arg_flat_params` was set for this build (its `removeLines` half).
pub(crate) const FLAT_PARAMS_BIT: u8 = 0b0000_0010;
/// The two bits above, which no builder discriminant may claim.
const STATE_BITS: u8 = Self::SKIP_ARROW_CHAIN_BIT | Self::FLAT_PARAMS_BIT;
}
// The invariant the whole share store rests on, stated where a new variant is written rather
// than only in prose: a builder that overlapped a state bit would answer a lookup made under
// different `expandLastArg` state, which is a hit that is NOT byte-identical to a rebuild.
const _: () = assert!(ShareTag::ArgExpression as u8 & ShareTag::STATE_BITS == 0);
const _: () = assert!(ShareTag::ExpandLastBody as u8 & ShareTag::STATE_BITS == 0);
const _: () = assert!(ShareTag::AssumedHugBody as u8 & ShareTag::STATE_BITS == 0);
const _: () = assert!(ShareTag::AssumedHugArg as u8 & ShareTag::STATE_BITS == 0);
const _: () = assert!(ShareTag::AssumedHugProbe as u8 & ShareTag::STATE_BITS == 0);
const _: () = assert!(ShareTag::ExpandLastBreakBody as u8 & ShareTag::STATE_BITS == 0);
const _: () = assert!(ShareTag::ChainBase as u8 & ShareTag::STATE_BITS == 0);
const _: () = assert!(ShareTag::ChainBreakProbe as u8 & ShareTag::STATE_BITS == 0);
const _: () = assert!(ShareTag::ChainTypeArgs as u8 & ShareTag::STATE_BITS == 0);
const _: () = assert!(ShareTag::SignatureParamsProbe as u8 & ShareTag::STATE_BITS == 0);
const _: () = assert!(ShareTag::ChainParenBase as u8 & ShareTag::STATE_BITS == 0);
const _: () = assert!(ShareTag::ChainExpandedParenBase as u8 & ShareTag::STATE_BITS == 0);
/// One break a doc build reads as AUTHORED although the source does not hold it — the state
/// [`Printer::assumed_break`] carries while a convergence state is built.
///
/// Prettier reaches some fixed points only on its second pass, which reads a break its first
/// pass printed for WIDTH as one the author wrote. tsv prints that settled form in one pass by
/// building it with the break assumed: each variant names one source position and the one
/// reader that answers for it, so no reader can match a position another kind recorded.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum AssumedBreak {
/// The object-type literal starting here breaks after its `{`
/// (`type_literal_force_multiline`).
TypeLiteral(u32),
/// The mapped type starting here breaks after its `{` (`build_mapped_type_doc`).
MappedType(u32),
/// The block comment starting here reads as written on a line of its own
/// (`is_own_line_comment`) — prettier's newline BEFORE a comment, which decides both
/// the hard separator after it and the break ahead of it.
OwnLineComment(u32),
}
impl AssumedBreak {
/// The source position the assumed break belongs to — inside every node whose doc the
/// assumption can change, and inside no other.
pub(crate) fn position(self) -> u32 {
match self {
Self::TypeLiteral(start) | Self::MappedType(start) | Self::OwnLineComment(start) => {
start
}
}
}
}
/// A set of spans marked during one document's print and never consumed — what a printer
/// cell keyed by span needs when one decision records a node for a reader that builds it
/// later (possibly more than once, across conditional-group variants), and one node can be
/// marked by several owners.
///
/// Hash-backed: a document marks and asks once per node of the kind it tracks, so a linear
/// list made both quadratic in that count.
#[derive(Default)]
pub(crate) struct SpanMarks(RefCell<FxHashSet<(u32, u32)>>);
impl SpanMarks {
/// Mark `span` (marking it again is a no-op).
pub(crate) fn mark(&self, span: Span) {
self.0.borrow_mut().insert((span.start, span.end));
}
/// Whether `span` was marked.
pub(crate) fn is_marked(&self, span: Span) -> bool {
self.0.borrow().contains(&(span.start, span.end))
}
}
/// The parent context that routes a curried arrow chain (`(a) => (b) => …`)
/// through a flattened chain layout, mirroring prettier's
/// `printArrowFunctionSignatures` parent-context branches. Set by the enclosing
/// printer (assignment chokepoint, call-argument printer, binary-operand
/// printer, callee printer) just before the chain's RHS / argument / operand is built; the
/// outermost chain arrow reads and clears it at entry (`replace(None)`) so
/// nested arrows in the chain don't inherit it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum ArrowChainContext {
/// No enclosing chain site (`export default`, `return`, an array element, a ternary
/// branch, …) — prettier's default branch, `group(indent(join(heads)))`: the heads
/// past the first indent one level under it. Also what every arrow reads once the
/// outermost chain arrow has consumed the context.
#[default]
None,
/// Assignment RHS (`const f = (a) => (b) => …`). The heads join into one
/// breakable group indented one level after `=` (a leading softline is the
/// break-after-`=`); all heads share the same indent when they break.
///
/// `leading_run` is the seam's operator→value comment run, handed IN so it prints
/// after that softline — leading the first head on whichever line the heads land on.
/// Printed ahead of the chain instead, the run stays on the operator's line while the
/// heads break below it (`= /* c */⏎(a) =>`), and the next pass reads that as a run the
/// author broke after. The seam hands it over through
/// [`Printer::build_led_curried_chain_doc`], which takes it back when no chain layout
/// consumed it.
AssignmentRhs { leading_run: Option<DocId> },
/// Assignment RHS whose seam has ALREADY broken after the operator and indented — a
/// comment in the operator→value gap forced it (`const f = // c⏎(a) => (b) => …`), or
/// the seam hangs the value itself. The [`Self::AssignmentRhs`] shape minus what the
/// seam supplied: the heads join into one breakable group at the indent they stand
/// at, with no leading softline and no indent of their own, so a commented chain
/// stacks its heads exactly as the comment-free one does.
AssignmentRhsHung,
/// Call argument or binaryish operand (`fn((a) => (b) => …)`,
/// `x ?? ((a) => (b) => …)`) — prettier handles both in one
/// `printArrowFunctionSignatures` branch. Progressive indent: the first head
/// stays on the line, the rest indent one level
/// (`group([sig0, " =>", indent([line, join([" =>", line], rest)])])`).
CallArgOrBinaryish,
/// Callee of a call or `new` (`((a) => (b) => …)()`) — prettier's `isCallee` branch.
/// The heads join at one shared indent inside the callee's own parens, which open
/// onto their own lines when the chain breaks (`(⏎heads =>⏎body⏎)()`); a body that
/// cannot stay on the last head's line forces that break.
Callee,
}
/// Printer state for building output
pub(crate) struct Printer<'a> {
/// Output buffer
buffer: OutputBuffer,
/// Current indentation level
pub(crate) indent_level: usize,
/// Embedding context (base indent offset, first-line offset, layout mode, etc.).
pub(crate) embed: EmbedContext,
/// Arena allocator for doc nodes (borrowed from caller or locally owned)
pub(crate) arena: &'a DocArena,
/// Original source code (for extracting raw values, preserving escape sequences, etc.)
pub(crate) source: &'a str,
/// Comments from the program (for printing leading/trailing comments)
pub(crate) comments: &'a [ast::Comment],
/// Whether any comment in this document is owned by a node (`owned_by_node`).
/// Document-level presence flag (from `PrinterInputs`), computed once per document
/// — never here, per that field's doc (the `.svelte` per-`{expr}` trap). Gates the
/// owned-leading-comment path so a document with no owned comment (~all of them)
/// skips its per-expression byte gate entirely.
pub(crate) has_owned_comments: bool,
/// Whether any comment in this document is a `format-ignore` directive.
/// Document-level presence flag (from `PrinterInputs`), computed once per document —
/// never here (the `.svelte` per-`{expr}` trap). Gates every entry of the
/// format-ignore seam (`printer/ignore.rs`) so a document with no format-ignore
/// directive (~all of them) skips the per-node range scan + directive-string match
/// entirely — each entry reads this flag before any span arithmetic behind it.
pub(crate) has_format_ignore: bool,
/// Where the document this printer prints begins: 0 for a whole file, the island's
/// first byte for a `<script>` body embedded in a Svelte host and spanned against it
/// (the program's own span start, set by the program builders in `lib.rs`). The one
/// reader is the format-ignore placement floor (`tsv_lang::directive_alone_on_line`):
/// the island is its own document there, so a directive with nothing but whitespace
/// between it and this position opens a line, whatever the host has before it on the
/// physical one. Every other printer (a template `{…}` island) keeps 0.
pub(crate) program_start: u32,
/// The document's line table (`tsv_lang::printing::LineTable` — a bounded scan of
/// the source, its on-demand table as the fallback) — the *layout* table.
///
/// Backs every newline-derived **layout** read: blank-line preservation
/// (`has_blank_line_between`) and expansion intent (`has_newline_between`); every
/// such read goes through those two wrappers, nothing reads the table directly.
/// The canonical reprint path (`crate::__private::format_canonical`) erases this table via
/// [`Self::set_canonical`] so those reads collapse (nothing is "on a new line", no
/// blank lines), erasing authoring intent.
///
/// **Never read this for comment classification** — use `comment_line_breaks`.
/// Under `set_canonical` this table is erased, so a comment-adjacency read
/// against it reports "same line" for every comment: a `//` comment stops
/// being followed by a break and the next token is glued onto its line,
/// swallowing content. The name is qualified precisely so the choice has to
/// be conscious at every call site.
pub(crate) layout_line_breaks: LineTable<'a>,
/// Line breaks used exclusively for *comment* position classification
/// (`is_same_line`, `ClassifiedComments`, `PartitionedComments`), kept
/// real even in the canonical path so a comment's trailing/leading/own-line
/// role stays correct and consecutive line comments never merge onto one
/// output line. In the normal path this is the same table as
/// `layout_line_breaks`; they diverge only under [`Self::set_canonical`].
pub(crate) comment_line_breaks: LineTable<'a>,
/// Whether this printer is producing the intent-erased *canonical* reprint
/// (see `crate::__private::format_canonical`).
///
/// Gates the direct source-newline scans — the ones that read `self.source`
/// instead of a line-break table (type-literal brace + first member, own-line
/// decorators). Those need an explicit flag because, unlike a table read, they
/// are **not** self-stabilizing across canonical passes: emptying the table
/// makes every table read return the same answer for any input, but a raw
/// source scan sees a newline in pass 1 (the original source) and none in
/// pass 2 (the collapsed canonical output), so an ungated scan silently breaks
/// idempotence. Any *new* direct source-newline scan in the printer must be
/// gated here — or deliberately paired and left un-erased, as the mapped-type
/// residual is (see `types::composite::build_mapped_type_doc`).
pub(crate) canonical: bool,
/// Whether we're currently inside an expression statement (for chain merging decisions)
/// Uses Cell for interior mutability so doc builders (&self) can set this
pub(crate) is_expression_statement: Cell<bool>,
/// Whether we're inside a curried arrow chain that `arrow_chain_should_break` forced
/// open, on the DEFAULT arrow path (the flattened `build_arrow_chain_doc` never sets it —
/// it stacks the heads itself). When true, every nested arrow breaks after its `=>`
/// whether or not it is the head that carried the trigger: `const f = (x: T): H => (y) =>
/// expr` stacks BOTH heads, not just the annotated one, which is the whole reason the
/// answer has to travel down rather than be re-asked per arrow.
pub(crate) in_stacked_arrow_chain: Cell<bool>,
/// tsv's spelling of prettier's `expandLastArg` — the argument is being printed for an
/// expand-last **hug** state, so it must render as prettier's `lastArg` rather than as
/// its `printedArguments` (the counterpart is
/// `calls::build_printed_argument_doc`, which every broken-out argument goes through).
///
/// ⚠️ **Its live job is the nested-arrow break, not the chain-layout bail.** Two readers
/// name it: `should_use_arrow_chain_layout`, where it is redundant only because the two
/// sites that set it (`build_block_arrow_hug_states` and its `new` twin) build the
/// argument WITHOUT going through `calls::build_printed_argument_doc`, so no
/// `ArrowChainContext` is in scope and that predicate declines on the context alone — and
/// `build_arrow_expression_body`'s `chain_should_break`, where it suppresses the break so
/// the hugged body stays on the `=>` line. That second reader is the whole reason the flag
/// still exists; deleting it as "the chain bail, already covered" would silently unhug every
/// typed curried callback (`calls/curried_arrow_chain` is the fixture that says so).
/// ⚠️ The first reader's redundancy is a fact about those two CALL SITES, not about the
/// predicate: `should_use_arrow_chain_layout` does not refuse a `shouldBreakChain`
/// chain outright (it is a break, in every context but the assignment RHS), so the
/// redundancy is not guaranteed by the predicate. A third setter that ran under a chain context
/// would make this term load-bearing.
///
/// ⚠️ **Ambient, where prettier's is per-`print()`.** It therefore has to be CLEARED on
/// the way into anything nested, which `build_printed_argument_doc` does — without that
/// it reached an unrelated chain inside the hugged body and suppressed its layout too.
pub(crate) skip_arrow_chain: Cell<bool>,
/// Whether to render arrow parameters flat (no break points) — mirrors
/// prettier's `expandLastArg`/`expandFirstArg` path, which prints the
/// signature with `removeLines` so an expanded last-arg arrow keeps its
/// params on one line and only the body breaks. Without this, a force-broken
/// arrow could shatter a destructuring param (`([a, b])`) instead of falling
/// through to the all-args-broken-out layout. Set around the arrow-doc build
/// in the expand-last-arg call-argument states.
pub(crate) expand_last_arg_flat_params: Cell<bool>,
/// Whether the next parameter list built belongs to a **test call's callback**, and so
/// renders flat at any width — prettier's `isParametersInTestCall`
/// (`print/function-parameters.js`, and `print/type-parameters.js`'s
/// `isParameterInTestCall` for the type parameters), which both ask `isTestCall` of the
/// function's *parent*. tsv's printer has no parent link, so the call sets this on the way
/// down instead; the test-call flat layout in `calls/call_formatting.rs` is the sole
/// `.set` site, and it re-sets per argument so the flag cannot outlive one it never spent.
///
/// ⚠️ **Two readers, and only one of them spends it.** The type-parameter builder PEEKS
/// (`.get()`) — `build_type_parameter_declaration_doc_wrapping`, the one declaration
/// printer an arrow and a function expression share — because a signature builds `<…>`
/// before `(…)`, so consuming there would starve the value parameters.
/// [`Self::build_params_doc_with_comments`] CONSUMES (`.replace(false)`)
/// at its top, before any child doc exists, which is what bounds the flag to the
/// callback's own list: a function in the body, or in a parameter default, is built after
/// the spend and keeps ordinary width-driven params. Left set for the whole argument
/// build, it would flatten those too.
///
/// It is the *list* that goes flat, never a parameter's own doc — a destructured pattern
/// still expands on its own, which is why this is not a `remove_lines` over the signature.
pub(crate) test_call_flat_params: Cell<bool>,
/// The span start of one object-type literal whose **authored** expansion is not read —
/// set only while the arrow-hug signature policy asks whether a hugged sole parameter
/// will break (`expressions/functions.rs`, `sole_param_object_type_start`).
///
/// Prettier's `printObject` returns a hugged sole parameter's top-level object type
/// without a group, so the author's expansion of that one literal never reaches
/// `willBreak` and never bails the hug out; tsv prints it expanded all the same (the
/// cataloged divergence). Every other break the parameter carries is still read. Keyed
/// by the literal's own position, so a nested literal built under the question keeps
/// its authored expansion — and refuses the hug, as prettier's does.
pub(crate) unforced_param_type_literal: Cell<Option<u32>>,
/// The one break the doc being built reads as authored ([`AssumedBreak`]), or `None`.
///
/// Set only around the build of a convergence state's contents
/// ([`Self::with_assumed_break`]) and read by the one predicate each variant names, so a
/// build outside that scope never sees it. A doc that depends on it is a doc of a node
/// whose span holds the assumed position, which is what keeps the chain share sound under
/// it ([`Self::chain_share_key`]).
pub(crate) assumed_break: Cell<Option<AssumedBreak>>,
/// The callback argument (by span start) whose member-chain call must print one of its
/// `calls::AssumedParamHug` hug states outright, and which one — set by the chain printer
/// while it builds the chain's own convergence states, and CONSUMED (cleared) by the
/// argument printer that takes it, which is how the chain learns the state exists.
pub(in crate::printer) force_assumed_param_hug: Cell<Option<(u32, calls::AssumedHugKind)>>,
/// How many trailing runs [`Self::append_trailing_paren_comments`] has DEFERRED out of a
/// grouping pair the printer strips (`x + (y * z // c⏎)`): a `line_suffix` that no
/// break inside the operand ends, so it rides to wherever the enclosing line ends.
///
/// A comma list reads it around each element's build ([`Self::build_list_element_doc`])
/// and breaks when the element deferred, so the run flushes at that element's comma —
/// prettier's mechanism, whose trailing line comment carries its `breakParent` to the
/// list. Only a list asks: a statement ends the line on its own and discards what its
/// build added ([`Self::with_line_ending_construct`]), and every other construct leaves
/// the run to its enclosing list or statement. A counter rather than a flag so a list
/// can tell ITS elements' runs from those built before it.
pub(crate) deferred_paren_runs: Cell<u32>,
/// The start of every list element whose build deferred such a run, so the list
/// breaks around it ([`Self::build_list_element_doc`]) — the element's own record of
/// the fact, which a doc built once and SHARED across layout states cannot re-report
/// through the counter. Read by [`Self::span_holds_forced_list_break`]; empty in almost
/// every document.
pub(crate) forced_list_elements: RefCell<Vec<u32>>,
/// Every nullable-union `<…>` built so far, as `(list start, conditional doc, broken
/// doc)` — [`Self::build_nullable_union_type_args_doc`] records each build, and
/// [`Self::value_with_nullable_union_broken`] swaps the conditional for the broken form
/// inside an enclosing value's doc, the one piece an after-operator fallback state
/// needs in its other form. Empty in almost every document.
pub(crate) nullable_union_type_args: RefCell<Vec<(u32, DocId, DocId)>>,
/// The start of the one type-argument list prettier's `isArrowFunctionVariable`
/// opts out of the inline and nullable-union layouts — the top-level `<…>` of a
/// declarator binding's annotation when the initializer is an arrow function
/// ([`Self::arrow_variable_type_args_start`]); it takes the ordinary group. Set only
/// while that binding builds.
pub(crate) arrow_variable_type_args: Cell<Option<u32>>,
/// Span of the node at the leftmost position of an arrow body that must be wrapped in
/// parens because a concise body cannot start with its first token — an object's `{`
/// (read as a block body) or a decorated class's `@`: `() => ({}) as Logger`,
/// `() => ({}).prop`, `() => ({}) && a`, `() => ({}).b++`,
/// `() => (@dec class {}).k`. Matches prettier's `startsWithNoLookaheadToken`
/// traversal. Keyed by span (not consumed) so a chain rebuilding its base across
/// conditional-group variants wraps consistently, and a same-shaped node nested
/// deeper (a call argument) never matches.
pub(crate) arrow_body_leftmost_parens_target: Cell<Option<Span>>,
/// Span of the object/function/class node that starts an expression statement
/// and must be wrapped in parens, even when nested as the leftmost token of a
/// member/binary/etc. chain: `(class {}).foo`, `({}).foo`, `(class {}) + 1`,
/// `({a: 1}).b().c()`. Matches prettier's `startsWithNoLookaheadToken` traversal.
/// Keyed by span (not consumed, like `arrow_body_leftmost_parens_target`) so a chain
/// rebuilding its base across conditional-group variants wraps consistently; cleared
/// once per statement in `build_expression_statement_doc`.
pub(crate) expr_stmt_paren_target: Cell<Option<Span>>,
/// Span of a ternary whose enclosing parens must **expand** onto their own lines
/// (`(⏎\tcond ? a : b⏎) as T`) instead of hanging — prettier's
/// `shouldExtraIndentForConditionalExpression`. Recorded by
/// `Printer::mark_ternary_extra_indent` at the value positions prettier's
/// `ancestorNameMap` lists, and read where the parens are supplied (a binary cast's
/// operand, a non-null assertion's operand). Keyed by span and not consumed, like
/// the two targets above.
pub(crate) ternary_hang_target: Cell<Option<Span>>,
/// Span of the value an ASSIGNMENT position is building — a declarator's
/// initializer, an assignment's RHS, an object property's value, a class property's
/// initializer: the parents prettier's binaryish `shouldIndentIfInlining` names.
/// Recorded by `Printer::mark_assignment_value`, read by `Printer::build_binary_chain_doc`
/// — where it carries the WHOLE `shouldIndentIfInlining` exemption, since the chain's
/// default is continuation indent and this mark is what makes such a value flat. (The
/// one arm keyed on more than the mark is an inlining chain with earlier operators,
/// which indents; `build_binary_chain_doc_core` resolves it.) Keyed by span and not
/// consumed, like the targets above.
pub(crate) assignment_value_target: Cell<Option<Span>>,
/// Span of a binary chain whose position is one of prettier's `shouldNotIndent`
/// parents (`shouldNotIndent`, `print/binaryish.js`), so the chain takes the FLAT layout instead of the
/// continuation-indent default.
///
/// The other half of the rule `assignment_value_target` carries, and recorded the same
/// way ([`Printer::mark_flat_chain`]) for the same reason: tsv has no parent pointer.
/// Only for a position that hands its value to a **generic value builder** and so
/// cannot name the style at the build site; a position that builds the expression
/// itself says so directly instead. Both the setters and the full `shouldNotIndent`
/// position list are on [`Printer::build_flat_chain_expression_doc`]. Keyed by span and
/// not consumed, like the targets above.
pub(crate) flat_chain_target: Cell<Option<Span>>,
/// Span of a call printed as the CALLEE of a call taking fewer arguments (at least one) —
/// prettier's `isLongCurriedCallExpression`, which leaves that callee's argument list
/// without a group. Set by the outer call's head around its callee build alone
/// (`calls::call_formatting::build_call_head`), the one printer that builds a call's
/// callee whole; the callee reads it on entry — the member chain for its last link
/// ([`Printer::build_call_doc`]), the plain call for its own default tail. Keyed by span
/// and restored after the build, like the targets above.
pub(crate) long_curried_target: Cell<Option<Span>>,
/// Span of a JSDoc cast sitting directly in a **value gap**, whose comment→`(`
/// separator therefore reflows to a space instead of taking the soft `line`
/// ([`Printer::build_comment_bound_pair_lead_doc`]).
///
/// The gap the cast's comment sits in is a value gap or it isn't, and only the
/// enclosing printer knows which — the byte before the comment cannot tell an object
/// value's `key:` from a `label:`. So the value gaps record their cast here
/// ([`Printer::mark_jsdoc_cast_value_gap`]) and the cast reads it, exactly as the three
/// targets above route a parent's fact into a child. Keyed by span and not consumed,
/// like them: a cast rebuilt across conditional-group variants must answer the same way
/// every time, and a cast nested deeper (a call argument inside the value) has a
/// different span and so is left alone — which is the point, since only the direct
/// value's break is the one `docs/conformance_prettier.md` §Authored breaks in value
/// position reflows.
pub(crate) jsdoc_cast_value_gap_target: Cell<Option<Span>>,
/// Span of the JSDoc cast **leading** an embedded value whose gap CANNOT hang — the
/// braced-head category (`EmbedContext::jsdoc_cast_cannot_hang`): the comment→`(`
/// separator reflows to a space in **every** authoring, the own-line hardline arm
/// included, because the host has no operator line to end and the hardline would
/// strand the `(` at the head's own column ([`Printer::build_comment_bound_pair_lead_doc`]).
///
/// Set once per expression entry (`tsv_ts::__private::build_expression_doc` →
/// [`Printer::mark_jsdoc_cast_cannot_hang_gap`]) on the value's **left-spine** cast —
/// the one whose comment leads the value, the same `leading_comment_bound_pair` walk the TS
/// hang predicates use — and never touched during the walk, unlike
/// [`Self::jsdoc_cast_value_gap_target`], whose per-gap marks overwrite each other.
/// Span-keyed like it: a cast nested off the spine keeps its width-decided layout.
pub(crate) jsdoc_cast_cannot_hang_target: Cell<Option<Span>>,
/// Span of the member chain whose EVERY `.prop` lookup carries no break point —
/// prettier's `printMemberExpression` `shouldInline` (member.js), in the two clauses
/// a chain's PARENT supplies: the assignment **target**
/// (`firstNonMemberParent.type === "AssignmentExpression" &&
/// firstNonMemberParent.left.type !== "Identifier"`) and the `new` **callee**
/// (`shouldInlineNewExpressionCallee`).
///
/// Either way the chain is one unbreakable unit, so the width falls to what the chain
/// is built FROM — an over-width assignment breaks after the operator instead of
/// splitting the thing being assigned to, and an over-width `new` sheds into its
/// argument list. When nothing else can break the line overflows: an unbreakable value
/// stays welded to the operator (`chooseLayout`'s `!canBreakLeftDoc` gate, which the
/// per-lookup groups were falsifying), and an argument-less `new` runs long.
///
/// Marked by [`Printer::mark_assignment_target_member_lookups`] /
/// [`Printer::mark_new_callee_member_lookups`] and read once at the chain root
/// (`chain::resolve_inline_lookups`, which also states the optional-chain decline).
/// Keyed by span and not consumed, like the four targets above: a chain rebuilt across
/// `conditional_group` variants must answer the same way every time, and a same-shaped
/// chain nested deeper — in a computed index, or in the assignment's VALUE — has a
/// different span and keeps its width-driven break points. That last one is deliberate:
/// prettier's `findAncestor` walk is position-blind and inlines the value's chain too
/// (`a.b = <long chain>` overflows there), which is an artifact of the walk rather than
/// the rule, and against tsv's print-width stance — see
/// `docs/conformance_prettier_ts.md` §Assignment target member chains. The `new`
/// callee's walk carries no such artifact: it identity-checks every link
/// (`ancestor.object === child`, `ancestor.callee === child`), so tsv adopts it whole.
///
/// A **computed** lookup is unaffected: its brackets are built by `computed_lookup_doc`,
/// which prettier also leaves breakable (`shouldInline` includes `node.computed` for the
/// break point *before* the `[`, never for the brackets themselves). That is what keeps
/// `canBreakLeftDoc` true for `params['key'] = …`.
pub(crate) inline_every_member_lookup: Cell<Option<Span>>,
/// Span of the member chain sitting DIRECTLY under an assignment or a variable
/// declarator — prettier's `shouldInline` call-object clause,
/// `(firstNonChainElementWrapperParent.type === "AssignmentExpression" ||
/// "VariableDeclarator") && isCallExpressionWithArguments(node.object)`.
///
/// Unlike the mark above this one names only a POSITION: the shape half — the tail
/// being a lone `.prop` off a call that has arguments — is the chain's own to check, at
/// the peel. The effect is correspondingly narrow: that one lookup loses its break
/// point, so `const x = fn(a).prop` sheds width into `fn`'s arguments rather than
/// dropping `.prop` to a line of its own.
///
/// Marked by [`Printer::mark_member_call_tail_operand`], read beside the mark above at
/// the chain root. Prettier's second disjunct there, `objectDoc.label?.memberChain`,
/// needs no mark: the label exists only when `printMemberChain` gets past its
/// `groups.length <= cutoff` early return (which returns `group(oneLine)` UNLABELLED),
/// and a chain long enough for that is never the bare-base call the peel gives a break
/// point to — so tsv already glues that tail, by a different route.
pub(crate) inline_member_call_tail: Cell<Option<Span>>,
/// Start of an owned leading comment an **enclosing** node already claims, so the node
/// beginning there must not claim it a second time
/// ([`Printer::prepend_owned_leading_comment`]).
///
/// One shape needs it: a **paren-less** arrow, whose span starts at its own sole
/// parameter (`x => x`, `params_start: None`). Both nodes begin at the comment's token,
/// so the position-keyed lookup answers for both and the comment printed TWICE
/// (`/* c */ (/* c */ x) => x`). The claim stays on the ARROW, and the parameter declines
/// through this mark. Handing the claim DOWN — the `left_spine_child` repair every other
/// same-start pair takes, `[/* c */ a = 1]`'s `AssignmentPattern` among them — is not
/// available here: those parents print nothing before their left child, so the innermost
/// node still prints first, while this one emits a *synthesized* `(` that would land the
/// comment inside a paren the author never wrote.
///
/// Set by [`Printer::build_arrow_params_doc_ungrouped`], the one spelling of an arrow's
/// parameter list, and save/restored around that build like
/// [`Printer::with_jsdoc_cast_cannot_hang_gap`]: nothing may leak into the body, where a
/// nested arrow's own comment sits. Every path that reaches it already claims at
/// `arrow.span.start` — `build_expression_doc`'s seam for a plain arrow and a chain
/// head, `prepend_owned_leading_comment_at` for the `build_arrow_sig_doc` reassembly and
/// a chain's inner arrows — which is what makes the suppression a de-duplication rather
/// than a DROP (`docs/comments.md` hazard 1).
pub(crate) claimed_owned_comment_start: Cell<Option<u32>>,
/// Span of the innermost statement being built — set by
/// [`Printer::build_statement_doc`] and save/restored around it, so a builder nested in
/// a function or class body reads its own statement, never an enclosing one's.
///
/// The bound of a read that must see everything the statement can print on one output
/// line after a position inside it ([`Printer::comment_reaches_line_end_after`]): the
/// statement ends its own line, so nothing past it but its own trailer can reach a line
/// end that a construct inside it defers to.
pub(crate) enclosing_statement: Cell<Option<Span>>,
/// The span of a redundant paren shell whose LEADING run an enclosing keyword→value
/// gap already claims, so the shell's own emitter
/// ([`Printer::build_parenthesized_type_unwrap_doc`]) must not print it a second time.
///
/// Set only where the shell sits at the value's leading EDGE rather than being the
/// value — `: (⏎// c⏎A)[]` and its indexed-access / conditional-check siblings — since
/// there the seam has no node to substitute and the shell is still built
/// ([`types::StrippedParenHang::claimed_shell`]). Where the shell IS the value it is
/// substituted away and never reached, so no mark is needed.
///
/// A **span**, not a position, because `unwrap_parenthesized` peels every layer: the
/// claim covers `((⏎// c⏎A))`'s inner shell too, which holds the comment while the
/// outer holds nothing. Save/restored around the build like
/// [`Printer::claimed_owned_comment_start`] — a mark that outlived its emitter would
/// suppress a run nothing else prints, which is a DROP
/// ([`comments.md`](../../../../docs/comments.md) hazard 1).
pub(crate) claimed_shell_leading_run: Cell<Option<Span>>,
/// The leading-edge descent's per-call memo ([`types::HeadMemo`]), empty between calls.
pub(in crate::printer) head_memo: types::HeadMemo,
/// The left-spine shell REGION one expression claims for every node below it
/// ([`comments::LeftSpineClaim`]): the stripped grouping shells from the
/// claimer's own start down to its left spine's leaf, set where a printed pair on
/// that spine holds a comment below it. The claimer prints the whole region's run
/// ahead of its doc, and every hoist site below it declines the part of its gap the
/// region covers ([`Printer::removed_paren_comments_opt`]).
///
/// Save/restored around the claimer's build like
/// [`Printer::claimed_shell_leading_run`], and keyed on the claimer's span rather
/// than consumed on read, so a conditional group that builds the claimer more than
/// once prints the run in every candidate.
pub(crate) claimed_left_spine_run: Cell<Option<comments::LeftSpineClaim>>,
/// The stripped trailing shell REGION a chain's first member gap claims from its
/// parenthesized base ([`Printer::member_gap_start`]): from the base's printed end to
/// its span's end. The base's own trailing emitters decline it
/// ([`Printer::wrap_chain_with_paren_comments`]) and the gap prints it after the `)`.
/// Save/restored around the chain's build.
pub(crate) claimed_right_spine_run: Cell<Option<Span>>,
/// The statement whose value is being built, when its terminator gap may take over the
/// value's TRAILING region ([`Printer::with_statement_tail`]): where the value ends, and
/// where the region handed to the terminator so far begins. Save/restored around the
/// value's build, so a statement nested inside the value states its own.
pub(in crate::printer) statement_tail: Cell<Option<statement_tail::StatementTail>>,
/// Whether any block comment in the document is followed, past whitespace, by a `)` — the
/// one shape a statement tail can take over ([`Printer::with_statement_tail`]); `None`
/// until a statement first asks. A document without one arms no tail at all.
pub(in crate::printer) has_paren_closing_block: Cell<Option<bool>>,
/// The span a linearized chain's BASE freezes — an alone-on-line format-ignore directive
/// in the grouping parens the parser erased ahead of it
/// ([`Printer::left_spine_operand_frozen_span`]).
///
/// A cell because the base's doc is built deep inside the chain's layout selection
/// (`chain::printing::print_node_inner`), which the chain's own seam
/// (`chain::analysis::build_linearized_chain_doc`) is the only place that knows the
/// chain's leading region. **Span-keyed and NOT consumed on read**: a conditional group
/// builds the same chain doc more than once, and a freeze that fired only on the first
/// build would make the candidates disagree about the base's bytes. Save/restored around
/// the build like [`Printer::claimed_shell_leading_run`].
pub(crate) frozen_chain_base: Cell<Option<Span>>,
/// The parent context for a curried arrow-chain value, set by the enclosing
/// printer (assignment chokepoint, call-argument printer, binary-operand
/// printer) just before the chain is built. The arrow printer reads and
/// clears it at entry so the outermost chain arrow picks the right flattened
/// layout (assignment-RHS vs progressive call-arg/binaryish) while nested
/// arrows don't inherit it. Mirrors prettier routing the parent context
/// (`args.assignmentLayout`, `isCallLikeExpression(parent)`,
/// `isBinaryish(parent)`) into `printArrowFunctionSignatures`.
pub(crate) arrow_chain_context: Cell<ArrowChainContext>,
/// Whether we're building the **init** clause of a C-style `for` header.
/// In that clause an `in` binary expression must be parenthesized to keep it
/// distinct from the `for (x in y)` separator — prettier parenthesizes every
/// `in` anywhere lexically under the init (not just where strictly required),
/// so this flag is set while building the init subtree and propagates through
/// it (including nested function/class bodies). Read by `needs_parens` and the
/// surgical `in`-wrap at positions that build an expression without a
/// `needs_parens` check (assignment RHS, ternary branches/test).
/// Uses Cell for interior mutability so doc builders (&self) can set this.
pub(crate) in_for_init: Cell<bool>,
/// Where the `for` header CLAUSE now being built ENDS — the position its own `;`
/// separator (or, for the update clause, the header's `)`) follows.
///
/// A value shell whose trailing gap closes exactly there is the clause-TERMINAL one, so
/// the `;` behind it separates clauses rather than terminating a statement and a
/// trailing block must not defer past it ([`Printer::shell_closes_for_clause`], which is
/// the one predicate this cell is read through).
///
/// A position rather than a flag, because the ambient [`Printer::in_for_init`] cannot
/// answer this: it spans nested function and class bodies, where a real statement
/// terminator does exist and the deferral is correct
/// (`for (a = function () { x = (b /* c */); }; ;)`). Every node whose span ends at the
/// clause's end is on the clause's own right spine — a chain's inner `=`, an arrow's
/// concise body — which is exactly the set that meets the separator with no `;` of its
/// own in between; a nested statement's value always ends before its enclosing `}`.
///
/// Save/restored around each clause, so a nested header's clause cannot answer for the
/// outer one.
pub(crate) for_clause_end: Cell<Option<u32>>,
/// Whether the scoped argument share for member-chain building is active: an AST
/// node's pointer **plus a build tag** ([`ShareTag`]) → the `DocId` already built
/// for it. A member chain renders the same group **flat** (`print_group`) and
/// **expanded** (`print_group_expanded`) across `conditional_group` candidates;
/// without sharing, each recursive build runs once per candidate and a nested chain
/// in a call arg compounds to O(4^depth) — the member-chain rebuild blowup.
///
/// **The tag is what makes a hit byte-identical.** A given node is reached under
/// identical Printer state in both candidates *except* for `skip_arrow_chain` /
/// `expand_last_arg_flat_params` (prettier's `expandLastArg`, which
/// `calls/chain_args.rs` sets to build the hugged printing of an argument beside its
/// `printedArguments` one), and except for which *builder* is asking — the chain-share
/// builders [`ShareTag`] names, whose docs for one node are not interchangeable (the
/// expand-last object/array body prebuild wraps an object body in the grammar's parens,
/// which the argument builder never does). Both distinctions ride in the tag, so each
/// variant caches separately instead of the cache being refused for the whole node.
/// Every other flag is statement-constant during a chain or set identically by the
/// shared AST traversal.
///
/// **A hit must leave the printer as a rebuild would**, so every entry records, beside
/// its doc, the side effects of its build that skipping the build would lose, and
/// [`Self::chain_shared_doc`] replays them on a hit. The rule, per piece of state a
/// build touches:
///
/// - [`Self::deferred_paren_runs`] — **replayed.** A build that defers a run out of a
/// stripped pair raises the count and leaves the run for the ENCLOSING list element
/// to flush ([`Self::build_list_element_doc`]). That element — a parameter default,
/// an array or object pattern element — may be rebuilt in another state while the
/// entry inside it hits, and without the replay it would see no run, print flat, and
/// carry the comment out of its own construct. An argument entry records its doc
/// with its own list-element wrapping included (`build_list_element_doc` runs inside
/// the share), which restores the count, so its replay adds nothing; an entry whose
/// build ends unwrapped is the one that needs it. A chain base, bare
/// ([`ShareTag::ChainBase`]) or parenthesized ([`ShareTag::ChainParenBase`]), is one:
/// a base is no list element, so a run its build defers is the enclosing element's to
/// flush, on every build of that element.
/// - [`Self::forced_list_elements`] / [`Self::nullable_union_type_args`] — **not
/// replayed.** Both are append-only for the whole document and only searched: the
/// first build's entries stay, naming the very element starts and docs a hit returns,
/// and a rebuild would only push duplicates.
/// - [`Self::force_assumed_param_hug`] — **not replayed.** The chain learns that a hug
/// state exists BY its request being consumed, so a hit that skipped a consumption
/// would drop the state. None can: the chain sets the request around its own last
/// group and the call printer consumes it with no shared build between the two, a
/// shared node never enclosing the call it names — `chain_shared_doc` asserts in debug
/// builds that no recorded build changed the request.
/// - `computed_index_breaks` (debug-only) — **not replayed.** The assignment's assert
/// window ORs it across nested builds, and the first build of a shared node sets it
/// inside the same window a hit reads.
///
/// Active only between `enter_chain_arg_share`/`exit_chain_arg_share` (the outermost
/// `build_chain_doc`); the pointer is stable, the AST arena being immutable during
/// formatting. The store itself is the doc arena's parked
/// [`DocArena::chain_share_store`] — a linear tier for the common chain of a few
/// entries, a hashed tier for the rare deep one — cleared at both enter and exit, so
/// between chains (and across printers sharing one arena) it is logically empty and
/// only its capacity persists.
pub(crate) chain_arg_share_active: Cell<bool>,
/// Expand-last-arg body reuse: `(body-expr span start, pre-built body DocId)`.
/// The call/new expand-last paths build an arrow's call body **once** up front
/// and set this before building the whole-arrow argument doc via
/// `build_args_split_last`; [`Self::build_arrow_body_doc`] then returns the
/// pre-built DocId for that exact node instead of rebuilding it. Building the
/// body twice — inside the whole arrow *and* separately for the break-body
/// state — recurses into itself for a call-bodied arrow whose body is another
/// such call (`f(lead, x => f(lead, y => …))`), making the doc-node count
/// O(2^depth). Reusing the one build keeps it linear and is byte-identical (the
/// injected DocId is exactly what the arrow's own body build would produce for a
/// call body — `build_arrow_body_doc` returns `build_expression_doc` there).
/// Keyed by span (unique per source position); nested expand-last calls
/// save/restore, so only the node currently being reused ever matches. A
/// `function` callback's block body is injected through the same slot
/// (`append_body_with_sig_comments`).
pub(crate) arrow_body_inject: Cell<Option<(u32, DocId)>>,
/// Whether the member chain currently being built has any comment anywhere in its
/// span. Set once per `build_chain_doc` (save/restore, so a nested chain in a call
/// arg / base restores the parent's value on exit — re-entrancy-safe like
/// [`Self::chain_arg_share_active`]). The chain print path reads it to skip per-member
/// comment classification when the whole chain is comment-free (the common case).
/// Safe by construction: the flag only *enables* a skip whose soundness is
/// span-containment (a member's comment gap ⊆ the chain span), so a stale value can
/// only cause more work, never a dropped comment. Defaults `true` (do the full
/// classify) so any member print reached without a preceding set is fail-safe.
pub(crate) chain_has_comments: Cell<bool>,
/// Whether the member chain currently being built holds a call that prints its argument
/// list without a group (`chain::ChainCall::ungroup_args`) and is followed by another
/// call of the chain. Set once per `chain::build_linearized_chain_doc` (save/restore,
/// like [`Self::chain_has_comments`]) and read by the group printer, which then scans the
/// group for the calls that close a group around a curried prefix — a scan every other
/// chain skips. Defaults `true` for the same fail-safe reason: a group printed without a
/// preceding set only scans.
pub(crate) chain_ungroups_args: Cell<bool>,
/// Whether a computed member's index doc built since the assignment builder last cleared
/// this will break — a break that originates inside the `[…]`. Debug-only: the one
/// reader is the `is_poorly_breakable_chain` assert in `expressions/assignment.rs`,
/// which clears it around the value's build (save/restore, so a nested assignment's
/// value folds into the enclosing one's answer) and exempts a forced break only when
/// it is set. The classifier never reads a computed property — prettier's
/// `isPoorlyBreakableMemberOrCallChain` descends a member's object alone — so a break
/// the index forces is the index's own, not a chain break point the static walk
/// missed. Set by the chain printer's computed-member arm (`chain/printing.rs`), the
/// one builder of a computed lookup's brackets. The witness is value-wide: once any
/// index in the value breaks, it exempts every other forced break in that value too.
#[cfg(debug_assertions)]
pub(crate) computed_index_breaks: Cell<bool>,
/// Spans of the bare negative literals an array type's element run of indexed accesses
/// leads (`-1[K][]`), which the indexed-access object rule prints with a pair
/// (`(-1)[K][]`; `array_run_negative_literal`). Recorded by the array
/// builder ahead of its element, read by the object rule, which cannot see the `[]`
/// further out. Keyed by span and never consumed, so a node rebuilt across
/// conditional-group variants answers the same way every time. Sound because a recorded
/// span only ever adds a pair: every reader that can run BEFORE the array records it (the `=` hug
/// gate, the head-shell and open-pair gates) acts only on a paren SHELL, and the bare
/// literal a span names has none, so an early reader's unrecorded answer is the answer
/// it would give anyway.
pub(crate) negative_literal_pair_targets: SpanMarks,
/// Spans of the function and class expressions a division's left operand ENDS on from
/// inside it — under a prefix operator, as the right operand of an operator that binds
/// at least as tightly, under an angle-bracket assertion (`!function () {} / 2`) —
/// which take a pair so no body's `}` prints directly before the `/`
/// ([`Self::mark_dividend_tail`]). Recorded by the binary chain collectors ahead of
/// the operand's build, read by [`Self::needs_parens_at_operand_end`], which the
/// position that prints the node asks and cannot see the `/` further out. Keyed by span
/// and never consumed,
/// like [`Self::negative_literal_pair_targets`]. Sound because a recorded span only
/// ever adds a pair, and only the node's own position asks about it — which is built
/// inside the operand the collector records ahead of.
pub(crate) dividend_tail_targets: SpanMarks,
/// Spans of the nodes whose CLARITY pair is withheld: the ones that would print a
/// `(` of the printer's own directly behind a `>` / `>>` / `>>>` that may close a
/// type-argument region, where that `(` commits the list
/// ([`Self::withhold_clarity_pairs_behind_region_close`]). Recorded by the binary
/// chain collectors ahead of the operand's build, read by [`Self::needs_parens`] at a
/// binary operand's position, a callee's and a tag's, and by the chain linearizer for
/// a callee that is a chain's base ([`Self::linearize_input`]). Keyed by span and
/// never consumed, like [`Self::dividend_tail_targets`]. Sound because a recorded
/// span only ever removes a pair the tree does not need, and only the node's own
/// position asks about it — one built inside the comparison the collector records
/// ahead of; an operand and the node it leads share a start, never a span.
pub(crate) withheld_clarity_pair_targets: SpanMarks,
/// Authored pairs around a node holding a `<` / `<<` inside a comma-list region
/// ([`Self::mark_region_source_pairs`]), marked ahead of the element builds and read
/// wherever the printer decides that node's pair: [`Self::needs_parens`] at every
/// position, a conditional's test and branches, an assignment's value, and the
/// carriers that print their elements themselves. Keyed by span and never consumed so
/// conditional-group candidates share the same answer.
pub(crate) relational_sibling_pair_targets: SpanMarks,
/// Fast gate for the rare document carrying a retained comma-sibling pair.
pub(crate) has_relational_sibling_pairs: Cell<bool>,
/// Spans whose style pair a comma-list region leaves out because the author wrote none
/// ([`Self::mark_region_source_pairs`]): an assignment at a style position, an arrow
/// function's conditional body, and an arrow function whose lone parameter is bare.
/// Each reader asks about its own kind of node, so the kinds share one set. Keyed by
/// span and never consumed, like [`Self::relational_sibling_pair_targets`].
pub(crate) region_bare_targets: SpanMarks,
/// Spans that keep the pair the author wrote directly behind a `typeof` or a unary `-`
/// or around an assignment's target inside a comma-list region
/// ([`Self::mark_region_source_pairs`]), read by the unary printer, by
/// [`Self::needs_parens`] at an assignment target, a callee, a tag and a non-null
/// operand and, for the head of a longer operand, by the chain linearizer.
pub(crate) region_kept_shell_targets: SpanMarks,
/// Fast gate for the rare document carrying either region mark set.
pub(crate) has_region_source_marks: Cell<bool>,
/// List elements a region walk ([`Self::mark_region_source_pairs`]) has already
/// covered, subtree and all. Every mark the walk sets is a fact about the node alone,
/// so a nested list's owner, or the same owner re-entered by another layout
/// candidate, skips an element already walked instead of walking its subtree again.
pub(crate) region_walked_elements: SpanMarks,
/// The comment-free window the last comment search on this printer established
/// ([`CommentFreeWindow`]: the mechanism, its soundness argument, and the gate's
/// inline/outlined shape live there).
///
/// Every existence wrapper's outlined half reads it *ahead* of the search — the
/// on-page wrapper, the one the layout gates ask about whole node spans, reads it at
/// the site ahead of the call — and so does every range walk (the
/// `comments_*_between` iterator wrappers, through [`Self::first_index_between`]): an
/// ask lying wholly inside the window is `false` / an empty walk with no load through
/// the comment array. Every search refreshes it, whatever it answered
/// ([`Self::search_comments_from`]). A window is a fact about `comments`, so it is
/// handed between printers over the same array — an island printer starts from its
/// embedding printer's (`PrinterInputs::comment_free_window`) and hands its own back —
/// and never read against another; a document's own printer starts with the empty one.
pub(crate) comment_free_gap: CommentFreeWindow<'a>,
}
impl<'a> Printer<'a> {
/// Create a new printer borrowing the given arena, `PrinterInputs`, and
/// embedding context.
///
/// `buffer_capacity` pre-sizes the output buffer: the source length for the
/// rendering path, or `0` for doc-only embedding builds that never write
/// output (see `make_printer` / `make_doc_printer` in `lib.rs`).
pub(crate) fn with_context(
arena: &'a DocArena,
inputs: &PrinterInputs<'a>,
embed: EmbedContext,
buffer_capacity: usize,
) -> Self {
// An island printer starts from the embedding printer's window
// (`PrinterInputs::comment_free_window`); a document's own starts empty.
let comment_free_gap = CommentFreeWindow::new(inputs.comments);
if let Some(window) = inputs.comment_free_window {
comment_free_gap.take_from(window);
}
Self {
buffer: OutputBuffer::with_capacity(buffer_capacity),
indent_level: 0,
embed,
arena,
source: inputs.source,
comments: inputs.comments,
has_owned_comments: inputs.has_owned_comments,
has_format_ignore: inputs.has_format_ignore,
program_start: 0,
layout_line_breaks: inputs.line_table,
// Normal path: comment classification shares the one real table. The
// canonical path re-points `layout_line_breaks` at an empty table but
// leaves this one real (see `set_canonical`).
comment_line_breaks: inputs.line_table,
canonical: false,
is_expression_statement: Cell::new(false),
in_stacked_arrow_chain: Cell::new(false),
skip_arrow_chain: Cell::new(false),
expand_last_arg_flat_params: Cell::new(false),
test_call_flat_params: Cell::new(false),
unforced_param_type_literal: Cell::new(None),
assumed_break: Cell::new(None),
force_assumed_param_hug: Cell::new(None),
deferred_paren_runs: Cell::new(0),
forced_list_elements: RefCell::new(Vec::new()),
nullable_union_type_args: RefCell::new(Vec::new()),
arrow_variable_type_args: Cell::new(None),
arrow_body_leftmost_parens_target: Cell::new(None),
expr_stmt_paren_target: Cell::new(None),
ternary_hang_target: Cell::new(None),
assignment_value_target: Cell::new(None),
flat_chain_target: Cell::new(None),
long_curried_target: Cell::new(None),
jsdoc_cast_value_gap_target: Cell::new(None),
jsdoc_cast_cannot_hang_target: Cell::new(None),
inline_every_member_lookup: Cell::new(None),
inline_member_call_tail: Cell::new(None),
claimed_owned_comment_start: Cell::new(None),
enclosing_statement: Cell::new(None),
claimed_shell_leading_run: Cell::new(None),
head_memo: types::HeadMemo::default(),
claimed_left_spine_run: Cell::new(None),
claimed_right_spine_run: Cell::new(None),
statement_tail: Cell::new(None),
has_paren_closing_block: Cell::new(None),
frozen_chain_base: Cell::new(None),
arrow_chain_context: Cell::new(ArrowChainContext::None),
in_for_init: Cell::new(false),
for_clause_end: Cell::new(None),
chain_arg_share_active: Cell::new(false),
arrow_body_inject: Cell::new(None),
chain_has_comments: Cell::new(true),
chain_ungroups_args: Cell::new(true),
#[cfg(debug_assertions)]
computed_index_breaks: Cell::new(false),
negative_literal_pair_targets: SpanMarks::default(),
dividend_tail_targets: SpanMarks::default(),
withheld_clarity_pair_targets: SpanMarks::default(),
relational_sibling_pair_targets: SpanMarks::default(),
has_relational_sibling_pairs: Cell::new(false),
region_bare_targets: SpanMarks::default(),
region_kept_shell_targets: SpanMarks::default(),
has_region_source_marks: Cell::new(false),
region_walked_elements: SpanMarks::default(),
comment_free_gap,
}
}
/// The search behind every window-reading lookup on this printer: the first comment
/// starting at or after `start`, with the comment-free window
/// ([`Self::comment_free_gap`]) refreshed from where it landed
/// ([`CommentFreeWindow::search_from`]).
#[inline]
fn search_comments_from(&self, start: u32) -> usize {
self.comment_free_gap.search_from(start)
}
/// Whether `[start, end)` is known to hold no comment of any kind **without a search**
/// ([`CommentFreeWindow::known_comment_free`] over [`Self::comment_free_gap`]). The
/// inline head of [`Self::first_index_between`], and the gate a builder puts ahead of
/// work whose only product on a comment-free gap is "nothing".
#[inline]
pub(crate) fn gap_known_comment_free(&self, start: u32, end: u32) -> bool {
self.comment_free_gap.known_comment_free(start, end)
}
/// The index a range walk over `[start, end)` on this printer starts at: `len` (an
/// empty walk) for a gap too narrow to hold a comment or lying inside the comment-free
/// window ([`Self::gap_known_comment_free`]), else the outlined search — the windowed
/// form of tsv_lang's `first_index_in_range`. The range walks ask about the same nested
/// spans the existence gates do, so the window answers most of them the same way (four
/// range walks in five on a real corpus); a builder that classifies a gap itself
/// (`ClassifiedComments::from_index`) asks this and hands the index on.
#[inline]
pub(crate) fn first_index_between(&self, start: u32, end: u32) -> usize {
if self.gap_known_comment_free(start, end) {
self.comment_free_gap.comments().len()
} else {
self.first_index_between_wide(start)
}
}
/// The search half of [`Self::first_index_between`] — one outlined copy, so the gate
/// ahead of it sits inline at every walk's site while the search stays one body. Takes
/// the printer, which every site already holds; a body taking the window's address
/// instead kept that derived value in a register across the call and grew two
/// recursive builders' frames (`CommentFreeWindow`'s doc has the measure).
#[inline(never)]
fn first_index_between_wide(&self, start: u32) -> usize {
self.search_comments_from(start)
}
/// Arm expand-last-arg body reuse for the node at `span`: the next
/// [`Self::build_arrow_body_doc`] for that exact node returns `doc` instead of
/// rebuilding it. Returns the previous injection (nested expand-last calls each
/// arm their own, restoring the outer one after). See the `arrow_body_inject` field.
pub(crate) fn inject_arrow_body(&self, span: u32, doc: DocId) -> Option<(u32, DocId)> {
self.arrow_body_inject.replace(Some((span, doc)))
}
/// Restore the previous expand-last-arg body injection (from `inject_arrow_body`).
pub(crate) fn restore_arrow_body_inject(&self, prev: Option<(u32, DocId)>) {
self.arrow_body_inject.set(prev);
}
/// Run `build` with `inject` armed ([`Self::inject_arrow_body`]), restoring the previous
/// injection after. `None` arms nothing and just runs `build`.
///
/// The scoped form, because the injection is a single slot on the printer and every arm
/// that arms one must hand it back — a call/`new` expand-last path re-arms the SAME
/// injection around each printing of its argument, so the pairs interleave and a
/// hand-rolled `if let Some(prev)` per site is where one goes missing.
pub(crate) fn with_arrow_body_inject<R>(
&self,
inject: Option<(u32, DocId)>,
build: impl FnOnce() -> R,
) -> R {
let Some((span, doc)) = inject else {
return build();
};
let prev = self.inject_arrow_body(span, doc);
let out = build();
self.restore_arrow_body_inject(prev);
out
}
/// Run `build` with `assumed` as the [assumed break](Self::assumed_break), restoring the
/// previous one after — the scope a convergence state's contents is built in.
pub(crate) fn with_assumed_break<R>(
&self,
assumed: AssumedBreak,
build: impl FnOnce() -> R,
) -> R {
let prev = self.assumed_break.replace(Some(assumed));
let out = build();
self.assumed_break.set(prev);
out
}
/// Build one `for` header clause with its END recorded, restoring the previous
/// clause's ([`Printer::for_clause_end`]).
///
/// Scoped rather than set-and-forget: the header builds three clauses in a row and then
/// the loop BODY, and a stale end left behind would answer for whatever a nested
/// statement's value shell happens to close at.
pub(crate) fn with_for_clause_end<R>(&self, end: Option<u32>, build: impl FnOnce() -> R) -> R {
let saved = self.for_clause_end.replace(end);
let out = build();
self.for_clause_end.set(saved);
out
}
/// Wrap `doc` in parens when `expr` is an `in` binary built directly inside a
/// `for` header init. Used at positions that build an expression *without* a
/// [`fn@needs_parens`] check — assignment RHS, ternary branches/test, and the
/// init clause's own expression/declarator — so the for-init `in` rule still
/// applies there. Positions that already route through [`fn@needs_parens`] (call
/// args, object values, binary operands, …) get the same wrap via that path.
#[inline]
pub(crate) fn wrap_for_init_in(&self, expr: &ast::Expression<'_>, doc: DocId) -> DocId {
if self.for_init_in_needs_parens(expr) {
self.arena.parens(doc)
} else {
doc
}
}
/// The ambient for-header rule alone — an `in` binary lexically under a `for` init
/// takes a pair whatever its position — for a seam that asks no [`ParenContext`] of
/// its own (the assignment RHS): the verdict [`Self::wrap_for_init_in`] wraps by, and
/// what such a seam hands the value-gap hoist as its pair
/// ([`Self::hoisted_owned_value_gap_run_opt`]).
pub(crate) fn for_init_in_needs_parens(&self, expr: &ast::Expression<'_>) -> bool {
self.in_for_init.get() && is_in_binary(expr)
}
/// [`Self::wrap_for_init_in`] for a value the position FROZE.
///
/// The ambient rule asks "is this node an `in` binary?" of the node a position holds,
/// and that is enough for a printed value: every `in` deeper in the tree reaches a
/// position of its own, which parenthesizes it there. A frozen slice has no deeper
/// positions — it prints verbatim — so the question becomes "does this slice hand the
/// header a bare `in` anywhere?" ([`Self::frozen_slice_hands_header_a_bare_in`]) and the
/// one pair goes around the whole slice.
///
/// `already_parenthesized` is the calling position's own answer to "is something else
/// already putting a pair around this slice?" — its clarity parens, a retained author
/// shell, a ternary branch's pair. Each site holds that fact; a second pair is valid but
/// is neither prettier's output nor the unfrozen twin's.
///
/// The wrap is keyed on the ambient init flag, not on the calling position's own `[?In]`
/// threading, so a frozen arrow body at a `[+In]` host under the init (a call argument, an
/// object value, a template substitution) takes the pair too. That matches its unfrozen
/// twin, which prettier parenthesizes as well; prettier's FROZEN output prints that one
/// bare and still parses, so it is the one cell of this family where tsv adds a pair a
/// valid prettier output lacks. One answer is the NODE's rather
/// than the position's and so is read here: every caller reaches
/// [`Self::build_frozen_expression_doc`], which gives a `SequenceExpression` back the
/// grouping pair it prints for itself. (The `for` init CLAUSE is the one place a frozen
/// sequence prints bare, and it asks the walk directly rather than through this
/// wrapper.)
pub(crate) fn wrap_frozen_for_init_in(
&self,
expr: &ast::Expression<'_>,
frozen: Span,
already_parenthesized: bool,
doc: DocId,
) -> DocId {
let self_parenthesizing = matches!(expr.kind, ast::ExpressionKind::SequenceExpression(_));
if self.in_for_init.get()
&& !already_parenthesized
&& !self_parenthesizing
&& self.frozen_slice_hands_header_a_bare_in(expr, frozen.end)
{
self.arena.parens(doc)
} else {
doc
}
}
/// Whether a frozen slice printed bare in a `for` header's init would hand the header
/// an `in` it reads as the `for (x in y)` separator.
///
/// The walk descends exactly the child positions the grammar threads `[?In]` through:
/// a sequence's operands, an assignment's value (plain and compound), a binary's two
/// operands (logical operators included), a conditional's test and ALTERNATE, an
/// arrow's concise body, a `yield` / `yield*` argument, and an `as` / `satisfies`
/// operand. A conditional's CONSEQUENT is absent on purpose: ecma262 gives it
/// `AssignmentExpression[+In]` whatever the conditional's own parameter, so an `in`
/// there is already legal (`for (a ? b in c : d; ;)` parses).
///
/// Everything else is absent because its operand cannot BE an unparenthesized `in`
/// binary: a prefix operator (`!`, `typeof`, `void`, `delete`, a sign, `await`, a
/// `<T>` assertion) binds tighter, so `typeof a in b` is `(typeof a) in b` and the
/// `in` is the root the walk already answers; a postfix `!`, a member access, a chain
/// base and a template tag are the same story; and a call or `new` argument, an array
/// element, an object value, a computed index, a template substitution and any braced
/// body are `[+In]` in their own right.
///
/// ⚠️ Two of those `[+In]` positions carry a spec-vs-tsc split rather than a clean
/// skip: an **array element** and a **parameter default**. The spec threads `[+In]`
/// into both, so `for (['a' in b]; ;)` and `for (function (q = 'a' in b) {}; ;)` are
/// legal and V8 accepts them, but tsc reports `',' expected`; tsv's own parser sides
/// with tsc on the arrow spelling (`for ((q = 'a' in b) => 1; ;) {}` is rejected at parse), and its
/// unfrozen printer supplies the pair like prettier does. The frozen path supplies it
/// too for a BARE element or default, through this position's own `needs_parens`. What
/// is left is a DESCENDED `in` there (`[c || 'a' in b]`), which this walk does not reach
/// because it does not descend into a `[+In]` position. Pre-existing, and not
/// re-synthesized by the freeze.
///
/// A child the AUTHOR parenthesized stops the walk: the slice is verbatim, so it
/// carries that pair. The test is the next significant byte past the child, bounded by
/// the slice's own end — unbounded it would find the pair the slice EXCLUDES (a clause
/// written `(aaa, bbb in ccc)` freezes as `aaa, bbb in ccc`, with the author's `)`
/// sitting just past the span) and call every such clause already parenthesized.
///
/// Prettier's rule is the mirror image and has no boundary at all
/// (`isPathInForStatementInitializer` walks every ancestor to the root), so the two
/// agree on which `in`s are covered; they differ only in where the pair can go, which
/// is what a verbatim slice decides.
pub(crate) fn frozen_slice_hands_header_a_bare_in(
&self,
expr: &ast::Expression<'_>,
slice_end: u32,
) -> bool {
use ast::ExpressionKind as E;
if is_in_binary(expr) {
return true;
}
let mut reaches = |child: &ast::Expression<'_>| {
!self.frozen_slice_child_is_parenthesized(child, slice_end)
&& self.frozen_slice_hands_header_a_bare_in(child, slice_end)
};
match &expr.kind {
E::SequenceExpression(seq) => seq.expressions.iter().copied().any(&mut reaches),
E::AssignmentExpression(assign) => reaches(assign.right),
E::BinaryExpression(binary) => reaches(binary.left) || reaches(binary.right),
E::ConditionalExpression(cond) => reaches(cond.test) || reaches(cond.alternate),
E::ArrowFunctionExpression(arrow) => match &arrow.body {
ast::ArrowFunctionBody::Expression(body) => reaches(body),
ast::ArrowFunctionBody::BlockStatement(_) => false,
},
// `yield AssignmentExpression[?In]`, `yield*` alike; a bare `yield` has none.
E::YieldExpression(yield_expr) => yield_expr.argument.is_some_and(reaches),
// `'a' in b as T` parses as a cast OVER the binary, so the `in` is the cast's
// operand rather than the root.
E::TSAsExpression(cast) => reaches(cast.expression),
E::TSSatisfiesExpression(cast) => reaches(cast.expression),
_ => false,
}
}
/// Whether the author wrote a grouping pair around `child` inside a frozen slice that
/// ends at `slice_end` — read as "the next significant byte past the child, still
/// inside the slice, is `)`". See [`Self::frozen_slice_hands_header_a_bare_in`] for why
/// the bound is load-bearing.
fn frozen_slice_child_is_parenthesized(
&self,
child: &ast::Expression<'_>,
slice_end: u32,
) -> bool {
next_significant_byte(self.source, child.span().end, slice_end)
.is_some_and(|pos| self.source.as_bytes()[pos] == b')')
}
/// Print-context-aware wrapper over the free [`fn@needs_parens`]: supplies the
/// ambient for-header-init flag so the for-init `in` rule applies at every
/// call site without threading the flag by hand. Prefer this inside `Printer`
/// methods; the free function (which still requires the flag explicitly) is
/// only for the few free helpers that have no `self`.
///
/// It also withholds a clarity pair where it would commit a type-argument list
/// ([`Self::clarity_pair_is_withheld`]), and the assignment pair a comma-list region
/// leaves out where the author wrote none ([`Self::region_withholds_assignment_pair`]),
/// and prints the pairs such a region keeps where the author wrote them
/// ([`Self::region_keeps_authored_pair`]).
#[inline]
pub(crate) fn needs_parens(&self, expr: &ast::Expression<'_>, ctx: ParenContext) -> bool {
(needs_parens(expr, ctx, self.in_for_init.get())
&& !self.clarity_pair_is_withheld(expr, ctx)
&& !self.region_withholds_assignment_pair(expr, ctx))
|| ((self.has_relational_sibling_pairs.get() || self.has_region_source_marks.get())
&& self.region_keeps_authored_pair(expr, ctx))
}
/// The pairs a comma-list region keeps where the author wrote them, asked only in a
/// document that marked one: an authored pair around a node holding a comparison, at
/// every position ([`Self::relational_sibling_pair_is_kept`]), and the shell around an
/// assignment target or the head a `typeof` / unary `-` operand opens on
/// ([`Self::region_keeps_shell`]).
#[cold]
#[inline(never)]
fn region_keeps_authored_pair(&self, expr: &ast::Expression<'_>, ctx: ParenContext) -> bool {
self.relational_sibling_pair_is_kept(expr)
|| (matches!(
ctx,
ParenContext::AssignmentTarget { .. }
| ParenContext::Callee
| ParenContext::TaggedTemplateTag
| ParenContext::NonNull
) && self.region_keeps_shell(expr))
}
/// Whether `expr` is an assignment at one of the positions whose pair is the printer's
/// style — an argument or array element, a property value, a template substitution, an
/// arrow body — that a comma-list region prints bare ([`Self::region_withholds_pair`]).
/// The sequence operand, which prints its pair itself, asks the region directly.
#[inline]
fn region_withholds_assignment_pair(
&self,
expr: &ast::Expression<'_>,
ctx: ParenContext,
) -> bool {
self.has_region_source_marks.get()
&& matches!(
ctx,
ParenContext::Argument
| ParenContext::ObjectPropertyValue
| ParenContext::TemplateLiteralExpression
| ParenContext::ArrowBody
)
&& matches!(expr.kind, ast::ExpressionKind::AssignmentExpression(_))
&& self.region_withholds_pair(expr)
}
/// Whether `expr`, at the position `ctx`, is a node whose clarity pair
/// [`Self::withhold_clarity_pairs_behind_region_close`] recorded — a binary operand's,
/// or a called or tagging function expression's. Asked only once the position has
/// answered that it would print a pair, so every other expression pays nothing.
///
/// The chain linearizer answers the same question for a base that is its first
/// call's callee, off the same marks ([`Self::linearize_input`]).
#[cold]
fn clarity_pair_is_withheld(&self, expr: &ast::Expression<'_>, ctx: ParenContext) -> bool {
let takes_a_clarity_pair = match ctx {
ParenContext::BinaryLeft { .. } | ParenContext::BinaryRight { .. } => matches!(
expr.kind,
ast::ExpressionKind::AwaitExpression(_) | ast::ExpressionKind::BinaryExpression(_)
),
ParenContext::Callee | ParenContext::TaggedTemplateTag => is_called_function(expr),
_ => false,
};
takes_a_clarity_pair && self.withheld_clarity_pair_targets.is_marked(expr.span())
}
/// Record the nodes that print BARE behind `expr`, where it is a `>` / `>>` / `>>>`
/// that may close a type-argument region
/// (`BinaryExpression::may_close_type_arguments`): a `(` the printer invents directly
/// behind that token commits the list on any line, so the clarity pairs between it
/// and the right operand's first token are left out. The rule, what ends its walk
/// and what it leaves behind: `needs_parens`'s `clarity_pairs_behind_region_close`.
///
/// Called by both chain collectors on every binary they visit, ahead of any operand's
/// build, beside [`Self::mark_dividend_tail`] — a flattened chain's inner comparison
/// is visited, never built as a node.
#[inline]
pub(in crate::printer) fn withhold_clarity_pairs_behind_region_close(
&self,
expr: &ast::BinaryExpression<'_>,
) {
if expr.may_close_type_arguments {
self.record_withheld_clarity_pairs(expr);
}
}
/// [`Self::withhold_clarity_pairs_behind_region_close`]'s recording, out of line: a
/// comparison behind an open `<` region is the rare one.
#[cold]
fn record_withheld_clarity_pairs(&self, close: &ast::BinaryExpression<'_>) {
let withheld =
clarity_pairs_behind_region_close(close, self.in_for_init.get(), |operand| {
self.source_holds_pair_around(operand)
});
for span in withheld {
self.withheld_clarity_pair_targets.mark(span);
}
}
/// Whether the author wrote a paren pair directly around `expr`: the token before it
/// is a `(` and the token after it a `)`, whitespace and comments stepped over. Read
/// only where one side of `expr` is its parent's own token, so a `(` right before and
/// a `)` right after are a pair around `expr` itself or around a run of pairs that
/// begins and ends on it.
fn source_holds_pair_around(&self, expr: &ast::Expression<'_>) -> bool {
paren_shell_close_after(self.source, expr.span().end).is_some()
&& self.prev_significant_byte(expr.span().start) == Some(b'(')
}
/// [`Self::needs_parens`] at a position a division's left operand can END on — a prefix
/// unary's argument, a binary's right operand, an angle-bracket assertion's expression,
/// the three `Printer::mark_dividend_tail` descends through — where a function or class
/// expression recorded there takes a pair as well ([`Self::is_dividend_tail`]).
#[inline]
pub(crate) fn needs_parens_at_operand_end(
&self,
expr: &ast::Expression<'_>,
ctx: ParenContext,
) -> bool {
self.needs_parens(expr, ctx) || self.is_dividend_tail(expr)
}
/// Whether `expr` is a function or class expression a division's left operand ends on
/// ([`Self::dividend_tail_targets`]).
#[inline]
pub(crate) fn is_dividend_tail(&self, expr: &ast::Expression<'_>) -> bool {
matches!(
expr.kind,
ast::ExpressionKind::FunctionExpression(_) | ast::ExpressionKind::ClassExpression(_)
) && self.dividend_tail_targets.is_marked(expr.span())
}
/// The byte before `pos` once whitespace — the lexer's JavaScript class — and comments
/// are stepped over backwards: the reverse of [`next_significant_byte`], reading the
/// comment table rather than the bytes to know where a comment began.
fn prev_significant_byte(&self, pos: u32) -> Option<u8> {
self.prev_significant_pos(pos)
.map(|at| self.source.as_bytes()[at as usize])
}
/// Where [`Self::prev_significant_byte`]'s byte sits — the offset of the last byte
/// before `pos` that is neither whitespace nor comment text.
pub(crate) fn prev_significant_pos(&self, mut pos: u32) -> Option<u32> {
loop {
while let Some(c) = self.source[..pos as usize].chars().next_back()
&& tsv_lang::is_js_whitespace(c)
{
pos -= c.len_utf8() as u32;
}
if pos == 0 {
return None;
}
let before = self.comments.partition_point(|c| c.span.start < pos);
match before.checked_sub(1).map(|i| self.comments[i].span) {
Some(comment) if comment.end >= pos => pos = comment.start,
_ => return Some(pos - 1),
}
}
}
/// [`instantiation_keeps_pair_before_type_args`] over this printer's source.
pub(crate) fn instantiation_keeps_pair(
&self,
head: &ast::Expression<'_>,
follow: SecondTypeArgs,
) -> bool {
instantiation_keeps_pair_before_type_args(self.source, head, follow)
}
/// Get a reference to the doc arena.
#[inline]
pub(crate) fn d(&self) -> &DocArena {
self.arena
}
/// Write a string to the buffer
pub(crate) fn write(&mut self, s: &str) {
self.buffer.write(s);
}
/// Write a DocId to the buffer, accounting for current column and indent level
///
/// This handles the common pattern of:
/// 1. Take the current column
/// 2. Print doc with indent-aware width calculations
/// 3. Write the result to the buffer — in place when the buffer is empty
/// (`print_program`, the whole-file render), otherwise through the
/// arena-parked scratch, since the renderer's trims read backwards with
/// no floor. See the two arms below.
pub(crate) fn write_arena_doc(&mut self, d: DocId) {
let current_col = self.current_column();
// An empty buffer *is* the render target, and `as_empty_render_target`
// hands it over only while it is one. The renderer's trim
// (`trim_line_end` at every non-literal break, `trim_trailing_whitespace`
// once more at the end) is the only thing that reads back past what the
// doc wrote, and with nothing in front of the doc's first byte it cannot
// reach text this render did not produce — every other `output.len()` the render
// loop takes is a marker captured mid-render, so it is already relative.
// Rendering in place is therefore byte-identical to rendering into the
// scratch and copying, and it retires the copy: `print_program` — today
// the sole caller, and always on a fresh printer — was moving one copy of
// every formatted byte of the file (637 calls / 5.6 MB on a fuz_app pass).
if let Some(output) = self.buffer.as_empty_render_target() {
doc::arena_print_doc_with_indent_resolved_into(
self.arena,
d,
&self.embed,
current_col,
self.indent_level,
self.source,
output,
);
return;
}
// Render into the arena-parked scratch: one warm buffer across the
// file's renders (one per template expression when Svelte-embedded)
// instead of an alloc/free per call.
let mut output = self.arena.take_render_scratch();
// Pass the document source so `DocText::SourceSpan` nodes (verbatim
// comment/literal slices) resolve at render without a `DocArena` lifetime.
doc::arena_print_doc_with_indent_resolved_into(
self.arena,
d,
&self.embed,
current_col,
self.indent_level,
self.source,
&mut output,
);
self.write(&output);
self.arena.park_render_scratch(output);
}
/// Render an arena DocId to a flat string with effectively infinite width.
pub(crate) fn render_arena_doc_flat(&self, d: DocId) -> String {
doc::arena_measure_doc_flat_resolved(self.arena, d, &self.embed, self.source)
}
/// Get the formatted output
pub(crate) fn into_string(self) -> String {
self.buffer.into_string()
}
/// Get the current column position (for doc-builder width calculations)
pub(crate) fn current_column(&self) -> usize {
self.buffer.current_column(TAB_WIDTH)
}
/// Check if two positions are on the same line.
///
/// Reads `comment_line_breaks` (not `line_breaks`) so comment position
/// classification stays correct even when the canonical path has emptied the
/// layout table. In the normal path the two tables are identical.
///
/// Answered by a bounded scan of the source bytes with the table's binary search
/// as the fallback (`printing::is_same_line_scan`, gated on the table's `lf_only`
/// verdict): the first `\n` after `prev_end` is nearly always the
/// end of the line the question was asked on, a handful of bytes away, where the
/// search is `log2(lines)` dependent steps.
#[inline]
pub(crate) fn is_same_line(&self, prev_end: u32, curr_start: u32) -> bool {
printing::is_same_line_scan(
self.source.as_bytes(),
self.comment_line_breaks,
prev_end,
curr_start,
)
}
/// Switch this printer into the intent-erased *canonical* reprint mode.
///
/// Empties the layout line-break table (so `has_blank_line_between` /
/// `has_newline_between` and every direct `*_fast` reader collapse — no blank
/// lines, nothing forced multiline by a source newline) and sets the
/// `canonical` flag that gates the direct source-newline scans. Comment
/// classification keeps the real `comment_line_breaks` table, so comments are
/// preserved losslessly (merely re-placed deterministically). Cold path:
/// called once, right after construction, before any doc is built.
///
/// Erasing intent takes two mechanisms because there are two kinds of read, and
/// only one of them is self-stabilizing:
///
/// - **Table reads** (this empty table) erase automatically, and stay idempotent
/// for free: an empty table answers "no newline anywhere" regardless of what
/// the source said, so pass 1 and pass 2 agree by construction. A future
/// layout read routed through `layout_line_breaks` needs no further thought.
/// - **Direct source scans** (`self.source[..].contains('\n')`) bypass the table
/// entirely, so they see the original source in pass 1 and the collapsed
/// output in pass 2 and *disagree*. Each one must be gated on `canonical`.
///
/// That asymmetry is the rule for new code: route a layout read through the
/// table and it is handled; scan the source directly and you must gate it.
pub(crate) fn set_canonical(&mut self) {
self.canonical = true;
// `LineTable::EMPTY` is `'static`, which coerces to the field's `'a`; the scan
// forms treat an empty table as authoritative under either verdict.
self.layout_line_breaks = LineTable::EMPTY;
}
/// Check if there's a blank line (2+ newlines) between two positions.
///
/// A *layout* read: erased in the canonical reprint (see [`Self::set_canonical`]) —
/// the scan form honors the emptied table (an empty table is authoritative), so
/// routing through it keeps the erasure. See [`Self::is_same_line`] for the shape.
#[inline]
pub(crate) fn has_blank_line_between(&self, prev_end: u32, curr_start: u32) -> bool {
printing::has_blank_line_between_scan(
self.source.as_bytes(),
self.layout_line_breaks,
prev_end,
curr_start,
)
}
/// Check if there's any newline between two positions.
///
/// A *layout* read: erased in the canonical reprint (see [`Self::set_canonical`];
/// the scan form honors the emptied table). For comment adjacency use
/// [`Self::comment_has_newline_between`] instead — this one reports "no newline"
/// for everything under `set_canonical`. See [`Self::is_same_line`] for the shape.
#[inline]
pub(crate) fn has_newline_between(&self, start: u32, end: u32) -> bool {
printing::has_newline_between_scan(
self.source.as_bytes(),
self.layout_line_breaks,
start,
end,
)
}
/// [`Self::has_newline_between`] against the *comment* line-break table.
///
/// For comment-adjacency classification — is a comment on its neighbor's
/// source line? — which must stay real in canonical mode ([`Self::set_canonical`])
/// just like [`Self::is_same_line`]: these reads decide whether a `//` line
/// comment's emission is followed by a break, and erasing them lets content
/// trail onto a line-comment's output line (swallow / merge — content loss).
/// In the normal path the two tables are identical, so this is byte-identical
/// to `has_newline_between` there.
#[inline]
pub(crate) fn comment_has_newline_between(&self, start: u32, end: u32) -> bool {
printing::has_newline_between_scan(
self.source.as_bytes(),
self.comment_line_breaks,
start,
end,
)
}
/// The share-map key for `node` under `builder`, or `None` when no member chain is
/// building and there is nothing to share with.
///
/// The key carries the builder AND the two `expandLastArg` flags, which are the only
/// Printer state a chain's flat and expanded candidates reach a given node under
/// differently — so a hit is byte-identical to a rebuild by construction rather than
/// by the caller having checked. See the `chain_arg_share_active` field doc. A builder
/// whose doc reads one more such state keys it in its tag
/// ([`ShareTag::ChainExpandedParenBase`]).
///
/// An [assumed break](Self::assumed_break) is the one other state, and it changes the doc
/// of exactly the nodes whose span holds its position: those are built fresh (`None`),
/// every other node keeps its key, so the subtrees a convergence state rebuilds beside the
/// assumption still share. A tag bit could not stand in for it — two assumptions at two
/// positions would read each other's entries.
pub(crate) fn chain_share_key(
&self,
node: &ast::Expression<'_>,
builder: ShareTag,
) -> Option<ShareKey> {
self.chain_share_key_of(node, node.span(), builder)
}
/// [`Self::chain_share_key`] for an AST node of any kind, `span` being the source its doc
/// prints — the range an [assumed break](Self::assumed_break) is checked against.
pub(crate) fn chain_share_key_of<T>(
&self,
node: &T,
span: Span,
builder: ShareTag,
) -> Option<ShareKey> {
if !self.chain_arg_share_active.get() {
return None;
}
if let Some(assumed) = self.assumed_break.get()
&& (span.start..span.end).contains(&assumed.position())
{
return None;
}
let mut tag = builder as u8;
if self.skip_arrow_chain.get() {
tag |= ShareTag::SKIP_ARROW_CHAIN_BIT;
}
if self.expand_last_arg_flat_params.get() {
tag |= ShareTag::FLAT_PARAMS_BIT;
}
Some((std::ptr::from_ref(node) as usize, tag))
}
/// Look up `key` in the chain share store, or build and record it.
///
/// A hit replays what the recorded build did to the deferred-run count, so the
/// enclosing list element sees the run whether its doc was built or found (see the
/// `chain_arg_share_active` field doc for the replay rule).
pub(crate) fn chain_shared_doc(
&self,
key: Option<ShareKey>,
build: impl FnOnce() -> DocId,
) -> DocId {
let Some(key) = key else {
return build();
};
let store = self.arena.chain_share_store();
if let Some(shared) = store.borrow().get(key) {
self.deferred_paren_runs
.set(self.deferred_paren_runs.get() + u32::from(shared.deferred_runs));
return shared.doc;
}
let runs_before = self.deferred_paren_runs.get();
#[cfg(debug_assertions)]
let hug_request = self.force_assumed_param_hug.get();
let doc = build();
#[cfg(debug_assertions)]
debug_assert!(
self.force_assumed_param_hug.get() == hug_request,
"a shared build neither consumes nor leaves a hug request, so a hit needs no replay of one"
);
let runs_after = self.deferred_paren_runs.get();
debug_assert!(
runs_after >= runs_before,
"a build only restores the count it found"
);
let added = runs_after.saturating_sub(runs_before);
store.borrow_mut().insert(
key,
SharedBuild {
doc,
deferred_runs: u16::try_from(added).unwrap_or(u16::MAX),
},
);
doc
}
/// Activate the chain-arg share for the outermost `build_chain_doc` only. Returns the
/// prior active state; nested chains observe `true` and become no-ops (the store
/// persists across the whole top-level chain so every nesting level shares).
pub(crate) fn enter_chain_arg_share(&self) -> bool {
let was_active = self.chain_arg_share_active.get();
if !was_active {
self.chain_arg_share_active.set(true);
self.arena.chain_share_store().borrow_mut().clear();
}
was_active
}
/// Deactivate + clear the chain-arg share when leaving the outermost `build_chain_doc`
/// (`was_active` false). Nested exits are no-ops.
pub(crate) fn exit_chain_arg_share(&self, was_active: bool) {
if !was_active {
self.chain_arg_share_active.set(false);
self.arena.chain_share_store().borrow_mut().clear();
}
}
/// Check if identifier has a complex type annotation (nested generics)
///
/// Corresponds to prettier's `hasComplexTypeAnnotation`:
/// - Type reference with >1 type parameters
/// - At least one type param has nested generics OR is a conditional type
///
/// Example: `Map<string, Array<number>>` - Map has 2 params, second has nested generic
pub(crate) fn id_has_complex_type_annotation(&self, expr: &ast::Expression<'_>) -> bool {
crate::__private::pattern_type_annotation(expr)
.is_some_and(|ann| self.type_has_complex_annotation(ann.type_annotation))
}
/// Check if a type has complex nested type parameters
///
/// Both arms ask the type *inside* any redundant paren shell (`unwrap_parenthesized`):
/// prettier's AST holds no `TSParenthesizedType`, so its predicate sees the reference /
/// conditional directly, and a shell the output strips must not flip the layout
/// (`(A extends B ? C : D)` as an argument, `(Map<…>)` around the annotation).
fn type_has_complex_annotation(&self, ts_type: &ast::TSType<'_>) -> bool {
match unwrap_parenthesized(ts_type) {
ast::TSType::TypeReference(type_ref) => {
// Must have >1 type argument
let type_args = match &type_ref.type_arguments {
Some(args) => &args.params,
None => return false,
};
if type_args.len() <= 1 {
return false;
}
// At least one arg must have nested generics or be a conditional type
type_args
.iter()
.any(|param| self.type_has_nested_generics(param))
}
_ => false,
}
}
/// Check if a type has nested type parameters or is a conditional type
fn type_has_nested_generics(&self, ts_type: &ast::TSType<'_>) -> bool {
match unwrap_parenthesized(ts_type) {
ast::TSType::TypeReference(type_ref) => {
// Has type arguments means nested generics
type_ref.type_arguments.is_some()
}
ast::TSType::Conditional(_) => true,
_ => false,
}
}
/// Check if a type alias has complex type parameters
///
/// Corresponds to prettier's `isComplexTypeAliasParams`:
/// - >1 type parameter
/// - At least one has a constraint or default value
///
/// Example: `type Foo<T extends string, U = number> = ...`
pub(crate) fn type_alias_has_complex_params(
&self,
type_params: Option<&ast::TSTypeParameterDeclaration<'_>>,
) -> bool {
let params = match type_params {
Some(p) => &p.params,
None => return false,
};
if params.len() <= 1 {
return false;
}
// At least one param has a constraint or default
params
.iter()
.any(|param| param.constraint.is_some() || param.default.is_some())
}
/// Whether `expr` is a COMPLEX destructuring target — prettier's
/// `isComplexDestructuring`: an object pattern with more than two properties, at
/// least one of which is renamed (not shorthand) or carries a default.
///
/// Example: `const { a, b = 1, c } = obj` — 3 properties, one has a default.
///
/// The two hosts prettier asks it for are the two this serves: a declarator's `id`
/// and an assignment expression's `left` (`isAssignmentOrVariableDeclarator`). Both
/// answer it the same way — `AssignmentLeft::ComplexPattern` / the declarator's
/// break-lhs arm — so keep them in step: a rule added to one is a rule missing from
/// the other.
///
/// A compound operator's object LITERAL left answers it too, as the pattern prettier's
/// parser reads there ([`Printer::build_compound_target_doc`]): a renamed property, or a
/// shorthand with a default, which the literal holds as an `=` assignment.
pub(crate) fn is_complex_destructuring_target(&self, expr: &ast::Expression<'_>) -> bool {
if let ast::ExpressionKind::ObjectExpression(obj) = &expr.kind {
return obj.properties.len() > 2
&& obj.properties.iter().any(|prop| match prop {
ast::ObjectProperty::Property(p) => {
!p.shorthand
|| matches!(p.value.kind, ast::ExpressionKind::AssignmentExpression(_))
}
ast::ObjectProperty::SpreadElement(_) => false,
});
}
let ast::ExpressionKind::ObjectPattern(obj) = &expr.kind else {
return false;
};
if obj.properties.len() <= 2 {
return false;
}
// At least one property has a default value or is not shorthand
obj.properties.iter().any(|prop| {
match prop {
ast::ObjectPatternProperty::Property(p) => {
// Has default if value is AssignmentPattern
let has_default =
matches!(p.value.kind, ast::ExpressionKind::AssignmentPattern(_));
// Not shorthand if key != value
let not_shorthand = !p.shorthand;
has_default || not_shorthand
}
ast::ObjectPatternProperty::RestElement(_) => false,
}
})
}
/// Find the position of `=` character in the source between two positions
/// Skips over comments to avoid matching `=` inside them.
/// Also skips `==` and `===` comparison operators (we want assignment `=`).
pub(crate) fn find_equals_position(&self, start: u32, end: u32) -> u32 {
let bytes = self.source.as_bytes();
let start_pos = start as usize;
let end_pos = end as usize;
let mut i = start_pos;
while i < end_pos {
if let Some(new_i) = tsv_lang::source_scan::skip_comment(bytes, i, end_pos) {
i = new_i;
continue;
}
// Check for assignment `=` (not `==` or `===`)
if bytes[i] == b'=' && (i + 1 >= end_pos || bytes[i + 1] != b'=') {
return i as u32;
}
i += 1;
}
// Fallback: return midpoint if `=` not found
usize::midpoint(start_pos, end_pos) as u32
}
/// **to emit**: whether *this* caller has a comment to print in `[start, end)`.
///
/// Skips a comment a node owns and prints itself
/// ([`tsv_lang::Comment::owned_by_node`]). See `tsv_lang::comment` for the three
/// axes — a *layout* gate wants [`Self::has_comments_on_page_between`] and a source
/// cursor wants [`Self::comments_in_source_between`].
///
/// Two tiers, like every existence wrapper below: the narrow-gap test is inline at
/// the site (a gap too short to hold a comment is answered by two compares), and
/// only a gap that could hold one pays the call into the outlined search. The
/// printers ask these wrappers by the hundred thousand a document and most of the
/// gaps are a single token wide, so the split is what keeps the common ask off the
/// call boundary while the search stays one copy.
#[inline]
pub(crate) fn has_comments_to_emit_between(&self, start: u32, end: u32) -> bool {
!range_too_narrow_for_a_comment(start, end)
&& self.has_comments_to_emit_between_wide(start, end)
}
/// The search half of [`Self::has_comments_to_emit_between`] — one outlined copy,
/// behind the comment-free window ([`Self::comment_free_gap`]).
#[inline(never)]
fn has_comments_to_emit_between_wide(&self, start: u32, end: u32) -> bool {
if self.comment_free_gap.contains(start, end) {
return false;
}
has_comments_to_emit_from(
self.comment_free_gap.comments(),
self.search_comments_from(start),
end,
)
}
/// **on page**: whether any comment occupies the page in `[start, end)` — an owned
/// comment **counted**.
///
/// The existence check for a *layout* gate (break / expand / hug / paren /
/// fast-path). An owned comment is printed by its own node rather than by this gap,
/// but it is still in the output and still occupies width, so a layout decision must
/// see it. Using [`Self::has_comments_to_emit_between`] here makes the comment
/// silently vanish from a decision it is visibly part of.
#[inline]
pub(crate) fn has_comments_on_page_between(&self, start: u32, end: u32) -> bool {
!self.gap_known_comment_free(start, end)
&& self.has_comments_on_page_between_wide(start, end)
}
/// The search half of [`Self::has_comments_on_page_between`] — one outlined copy; the
/// comment-free window ([`Self::comment_free_gap`]) is read at the site, ahead of the
/// call, and refreshed here.
#[inline(never)]
fn has_comments_on_page_between_wide(&self, start: u32, end: u32) -> bool {
has_comments_on_page_from(
self.comment_free_gap.comments(),
self.search_comments_from(start),
end,
)
}
/// **to emit**: the comments in `[start, end)` that *this* caller must print — an
/// owned comment **skipped** (the node its token begins prints it).
///
/// The printer's form of `tsv_lang::comments_to_emit_in_range`, and the one every gap
/// emitter in this printer asks: it reads the comment-free window
/// ([`Self::comment_free_gap`]) ahead of the search, where the free function cannot.
#[inline]
pub(crate) fn comments_to_emit_between(
&self,
start: u32,
end: u32,
) -> impl Iterator<Item = &'a ast::Comment> {
tsv_lang::comments_to_emit_from(
self.comment_free_gap.comments(),
self.first_index_between(start, end),
end,
)
}
/// **on page**: every comment occupying the page in `[start, end)` — an owned comment
/// **counted**.
///
/// The iterator form of [`Self::has_comments_on_page_between`], for a layout gate whose
/// rule is per-comment (`.any(|c| …)`). Same membership as
/// [`Self::comments_in_source_between`] — on-page and in-source both count an owned
/// comment, only *to emit* skips it — but the name says which question is being asked.
/// Reads the comment-free window ahead of the search, as every walk here does.
#[inline]
pub(crate) fn comments_on_page_between(
&self,
start: u32,
end: u32,
) -> impl Iterator<Item = &'a ast::Comment> {
tsv_lang::comments_on_page_from(
self.comment_free_gap.comments(),
self.first_index_between(start, end),
end,
)
}
/// **in source**: every comment physically inside `[start, end)` — an owned comment
/// **counted**.
///
/// For a cursor stepping over comment *bytes*: a blank-line scan, an offset, a
/// `prev_end`. The bytes are in the file regardless of who prints them, so a scan
/// that skipped an owned comment would read its own newlines as an author's blank
/// line — and emit a blank line that was never written. Reads the comment-free window
/// ahead of the search, as every walk here does.
#[inline]
pub(crate) fn comments_in_source_between(
&self,
start: u32,
end: u32,
) -> impl Iterator<Item = &'a ast::Comment> {
tsv_lang::comments_in_source_from(
self.comment_free_gap.comments(),
self.first_index_between(start, end),
end,
)
}
/// **in source**: the comment whose span ends inside `[lo, hi]`, where `lo..hi` holds
/// only bytes a backward trim stepped over (whitespace, or a walk's own transparent
/// punctuation) — the comment such a walk reaches next.
///
/// Asking for a comment ending exactly at the trimmed `lo` misses one: a line comment's
/// span runs to its line terminator, so whitespace the author left after its text is the
/// comment's own, and the trim walks into it (`b // c ⏎;` trims to `c`). A block comment
/// closes on `/`, which no such trim steps over, so it can only end at `lo` itself; only a
/// line comment's tail can straddle it. Comments never overlap, so at most one qualifies:
/// the last one ending at or before `hi`.
pub(crate) fn comment_ending_within(&self, lo: u32, hi: u32) -> Option<&'a ast::Comment> {
let idx = self.comments.partition_point(|c| c.span.end <= hi);
let comment = self.comments.get(idx.checked_sub(1)?)?;
(comment.span.end >= lo).then_some(comment)
}
/// **in source**: every comment physically at or after `pos`, to the document's end —
/// `tsv_lang::comments_in_source_from` with no end bound. An unbounded walk has
/// no range for the window to answer, but its search still refreshes the window
/// ([`Self::search_comments_from`]) for the asks that follow; a caller whose question
/// is only *where the next comment starts* asks the window itself first
/// (`CommentFreeWindow::next_comment_start`, as `find_end_with_trailing_comments`
/// does) and reaches this only when it has to walk.
#[inline]
pub(crate) fn comments_in_source_after(
&self,
pos: u32,
) -> impl Iterator<Item = &'a ast::Comment> {
tsv_lang::comments_in_source_from(
self.comment_free_gap.comments(),
self.search_comments_from(pos),
u32::MAX,
)
}
/// Find the first occurrence of a byte in source between `start` and `end`
/// that is NOT inside a comment. Returns absolute position.
pub(crate) fn find_char_outside_comments(&self, start: u32, end: u32, ch: u8) -> Option<u32> {
tsv_lang::source_scan::find_char_skipping_comments(
self.source.as_bytes(),
start as usize,
end as usize,
ch,
)
.map(|pos| pos as u32)
}
/// Position of the comma separating two consecutive list items in
/// `[prev_end, next_start)`, ignoring commas inside comments. The shared anchor
/// for splitting a gap's comments into before-comma (trailing the previous item)
/// and after-comma (leading the next / stranded). Falls back to `next_start` when
/// none is found, which keeps the split lossless: the whole gap then reads as
/// before-comma (trailing the previous item), so no comment is dropped. A parsed
/// program always has the comma; the fallback is reached by a program the Svelte
/// compiler synthesizes (`tsv_svelte_compile`, printed through `format_canonical_in`),
/// whose borrowed spans need not run in source order — `prev_end` can lie past
/// `next_start`, and there the gap is empty.
pub(crate) fn comma_between(&self, prev_end: u32, next_start: u32) -> u32 {
self.find_char_outside_comments(prev_end, next_start, b',')
.unwrap_or(next_start)
}
/// Whether any **line** comment lies in `[start, end)`.
///
/// The one lookup in the family that names no axis, because it provably has none:
/// `owned ⇒ is_block`, so skipping and counting agree here by construction — the
/// rationale is on [`tsv_lang::has_line_comments_in_range`], and it is what a
/// caller may rely on when the run it is asking about is one it emits.
///
/// Uses binary search: O(log n + k) where k is comments in range
#[inline]
pub(crate) fn has_line_comments_between(&self, start: u32, end: u32) -> bool {
!range_too_narrow_for_a_comment(start, end)
&& self.has_line_comments_between_wide(start, end)
}
/// The search half of [`Self::has_line_comments_between`] — one outlined copy, behind
/// the comment-free window ([`Self::comment_free_gap`]).
#[inline(never)]
fn has_line_comments_between_wide(&self, start: u32, end: u32) -> bool {
if self.comment_free_gap.contains(start, end) {
return false;
}
has_line_comments_from(
self.comment_free_gap.comments(),
self.search_comments_from(start),
end,
)
}
/// **to emit**: whether a comment in `[start, end)` occupies more than one output
/// line — a `//` or a multi-line block
/// ([`tsv_lang::has_line_spanning_comments_to_emit_in_range`]). The question every
/// `[no LineTerminator here]` gap asks of the grouping shell that holds it: such a
/// comment cannot be inlined ahead of the restricted token, so the shell is kept
/// and opened (`docs/comments.md`; the cataloged ASI-gap divergences).
pub(crate) fn has_line_spanning_comments_to_emit_between(&self, start: u32, end: u32) -> bool {
!range_too_narrow_for_a_comment(start, end)
&& self.has_line_spanning_comments_to_emit_between_wide(start, end)
}
/// The search half of [`Self::has_line_spanning_comments_to_emit_between`] — one
/// outlined copy, behind the comment-free window ([`Self::comment_free_gap`]).
#[inline(never)]
fn has_line_spanning_comments_to_emit_between_wide(&self, start: u32, end: u32) -> bool {
if self.comment_free_gap.contains(start, end) {
return false;
}
has_line_spanning_comments_to_emit_from(
self.comment_free_gap.comments(),
self.search_comments_from(start),
end,
)
}
/// Check if there are multiline block comments between two positions
///
/// Multiline block comments (containing newlines) force break-after-operator
/// layout in assignments and property values.
/// Prettier ref: `hasLeadingOwnLineComment` in assignment.js `chooseLayout`
#[inline]
pub(crate) fn has_multiline_block_comments_on_page_between(
&self,
start: u32,
end: u32,
) -> bool {
!range_too_narrow_for_a_comment(start, end)
&& self.has_multiline_block_comments_on_page_between_wide(start, end)
}
/// The search half of [`Self::has_multiline_block_comments_on_page_between`] — one
/// outlined copy, behind the comment-free window ([`Self::comment_free_gap`]).
#[inline(never)]
fn has_multiline_block_comments_on_page_between_wide(&self, start: u32, end: u32) -> bool {
if self.comment_free_gap.contains(start, end) {
return false;
}
has_multiline_block_comments_on_page_from(
self.comment_free_gap.comments(),
self.search_comments_from(start),
end,
)
}
/// Where the gap between a member's object and its property OPENS: the object's end,
/// or — when the object's right spine ends in a binary whose last operand sits in
/// stripped grouping shells holding a comment — the end of what the object prints, so
/// the gap claims the shells' run and prints it after the object's `)`, exactly as it
/// prints a comment written directly before that `)` (`(a + (b // c⏎)).h` reads as
/// `(a + b // c⏎).h`). Left to the binary, the run deferred past the `)` to the line's
/// end, behind anything the gap printed first, and out of reach of every predicate that
/// reads the gap ([`Printer::claimed_right_spine_run`]). Every reader of a member gap
/// opens it here.
#[inline]
pub(crate) fn member_gap_start(&self, object: &ast::Expression<'_>) -> u32 {
use ast::ExpressionKind as K;
// Only the walk's own heads can end in a binary's shells: most member objects are
// none of them, and every member linearization asks.
if matches!(
object.kind,
K::BinaryExpression(_)
| K::ArrowFunctionExpression(_)
| K::ConditionalExpression(_)
| K::AssignmentExpression(_)
| K::YieldExpression(_)
) {
self.member_gap_start_after_spine(object)
} else {
object.span().end
}
}
/// [`Self::member_gap_start`] for an object the walk can descend, out of line.
#[inline(never)]
fn member_gap_start_after_spine(&self, object: &ast::Expression<'_>) -> u32 {
let end = object.span().end;
// Only a comment inside the object can be claimed, and most objects hold none.
if !self.has_comments_to_emit_between(object.span().start, end) {
return end;
}
let start = self.right_spine_printed_end(object);
if start < end && self.has_comments_to_emit_between(start, end) {
start
} else {
end
}
}
/// Where `expr`'s printed content ends ahead of the stripped grouping shells a binary
/// on its right spine prints after it: down the last child that ends where its parent
/// does — an arrow's expression body, a conditional's alternate, an assignment's or a
/// `yield`'s operand — and through a binary's bare last operand
/// ([`Self::binary_printed_last_operand`]). A child that sits behind a grouping shell
/// (`yield (x)`, `() => (x)`, `x = (y = …)`) ends the walk at the parent's end — whether
/// that shell prints is its parent printer's decision, not re-derived here — so a comment
/// in a shell below it still defers (a known gap); a kept pair ends it at the pair's `)`.
pub(in crate::printer) fn right_spine_printed_end(&self, expr: &ast::Expression<'_>) -> u32 {
self.right_spine_printed_end_with(expr, false)
}
/// [`Self::right_spine_printed_end`], optionally stopping at an arrow whose expression
/// body is a conditional — the one step of the walk where the printer adds a pair of its
/// own (`() => (c ? d : e)`), which a statement tail must not reach past
/// ([`Printer::with_statement_tail`]).
pub(in crate::printer) fn right_spine_printed_end_with(
&self,
expr: &ast::Expression<'_>,
stop_at_printed_pair: bool,
) -> u32 {
use ast::ExpressionKind as K;
let mut expr = expr;
loop {
let next = match &expr.kind {
K::BinaryExpression(binary) => match self.binary_printed_last_operand(binary) {
Ok(operand) => {
expr = operand;
continue;
}
Err(close) => return close,
},
K::ArrowFunctionExpression(arrow) => match arrow.body {
ast::ArrowFunctionBody::Expression(body)
if stop_at_printed_pair
&& matches!(body.kind, K::ConditionalExpression(_)) =>
{
return expr.span().end;
}
ast::ArrowFunctionBody::Expression(body) => body,
ast::ArrowFunctionBody::BlockStatement(_) => return expr.span().end,
},
K::ConditionalExpression(conditional) => conditional.alternate,
K::AssignmentExpression(assignment) => assignment.right,
K::YieldExpression(ast::YieldExpression {
argument: Some(argument),
..
}) => argument,
_ => return expr.span().end,
};
if next.span().end != expr.span().end {
return expr.span().end;
}
expr = next;
}
}
/// What the chain linearizer reads — the printer itself, and the region marks when the
/// document has any — as one value, so no call site can hand one over without the other.
pub(crate) fn linearize_input(&self) -> chain::LinearizeInput<'_> {
chain::LinearizeInput {
printer: self,
region_kept_shells: self
.has_region_source_marks
.get()
.then_some(&self.region_kept_shell_targets),
}
}
/// Whether comments in the range force the following value onto its own line.
/// Two comment shapes hang the value: a **line** comment (runs to
/// end-of-line — inlining would swallow the value), and a **multiline** block
/// comment the author wrote on its own line (`kw⏎/* … */⏎v`, i.e. a newline
/// after it). Everything else collapses to the inline form (`kw /* c */ v`):
/// a single-line block in *any* position (glued, trailing the keyword, or
/// own-line), and a **glued** multiline block — one whose operand shares the
/// comment's closing line (`kw /* …⏎… */ v`), the way prettier keeps it.
///
/// This is the gate for the keyword→value gaps (as/satisfies,
/// heritage/conditional `extends`, keyof/typeof/readonly, infer,
/// type-param constraint/default, predicate `is`, indexed access,
/// `export default`) and the type-alias `=` layout. Keying the multiline case on the newline *after*
/// the comment (not before) keeps it idempotent: a block glued to the value
/// stays inline even at line start in already-broken output, and only an
/// authored break hangs it. Contrast
/// [`Self::comment_hangs_binary_operand`], which *also* hangs a
/// single-line own-line block (it has no `c.multiline` guard) but demands the
/// comment genuinely own its line, which this one never asks; use that variant
/// only at the one carve-out site where prettier *keeps* that break
/// (binary/logical operands).
///
/// Two tiers like the existence wrappers above: the narrow-gap test inline, the walk
/// one outlined copy.
#[inline]
pub(crate) fn comments_force_own_line_between(&self, start: u32, end: u32) -> bool {
!range_too_narrow_for_a_comment(start, end)
&& self.comments_force_own_line_between_wide(start, end)
}
/// The walk half of [`Self::comments_force_own_line_between`] — one outlined copy.
#[inline(never)]
fn comments_force_own_line_between_wide(&self, start: u32, end: u32) -> bool {
self.any_comment_on_page_with_next(start, end, |c, next| self.comment_hangs_next(c, next))
}
/// Whether this one comment hangs what follows it onto its own line, where `next` is
/// the start of the following comment, or the gap's end for the last.
///
/// The single statement of the rule: a **line** comment (runs to end-of-line, so
/// inlining would swallow what follows) or a **multiline** block the author wrote on
/// its own line (a newline after it — inlining would reflow the author's break).
/// Everything else collapses inline. The hang counterpart of
/// [`Self::comment_hugs_next`], and keyed like it on what follows the comment.
///
/// One question, one predicate: both the gate
/// ([`Self::comments_force_own_line_between`]) and the emitter that gate selects
/// ([`Self::build_trailing_comments_hang_next`]) ask it, so they cannot answer
/// differently. ⚠️ Do not re-derive it at a call site. Keying an emitter on
/// `is_block` alone reads as plausible code but silently collapses the own-line
/// multiline case the gate had just flagged as hanging — and then a layout keyed on
/// the authored newline is decided by a newline that very collapse destroys, so the
/// format stops being idempotent on its own output.
///
/// Contrast [`Self::comment_cannot_glue_to_operator`], the operator-glue rule, which keys on
/// the newline *before* a comment and hangs an own-line **single-line** block too.
///
/// ⚠️ A third shape hangs: an **honored format-ignore directive**
/// ([`Self::is_honored_directive`]), whatever its spelling. A directive that shares its
/// line with what follows is inert under the placement floor, so collapsing one inline
/// would silently cost the freeze it earns on the very next pass — the wrong output being
/// its own fixed point, invisible to every gate. It belongs in this predicate rather than
/// at the emitters because the gate and the emitter must keep answering as one, and
/// because the rule is about the DIRECTIVE, not about whether this particular gap freezes:
/// a gap that doesn't freeze today can only start honoring one if the placement survives
/// to be read. The leading-separator half is
/// [`Self::leading_comment_is_honored_directive`], and the same rule at the declaration
/// headers is `Printer::build_header_comment_run`.
///
/// ⚠️ **"A newline after it" is asked of the comment's own neighbour**
/// ([`Printer::comment_hugs_next`]), never of the distance to `next`. The distance reading
/// counted a break INSIDE a paren the author wrote around the value as a break after the
/// comment (`export default /* c⏎d */ (⏎v)`), hanging the value where the bare spelling and
/// prettier keep it on the comment's line — so the paren spelling's first pass and its
/// reparse took two different layouts.
pub(crate) fn comment_hangs_next(&self, c: &ast::Comment, next: u32) -> bool {
!c.is_block
|| (c.multiline && next > c.span.end && !self.comment_hugs_next(c))
|| self.is_honored_directive(c)
}
/// Whether a comment in `(start, end)` forces the *following* value onto its own
/// line: a line comment (runs to EOL), or a block comment the author gave a line of
/// its **own** — a newline after it (toward the next comment, or `end` for the last)
/// **and** nothing before it on its line. Keying the newline half on what comes
/// *after* the comment (not before) keeps the layout idempotent: a block glued to the
/// value (`/* c */ v`, even at line start in already-broken output) stays inline, and
/// only an authored break (`/* c */⏎v`) forces the value down.
///
/// ⚠️ **Both halves, per comment — this is prettier's `printLeadingComment` hardline
/// arm, not its `hasLeadingOwnLineComment`.** The newline-after half alone reads the
/// LAST comment of a run the author glued onto one line as owning it (`x +⏎/* c1 */
/// /* c2 */⏎y`: `c2` has a newline after it, `c1` one before, and neither owns a
/// line), which forced the chain — and its enclosing `=` — open on an expression
/// prettier keeps flat (`docs/comments.md` §Own-line-ness is a SOURCE question).
/// `hasLeadingOwnLineComment` really is the newline-after half alone, but prettier
/// asks it at the **assignment** seam, of the node the comments lead; at an
/// operator→operand gap prettier asks nothing and prints the run. Pinned by
/// `expressions/binary/operator_glued_comment_run` and, for the position tsv holds
/// against prettier's relocation, `operator_trailing_block_comment_prettier_divergence`.
///
/// ⚠️ **Site-specific — the name says which.** This serves binary/logical
/// operands only (`operators.rs`), where prettier *keeps* an own-line operand break
/// so hanging is the smaller divergence than collapsing. It is NOT a
/// general keyword→value gate: `export default` reached for it (under its old,
/// general-sounding name) and became the lone value gap preserving an unforced
/// break, disagreeing with its own twin `export =`. A keyword→value gap wants
/// [`Self::comments_force_own_line_between`]; an operator→value gap wants
/// [`Self::comment_cannot_glue_to_operator`]. Contrast
/// [`Self::comments_force_own_line_between`], which collapses an authored
/// own-line single-line block inline; that is the gate for every other
/// keyword→value gap.
///
/// ⚠️ **A gap whose shell is a real GROUP wants no gate at all** — the third case,
/// and the one the routing rule above will mislead you into missing. The unary
/// comment-holder parens took `comment_cannot_glue_to_operator` and pre-empted their
/// own width decision with it (`docs/comments.md` §Own-line-ness is a SOURCE
/// question); they now emit prettier's `group(["(", indent([softline, …]), softline,
/// ")"])` and let the leading run's own soft `line` decide. Reach for a gate only
/// where the layout genuinely cannot express both forms.
pub(crate) fn comment_hangs_binary_operand(&self, start: u32, end: u32) -> bool {
// The glue half asks the comment's own neighbours ([`Self::comment_hugs_next`]),
// never the distance to `next` — for the last comment that is the operand'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
// ([`Self::comment_hangs_value_after_operator`] carries the family-wide note). It
// subsumes the `!c.is_block` arm too: a line comment never hugs what follows.
self.any_comment_on_page(start, end, |c| {
!self.comment_hugs_next(c) && self.is_own_line_comment(c)
})
}
/// Whether the gap's comment run is **glued through to the value**: no comment in
/// `(start, end)` has a newline after it (toward the next comment, or `end` for
/// the last). Line breaks *inside* a preserved multiline block don't count — the
/// run still delivers the value on its closing line, so a `will_break` on the
/// run's doc is the comment's interior, not an own-line separator. Feeds
/// `RhsCommentInfo::glued_through`: prettier does not hang such a run
/// (`= /* c */ /* x⏎y */ v`, `hasLeadingOwnLineComment` false — it keys on this
/// same trailing newline), where a bare `will_break` reading hangs the value; the
/// width-decided layout then places it by the run's first line, as the fits walk
/// charges it.
///
/// **on page**: an owned member of the run (`/* x⏎y */` glued to the value) is
/// part of the glue geometry even though the gap emits nothing for it.
pub(crate) fn comment_run_glued_through(&self, start: u32, end: u32) -> bool {
// Per comment, against its OWN neighbours ([`Self::comment_hugs_next`]) rather
// than against a boundary: measuring the last comment's glue by the distance to
// `end` reads whatever the printer erases or the value's shell contributes. A
// grouping paren between the run and the value (`= /* c */ (⏎v)`) put the
// author's break INSIDE the shell and reported the run as not-glued, which hung a
// value prettier keeps on the operator's line — and, once the sibling gate
// stopped agreeing with it, made the pair non-idempotent.
!self.any_comment_on_page(start, end, |c| !self.comment_hugs_next(c))
}
/// Whether a comment in the **operator→value** gap hangs the value under the
/// operator — the one rule, for the one family that asks it.
///
/// Two conjuncts, and neither is sufficient alone:
///
/// - the comment does not glue to what follows it ([`Self::comment_hugs_next`],
/// prettier's `hasNewline(text, locEnd(comment))`), **and**
/// - it is a kind that cannot glue to the operator behind it either
/// ([`Self::comment_cannot_glue_to_operator`] — a line comment, a multiline block,
/// or an own-line block).
///
/// Dropping the first hangs the head of a run whose glue chain reaches the value
/// (`= /* c */ /* x⏎y */ v` and `=⏎/* c */ /* x⏎y */ v` both collapse inline in both
/// formatters). Dropping the second hangs a single-line block the author merely broke
/// after (`= /* c */⏎v`), which prettier keeps on the operator's line.
///
/// ⚠️ **The glue half asks each comment's own NEIGHBOURS, never a boundary.** Every
/// site that spelled it as a distance to the value's span start was blind to a
/// grouping paren standing between them: the author's break sits INSIDE the shell
/// (`= /* c */ (⏎v)`), the printer does not reproduce it, and the gate read it as a
/// break after the comment and hung a value prettier hugs. Six sites shared that one
/// defect, which is why the question is stated here once rather than at each of them;
/// the enumeration and the fixtures live in `docs/comments.md` §Own-line-ness is a
/// SOURCE question, so this doc does not keep a second copy of the list to drift.
///
/// The **operator→value** rule. Contrast [`Self::comments_force_own_line_between`],
/// the keyword→value rule, which collapses an authored own-line single-line block
/// where this one preserves it — each is right for its family, and prettier agrees
/// with both.
///
/// The **on-page** axis: an owned comment glued to the value is printed by the
/// value's own doc, but it still occupies the page and still decides this layout.
pub(crate) fn comment_hangs_value_after_operator(&self, start: u32, end: u32) -> bool {
self.any_comment_on_page(start, end, |c| {
!self.comment_hugs_next(c) && self.comment_cannot_glue_to_operator(c)
})
}
/// **on page**: whether an **indentable** block comment leads the value in the
/// operator→value gap — prettier's `chooseLayout` fourth disjunct
/// (`hasComment(rightNode, Leading, isIndentableBlockComment)` →
/// `break-after-operator`), which sits ahead of every layout the value or the binding
/// would otherwise take.
///
/// ⚠️ **The axis is LEADING-ness, not ownership**, and confusing the two is what this
/// exists to stop. A reading keyed on the comment the value OWNS resolves it from the
/// value's own first token, so anything standing between the two hides it — a grouping
/// paren the printer discards (`= /**⏎ */ (x)`), a second comment
/// (`= /**⏎ */ /* c */ x`), a JSDoc cast's retained `(`. Prettier asks the question of
/// every comment attached LEADING the value, and every comment in this gap is one, so
/// the gap is the range and the physical (on-page) axis is the reading: an owned member
/// is printed by the value's own doc but still occupies the page and still decides the
/// layout. That is why this subsumes the owned reading rather than sitting beside it —
/// an owned leading comment is whitespace-adjacent to the value's first token, hence
/// inside this range by construction.
///
/// ⚠️ **The run this fires on is HOISTED out of the value's doc**
/// ([`Printer::hoist_owned_value_gap_run`]) — but by a WIDER licence than this
/// predicate, and the two must not be re-fused. A comment the value OWNS travels
/// *inside* its node's doc — tsv's ownership is innermost-wins — and its reprinted body
/// is a `MultilineText`, which the layout memo answers `LAYOUT_BREAKS_FORCED`, so the
/// value's own group broke and a value prettier keeps flat exploded
/// (`= /**⏎ * c⏎ */ b + c + d` broke the binary chain, `… */ f ? g : h` the ternary, and
/// the type alias's union gained a fabricated leading `|`). That is a property of the
/// comment being MULTI-LINE, which indentability merely implies, so the hoist asks for
/// a multi-line block and fires at every value gap — the binding default and the enum
/// member included, both of which decline the hang and so never reach this question.
/// Prettier attaches such a comment to the OUTERMOST node starting there and prints it
/// outside that group, which is the shape those seams already build; the hoist is what
/// routes the comment into it. Pinned by
/// `expressions/assignment/operator_value_indentable_block_comment_flat` and its
/// preserved-kind, binding-default and enum-member siblings.
///
/// ⚠️ **Distinct from [`Self::comment_hangs_value_after_operator`]**, the gap's other
/// hang rule, and NOT foldable into it: that one is prettier's
/// `hasLeadingOwnLineComment`, which every operator→value family asks (an arrow body,
/// a binding default, an enum member). This one belongs to `printAssignment` alone —
/// prettier hangs an indentable block at a declarator, an assignment expression, a
/// class field, an object property and a type alias, and keeps it inline at a binding
/// default (`AssignmentPattern` prints as a plain `[left, " = ", right]`) and an enum
/// member. Asking it at a gap outside that family hangs values prettier leaves alone.
///
/// The *preserved* (non-indentable) multi-line block is the null control **for the
/// hang** — it still takes the hoist, which is the wider question above — and needs no
/// rule of its own here: its interior prints verbatim through `literalline`s, and the fits
/// walk charges its first line to the operator's line and ends the measure at its
/// newline (`CachedWidth::HasNewline`), so the width-decided layouts place it exactly as
/// prettier's opaque-string measure does — `const a = /* line1⏎line2 */ x;` stays when
/// that first line fits and hangs when it does not
/// (`preserved_multiline_block_comment_long`). ⚠️ [`DocArena::will_break`] cannot stand
/// in for the test: tsv emits a preserved interior through `literalline`s so `fits` sees
/// the newlines, and it therefore answers `true` for BOTH shapes. Keying the layout on
/// `multiline` (or on `will_break`) hangs values prettier leaves inline.
///
/// Pinned by `operator_value_indentable_block_comment_hang` (every seam, and the gap
/// reading's own shapes), `chain_value_glued_multiline_block_comment`,
/// `binding_layout_glued_multiline_block_comment` and `pure_annotation_value_hang`.
pub(crate) fn indentable_block_leads_value(&self, start: u32, end: u32) -> bool {
self.any_comment_on_page(start, end, |c| self.block_comment_is_indentable(c))
}
/// Whether the comment leading `value` **hangs** it under the operator — the whole
/// operator→value hang question for the `printAssignment` family, in one place because
/// its two twins (`build_declarator_init_doc`'s cascade and
/// [`Self::build_assignment_layout`]) must never answer it differently. That drift is
/// the standing hazard on the declarator builder, and this rule is its likeliest
/// casualty: a fact added to one twin alone is invisible until a fixture happens to
/// spell the other's position.
///
/// Two halves, and they key on different things:
///
/// - `indentable_leads_value` — [`Self::indentable_block_leads_value`] over the gap,
/// keyed on the COMMENT's print shape. Precomputed by the caller, which is the one
/// that holds the gap's spans (`RhsCommentInfo::indentable_leads_value`,
/// `DeclaratorEqGap::indentable_leads_value`);
/// - [`Self::is_own_line_jsdoc_cast`] — keyed on the CAST's shape instead, so it hangs a
/// single-line cast comment too, and it is the one hang every value gap owes (a
/// binding default and an enum member ask it alone). Indentability never enters
/// there, which is why the two cannot be folded into one test.
///
/// The first half is asked once more one shell deeper, of a block the value's printer
/// hoists out of a stripped shell on its left spine
/// ([`Self::left_spine_indentable_leads_value`]): the gap's range ends at the value's
/// span, which opens at that shell's `(`.
///
/// ⚠️ **No layout arm may sit ahead of this.** Prettier hangs under every layout the
/// value or the binding would otherwise take — a never-break `1`, a short key, a
/// complex destructuring pattern, a curried arrow chain — so a shape-keyed arm placed
/// first shadows the rule for exactly that shape. Both twins ask it early (the
/// declarator's cascade, and [`Self::build_assignment_layout`] overriding `BreakLhs` /
/// `Fluid`, each after the chain-line-comment override), and a value-shape arm prettier
/// answers `fluid` by default belongs ahead of it in neither.
pub(crate) fn value_hangs_under_operator(
&self,
indentable_leads_value: bool,
value: &ast::Expression<'_>,
) -> bool {
indentable_leads_value
|| self.is_own_line_jsdoc_cast(value)
|| self.left_spine_indentable_leads_value(value)
}
/// Whether a comment in `(start, end)` is separated from what follows it (the
/// next comment, or `end`) by a blank line. Used where a blank line after a
/// comment is itself a break trigger — e.g. a ternary branch (`a ? /* c */⏎⏎b`),
/// where prettier breaks on the blank even though the own-line comment alone
/// does not.
///
/// A **break gate must ask its emitter's question in the emitter's spelling**, so this
/// is the STRICT scan: the ternary branch gap's own blank emitter takes it, and `end`
/// there is a branch span that starts inside a stripped paren shell. The table-only
/// count reads the erased `(`'s two line breaks as an author blank and opens a ternary
/// that fits — the gate and the emitter fabricating in lockstep, which is why the
/// output stayed a fixed point and only a prettier `compare` found it.
pub(crate) fn comment_followed_by_blank(&self, start: u32, end: u32) -> bool {
self.any_comment_on_page_with_next(start, end, |c, next| {
self.has_blank_line_between_strict(c.span.end, next)
})
}
/// Scan the comments in `(start, end)`, returning true if `pred(comment)` holds for
/// any — the anchor-free sibling of [`Self::any_comment_on_page_with_next`], for a
/// gate whose per-comment rule asks only the comment's own neighbours.
///
/// ⚠️ **Prefer this whenever the rule does not need the anchor.** The anchor is the
/// distance to the NEXT comment or to `end`, and for the last comment `end` is the
/// value's span start — which sits inside any grouping paren the author wrote. Every
/// glue question spelled against it was blind to that shell
/// ([`Self::comment_hangs_value_after_operator`] carries the account), so a gate that
/// binds `next` only to ignore it is inviting the next author to re-derive glue from
/// the wrong thing.
///
/// **on page**, like its sibling: an owned comment is printed by its node's doc but
/// still occupies the page, so a layout gate must count it.
fn any_comment_on_page(
&self,
start: u32,
end: u32,
pred: impl Fn(&ast::Comment) -> bool,
) -> bool {
self.comments_in_source_between(start, end).any(pred)
}
/// Scan the comments in `(start, end)` with one-ahead lookahead, returning true
/// if `pred(comment, next_start)` holds for any — where `next_start` is the
/// following comment's start, or `end` for the last. The shared primitive behind
/// the gap predicates above (each keys a per-comment rule on the gap to whatever
/// follows it). `peekable`, so no allocation.
///
/// **on page**: every caller is a layout gate (prettier's `hasLeadingOwnLineComment`
/// and friends — does the comment hang the value / force the break?). An owned
/// annotation is on the page and hangs the value exactly as any other own-line comment
/// does, so the scan is physical.
fn any_comment_on_page_with_next(
&self,
start: u32,
end: u32,
pred: impl Fn(&ast::Comment, u32) -> bool,
) -> bool {
let mut comments = self.comments_in_source_between(start, end).peekable();
while let Some(c) = comments.next() {
let next = comments.peek().map_or(end, |n| n.span.start);
if pred(c, next) {
return true;
}
}
false
}
/// Whether anything at all precedes this comment on its physical line — the
/// **source** reading of "does this comment trail what came before it", asked of
/// BOTH comment kinds and taking no anchor: `has_newline_before_position` walks
/// back over spaces/tabs, so a comment following another on the same line
/// (`/* a */ /* b */`) follows content, and one the author put on a fresh line does
/// not. Unlike the neighbor-anchored `is_same_line(prev, …)` /
/// `has_newline_between(prev, …)` checks it is blind to nothing: the text it sees
/// includes what no item span covers — a **stripped paren shell**'s `)` and a
/// list's own **comma** — which is exactly why the element-comma seam asks it
/// ([`Self::collect_trailing_comments`], `docs/comments.md` §The element-comma seam).
pub(crate) fn comment_follows_content_on_its_line(&self, comment: &ast::Comment) -> bool {
!has_newline_before_position(self.source, comment.span.start)
}
/// Whether a comment may reach the output line end that a construct ending at `pos`
/// defers a same-line `//` to. Two reads, both on the **in-source** axis (an owned
/// comment counts: it occupies the line all the same), against the real source, so
/// they stay true under [`Self::set_canonical`]. The on-page axis counts the same
/// comments here; in-source is asked because the answer must be a fact of the source
/// alone, never of what some printer has claimed, which is what keeps it the same on
/// both passes:
///
/// 1. **any** comment between `pos` and the end of the innermost enclosing statement
/// ([`Printer::enclosing_statement`]), behind whatever tokens: a lookup's brackets
/// and the call they are the callee of (`obj[fn() // c⏎.bar](); /* c1 */`), an
/// arrow body and a lookup on the arrow (`(() => fn() // c⏎.bar)[i] /* c1 */`), an
/// operator's right side, a following sibling in a list;
/// 2. a **trailer** — a comment following content on its own line — that is the next
/// thing past the statement's end (or past `pos`, with no statement in reach) once
/// closing punctuation is stepped over: only whitespace and closers (`)` `]` `}`
/// `;` `,`) before it. A statement's own trailer (`fn().bar; // c1`), and one past an
/// enclosing block's `}`.
///
/// The question a same-line-`//` deferral must ask before taking the line end for
/// itself: whatever prints flat after `pos` carries such a comment onto the same
/// output line, where it welds onto the deferred one (`// c // c1`, the second `//`
/// becoming text of the first) or is reordered behind it (`/* c1 */ // c`), so the
/// deferral is lossless only when this is false.
///
/// ⚠️ The first read counts a comment of ANY kind, not only a trailer, because that is
/// what makes the answer stable across passes. A comment behind further tokens moves
/// with the layout: the expanded reprint can give it a line of its own (a block leading
/// the next argument once the list breaks), so a trailer-only read expands on pass 1
/// and collapses back on pass 2. The count of comments inside the statement does not
/// move with the layout, and one the reprint carries past the statement's `;` is the
/// second read's. The second read stays
/// trailer-only and closer-bounded, as reading through closers is what makes it
/// stable: the expanded reprint puts `);` on a line below the member. Both are
/// conservative by design — a break the enclosing layout takes between the two (a
/// broken list's own line, a block's `}`) only makes the caller's expansion unneeded,
/// never wrong, and the expansion is the shape prettier prints anyway.
pub(crate) fn comment_reaches_line_end_after(&self, pos: u32) -> bool {
let from = match self.enclosing_statement.get() {
Some(stmt) if stmt.start <= pos && pos <= stmt.end => {
if self
.comments_in_source_between(pos, stmt.end)
.next()
.is_some()
{
return true;
}
stmt.end
}
_ => pos,
};
self.comments_in_source_after(from).next().is_some_and(|c| {
self.comment_follows_content_on_its_line(c)
&& self.source.as_bytes()[from as usize..c.span.start as usize]
.iter()
.all(|b| {
b.is_ascii_whitespace() || matches!(b, b')' | b']' | b'}' | b';' | b',')
})
})
}
/// Whether a single comment occupies its own physical line — a line comment
/// (always runs to end-of-line, so it owns whatever line it is on), or a block
/// comment that starts a fresh line ([`Self::comment_follows_content_on_its_line`]).
///
/// ⚠️ The `!is_block` short-circuit is a **layout** answer, and the reason this is
/// not the predicate a comment-position seam wants: asked of a line comment it says
/// "own line" whatever the author wrote before it on that line. A seam deciding
/// which element a `//` trails must ask the physical question directly.
///
/// A convergence state's build reads one block comment as own-line whatever the
/// source holds ([`AssumedBreak::OwnLineComment`]).
pub(crate) fn is_own_line_comment(&self, comment: &ast::Comment) -> bool {
!comment.is_block
|| !self.comment_follows_content_on_its_line(comment)
|| self.assumed_break.get() == Some(AssumedBreak::OwnLineComment(comment.span.start))
}
/// Whether a multi-line block comment **prints as indented lines**
/// ([`tsv_lang::is_indentable_block`]) — the form `build_comment_doc` reprints as a
/// [`tsv_lang::doc::arena::DocNode::MultilineText`] whose newlines are hard lines.
///
/// The layout question a *glued* multi-line comment poses, and **not** `multiline`:
/// only this form carries a break out to the enclosing group.
///
/// ⚠️ **Not** `DocArena::will_break` on the comment's doc, which answers `true` for
/// both shapes: tsv emits a preserved block's interior through `literalline`s (a
/// genuine newline in the output, so `fits` must see it), where prettier emits one
/// opaque string. That difference is deliberate and lives in the renderer; the
/// *layout* question is this one.
pub(crate) fn block_comment_is_indentable(&self, comment: &ast::Comment) -> bool {
tsv_lang::is_indentable_block(self.source, comment)
}
/// Whether a block comment in `(start, end)` sits **alone on its own physical
/// line** — a newline both before it ([`Self::is_own_line_comment`]) *and* after
/// it (toward the next comment, or `end` for the last). This is the idempotent key
/// for "keep the comment on its own line and break the value below it" across a
/// keyword/operator that itself breaks (the type-alias `=`): prettier preserves an
/// isolated own-line block (`type X =⏎/* c */⏎Y`) but **collapses** a block merely
/// glued to the value across the `=`-break (`type X =⏎/* c */ Y` → inline, then
/// width decides). Keying on the newline *before* alone ([`Self::is_own_line_comment`])
/// would spuriously force the break on already-broken output and lose idempotency —
/// the same hazard [`Self::comment_hangs_next`] documents.
pub(crate) fn block_comment_isolated_own_line_between(&self, start: u32, end: u32) -> bool {
self.any_comment_on_page_with_next(start, end, |c, next| {
self.block_comment_isolated_own_line(c, next)
})
}
/// [`Self::block_comment_isolated_own_line_between`] for ONE comment, `next` being the
/// following comment's start or the gap's end — the form an emitter walking a run asks
/// of each comment in turn, so the gate and the emitter cannot answer differently
/// ([`Self::comment_hangs_next`] states the same discipline for its own pair).
///
/// Per comment, deliberately: prettier's `printLeadingComment` decides each separator
/// from that comment's own two sides, so `/* c1 */ /* c2 */` written together on an own
/// line keeps ONE line between them all (neither is isolated) while `/* c1 */⏎/* c2 */ v`
/// splits after `c1` alone.
pub(crate) fn block_comment_isolated_own_line(
&self,
comment: &ast::Comment,
next: u32,
) -> bool {
comment.is_block
&& self.is_own_line_comment(comment)
&& self.has_newline_between(comment.span.end, next)
}
/// Whether a comment must occupy its own line rather than gluing inline to the
/// operator that precedes it: a line comment, a multiline block, or an own-line
/// block (a newline precedes it). This is exactly the negation of the single-line
/// glued block that
/// [`build_rhs_comments_glued_opt`](Self::build_rhs_comments_glued_opt) hugs across
/// a source newline, so the two stay in lockstep.
///
/// The **operator→value** rule (assignment `=` and friends), as distinct from the
/// **keyword→value** rule ([`Self::comments_force_own_line_between`]): the two
/// differ on an own-line *single-line* block, which this hangs and that collapses.
/// Both are right for their family — assignment preserves it
/// (`assignment/rhs_block_comment_newline`), `as`/`keyof`/`export default` reflow
/// it, and prettier agrees with each.
///
/// ⚠️ A **line** comment satisfies this via `is_own_line_comment`'s `!is_block`
/// disjunct, not via any newline scan — so this never collapses one, and reading
/// the name as "own line means a newline precedes it" is a mistake that has been
/// made. Open the definition before reasoning about a line comment here.
pub(crate) fn comment_cannot_glue_to_operator(&self, comment: &ast::Comment) -> bool {
comment.multiline || self.is_own_line_comment(comment)
}
/// Whether any comment in `[start, end)` forces a break **by a property the surrounding
/// layout cannot manufacture** — a line comment, or a block whose own text spans lines.
///
/// ⚠️ The **on-page** axis, deliberately: an owned comment is printed by its node's own
/// doc rather than by the surrounding gap, but it still occupies the page, so a glued
/// *multiline* block leading a parameter (`(/* a⏎b */ x) => …` — owned by `x`) breaks the
/// signature exactly like an unowned one. Asking the emit axis here reports a region clear
/// while its output spans three lines, which is hazard 2 in `docs/comments.md` and the
/// standing rule that a layout gate is an on-page question.
///
/// ⚠️ **Own-line-ness is deliberately NOT a disjunct here, and that is a STABILITY
/// requirement rather than a scope choice.** It is the one break-forcing property a layout
/// can *create*: break a parameter list and every comment glued to a `(` or a comma lands
/// at a line start, becoming own-line in the output that a hug decision had already been
/// taken on. A refusal that asks it over a region the break relocates is therefore
/// self-fulfilling — `fn((/* c */ a,⏎⏎b) => call(a))` hugged on pass 1, the hug's own break
/// moved `/* c */` to a line start, and pass 2 read it as forcing and expanded. That is an
/// **F1 violation**, not a divergence, and `blanks:audit` is what catches it.
///
/// A caller that genuinely needs the own-line kind must ask it only where breaking cannot
/// introduce it — see [`Printer::arrow_trailing_param_comment_forces_break`], whose region
/// sits *after* the last parameter, so a comment glued there stays glued to that parameter
/// however the list breaks.
pub(crate) fn range_has_layout_stable_break_forcing_comment(
&self,
start: u32,
end: u32,
) -> bool {
self.comments_on_page_between(start, end)
.any(|comment| comment.multiline || !comment.is_block)
}
/// Check if a delimited list (tuple, type params, etc.) has line comments
/// between any elements OR after the last element.
///
/// Used to determine if a list should be forced to multiline formatting.
pub(crate) fn has_line_comments_in_delimited_list<T, F>(
&self,
items: &[T],
get_span: F,
end_boundary: u32,
) -> bool
where
F: Fn(&T) -> Span,
{
let between = items.windows(2).any(|pair| {
self.has_line_comments_between(get_span(&pair[0]).end, get_span(&pair[1]).start)
});
let trailing = items
.last()
.is_some_and(|last| self.has_line_comments_between(get_span(last).end, end_boundary));
between || trailing
}
/// Check if a delimited list contains own-line single-line block comments.
///
/// The per-comment classification is [`Printer::block_comment_owns_its_line`] — both
/// halves read the SOURCE, never a neighbouring item's boundary — and that function
/// carries the argument. What is this gate's own is the SCOPE it applies it over:
/// which comments are in range, which are the list's rather than an element's, and
/// whether an element follows at all.
///
/// ⚠️ **This gate and the element→`,` seam must ask the same question.** The array
/// literal proved the coupling: on the item-boundary reading the gate's blindness
/// *cancelled* the seam's, so tightening either one alone lost a comment outright. The
/// seam keys on the same source reading and partitions its gap by a split point
/// ([`Printer::element_gap_split`]); a future family that strips a delimiter needs both
/// halves, not just this one.
///
/// `span` is the whole construct's span and only its ENDS are read (`span.start` as the
/// opening delimiter, `span.end - 1` as the closing one), so the delimiter pair is the
/// caller's business, not this function's: tuples and array patterns pass `[…]`,
/// type-parameter/argument lists `<…>`, and the object pattern and specifier list
/// `{…}`. Every such list is one delimiter byte at each end, which is the only shape
/// assumption here — a caller whose node span runs PAST the closer (an object pattern's
/// `: T` annotation) trims it back to the closer before calling.
///
/// ⚠️ `get_printed_span` yields the item's **PRINTED** span, not necessarily its node
/// span: the "inside an element" test below asks whether the ELEMENT'S OWN DOC prints
/// the comment, and where a node's span was extended over an erased closer
/// (`docs/comments.md` §The element-comma seam) it does not. The two coincide for most
/// callers — a grouping paren is no node, so an argument's or a type item's span never
/// swallows one — and differ exactly where the destructuring patterns' element ends
/// past a stripped shell, which is why they narrow it
/// ([`crate::ast::Expression::printed_end`]). Handing back the node span
/// there counts a shell-interior comment as the element's, and it reaches no gate at
/// all while the seam emits it.
pub(crate) fn has_own_line_block_comments_in_bracket_list<T, F>(
&self,
span: Span,
items: &[T],
get_printed_span: F,
) -> bool
where
F: Fn(&T) -> Span,
{
let open_bracket = span.start;
for comment in self.comments_in_source_between(open_bracket + 1, span.end - 1) {
if !comment.is_block || comment.multiline {
continue;
}
// Skip comments that are inside an element (they belong to that element, not this list)
let inside_element = items
.iter()
.any(|e| get_printed_span(e).contains(comment.span));
if inside_element {
continue;
}
// Whether an element follows at all — the only thing the position of the next
// element is still asked for, the glue half reading the source instead.
//
// ⚠️ `>=`, not `>`: an element GLUED to the comment starts exactly where the
// comment ends (`[a,⏎/* c */b]`), and a strict `>` skipped it — so a comment
// that plainly leads the next element fell through to the dangling arm and
// expanded the list. That was **non-idempotent**, not merely a divergence: the
// expanded print re-emits the run spaced (`/* c */ b`), which un-glues it, so
// the second pass collapsed. All seven families carried it, and the space is
// the only thing that ever distinguished them — authorship no reader could
// see.
let element_follows = items
.iter()
.map(|e| get_printed_span(e).start)
.any(|start| start >= comment.span.end);
if self.block_comment_owns_its_line(comment, element_follows) {
return true;
}
}
false
}
/// Whether a bracketed comma list holds a comment that forces it OPEN.
///
/// The three clauses every such list asks, stated once: a line comment in the
/// opener→first-item gap, a line comment anywhere between or after the items, or an
/// own-line single-line block comment ([`Self::has_own_line_block_comments_in_bracket_list`]).
/// Held by the type-argument list, the type-parameter declaration and the
/// import/export specifier list; a caller with a further question of its own `||`s it on.
///
/// ⚠️ **The opener→first-item clause is the one that has been forgotten, and forgetting
/// it loses content.** `has_line_comments_in_delimited_list` covers only *between* and
/// *after* the items, so a `//` trailing the opener (`<// c⏎T>`) reaches no clause; the
/// inline path then runs, emits block comments only, and the line comment is DROPPED.
/// That is why the question lives here rather than at each site.
///
/// The zero-comment window gate is an **on-page** question: it guards a LAYOUT
/// decision, and an emit-keyed gate would skip an owned comment and answer for a page
/// that is not empty. Every clause below is bounded within `span`, so with nothing on
/// the page all three are provably false — this skips them on the common bare list.
pub(crate) fn has_expanding_comments_in_bracket_list<T, F>(
&self,
span: Span,
items: &[T],
get_span: F,
) -> bool
where
F: Fn(&T) -> Span,
{
// An empty list short-circuits BEFORE the own-line-block clause, which reads
// `element_follows: false` with no items and would call a lone own-line block in
// an empty `<>` an expansion trigger.
if items.is_empty() {
return false;
}
if !self.has_comments_on_page_between(span.start, span.end) {
return false;
}
self.has_expanding_line_comments_in_bracket_list(span, items, &get_span)
|| self.has_own_line_block_comments_in_bracket_list(span, items, &get_span)
}
/// The two LINE-comment clauses of [`Self::has_expanding_comments_in_bracket_list`] —
/// a `//` in the opener→first-item gap, and one anywhere between or after the items.
///
/// Split out for the caller whose own-line-BLOCK clause differs: the `with { … }`
/// import-attribute clause, which must ask
/// [`Printer::has_own_line_attribute_comments`] instead (a glued attribute makes the
/// bracketed-list spelling non-idempotent — see that function). It is the only
/// legitimate reason to bypass the three-clause predicate above; a caller with an
/// *extra* question `||`s it onto that one rather than restating any clause here.
///
/// ⚠️ **Both clauses take their bounds from `span`, and that is the point.** Restating
/// them per site means restating the closing-delimiter offset, and the attribute clause
/// got it wrong — it reached to the statement's `;` instead of the `}`, handing the
/// `}`→`;` gap a second emitter on top of the caller's post-`;` run and DOUBLE-PRINTING
/// every comment there (`docs/comments.md` §The element-comma seam). Deriving
/// `span.end - 1` in one place makes that slip unspellable.
pub(crate) fn has_expanding_line_comments_in_bracket_list<T, F>(
&self,
span: Span,
items: &[T],
get_span: F,
) -> bool
where
F: Fn(&T) -> Span,
{
let Some(first) = items.first() else {
return false;
};
self.has_line_comments_between(span.start + 1, get_span(first).start)
|| self.has_line_comments_in_delimited_list(items, &get_span, span.end - 1)
}
/// Find the closing `)` between a start position and end boundary.
///
/// Scans the source to find the `)` that closes the params. Returns
/// the position AFTER the `)` for use as a boundary.
pub(crate) fn find_closing_paren(&self, start: u32, end: u32) -> Option<u32> {
let source = self.source.as_bytes();
let end = (end as usize).min(source.len());
let mut depth = 0;
let mut i = start as usize;
// The regex-vs-division anchor, rebuilt where a `/` asks for it rather than
// maintained per byte; `OperandAnchor` owns the rule, including which kinds
// of skipped trivia end an operand.
let mut anchor = OperandAnchor::new(start as usize);
while i < end {
// Only `PAREN_HOP_NEEDLES` can move this scan; every other byte
// reaches the `_ => {}` arm below and was stepped over one at a time,
// so the walk hops between them a word at a time
// (`swar::next_byte_of`). Sound past `anchor` for the reason
// `source_scan::scan_to_matching_brace` states: `OperandAnchor`
// resolves lazily and backwards from the `/` that asks.
//
// Per pass over 1,666 TypeScript files this crosses 458,874 inert
// bytes — the largest inert mass in the family — 94% of them in runs
// of eight or more. ⚠️ Its run distribution is two modes: 80% of the
// hops are adjacent (a `()`/`))` pair) and the rest average 18.7. The
// `is_hop_needle` pre-test in front is what makes the first mode
// free; see the note on `matching_paren_close`, which measures the
// pair with and without it.
if !is_hop_needle(source[i], PAREN_HOP_NEEDLES) {
i = tsv_lang::swar::next_byte_of(&source[..end], i, PAREN_HOP_NEEDLES);
if i >= end {
break;
}
}
if let Some(past) = skip_trivia(source, i, end, TriviaProfile::JS) {
anchor.skipped_trivia(
source,
i,
past,
start as usize,
crate::__private::OPERAND_GRAMMAR,
);
i = past;
continue;
}
match source[i] {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
return Some((i + 1) as u32);
}
}
// Regex literals are the one trivia kind the shared cursor leaves
// significant (it needs previous-token context), so they are
// skipped here. A regex whose body holds comment bytes — `/\//`,
// `/[//]/`, `/\/*/` — would otherwise read as a comment from the
// inside and swallow the rest of the line, losing the `)` this
// scan is looking for and running its range on to some unrelated
// paren. The scan reaches the literal's OPENING `/` first, whose
// next byte is never `/` or `*`, so `skip_trivia` can't claim it.
b'/' => {
if anchor.starts_regex(
source,
i,
start as usize,
crate::__private::OPERAND_GRAMMAR,
) && let Some(past) = skip_regex_literal(source, i, end)
{
i = past;
anchor.skipped_operand(i);
continue;
}
}
_ => {}
}
i += 1;
}
None
}
/// Position of the `)` that closes the `(` at `open` — the index OF the `)`,
/// located with the depth-tracked, comment-aware scan over the rest of the
/// source. Use when only the open-paren position is known and the close lies
/// somewhere ahead; call `find_closing_paren` directly when a tighter search
/// bound is available. Returns `None` if no matching `)` is found.
pub(crate) fn matching_close_paren(&self, open: u32) -> Option<u32> {
self.find_closing_paren(open, self.source.len() as u32)
.map(|after| after - 1)
}
/// Just past the `)` closing a parenthesized list or operand, read FORWARD from
/// `from`, the end of the last item the parens hold — the `)`
/// [`Self::find_closing_paren`] looks for from the `(`, read short. Between `from` and
/// that `)` only trivia (a space, tab, `\n` or `\r`, and comments) may sit, plus one `,` when
/// `trailing_comma` (a list's trailing comma) and the `extra` redundant `)` that close
/// the pairs wrapping the item. Any other byte, or reaching `end`, answers `None`, and
/// the caller runs the full scan.
///
/// Sound because an item's span covers the item whole — a parameter's runs through
/// its annotation, `?`, default and rest, and an expression's through every operand
/// and argument list it holds, a parenthesized one included — while a redundant pair
/// around an item opens before its span and closes after it. So every pair the full
/// scan meets before `from` is balanced by `from`, and past it the `extra + 1`-th
/// `)` is the list's own. A span that ended early could only turn this into `None`
/// (the bytes it left out are not trivia), never into another `)`, unless the bytes it
/// left out were themselves only `)` the item owns — the case the span rule excludes.
pub(in crate::printer) fn close_paren_after_last(
&self,
from: u32,
end: u32,
extra: u32,
trailing_comma: bool,
) -> Option<u32> {
let bytes = self.source.as_bytes();
let end = (end as usize).min(bytes.len());
let mut i = from as usize;
let mut need = extra + 1;
let mut comma = trailing_comma;
while i < end {
match bytes[i] {
b' ' | b'\t' | b'\n' | b'\r' => i += 1,
// A `/` that opens no comment is not trivia here: a regex or a
// division would be part of an item, so the short reading declines.
b'/' => i = skip_trivia(bytes, i, end, TriviaProfile::COMMENTS)?,
b',' if comma && need == 1 => {
comma = false;
i += 1;
}
b')' => {
i += 1;
need -= 1;
if need == 0 {
return Some(i as u32);
}
}
_ => return None,
}
}
None
}
/// How many redundant `(` open in `[from, to)`, the run between a list's `(` (or an
/// operand's keyword) and the first byte of the one item it holds — `None` when
/// anything but those and trivia sits there. The left half of
/// [`Self::close_paren_around`], which needs the count to know how many `)` to pass.
fn redundant_open_parens(&self, from: u32, to: u32) -> Option<u32> {
let bytes = self.source.as_bytes();
let to = (to as usize).min(bytes.len());
let mut i = from as usize;
let mut extra = 0;
while i < to {
match bytes[i] {
b' ' | b'\t' | b'\n' | b'\r' => i += 1,
b'/' => i = skip_trivia(bytes, i, to, TriviaProfile::COMMENTS)?,
b'(' => {
extra += 1;
i += 1;
}
_ => return None,
}
}
(i == to).then_some(extra)
}
/// The index OF the `)` closing the `(` at `open` whose last item is the operand
/// spanning `operand` — the `)` [`Self::matching_close_paren`] looks for, read short
/// where it can be: when only trivia and redundant `(` lie between `lead` and the
/// operand, the close is the `)` [`Self::close_paren_after_last`] finds past those
/// pairs' own. `lead` is where that run starts: just past the `(` for an `if` /
/// `while` condition, just past the keyword for a for-in / for-of right side (the
/// bytes before it — the `(`, the binding, the keyword — are balanced on their own).
pub(in crate::printer) fn close_paren_around(
&self,
open: u32,
lead: u32,
operand: Span,
) -> Option<u32> {
self.redundant_open_parens(lead, operand.start)
.and_then(|extra| {
self.close_paren_after_last(operand.end, self.source.len() as u32, extra, false)
})
.map(|after| after - 1)
.or_else(|| self.matching_close_paren(open))
}
/// Just past the `)` closing the parameter list whose `(` is at `open`, bounded by
/// `end` — [`Self::find_closing_paren`]'s answer, read short from the last
/// parameter's end when there is one, else from just past the `(`
/// ([`Self::close_paren_after_last`]). The full scan answers whatever that cannot,
/// and every position where `open` is not a `(`.
pub(in crate::printer) fn close_paren_after_params(
&self,
open: u32,
last_param_end: Option<u32>,
end: u32,
) -> Option<u32> {
if self.source.as_bytes().get(open as usize) == Some(&b'(') {
let (from, trailing_comma) = last_param_end.map_or((open + 1, false), |e| (e, true));
if let Some(after) = self.close_paren_after_last(from, end, 0, trailing_comma) {
return Some(after);
}
}
self.find_closing_paren(open, end)
}
/// The index OF the `)` closing the non-empty parameter list `params` whose `(` is at
/// `open` — [`Self::matching_close_paren`]'s answer, through
/// [`Self::close_paren_after_params`]'s short reading.
pub(in crate::printer) fn matching_close_paren_of_params(
&self,
open: u32,
params: &[ast::Expression<'_>],
) -> Option<u32> {
self.close_paren_after_params(
open,
params.last().map(|p| p.span().end),
self.source.len() as u32,
)
.map(|after| after - 1)
}
/// Find the end position of a keyword in source text.
///
/// Searches backward from `end` for `keyword` as a whole word (not part of
/// an identifier). Returns the byte position after the last character of the keyword,
/// or `None` if not found.
pub(crate) fn find_keyword_end(&self, keyword: &str, start: u32, end: u32) -> Option<u32> {
// The LAST whole-word occurrence that is not inside a comment — so a
// keyword buried in a comment (`from /* from */ 'x'`) isn't mistaken for
// the real one (which dropped/relocated the comment), while a later real
// keyword still wins over an earlier identifier containing it.
tsv_lang::source_scan::rfind_keyword(
self.source.as_bytes(),
start as usize,
end as usize,
keyword.as_bytes(),
TriviaProfile::JS,
)
.map(|i| (i + keyword.len()) as u32)
}
/// Find a keyword between a start position and end boundary.
///
/// Returns the position of the first character of the keyword if found.
/// Skips over comments and strings. Checks for word boundaries (keyword
/// must not be part of a larger identifier).
pub(crate) fn find_keyword_in_range(&self, start: u32, end: u32, keyword: &str) -> Option<u32> {
let source = self.source.as_bytes();
let end = (end as usize).min(source.len());
tsv_lang::source_scan::find_keyword(
source,
start as usize,
end,
keyword.as_bytes(),
TriviaProfile::JS,
)
.map(|i| i as u32)
}
/// Find the position of the first non-whitespace, non-comment token after `start`.
///
/// Skips spaces, tabs, newlines, line comments (`//`), and block comments (`/* */`).
/// Used to find where the first modifier keyword or identifier begins after decorators.
pub(crate) fn find_first_token_after(&self, start: u32) -> u32 {
let bytes = self.source.as_bytes();
let mut pos = start as usize;
while pos < bytes.len() {
match bytes[pos] {
b' ' | b'\t' | b'\n' | b'\r' => pos += 1,
b'/' if bytes.get(pos + 1) == Some(&b'/') => {
// Skip line comment
pos += 2;
while pos < bytes.len() && bytes[pos] != b'\n' {
pos += 1;
}
}
b'/' if bytes.get(pos + 1) == Some(&b'*') => {
// Skip block comment
pos += 2;
while pos + 1 < bytes.len() {
if bytes[pos] == b'*' && bytes[pos + 1] == b'/' {
pos += 2;
break;
}
pos += 1;
}
}
_ => return pos as u32,
}
}
pos as u32
}
/// Find the source position of a keyword that follows decorators.
///
/// Searches for `keyword` in the source text after the last decorator's end.
/// Returns `fallback` if there are no decorators or the keyword isn't found.
pub(crate) fn find_keyword_after_decorators(
&self,
decorators: Option<&[ast::Decorator<'_>]>,
keyword: &str,
fallback: u32,
) -> u32 {
decorators
.and_then(|decs| decs.last())
.and_then(|last| {
// Comment-aware + word-boundaried, so a keyword inside a comment
// between the decorator and the declaration (`@dec /* class */
// class C {}`) isn't matched (which would drop the comment).
self.find_keyword_in_range(last.span.end, self.source.len() as u32, keyword)
})
.unwrap_or(fallback)
}
/// Emit a node's source span verbatim. Used to round-trip the source of a
/// format-ignored node (statement, block statement, object/pattern
/// property, class/enum/interface/type-literal member) instead of
/// reformatting it.
/// Trailing whitespace is trimmed: a node's significant tokens never end in
/// whitespace, and prettier never preserves it — some spans (e.g. a
/// `TSConstructSignatureDeclaration`'s) over-extend to the next line's start.
fn raw_source_doc(&self, span: Span) -> DocId {
self.raw_source_range(span.start, span.end)
}
/// Emit `[start, end)` of the source verbatim. Like `raw_source_doc` but for a
/// format-ignored member whose verbatim slice must exclude a separator
/// the surrounding loop emits itself (e.g. a type-literal member's `;`), so
/// the terminator isn't duplicated.
///
/// Emitted as a `verbatim_source_span` over the whitespace-trimmed sub-span —
/// an ignored region can be large, and the verbatim slice needs no pool copy;
/// the variant keeps the frozen slice's embedded newlines opaque to
/// `will_break` (source layout, not a break the enclosing group must honor).
fn raw_source_range(&self, start: u32, end: u32) -> DocId {
let trimmed = self.source[start as usize..end as usize].trim_end();
let span = Span {
start,
end: start + trimmed.len() as u32,
};
// The comments inside a format-ignored node ride out in the raw slice, never
// reaching `build_comment_doc` — tell the ledger so they don't read as dropped.
#[cfg(feature = "_internal_comment_check")]
tsv_lang::comment_ledger::record_verbatim_range(self.source, span.start, span.end);
self.d().verbatim_source_span(span, self.source)
}
/// Emit an identifier-name doc node — the doc-side name-emission seam.
/// Span-identity names render as verbatim source (`DocText::SourceSpan`, width
/// resolved at build like every other doc text — the deferral names once had cost
/// more than it saved, see `DocArena::source_span`); the rare escaped name copies
/// its decoded `&str` into the doc text pool (`DocText::Pooled`).
///
/// ⭐ **A plain-ASCII name is not measured at all.** Its width IS its byte length,
/// which the lexer already established (`IdentName::plain_ascii`), so it goes to
/// `DocArena::source_span_plain` — and that is the majority of every width call a
/// format run makes, not a corner: 483,358 of 640,428 on a 1,666-file TypeScript
/// corpus, three of which hold a non-ASCII byte.
pub(in crate::printer) fn ident_name_doc(
&self,
name: ast::IdentName<'_>,
name_start: u32,
) -> DocId {
let d = self.d();
match name.escaped {
Some(s) => d.text_pooled(s),
None => {
let span = Span::new(name_start, name_start + name.raw_len as u32);
// Graded BOTH ways on every name in every debug build, because both
// directions are load-bearing and neither is visible in the output:
// over-claiming is a silent width error, and under-claiming silently
// retires the lever the flag exists for.
debug_assert_eq!(
name.plain_ascii,
!span
.extract(self.source)
.bytes()
.any(printing::is_width_relevant),
"`IdentName::plain_ascii` disagrees with the name's own bytes"
);
if name.plain_ascii {
d.source_span_plain(span)
} else {
d.source_span(span, self.source)
}
}
}
}
/// [`Self::ident_name_doc`] for an `Identifier` node (the name is the
/// leading token of the node span).
pub(in crate::printer) fn identifier_name_doc(&self, id: &ast::Identifier<'_>) -> DocId {
self.ident_name_doc(id.ident_name(), id.span.start)
}
/// Run `f` over a name channel resolved at `name_start` — the compare/width
/// seam. Span-identity names borrow the source slice; an escaped name compares its
/// arena string (so an escaped name still compares decoded).
pub(in crate::printer) fn with_ident_name_at<R>(
&self,
name: ast::IdentName<'_>,
name_start: u32,
f: impl FnOnce(&str) -> R,
) -> R {
match name.escaped {
Some(s) => f(s),
None => {
f(&self.source[name_start as usize..name_start as usize + name.raw_len as usize])
}
}
}
/// [`Self::with_ident_name_at`] for an `Identifier` node.
pub(in crate::printer) fn with_ident_name<R>(
&self,
id: &ast::Identifier<'_>,
f: impl FnOnce(&str) -> R,
) -> R {
self.with_ident_name_at(id.ident_name(), id.span.start, f)
}
/// Run `f` with [`Self::expr_stmt_paren_target`] set to `target`, restoring whatever
/// was there before.
///
/// Restore, not clear: a statement-head position can nest inside another one
/// (a `for` header inside an expression statement whose own target is still live),
/// so clearing would strand the outer wrap. Scoped rather than open-coded because the
/// callers have early returns above them, and a restore skipped by one leaks a paren
/// onto the next sibling — a shape the fixtures don't reach.
pub(in crate::printer) fn with_expr_stmt_paren_target<R>(
&self,
target: Option<Span>,
f: impl FnOnce() -> R,
) -> R {
let saved = self.expr_stmt_paren_target.replace(target);
let result = f();
self.expr_stmt_paren_target.set(saved);
result
}
}
#[cfg(test)]
mod tests {
/// A chain base that defers a trailing run out of a stripped pair, inside a parameter
/// default the arrow prints in more than one state: the base's shared doc is found, not
/// built, on the later builds, and the hit must still report the run
/// ([`super::Printer::chain_shared_doc`]'s replay), or the parameter list prints flat
/// and the comment rides into the body. The input is not one a fixture can carry: its
/// first pass is not its own format.
#[test]
fn a_shared_chain_base_reports_its_deferred_run_to_the_parameter_list() {
for base in ["new a[x + (y\n/* c */\n)]()", "import(x + (y\n/* c */\n))"] {
let source = format!("o.aaaa().bbbb(new A((p = {base}.b().c(), q) => {{ z(); }}));\n");
let once = crate::format(&source).expect("formats");
let comment = once.find("/* c */").expect("the comment prints");
let params_end = once.find(") => {").expect("the arrow prints");
assert!(
comment < params_end,
"the comment left the parameter list for {base:?}:\n{once}"
);
}
}
}