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
// Format-ignore directive honoring for type-member lists (union / intersection /
// tuple / type-parameter / type-argument members), single-child type heads (the
// type after an annotation's `:`, an alias's `=`, a type parameter's `extends`/`=`,
// a named tuple member's `label:`, a mapped type's `]:` value and key-side gaps),
// and annotation heads (the gap BEFORE a `:` — a binding, a property signature, an
// index signature's value, a signature's return type — where the whole `: type`
// freezes).
//
// Value-side argument and element lists join the list family through
// `args_frozen_span` / `element_frozen_span` and the freeze-aware item builder
// `build_arg_item_doc` — a call's, a `new`'s and a member chain's arguments, and an
// array literal's or array pattern's elements. A lone item whose gap anchor its
// printer already holds (dynamic `import()`'s two named fields, a JSDoc cast's
// interior) asks `gap_frozen_span` instead, the same question without the list
// index. Each freezes over the item's own node span, so a spread or rest `...` rides
// inside; an element SLOT may be a hole, which never freezes and contributes only
// its comma to the next slot's gap.
//
// Parameter lists (value-side and type-side alike) join the list family through
// `param_frozen_span`, over each parameter's RENDER span so a decorated parameter
// freezes from its first decorator. A decorated parameter has a SECOND freeze position
// inside itself — a directive between its decorators and its binding freezes just the
// binding — which lives with the decorator printer it composes with
// (`Printer::build_frozen_param_binding_doc`, `printer/decorators.rs`); an index
// signature's key parameter asks `member_gap_frozen` on the `[`→key gap directly.
//
// One seam that knows what a directive is and where it sits, so the printers
// only ever ask "freeze this member / this child / this whole node?" and
// never re-derive directive recognition. Recognition itself stays centralized in
// `tsv_lang::is_format_ignore_directive`; this module owns the *placement*
// classification (the out-of-span leading run vs. an in-span inter-member gap) and
// the paren-transparent freeze emitter. A single-child head is composite-transparent:
// a Union/Intersection child declines (`single_child_frozen`) and freezes via its own
// leading-run walk, so the member rules and the head rules can never both claim one
// directive.
//
// **Rule A — list-item freeze** (the single symmetric rule, every member list
// alike), with a **total, placement-only classification** per directive,
// exception-free: a directive ALONE ON ITS LINE (nothing but whitespace before or
// after it on its physical line) in a member list's leading OR inter-item gap
// freezes the *following* member — the first member and every later member
// identically. ANYTHING ELSE — a directive sharing its line with anything (trailing
// a member, a separator, an opening delimiter, or a declaration head, or glued
// before a value) — is permanently inert: an ordinary comment. This is the same
// semantics every honored site carries (a directive alone on its line between `{`
// and the first class member freezes that member, not the body). See
// docs/conformance_prettier_ignore.md §Format-ignore directive for the behavior contract.
//
// **Gating.** Every entry is gated on the document-level `has_format_ignore` flag, so
// a document with no directive (≈ all of them) pays nothing. The leading-run walk is a
// pure backward byte scan bounded by the first non-run byte — no allocation. The
// in-span gap check reuses the container's existing double-gate (its comment window is
// already open at the call site).
//
// **Comment-model discipline** (docs/comments.md). The directive itself sits OUTSIDE
// every frozen span here — it stays in the enclosing gap / leading run and keeps being
// emitted by the existing emitters (no new trailing-comment emitter is minted). The
// comments *inside* a frozen span ride out in the verbatim slice and are recorded by
// `raw_source_range`'s `record_verbatim_range`, so the print-once ledger counts them.
// The paren-transparent freeze slices the INNER node span only when the paren shell is
// comment-free (dropping a redundant paren losslessly); a shell that holds a comment
// (`(/* c */ a1)`) is frozen WHOLE-span instead, keeping the redundant paren so the
// comment survives — comment preservation outranks redundant-paren removal under a freeze
// (`union_prettier_ignore_paren_shell_comment` exercises it, `comments:audit` guards it).
use super::ParenContext;
use super::Printer;
use super::comments::next_significant_byte;
use super::needs_parens::joins_a_trailing_angle_bracket;
use super::statement_tail::StatementTerminator;
use super::types::TrailingBlock;
use super::types::helpers::{TypeParenRule, outermost_paren, paren_shell_gaps};
use super::unwrap_parenthesized;
use crate::ast::{self, Comment, TSType};
use smallvec::smallvec;
use std::borrow::Borrow;
use tsv_lang::doc::DocBuf;
use tsv_lang::doc::arena::DocId;
use tsv_lang::source_scan::find_char_skipping_comments;
use tsv_lang::{
Span, directive_alone_on_line, is_format_ignore_directive, is_honored_format_ignore,
};
/// The freeze implied by a format-ignore directive alone on its line in a union's or
/// intersection's leading run (the out-of-span region before the node's `span.start`):
/// the FIRST member freezes (Rule A). `multiline` is set when the frozen slice spans
/// lines, forcing the broken layout (a verbatim span is `will_break`-opaque, so the
/// forcing is explicit).
#[derive(Clone, Copy)]
pub(in crate::printer) struct LeadingRunFreeze {
pub(in crate::printer) multiline: bool,
}
impl LeadingRunFreeze {
/// The `(freeze_first, multiline)` flag pair of a resolved leading-run freeze.
pub(in crate::printer) fn first_member_flags(freeze: Option<Self>) -> (bool, bool) {
freeze.map_or((false, false), |f| (true, f.multiline))
}
}
/// Whether `child` is a freeze TARGET — the composite-transparency arm of
/// [`Printer::single_child_frozen`], asked alone by a head that has already resolved
/// the gap verdict as its own ROUTING question. The two are different questions (where
/// the directive sits vs. what freezes), and splitting them keeps a routed head from
/// re-running the gap scan it just ran.
///
/// A Union/Intersection (paren-unwrapped) declines, so the member rules keep applying
/// via the composite's own leading-run walk — the first member freezes (Rule A) — and
/// the head rules and the member rules can never both claim one directive.
/// The freeze SCOPE a routed head takes over its paren-stripped inner — the one axis
/// [`Printer::build_routed_inner_doc`]'s callers disagree on, and the reason they share a
/// seam rather than a family of near-identical names.
#[derive(Clone, Copy)]
pub(in crate::printer) enum RoutedScope {
/// Composite-transparent ([`Printer::build_routed_child_doc`]): a Union / Intersection
/// inner declines the freeze and applies Rule A through its own leading-run walk, which
/// reaches the same in-shell directive; anything else freezes whole. The head's child
/// never needs a precedence pair here, so a source paren is always redundant and drops.
/// The annotation `:`, alias `=`, named-tuple `label:`, tuple-rest `...` and array-element
/// heads.
Transparent,
/// [`Self::Transparent`] for a head whose child CAN need a precedence pair the strip has
/// to re-add — the conditional's `extends` operand, where a conditional or function-type
/// inner takes one. The two arms are the bare authoring's own two arms at that head, so
/// the shelled and paren-free spellings converge on one form.
TransparentParens(TypeParenRule),
/// Frozen WHOLE, composite or not — the conditional-type BRANCHES, the one position where
/// composite-transparency would be wrong.
///
/// A composite branch has nothing to bind the directive to in the paren-free authoring
/// (the interposing `?` / `:` blocks the member rules' leading-run walk), so it freezes
/// whole, operators and all. Inside the shell the `(` IS transparent and the composite
/// could bind it — taking that would hand the shelled spelling a first-member-only freeze
/// the bare one does not have, so the branch keeps its own scope and the two authorings
/// land on one form.
Whole,
}
/// What [`Printer::wrap_frozen_position_pair`] owes a frozen LEFT-SPINE operand, on top of
/// the leftmost-target pairs every position shares.
///
/// A frozen slice replaces the builder that would have asked `needs_parens` at this
/// position, so the pair has to be re-asked around the slice — and asked exactly ONCE, which
/// is what this names: a second pair is valid output but is neither prettier's nor the
/// unfrozen twin's.
#[derive(Clone, Copy)]
pub(in crate::printer) enum FrozenOperandPair {
/// The position's whole question: the `needs_parens` verdict its unfrozen twin takes,
/// plus the pair the SLICE needs beyond it. For a position that emits no pair of its own
/// around the seam's doc — a binary chain's first operand, a tagged template's tag, a
/// bare non-null operand, a bare callee, a linearized chain's base.
///
/// `required` is the verdict the position ALREADY HOLDS, where it holds one the free
/// function cannot reproduce. A linearized chain is that position: `ChainNode::Base`
/// carries `needs_parens: true` by construction at two sites the free function never
/// reaches from the node alone — a sealed optional chain and a paren base ahead of a
/// `!` — so re-deriving the verdict here
/// dropped the pair and re-bound the output (`(a?.b).k` printed `a?.b.k`, which
/// short-circuits where the input throws). Every other position holds no verdict of its
/// own and passes `false`; the free function is still asked either way, so the two are
/// an OR, never a substitution.
Position { ctx: ParenContext, required: bool },
/// Nothing. The position emits a pair of its own around the seam's doc, and no tail it
/// prints JOINS a slice's last token
/// ([`Printer::frozen_slice_absorbs_left_binding_suffix`]) — a ternary's test
/// (`Printer::parenthesize_ternary_test`; `?` neither binds to a bare `new X` nor opens
/// an expression after a `new X<T>`'s `>`), a cast's operand (`as` / `satisfies`, whose
/// ASI-sensitive gap retains the author's shell whenever a freeze can fire there at all),
/// and the interior of a paren the printer RETAINS, whose own `(` … `)` IS the pair.
Emitted,
}
impl FrozenOperandPair {
/// [`Self::Position`] for a position that holds no verdict of its own — the common
/// case, where the free function is the whole answer.
pub(in crate::printer) const fn position(ctx: ParenContext) -> Self {
Self::Position {
ctx,
required: false,
}
}
}
pub(in crate::printer) fn is_freeze_target(child: &TSType<'_>) -> bool {
!matches!(
unwrap_parenthesized(child),
TSType::Union(_) | TSType::Intersection(_)
)
}
impl<'a> Printer<'a> {
//
// Directive recognition and routing — is there a freeze, and what does it target
//
/// The format-ignore directive comment in the adjacent leading run ending at
/// `anchor` (a node's `span.start`), if any. The run is the maximal stretch of
/// whitespace, transparent leading punctuation (`|`, `&`, `(`), and comment spans
/// immediately before `anchor`; the backward walk stops at the first byte outside
/// that set (`=`, `:`, `<`, `[`, …), which bounds the run without a `prev_end`. The
/// directive nearest the anchor wins — the placement floor then decides honoring.
///
/// A directive that sits above the whole statement instead belongs to
/// `Program.body` (already honored at the statement site) and never reaches this
/// run: the walk halts at the alias's `=` (or the annotation's `:`), so the two
/// claims can't overlap. The in-span comment gate can't see this directive — it is
/// physically before the node — so the caller gates on `has_format_ignore` alone.
fn leading_run_directive(&self, anchor: u32) -> Option<&'a Comment> {
let bytes = self.source.as_bytes();
let mut pos = anchor as usize;
loop {
let untrimmed = pos as u32;
while pos > 0
&& matches!(
bytes[pos - 1],
b' ' | b'\t' | b'\n' | b'\r' | b'|' | b'&' | b'('
)
{
pos -= 1;
}
// The run continues only across an immediately-preceding comment span;
// anything else (an operator, a bracket, source text) bounds it. A line
// comment's span takes in everything to its line terminator, so the trim can
// end inside one whose text ends in a byte it steps over (whitespace, `|`, `&`,
// `(`) — [`Self::comment_ending_within`].
let c = self.comment_ending_within(pos as u32, untrimmed)?;
if is_format_ignore_directive(c.content(self.source)) {
return Some(c);
}
pos = c.span.start as usize;
}
}
/// Rule A leading-run freeze plan for a union or intersection whose `span.start` is
/// `node_start` and whose first member's paren-stripped span is `first_inner`. The
/// span-shaped core of [`Self::composite_leading_run_freeze`], which is the only
/// entry — and which owns the `has_format_ignore` gate, so nothing here re-reads it.
///
/// A directive alone on its line freezes the first member (with `multiline` set
/// when the frozen first slice spans lines, so the caller forces the broken
/// layout); any other placement is inert.
fn leading_run_freeze(
&self,
node_start: u32,
first_inner: Option<Span>,
) -> Option<LeadingRunFreeze> {
let directive = self.leading_run_directive(node_start)?;
// The alone-on-line floor, the same one `member_gap_frozen` applies. Without
// it, the walk's `|`/`&`/`(` transparency lets a NESTED composite member reach
// a directive the enclosing list deliberately rejected — one TRAILING a
// previous member or a declaration head (`type T = // prettier-ignore⏎ …`),
// or glued before the value — and resurrect it as a first-member freeze.
// Non-own-line placements are permanently inert (the module header).
if !self.directive_alone_on_line(directive) {
return None;
}
// A multi-line frozen slice makes the caller force the broken layout: a
// `verbatim_source_span` is `will_break`-opaque, so the trigger is asked
// here instead of propagating from the slice. `is_same_line` reads
// `comment_line_breaks`, which stays populated in every printer mode —
// the right table for a verbatim slice, whose emitted bytes physically
// contain the newlines regardless of mode.
let multiline = first_inner.is_some_and(|s| !self.is_same_line(s.start, s.end));
Some(LeadingRunFreeze { multiline })
}
/// [`Self::leading_run_freeze`] for a composite taken as its span start plus its
/// member slice — the one entry every union and intersection site uses, so the first
/// member's paren-strip is spelled here instead of at each of them.
///
/// Opens on the document-level bool, so a directive-free document pays one predicted
/// branch and never that paren-unwrap — every union and intersection in the document
/// asks this question (the same reason [`Self::list_item_frozen`] and
/// [`Self::single_child_frozen`] open with it).
#[inline]
pub(in crate::printer) fn composite_leading_run_freeze(
&self,
node_start: u32,
types: &[&TSType<'_>],
) -> Option<LeadingRunFreeze> {
if !self.has_format_ignore {
return None;
}
let first_inner = types.first().map(|t| unwrap_parenthesized(t).span());
self.leading_run_freeze(node_start, first_inner)
}
/// True when the gap `[prev_end, member_start)` before a list item carries a
/// format-ignore directive **alone on its line** — the one placement that freezes.
/// Any other placement — TRAILING the previous member, the separator
/// (`{ a: 1 } & // prettier-ignore`), an opening delimiter, a declaration head, or
/// glued before the member (`a | /* prettier-ignore */ b`) — is inert (the
/// wrong-node-misbind floor; the `trailing_inert` fixtures are its regression pins,
/// the `glued_inert` fixtures pin the glued side).
///
/// **The single Rule A predicate for every family**, value-level and type-level
/// alike: statement lists (`Program.body`, a block body), class / interface / enum
/// bodies, object properties and destructuring patterns, a member-chain `.` gap, and
/// — through [`Self::list_item_frozen`], [`Self::mapped_gap_frozen`],
/// [`Self::single_child_frozen`] and the annotation heads — every type position. All
/// of them ask the identical question about the identical window, so they share one
/// predicate rather than each re-deriving directive recognition.
///
/// The test keys on the directive's own line, NOT on `is_same_line` against
/// `prev_end`: a blank line injected between `prev_end` and a trailing directive would
/// move the directive off `prev_end`'s line yet leave it trailing the separator, and
/// keying on `prev_end` would flip the freeze on and off across that blank (a
/// non-idempotency `blank_audit` catches). `prev_end` still bounds the comment window.
///
/// Opens on the document-level `has_format_ignore` flag, so a directive-free document
/// (≈ every document) pays one predicted branch instead of the range scan and the
/// directive match — the gate every entry in this module shares.
///
/// **In-source axis** (`comments_in_source_range`) — the one deliberate axis every
/// directive-recognition seam in this module uses (`leading_run_directive` walks
/// physical comment spans; `frozen_paren_shell_has_comment` counts physical presence).
/// Directive recognition is a physical-presence question; a directive is never owned
/// (`owned` ⇒ a bundler annotation or JSDoc cast, never a `format-ignore` directive),
/// so the to-emit and in-source axes coincide, but naming the in-source one keeps the
/// module's axis choice single and deliberate (one question, one predicate).
/// The same fact also answers the emission-routing question at the annotation `:`
/// (`build_type_annotation_doc`), which hands an honored directive to its own builder
/// so it keeps its line and freezes the value: a head-trailing placement is inert, and
/// the relocated form would lose the freeze on the second pass. Asked for composite
/// children too there — the routing is about the directive's own placement, not the
/// freeze target.
pub(in crate::printer) fn member_gap_frozen(&self, prev_end: u32, member_start: u32) -> bool {
self.has_format_ignore
&& self
.comments_in_source_between(prev_end, member_start)
.any(|c| self.is_honored_directive(c))
}
/// Whether comment `c` is a format-ignore directive that HONORS — the recognizer plus
/// the placement floor below, in one predicate. The freeze tests ask it through
/// [`Self::member_gap_frozen`]; an emitter asks it directly when a comment's own
/// layout depends on the answer, because an honored directive must keep the line the
/// author gave it — collapsing it onto the frozen value's line would make it glued,
/// hence inert, and the freeze would be lost on the next pass.
///
/// The document-level `has_format_ignore` gate lives HERE rather than at each call
/// site: no directive in the document means no honored one, so a caller that spelled
/// the flag itself was restating the predicate — and a caller that forgot to differed
/// from its neighbors for no reason. [`Self::member_gap_frozen`] keeps its own copy,
/// where the flag buys something this one can't: skipping the range scan entirely.
pub(in crate::printer) fn is_honored_directive(&self, c: &Comment) -> bool {
self.has_format_ignore && is_honored_format_ignore(self.source, c, self.program_start)
}
/// Whether the FIRST comment a run in `[start, end)` emits is an honored directive —
/// the leading-separator half of the rule [`Self::comment_hangs_next`] states for the
/// separators inside a run: an honored directive must own its line, so a run that opens
/// with one opens on a line of its own rather than beside the token before it.
pub(in crate::printer) fn leading_comment_is_honored_directive(
&self,
start: u32,
end: u32,
) -> bool {
self.has_format_ignore
&& self
.comments_to_emit_between(start, end)
.next()
.is_some_and(|c| self.is_honored_directive(c))
}
/// Whether `next` — the comment that follows another in a leading run — is an honored
/// directive, which the comment before it may not glue onto. The in-run half of the
/// rule [`Self::leading_comment_is_honored_directive`] states for a run's opening
/// separator: an honored directive owns its line, and a comment glued ahead of it on
/// that line makes it inert, so the freeze it earned is gone on the next pass — the
/// wrong form being its own fixed point. The glue test reads the source right after the
/// comment ([`Self::comment_hugs_next`]), and where that is a comma the list re-emits
/// elsewhere (`b⏎/* t */,⏎// prettier-ignore⏎c`) it sees no line break before the
/// directive; the run's emitter ([`Self::push_leading_comment_run`]) asks this first,
/// and gives the directive its own line whatever the source around the comment says.
pub(in crate::printer) fn directive_follows(&self, next: Option<&Comment>) -> bool {
next.is_some_and(|c| self.is_honored_directive(c))
}
/// The placement floor alone — [`tsv_lang::directive_alone_on_line`] against this
/// document's source, from [`Printer::program_start`], for the emitters that ask about
/// placement without the recognizer.
pub(in crate::printer) fn directive_alone_on_line(&self, c: &Comment) -> bool {
directive_alone_on_line(self.source, c, self.program_start)
}
/// [`Self::member_gap_frozen`] for a mapped type's two key-side gaps, anchored per
/// the delimited-list convention (the gap opens just past the `{` / `[`): the
/// SIGNATURE gap (`{`→`[`), where an alone-on-line directive freezes the whole
/// `[K in ...]: V` clause (the mapped type's sole-member analog of Rule A), and the
/// BINDING gap (`[`→key), where a directive freezes just the `K in ...` binding —
/// freezing the whole node there would freeze the `[` that *precedes* the
/// directive, so prettier's whole-node redirect is deliberately not copied (the
/// `mapped_prettier_ignore_key` divergence).
pub(in crate::printer) fn mapped_gap_frozen(&self, gap_start: u32, target_start: u32) -> bool {
self.member_gap_frozen(gap_start, target_start)
}
/// [`Self::member_gap_frozen`] for a head position's SINGLE child — the type after
/// an annotation's `:`, an alias's `=`, a type parameter's `extends`/`=`, a named
/// tuple member's `label:`, or a mapped type's `]:`. An alone-on-line directive
/// in the head→child gap `[gap_start, child.span().start)` freezes the child whole.
///
/// **Composite-transparent**: a Union/Intersection child (paren-unwrapped) declines,
/// so the member rules keep applying via the composite's own leading-run walk —
/// the first member freezes (Rule A).
///
/// The window ends at the child's OWN span start, never a paren-stripped inner
/// start: an in-shell directive (`extends (// format-ignore⏎ T)`) never triggers
/// THIS seam, so a frozen whole-shell slice can never double-print a comment an
/// enclosing gap emitter also sees. The heads that honor an in-shell directive do
/// so via [`Self::paren_interior_routed_inner`] — the shell's own interior seam,
/// which freezes the paren-stripped INNER slice only and leaves every shell-gap
/// comment to the head's (window-widened) emitters, so the single-print invariant
/// holds there too.
pub(in crate::printer) fn single_child_frozen(
&self,
gap_start: u32,
child: &TSType<'_>,
) -> bool {
// The document-level bool first: several head sites (alias, named-tuple,
// mapped value) ask unconditionally on hot paths, so a directive-free
// document must pay exactly this one branch — never the paren-unwrap below
// (`member_gap_frozen` re-checks the flag, harmlessly, for its other callers).
if !self.has_format_ignore {
return false;
}
if !is_freeze_target(child) {
return false;
}
self.member_gap_frozen(gap_start, child.span().start)
}
/// The frozen `: type` for an annotation HEAD gap — the gap *before* the `:`, between
/// the head (a binding name plus any `?`/`!`, an index signature's `]`, a signature's
/// `)`) and the annotation. An alone-on-line directive there freezes the WHOLE
/// annotation node, the one it precedes: `Some` is the gap's own-line-preserving
/// comment run followed by the verbatim `: type` slice; `None` leaves the caller's
/// ordinary emission in place. The caller pushes the head itself (no trailing space)
/// and concatenates this after it.
///
/// **Span-shaped, and deliberately NOT composite-transparent** — unlike a single-child
/// head ([`Self::single_child_frozen`]), where a union / intersection child declines so
/// the member rules apply instead. Those rules can only apply where the composite's own
/// leading run REACHES the directive, and that run crosses whitespace and the
/// transparent `|` / `&` / `(` only — the annotation's `:` blocks it. So a composite
/// value here rides *inside* the frozen span, and the head rules and the member rules
/// still can never both claim one directive.
///
/// The emission is [`Self::append_keyword_value_line_comments`], the same
/// own-line-preserving run the single-child heads use and for the same reason: every
/// one of these heads' default emitters trails the gap's first comment on the head's
/// line, an inert placement that would lose the freeze on the second pass. The block
/// spelling routes here too — placement, not spelling, keys the freeze, and the run
/// keeps an own-line block on its own line.
///
/// The document-level flag is read HERE rather than left to
/// [`Self::member_gap_frozen`] two calls down: every annotation in the document asks
/// this question, so a directive-free document pays one predicted branch at the entry
/// instead of a call chain (the same reason [`Self::single_child_frozen`] opens with it).
pub(in crate::printer) fn build_frozen_annotation_head_doc(
&self,
gap_start: u32,
annotation: &ast::TSTypeAnnotation<'_>,
) -> Option<DocId> {
if !self.has_format_ignore {
return None;
}
self.build_frozen_head_span_doc(gap_start, annotation.span)
}
/// [`Self::build_frozen_annotation_head_doc`] for an annotation preceded by an optional
/// `?` / definite `!` MARKER, asked on the key→marker gap: a directive alone on its line
/// there precedes `?: T` as a unit, so the freeze starts at the marker and the caller
/// must emit neither the marker nor the annotation itself. `Some` carries the frozen
/// doc plus the cursor just past it (the annotation's span end), so the caller advances
/// exactly as its ordinary arm would. `None` leaves the ordinary emission in place —
/// including the case where the directive sits AFTER the marker, which the plain
/// annotation-head ask then routes on the marker→`:` gap.
///
/// `marker` and `annotation` are taken as options so the two hosts — a class property
/// (`?` or `!`) and a property signature (`?` only) — share one seam instead of each
/// spelling the have-both test. The marker scan runs only in a directive-bearing
/// document (the flag is checked first) and is bounded by the annotation's `:`.
pub(in crate::printer) fn build_frozen_marker_annotation_tail(
&self,
gap_start: u32,
marker: Option<u8>,
annotation: Option<&ast::TSTypeAnnotation<'_>>,
) -> Option<(DocId, u32)> {
if !self.has_format_ignore {
return None;
}
let (marker, annotation) = marker.zip(annotation)?;
let marker_pos = find_char_skipping_comments(
self.source.as_bytes(),
gap_start as usize,
annotation.span.start as usize,
marker,
)? as u32;
let frozen =
self.build_frozen_head_span_doc(gap_start, Span::new(marker_pos, annotation.span.end))?;
Some((frozen, annotation.span.end))
}
/// The shared core of the two annotation-head entries: the directive must be alone on its
/// line in `[gap_start, frozen.start)`, and `frozen` is emitted verbatim after the gap's
/// own-line-preserving comment run.
fn build_frozen_head_span_doc(&self, gap_start: u32, frozen: Span) -> Option<DocId> {
if !self.member_gap_frozen(gap_start, frozen.start) {
return None;
}
let d = self.d();
let mut parts: DocBuf = smallvec![];
self.append_keyword_value_line_comments(
&mut parts,
gap_start,
frozen.start,
self.build_frozen_span_doc(frozen),
);
Some(d.concat(&parts))
}
/// The strip target for a head whose child is a paren shell whose INTERIOR leading
/// gap — from just past the outermost `(` to the fully-unwrapped inner's span
/// start, spanning every nested layer — holds an alone-on-line format-ignore
/// directive: `Some(inner)`, the fully-unwrapped inner, is the ROUTING answer.
///
/// An in-shell directive is invisible to [`Self::single_child_frozen`] (whose
/// window deliberately ends at the shell's own span start), so a head that wants to
/// honor it strips the shell and emits the interior leading run itself (window
/// widened to the inner's start — the directive keeps its own line), then builds the
/// inner with [`Self::build_routed_child_doc`]: the freeze slices the INNER only,
/// never a whole-shell slice, so the shell-gap comments the routing hands to the
/// head's emitters can't also ride a verbatim slice. Comments in the shell's
/// TRAILING gaps are the caller's to lift (`with_stripped_paren_trailing`), so the
/// freeze stays lossless and every shell comment prints exactly once.
///
/// Routing only — whether the inner itself freezes is the separate
/// composite-transparency question (`is_freeze_target`, answered by
/// `build_routed_child_doc`): a Union/Intersection inner declines and freezes via
/// its own leading-run walk, whose `(`-transparency reaches the same in-shell
/// directive. Gated on `has_format_ignore`.
pub(in crate::printer) fn paren_interior_routed_inner<'t>(
&self,
shell: &'t TSType<'t>,
) -> Option<&'t TSType<'t>> {
self.paren_shell_routed_inner(outermost_paren(shell)?)
}
/// [`Self::paren_interior_routed_inner`] asked of the shell NODE rather than of the
/// type that may carry one — for the shell's own emitter, which holds the
/// `TSParenthesizedType` and has already established that there is a shell.
///
/// One body, so the routing verdict a head routes on and the one the shell's emitter
/// reads to freeze its stripped inner cannot come apart.
pub(in crate::printer) fn paren_shell_routed_inner<'t>(
&self,
shell: &'t ast::TSParenthesizedType<'t>,
) -> Option<&'t TSType<'t>> {
if !self.has_format_ignore {
return None;
}
// The window is [`paren_shell_gaps`]' deep leading half — redundant layers strip
// as a unit, so an in-shell directive binds from the OUTERMOST `(`.
let (leading, _) = paren_shell_gaps(shell);
self.member_gap_frozen(leading.start, leading.end)
.then(|| unwrap_parenthesized(shell.type_annotation))
}
/// [`Self::member_gap_frozen`] for list item `i`, the single home of the
/// gap-anchor convention: the FIRST item's gap opens at `container_start` (the
/// container's span start for a bare list like a union, or just past the opening
/// delimiter for a delimited one — `<`/`[` — where a leading-run freeze doesn't
/// apply); a LATER item's gap opens at the previous item's RAW span end — never a
/// comma- or trailing-comment-advanced cursor — so a directive after the
/// separator still binds forward while the own-line floor keeps a trailing
/// directive inert. Every container loop routes through here (or the slice
/// wrapper [`Self::list_member_frozen`]) rather than picking its own anchors.
/// Closure-shaped so the item type is family-agnostic (`TSType` members,
/// `TSTypeParameter` declarations).
///
/// Opens on the document-level bool — ~25 container loops ask this PER ITEM, the
/// hottest ignore-seam position by a wide margin, so a directive-free document must
/// pay exactly that one predicted branch and never the span closure behind it (the
/// same reason [`Self::single_child_frozen`] opens with it; `member_gap_frozen`
/// re-checks the flag, harmlessly, for its other callers).
#[inline]
pub(in crate::printer) fn list_item_frozen(
&self,
container_start: u32,
item_span: &impl Fn(usize) -> Span,
i: usize,
) -> bool {
if !self.has_format_ignore {
return false;
}
if i == 0 {
self.member_gap_frozen(container_start, item_span(0).start)
} else {
self.member_gap_frozen(item_span(i - 1).end, item_span(i).start)
}
}
/// [`Self::list_item_frozen`] for parameter list item `i`, over the parameter's
/// RENDER span ([`Self::param_render_span`]) rather than its node span, so a
/// decorated parameter's gap ends where its first decorator begins — the directive
/// precedes the decorator, not the binding.
///
/// `Some` is the span to freeze: that same render span, so a parameter property's
/// modifiers (`public p = 1`), a parameter decorator (`@dec() p: T`), and a rest
/// parameter's `...` all ride inside the freeze, matching prettier. The list's `,` is
/// parent-owned and stays outside. Handing back the span rather than a bool keeps the
/// decorator scan to one pass — every caller freezes exactly what was measured.
///
/// `open_paren` is the `(` position (the delimited-list anchor opens just past it);
/// with no located paren the first item's gap is empty, so nothing can freeze there.
#[inline]
pub(in crate::printer) fn param_frozen_span(
&self,
open_paren: Option<u32>,
params: &[ast::Expression<'_>],
i: usize,
) -> Option<Span> {
if !self.has_format_ignore {
return None;
}
let item_span = |j: usize| self.param_render_span(¶ms[j]);
let container_start = open_paren.map_or_else(|| item_span(0).start, |p| p + 1);
self.list_item_frozen(container_start, &item_span, i)
.then(|| item_span(i))
}
/// [`Self::member_gap_frozen`] resolved to the span it freezes, for a value-side item
/// whose gap anchor the caller already holds — a lone argument (the JSDoc cast's
/// interior, dynamic `import()`'s two named fields, which are not a slice at all) or
/// an element slot, whose anchor skips the holes before it.
///
/// `Some` is the span to freeze: the item's own node span, so a spread or rest `...`
/// rides inside the freeze (the node starts at the `...`, so its span already covers
/// it), matching prettier. The list's `,` is parent-owned and stays outside.
#[inline]
pub(in crate::printer) fn gap_frozen_span(&self, prev_end: u32, span: Span) -> Option<Span> {
self.member_gap_frozen(prev_end, span.start).then_some(span)
}
/// The freeze a construct's LEFTMOST operand takes when an alone-on-line format-ignore
/// directive sits in the grouping parens the PARSER erased ahead of it — the value side's
/// leading-EDGE question (`(⏎// prettier-ignore⏎{b: 1}⏎) ? c : d`, `… + 1`, `….k`,
/// `… as T`, `…!`, `…(1)`, `` …`t` ``, `…, y`).
///
/// Those parens leave no node behind, so the directive lands in the region between the
/// construct's own span start and its operand's — *inside* the construct's span. The
/// enclosing value head therefore cannot see it ([`Self::value_head_frozen_span`] reads
/// the gap BEFORE the value) and the construct itself has to answer, exactly as the type
/// side's leading-edge links answer for a paren shell they strip. Each construct already
/// EMITS that region ([`Printer::removed_paren_comments_opt`] /
/// [`Printer::prepend_removed_paren_comments`], which hoist the run ahead of the whole
/// construct — where the reparse reads it too), so the freeze is all that was missing:
/// without it the author's bytes normalized on the first pass and the directive, read
/// back from the head's gap on the second, then froze a form it never chose.
///
/// The scope is that ONE operand — the construct the directive precedes, Rule A's child
/// scope. The whole construct is not a candidate: its slice would have to contain the `)`
/// the strip removes.
///
/// ⚠️ The operand is the construct's IMMEDIATE child, never the left spine's leaf. A walk
/// to the leaf freezes the wrong unit the moment the author parenthesized more than one
/// level of it (`(⏎// prettier-ignore⏎a.b⏎) ? c : d` must freeze `a.b`, not `a`), and each
/// construct asking about its own child is what makes the outermost region — the one the
/// author's parens actually wrapped — the one that answers.
///
/// An operand the author left bare collapses the region to nothing, so the scan is empty;
/// the `has_format_ignore` gate inside [`Self::gap_frozen_span`] is what a directive-free
/// document pays.
#[inline]
pub(in crate::printer) fn left_spine_operand_frozen_span(
&self,
node_start: u32,
operand: &ast::Expression<'_>,
) -> Option<Span> {
self.gap_frozen_span(node_start, operand.span())
}
/// The pair a frozen LEFTMOST operand takes from its POSITION, had its own builder run.
///
/// Three questions, one answer, because a paren is a paren: **the position's own
/// precedence pair** ([`FrozenOperandPair`] — the `needs_parens` verdict the unfrozen
/// twin applies at this very position, plus the one the SLICE needs beyond it), the
/// expression statement's target ([`Printer::maybe_wrap_expr_stmt_paren`] — the node that
/// must not open the line with `{` / `function` / `class`) and the arrow body's
/// leftmost-object one ([`Printer::arrow_body_leftmost_parens_target`], whose `{` would
/// read as a block body).
///
/// All three are cells or verdicts the node's OWN builder reads, and a verbatim slice
/// replaces that builder — so a freeze that skips one drops a pair the position requires.
/// The leftmost-target pair is the unparseable half (`{b: 1}.k;`, `class {}();`,
/// `() =>⏎{b: 1}.k`); the precedence pair is the SILENT half, since the output usually
/// parses and simply binds differently (`(⏎// prettier-ignore⏎() => 1⏎) + 1` printed
/// `() => 1 + 1`, whose body has swallowed the `+ 1`). Every pair goes outside the slice,
/// where the unfrozen path puts it too.
///
/// At most ONE pair is emitted: a parenthesized operand can no longer open the statement's
/// line with `{`, nor read as an arrow's block body, so the precedence pair subsumes both
/// targets when it fires. The targets are matched by span and neither is consumed, so
/// re-running this across a conditional group's candidates is sound.
pub(in crate::printer) fn wrap_frozen_position_pair(
&self,
operand: &ast::Expression<'_>,
pair: FrozenOperandPair,
doc: DocId,
) -> DocId {
if self.frozen_operand_pair_needed(operand, pair) {
return self.d().parens(doc);
}
let span = operand.span();
let doc = if self.arrow_body_leftmost_parens_target.get() == Some(span) {
self.d().parens(doc)
} else {
doc
};
self.maybe_wrap_expr_stmt_paren(span, doc)
}
/// [`FrozenOperandPair`] resolved against the operand: whether THIS seam owes the pair.
///
/// The position half is the verdict the position already holds
/// ([`FrozenOperandPair::Position`]'s `required`) OR the plain
/// [`Printer::needs_parens`], so the frozen and unfrozen forms cannot disagree about the
/// shell — the same discipline [`Self::build_frozen_value_doc`] keeps at the value-side
/// positions. The slice half is [`Self::frozen_slice_absorbs_left_binding_suffix`].
fn frozen_operand_pair_needed(
&self,
operand: &ast::Expression<'_>,
pair: FrozenOperandPair,
) -> bool {
match pair {
FrozenOperandPair::Emitted => false,
FrozenOperandPair::Position { ctx, required } => {
required
|| self.needs_parens(operand, ctx)
|| self.frozen_slice_absorbs_left_binding_suffix(operand, ctx)
}
}
}
/// Whether a frozen operand's SLICE would absorb the tail its position prints just past
/// it — a pair the slice needs that the node does not.
///
/// **The axis is the JOIN between the slice's LAST token and the tail's FIRST**, not the
/// position's operator precedence. One operand family reaches it — a **`new` expression
/// the author wrote with no argument list** — because the printed form always carries one
/// (the unfrozen printer emits `new X()` even where the author wrote none), so
/// `needs_parens` leaves it bare everywhere while a verbatim slice carries only the
/// author's bytes. Its two spellings end in two different tokens, and each joins with a
/// different set of tails:
///
/// - `new X`, ending in the callee's own token, absorbs a LEFT-BINDING SUFFIX — `.k`,
/// `[0]`, `(1)`, `!`, `` `t` `` — which binds to the CALLEE instead: `new X.k` is
/// `new (X.k)`, a silent rebind, and `new X?.()` does not even typecheck. Every
/// operator tail reads `new X` as a whole, so those positions need nothing.
/// - `new X<T>`, ending in `>`, needs the pair at the same suffixes for a mix of reasons
/// — `(1)` and `` `t` `` rebind to the `new` as they do bare, `[0]` rebinds through the
/// RELATIONAL reading (`((new X) < T) > [0]`), and `.k` / `!` are rejected outright by
/// tsc, acorn-typescript and tsv alike — AND at the operator tails whose first token
/// joins that `>` ([`joins_a_trailing_angle_bracket`], whose doc carries the
/// per-operator table, and which the unfrozen printer's instantiation-tail rule
/// reads too): `+` and `-` continue it as a relational chain
/// (`new X<T> + 1` is `((new X) < T) > +1`, a silent rebind every parser agrees on), and
/// `>` / `>>` / `>>>` / `<` / `>=` / `<<` leave a form at least one of tsc and
/// acorn-typescript rejects. Every other operator — `*`, `**`, `<=`, `?:`, `??`, `||`,
/// `instanceof`, `,`, `=` — cannot open an expression after the `>`, so all three
/// parsers backtrack and read the type arguments; those spellings are AST-identical bare
/// and take no pair.
///
/// ⚠️ Three of the rejections are one parser's alone, so no reparse of tsv's own output
/// can grade them: tsv accepts `<`, `>=` (tsc rejects) and `<<` (acorn-typescript
/// rejects) as the same tree. The table is measured against those two parsers directly,
/// never against `tsv_debug ast_diff`, which is blind wherever tsv agrees with itself.
///
/// The `new` callee's own seam ([`Printer::build_frozen_new_callee_doc`]) asks
/// [`Self::frozen_new_slice_absorbs_tail`] for the same reason, one suffix over — the
/// argument list it prints past the slice, which both spellings absorb. `NewCallee` is
/// absent from the context list because it never reaches this seam: the `new`→callee gap
/// has a freeze of its own.
///
/// The sibling **instantiation** slice (`f<T>`) ends in `>` too and joins the same
/// tails, but needs nothing here: its printed form carries the same bytes as the slice
/// (there is no argument list to add), so the unfrozen verdict this seam ORs with —
/// `ends_with_instantiation_close` at a binary left and at the left of `as` /
/// `satisfies`, `NonNull`, a postfix update, a chain base — already keeps the pair at
/// every joining tail, frozen or not.
fn frozen_slice_absorbs_left_binding_suffix(
&self,
operand: &ast::Expression<'_>,
ctx: ParenContext,
) -> bool {
if !self.frozen_new_slice_absorbs_tail(operand) {
return false;
}
match ctx {
ParenContext::ChainBase
| ParenContext::Callee
| ParenContext::TaggedTemplateTag
| ParenContext::NonNull => true,
// The slice's last token is the `>` closing its TYPE ARGUMENTS exactly when it
// has them: the walk above proved no argument list follows the head, so with
// type arguments present the head ENDS at their `>` and only trivia follows.
ParenContext::BinaryLeft { parent_op } => {
joins_a_trailing_angle_bracket(parent_op)
&& matches!(
&operand.kind,
ast::ExpressionKind::NewExpression(new_expr)
if new_expr.type_arguments.is_some()
)
}
_ => false,
}
}
/// Whether a frozen slice of `expr` would ABSORB the left-binding tail printed after it —
/// true for a `new` expression the author wrote with no argument list of its own.
///
/// Everything between the `new`'s callee (with its type arguments) and its own span end
/// is either that callee's closing shell parens, trivia, or the argument list, so the
/// list is present exactly when a `(` turns up in the walk. Reading the source rather
/// than `arguments.is_empty()` is what separates `new X` from `new X()`, which parse to
/// the same node.
///
/// Two families ask: the `new` callee's own freeze
/// ([`Printer::build_frozen_new_callee_doc`]), whose tail is the argument list the
/// printer supplies and which both spellings absorb, and the left-spine positions
/// ([`Self::frozen_slice_absorbs_left_binding_suffix`]), which then split on which token
/// the slice ENDS in and which one the tail OPENS with.
pub(in crate::printer) fn frozen_new_slice_absorbs_tail(
&self,
expr: &ast::Expression<'_>,
) -> bool {
let ast::ExpressionKind::NewExpression(new_expr) = &expr.kind else {
return false;
};
let head_end = new_expr
.type_arguments
.as_ref()
.map_or_else(|| new_expr.callee.span().end, |args| args.span.end);
let mut pos = head_end;
while let Some(i) = next_significant_byte(self.source, pos, expr.span.end) {
if self.source.as_bytes()[i] == b'(' {
return false;
}
pos = i as u32 + 1;
}
true
}
/// [`Self::left_spine_operand_frozen_span`] resolved to the operand's doc: the verbatim
/// slice when the freeze fires, else the construct's own build, taken lazily.
#[inline]
pub(in crate::printer) fn build_left_spine_operand_doc(
&self,
node_start: u32,
operand: &ast::Expression<'_>,
pair: FrozenOperandPair,
ordinary: impl FnOnce() -> DocId,
) -> DocId {
self.build_left_spine_operand_doc_if(true, node_start, operand, pair, ordinary)
}
/// [`Self::build_left_spine_operand_doc`] told whether the position can take the freeze
/// at all.
///
/// `false` is a position that prints a REQUIRED pair of its own around the operand — a
/// callee that needs one, an IIFE's or a sealed chain's owned pair. That pair is emitted
/// around a doc the printer BUILDS, and a verbatim slice cannot be reached into, so a
/// freeze there drops the pair and can emit output that does not parse. The directive is
/// still honored one level out, where the hoisted run leads the whole statement and the
/// statement's own freeze holds that form — pair included — from the NEXT pass on. A
/// coarser claim, and one pass late: this pass normalizes the operand before the run
/// reaches the statement gap, so the author's own bytes are normalized once
/// (`UNHONORED CallExpression.callee` in the `ignore:audit` ratchet).
#[inline]
pub(in crate::printer) fn build_left_spine_operand_doc_if(
&self,
can_freeze: bool,
node_start: u32,
operand: &ast::Expression<'_>,
pair: FrozenOperandPair,
ordinary: impl FnOnce() -> DocId,
) -> DocId {
match can_freeze
.then(|| self.left_spine_operand_frozen_span(node_start, operand))
.flatten()
{
Some(frozen) => self.wrap_frozen_position_pair(
operand,
pair,
self.build_frozen_expression_doc(operand, frozen),
),
None => ordinary(),
}
}
/// Run `build` with a linearized chain's BASE freeze armed
/// ([`Printer::frozen_chain_base`]) — the chain's own spelling of
/// [`Self::build_left_spine_operand_doc`], which it cannot use directly because the
/// base's doc is built inside the layout selection rather than by the seam that knows
/// the chain's leading region.
///
/// A no-op with no freeze, so every chain door hands its build through unconditionally.
pub(in crate::printer) fn with_frozen_chain_base<T>(
&self,
frozen: Option<Span>,
build: impl FnOnce() -> T,
) -> T {
if frozen.is_none() {
return build();
}
let saved = self.frozen_chain_base.replace(frozen);
let built = build();
self.frozen_chain_base.set(saved);
built
}
/// The armed base freeze, when it is THIS node's
/// ([`Printer::frozen_chain_base`] — span-keyed, so a chain nested inside the base's own
/// doc reads nothing).
pub(in crate::printer) fn frozen_chain_base_span(
&self,
base: &ast::Expression<'_>,
) -> Option<Span> {
self.frozen_chain_base
.get()
.filter(|frozen| *frozen == base.span())
}
/// The value-side analog of [`Self::single_child_frozen`]: a construct that holds ONE
/// value behind a delimiter of its own — a `for` header's `(`→init / `;`→test /
/// `;`→update clauses, a restricted production's grouping `(` (`return` / `throw` /
/// `yield`), a Svelte `{…}` directive value or expression tag. An alone-on-line
/// directive in the delimiter→value gap freezes the value WHOLE.
///
/// `Some` is the span to freeze: the value's own node span, so the delimiter that
/// CLOSES it (the header's `;`, the grouping `)`, the closing `}`) stays parent-owned —
/// prettier's `for`-init slice swallows the `;` and emits a header that no longer
/// parses (`init_declaration_prettier_ignore_head_prettier_divergence`).
///
/// Span-shaped rather than node-shaped because a `for` init is a `VariableDeclaration`
/// as often as an `Expression`, and **not** composite-transparent: a value's own
/// member rules apply only where the value's leading run REACHES the directive, and
/// every delimiter here blocks that run — so a sequence value rides whole inside the
/// slice, which is exactly what prettier does, and the head rules and the member rules
/// can never both claim one directive. The member half is the sequence's inter-operand
/// gap, which asks [`Self::gap_frozen_span`] per operand.
///
/// The same predicate as [`Self::gap_frozen_span`] — the gap is a gap either way, and
/// the `has_format_ignore` gate is already spent inside it. The two names exist because
/// the *rules* differ (a head claims the whole value; a member claims one item), and a
/// call site that spells which one it is documents the shape it froze.
#[inline]
pub(in crate::printer) fn value_head_frozen_span(
&self,
gap_start: u32,
value: Span,
) -> Option<Span> {
self.gap_frozen_span(gap_start, value)
}
/// [`Self::value_head_frozen_span`] resolved to the doc it emits, for a value head whose
/// unfrozen emission is the plain [`Self::build_expression_doc`] — the `export`→value and
/// `export default`→value gaps, the twin heads that must not disagree, and an **enum
/// member's** `=`→value gap, whose enclosing member loop claims the value→`,` gap
/// (`Printer::collect_trailing_comments`) so no paren shell stands between the slice and
/// the separator.
///
/// Deliberately NOT for a head with an ordinary builder of its own (an `if` condition, a
/// `for` update clause, an assignment RHS, an object property value): those resolve the
/// span themselves and pair it with the builder the position calls for, and routing them
/// through here would silently substitute the plain one. The statement analog
/// ([`Self::build_statement_head_doc`]) takes its ordinary builder lazily for exactly
/// that reason — every statement head has a different one.
/// All three callers — an enum member's initializer, `export default` and `export =` —
/// name [`Self::build_expression_doc`] today. They agree because the binaryish default
/// answers all three, not because the seam decides for them: the parameter is what
/// keeps a future caller from having the plain builder substituted silently, which is
/// the bug the paragraph above warns about.
#[inline]
pub(in crate::printer) fn build_value_head_doc(
&self,
gap_start: u32,
value: &ast::Expression<'_>,
ordinary: impl FnOnce() -> DocId,
) -> DocId {
match self.value_head_frozen_span(gap_start, value.span()) {
Some(frozen) => self.build_frozen_expression_doc(value, frozen),
None => ordinary(),
}
}
/// The statement-side analog of [`Self::value_head_frozen_span`]: a head that introduces
/// ONE statement behind a delimiter of its own — an `if`'s `)`→consequent and
/// `else`→alternate gaps, and every loop's header→body gap (`while`, do-while, all three
/// `for` spellings, block and non-block bodies alike). An alone-on-line directive there
/// freezes the body whole, and `ordinary` — the caller's own builder, taken lazily so a
/// directive-free document never pays for the frozen path's setup — supplies the
/// unfrozen doc.
///
/// The frozen slice is the body statement's own span, so the introducing delimiter stays
/// parent-owned. Unlike the value heads there is no paren shell to re-synthesize — a
/// statement is never parenthesized by the printer — and the emission is
/// [`Self::build_frozen_statement_doc`], so a frozen body whose kind owes a `;` (a bare
/// `break` / `continue` / `debugger` relying on ASI) gets it restored, exactly as at the
/// statement-list seams. The `export default` declaration VALUES take the span-shaped
/// sibling ([`Self::build_declaration_value_head_doc`]) instead — those kinds never owe
/// one.
///
/// A head whose ordinary layout would pull the gap's comment onto the head's line must
/// ask this BEFORE choosing that layout and keep the directive's own line when it fires:
/// a head-trailing placement is inert under the floor, so the relocated form would lose
/// the freeze on the second pass.
#[inline]
pub(in crate::printer) fn build_statement_head_doc(
&self,
gap_start: u32,
stmt: &ast::Statement<'_>,
ordinary: impl FnOnce() -> DocId,
) -> DocId {
match self.gap_frozen_span(gap_start, stmt.span()) {
Some(_) => self.build_frozen_statement_doc(stmt),
None => ordinary(),
}
}
/// [`Self::build_statement_head_doc`] for a head whose value is SPAN-shaped rather than
/// a `Statement` — the `export default` declaration values (function / declare-function
/// / class / interface), none of which ever owes a `;`, so the emission is the plain
/// [`Self::build_frozen_node_doc`] over the slice.
#[inline]
pub(in crate::printer) fn build_declaration_value_head_doc(
&self,
gap_start: u32,
value: Span,
ordinary: impl FnOnce() -> DocId,
) -> DocId {
match self.gap_frozen_span(gap_start, value) {
Some(frozen) => self.build_frozen_node_doc(frozen),
None => ordinary(),
}
}
/// [`Self::list_item_frozen`] for a value-side ARGUMENT list item `i` — a call's,
/// a `new`'s or a member chain's arguments. `Some` is the span to freeze, per
/// [`Self::gap_frozen_span`].
///
/// `container_start` is where the FIRST item's gap opens, per each family's own
/// convention: just past the `[` for a bracketed list, and for a call the position
/// the `(` follows (the callee's, or the type-argument list's, end) — the same window
/// every leading-argument-comment emitter at those sites already opens, so a
/// directive written above the `(` leads the first argument exactly as it does for
/// those emitters (prettier-confirmed).
#[inline]
pub(in crate::printer) fn args_frozen_span(
&self,
container_start: u32,
args: &[&ast::Expression<'_>],
i: usize,
) -> Option<Span> {
if !self.has_format_ignore {
return None;
}
let item_span = |j: usize| args[j].span();
self.list_item_frozen(container_start, &item_span, i)
.then(|| item_span(i))
}
/// [`Self::args_frozen_span`] over an ELEMENT slot slice — an array literal's or
/// array pattern's `elements`, where a slot may be a HOLE (elision). Generic over the
/// slot's element: the literal holds `Option<&Expression>`, the pattern
/// `Option<Expression>`.
///
/// A hole never freezes: it has no span for a verbatim slice to cover. And a run of
/// holes before slot `i` contributes only its commas, so the gap window keeps the
/// same shape (commas, whitespace, comments) when anchored at the previous PRESENT
/// element's end — or at `container_start` when every earlier slot is a hole. That
/// anchor rule is why this is spelled here beside [`Self::list_item_frozen`], the
/// other home of the gap-anchor convention, rather than at the element loops.
pub(in crate::printer) fn element_frozen_span<'e, E: Borrow<ast::Expression<'e>>>(
&self,
container_start: u32,
elements: &[Option<E>],
i: usize,
) -> Option<Span> {
if !self.has_format_ignore {
return None;
}
let elem = elements[i].as_ref()?.borrow();
let prev_end = elements[..i]
.iter()
.rev()
.flatten()
.next()
.map_or(container_start, |e| e.borrow().span().end);
self.gap_frozen_span(prev_end, elem.span())
}
/// [`Self::list_item_frozen`] over a `TSType` slice, plus the union /
/// intersection `freeze_first` arm (the out-of-span leading-run directive, which
/// applies only to the first member of an undelimited list).
///
/// Opens on the same document-level bool, which also subsumes the `freeze_first`
/// arm: that flag comes from [`Self::leading_run_freeze`], itself gated on it, so it
/// can only be set in a directive-bearing document.
#[inline]
pub(in crate::printer) fn list_member_frozen(
&self,
container_start: u32,
types: &[&TSType<'_>],
i: usize,
freeze_first: bool,
) -> bool {
self.has_format_ignore
&& ((i == 0 && freeze_first)
|| self.list_item_frozen(container_start, &|j| types[j].span(), i))
}
//
// Frozen doc builders — what a resolved freeze emits
//
// The must-break rule and its span form come first: every builder below either
// ends in that rule or hands its multiline fact to a family layout instead.
//
/// The single-child heads' must-break, in one place: a frozen slice spanning
/// `slice` appends a `break_parent`, so the ENCLOSING width-decided groups (a
/// parameter list, an object type) break cleanly around it instead of gluing flat.
/// A `verbatim_source_span` is `will_break`-opaque, so without the explicit signal
/// the container never learns the slice spans lines. The list positions thread the
/// same fact through their family layouts (`frozen_member_forces_break` /
/// `frozen_list_member_multiline`); the heads have no family layout, so the signal
/// rides in the doc.
fn with_frozen_must_break(&self, doc: DocId, slice: Span) -> DocId {
let d = self.d();
if self.is_same_line(slice.start, slice.end) {
doc
} else {
d.concat(&[doc, d.break_parent()])
}
}
/// The frozen verbatim slice for a SPAN-shaped freeze target — a freeze whose
/// following node is not a `TSType` (the function/constructor return annotation
/// `=> T`, frozen whole when the directive sits in the `)`→`=>` gap) — with the
/// single-child heads' must-break. The span analog of
/// [`Self::build_frozen_single_child_doc`] (no paren-transparency: the span is
/// taken as-is).
pub(in crate::printer) fn build_frozen_span_doc(&self, span: Span) -> DocId {
self.with_frozen_must_break(self.raw_source_range(span.start, span.end), span)
}
/// What a freeze over a NODE's own span EMITS: the verbatim slice
/// ([`Self::build_frozen_span_doc`]) plus the one node-level fact the span alone can't
/// carry — a block comment **glued** before the node is owned by it, rides *inside* the
/// doc the slice replaces, and is skipped by every gap emitter, so the freeze must claim
/// it or nothing prints it (docs/comments.md hazard 1, the ownership-is-about-who-PRINTS
/// rule). Prettier keeps the comment right where it is, glued before the frozen slice.
///
/// The one emitter every node-shaped freeze shares, whatever rule resolved it: a list
/// item over its own span (a declarator, a namespace binding), a statement head's body
/// ([`Self::build_statement_head_doc`], and the `export` / `export default` heads that
/// resolve their own span because their gap emitter is the shared declaration-header run
/// they need either way), and the delimiter-owned value heads whose value is span-shaped
/// rather than an `Expression` (a `for` init clause, a for-in/of left, a restricted
/// production's operand). [`Self::build_frozen_expression_doc`] is this plus the one rule
/// that belongs to an `Expression` node rather than to its span.
///
/// `gaps:audit` found every miss this claim exists to prevent, by injecting a block
/// comment before a frozen node — which the print-once ledger over the fixtures as
/// authored cannot see, since no fixture glues a comment to a frozen item: a frozen
/// argument dropped it, and so did `if (a)⏎// prettier-ignore⏎/* c */ fn( b );` and
/// `export default⏎// prettier-ignore⏎/* c */ @dec class {}`.
#[inline]
pub(in crate::printer) fn build_frozen_node_doc(&self, frozen: Span) -> DocId {
self.prepend_owned_leading_comment_at(frozen.start, self.build_frozen_span_doc(frozen))
}
/// [`Self::build_frozen_node_doc`] for a frozen STATEMENT — a list member, a header's
/// body ([`Self::build_statement_head_doc`]), a labeled statement's body, or an
/// `export`ed declaration — plus the one rule a raw span cannot carry: **a statement's
/// terminator belongs to the printer, not to the author.** The slice ends at the
/// statement's own content ([`Self::frozen_statement_slice`]) and the `;` is emitted
/// here rather than copied.
///
/// tsv has to own the terminator at the ASI end regardless: a frozen
/// `VariableDeclaration` whose author relied on ASI must get the `;` restored, or the
/// NEXT statement's printed form can fabricate a leading `(` that re-binds the pair into
/// one broken statement (`const a = x` + `y => 1` → `x(y) => 1`) — output that does not
/// reparse. Owning it at *both* ends is what makes that one rule instead of two: an
/// authored `;` the author left separated from its statement (`const a = x⏎;`, the
/// semicolon-free style's `;[…]` idiom) is re-emitted glued rather than frozen where it
/// sat, which is prettier's answer too. Pinned by the
/// `typescript/syntax/asi/prettier_ignore_semicolon*` family (list, case-consequent,
/// header-body, `export`-head and detached-terminator spellings).
pub(in crate::printer) fn build_frozen_statement_doc(
&self,
stmt: &ast::Statement<'_>,
) -> DocId {
let (frozen, terminated) = self.frozen_statement_slice(stmt);
let doc = self.build_frozen_node_doc(frozen);
if terminated {
let d = self.d();
return d.concat(&[doc, d.text(";")]);
}
doc
}
/// The verbatim extent of a frozen statement, and whether a `;` follows it — the
/// [`Self::statement_content_end`] table plus prettier's
/// `shouldIgnoredNodePrintSemicolon`, which are one walk read twice.
///
/// Only a kind that OWNS a terminator ([`StatementTerminator`]) trims one, which is what
/// leaves the `;` of an empty-statement body inside the slice (`for ( ;; )⏎;`,
/// `while ( a )⏎;`, `l:⏎;` — there the `;` IS the body) and likewise the `;` of a kind
/// prettier's table does not list (`type A = B⏎;`). Both stay frozen exactly as
/// authored, on both formatters.
///
/// ⚠️ A comment stays inside the slice, where prettier's comment-stripped measure ejects
/// it past the terminator: `fn( a ) /* c */ ;` is `fn( a ); /* c */` there and
/// `fn( a ) /* c */;` here — the reading the directive's own promise asks for, and a
/// cataloged divergence (see `docs/conformance_prettier_ignore.md` §Format-ignore
/// directive).
///
/// ⚠️ **A `//` owns the rest of its line, so it ends the trim outright**
/// ([`Self::frozen_slice_ends_in_line_comment`]): pulling the terminator up onto a line
/// comment's line does not move it, it SWALLOWS it, and the statement loses the `;` the
/// following statement needs — `const a = x // c⏎;` + `(y) => 1;` welds to
/// `const a = x // c;`, which no longer parses at all. There the whole node freezes as
/// authored, terminator included, which is both lossless and the only reparsable answer.
fn frozen_statement_slice(&self, stmt: &ast::Statement<'_>) -> (Span, bool) {
let span = stmt.span();
let terminator = Self::statement_terminator(stmt);
if terminator == StatementTerminator::Never {
return (span, false);
}
let content_end = self.statement_content_end(stmt);
if content_end == span.end {
// ASI supplied the terminator: nothing to trim, and only a kind that always
// carries one gets it printed back.
return (span, terminator == StatementTerminator::Always);
}
if self.frozen_slice_ends_in_line_comment(span.start, content_end) {
return (span, false);
}
(Span::new(span.start, content_end), true)
}
/// Whether the last thing inside a frozen statement's trimmed slice is a `//` comment —
/// the one shape a printed terminator cannot follow on the same line.
///
/// **In-source axis** ([`Printer::comments_in_source_between`]): the question is which
/// bytes are physically there, not which comments this emitter owes, and a frozen slice
/// prints every comment inside it verbatim either way.
fn frozen_slice_ends_in_line_comment(&self, start: u32, content_end: u32) -> bool {
self.comments_in_source_between(start, content_end)
.last()
.is_some_and(|c| !c.is_block && c.span.end == content_end)
}
/// [`Self::build_frozen_node_doc`] minus the must-break — the claim, over a slice whose
/// multi-line-ness is deliberately kept **opaque** to its container.
///
/// The two differ by exactly that signal, and the difference is a real rule rather than an
/// oversight: a frozen MEMBER EXPRESSION is layout-opaque on purpose, so every container
/// around it keeps its own tight layout (`f1(fn(2.0000)⏎// prettier-ignore⏎.c)` stays
/// hugged, the ternary stays inline), which is what
/// `typescript/expressions/member/prettier_ignore_containers` pins. Handing this site the
/// must-break variant tears each of those containers open.
///
/// The claim itself is not optional either way: the slice starts at the member's own first
/// byte, so a block comment **glued** ahead of the chain base — owned by it, skipped by
/// every gap emitter — sits outside the slice and nothing else prints it
/// (`docs/comments.md` hazard 1, the replacement shape). Widening the slice is not the
/// alternative: the comment would then be frozen text and stop reindenting with its
/// context. Pinned by `typescript/expressions/member/prettier_ignore_base_comment`, and by
/// the Svelte attribute spelling `svelte/syntax/prettier_ignore/attr_expression`, whose
/// `={`→base gap is the same position one host out.
#[inline]
pub(in crate::printer) fn build_frozen_opaque_node_doc(&self, frozen: Span) -> DocId {
self.prepend_owned_leading_comment_at(
frozen.start,
self.raw_source_range(frozen.start, frozen.end),
)
}
/// The frozen verbatim slice for a value in a paren-context position, with the clarity
/// parens that context supplies re-synthesized around it: an assignment prints as
/// `fn((a = b))` as an argument and as `if ((a = b))` as a condition, and those parens
/// are the printer's rather than the author's, so they belong OUTSIDE the frozen slice
/// — prettier freezes the inner and keeps them too (`fn(⏎// prettier-ignore⏎(a = b +
/// c))`). Same rule, and the same reason, as [`Self::build_frozen_member_doc`]'s
/// bare-member arm.
///
/// `ctx` is the position's own [`ParenContext`] — the one its ordinary path passes to
/// [`Self::needs_parens`], so the frozen and unfrozen forms can never disagree about
/// the shell.
///
/// The must-break rides in the doc ([`Self::build_frozen_span_doc`]): every layout in
/// this family is width-decided, and a `verbatim_source_span` is `will_break`-opaque,
/// so a multi-line frozen item has to say so explicitly.
///
/// ⚠️ **This emits the pair and nothing else, so it is only for a position whose
/// ENCLOSING seam claims the slice→`)` gap** — an argument or element (the element-comma
/// seam), a statement test, a binding default. Those all float such a comment out past
/// the `)`, which is what their unfrozen twins do too. A position that owns everything
/// inside its own boundary — a declarator initializer, an assignment RHS — must take
/// [`Self::build_frozen_value_shell_doc`] instead: nothing out there can see between the
/// parens, so a bare pair leaves that gap with no emitter and DROPS the comment
/// (`docs/comments.md` hazard 4), which is what both of those hosts did while they
/// spelled the bare pair here.
pub(in crate::printer) fn build_frozen_value_doc(
&self,
value: &ast::Expression<'_>,
frozen: Span,
ctx: ParenContext,
) -> DocId {
let doc = self.build_frozen_expression_doc(value, frozen);
if self.needs_parens(value, ctx) || self.frozen_slice_needs_pair(frozen, ctx) {
// Parens outside the claimed comment, exactly as the ordinary path nests them
// (`d.parens(build_expression_doc(..))`, whose owned-comment prepend is inside).
self.d().parens(doc)
} else {
doc
}
}
/// Whether a frozen SLICE needs a paren pair its node does not — an arrow body whose
/// slice opens with `{`, which the grammar reads as a block body.
///
/// The rule is about the slice rather than the node, and that is the whole reason it
/// exists: the unfrozen path has a finer pair available and takes it, parenthesizing the
/// leftmost OBJECT inside the expression (`() => ({ b: 1 }) | M`). A verbatim slice cannot
/// be reached into, so the only pair left is one around the whole body — and without it
/// the output does not reparse AT ALL (`() =>⏎// prettier-ignore⏎{b: 1} | M`). Prettier
/// emits exactly that and cannot read it back either, so there is no parity to keep: a
/// formatter's output has to parse.
///
/// Only the arrow body asks. Every other [`ParenContext`] either already parenthesizes
/// the shapes that can open with `{` (an object expression at a value head is its own
/// node, which `needs_parens` answers) or sits where a leading `{` is unambiguous.
fn frozen_slice_needs_pair(&self, frozen: Span, ctx: ParenContext) -> bool {
matches!(ctx, ParenContext::ArrowBody)
&& self.source.as_bytes().get(frozen.start as usize) == Some(&b'{')
}
/// [`Self::build_frozen_value_doc`] at an ARGUMENT or ELEMENT item, the family that
/// names its own emitter because every argument/element loop dispatches through it.
#[inline]
pub(in crate::printer) fn build_frozen_arg_doc(
&self,
arg: &ast::Expression<'_>,
frozen: Span,
) -> DocId {
self.build_frozen_value_doc(arg, frozen, ParenContext::Argument)
}
/// [`Self::build_frozen_node_doc`] for an EXPRESSION — the verbatim slice and the
/// owned-comment claim, plus the one rule that is a property of the *node* rather than
/// of its span.
///
/// A **`SequenceExpression`** gets back the grouping parens it prints for itself. Those
/// parens are required (`fn((0, 1))` passes ONE argument; `fn(0, 1)` passes two) but
/// they are printed by `build_sequence_doc`, not by the enclosing context — which is why
/// `needs_parens` deliberately answers `false` for it — and they sit OUTSIDE the node's
/// span, so a verbatim slice of the span alone drops them and changes the meaning. The
/// rule belongs here rather than at [`Self::build_frozen_value_doc`] because it is a
/// property of the node, not of the position: the cast interior needs it too.
///
/// ⚠️ The ambient `[~In]` pair a `for` header's init owes is deliberately NOT applied
/// here. It is one question with one answer, and `needs_parens` already ORs it ahead of
/// every context — so [`Self::build_frozen_value_doc`], which every positional caller
/// routes through, has it. Asking again here doubled the pair at eleven positions (a
/// call / `new` argument, an array element, a spread, an object property value, a
/// template `${}`, an arrow body, a binding or parameter default, a class field). The
/// frozen sites that bypass `needs_parens` supply it themselves, exactly as their
/// unfrozen twins do: [`Printer::build_shell_value_doc`]'s `ForClauseSeparator` tail,
/// the assignment RHS, the `for` clause's sequence operands and the ternary branches.
///
/// The value heads whose value is span-shaped rather than an `Expression` (a `for` init
/// clause, a for-in/of left, a restricted production's operand — where the layout's own
/// hanging parens ARE the grouping, so a re-synthesized pair would double it) take
/// [`Self::build_frozen_node_doc`] directly.
pub(in crate::printer) fn build_frozen_expression_doc(
&self,
expr: &ast::Expression<'_>,
frozen: Span,
) -> DocId {
let doc = self.build_frozen_node_doc(frozen);
if matches!(expr.kind, ast::ExpressionKind::SequenceExpression(_)) {
self.d().parens(doc)
} else {
doc
}
}
/// The doc for argument item `i`: the frozen verbatim slice when Rule A fires
/// ([`Self::args_frozen_span`]), else the ordinary argument-context doc. The one
/// freeze-aware twin of `build_arg_expression_doc`, so no argument loop spells the
/// dispatch itself.
///
/// Its sole caller is `calls::build_joined_argument_doc`, which every call, `new` and
/// member-chain argument loop routes through — the layer that adds the *other*
/// per-argument question (a curried chain's progressive layout) to this one. Reach for
/// that builder at a new argument loop, not for this directly: skipping it is how an
/// argument silently loses that routing.
///
/// The document-level flag is read HERE, with the ordinary build on its own arm, rather
/// than left to [`Self::args_frozen_span`] one call down: this is the hottest position
/// in the module — every argument of every call, `new` and chain call asks it — so a
/// directive-free document (≈ every document) pays exactly one predicted branch and
/// never the `Option` or the span closure behind it. The same reason
/// [`Self::list_item_frozen`] and [`Self::single_child_frozen`] open with it. (Measured
/// as null either way on real corpora — the shape is doctrine, not a won benchmark.)
#[inline]
pub(in crate::printer) fn build_arg_item_doc(
&self,
container_start: u32,
args: &[&ast::Expression<'_>],
i: usize,
) -> DocId {
if !self.has_format_ignore {
return self.build_arg_expression_doc(args[i]);
}
self.args_frozen_span(container_start, args, i).map_or_else(
|| self.build_arg_expression_doc(args[i]),
|frozen| self.build_frozen_arg_doc(args[i], frozen),
)
}
/// The doc for a comma-list item identified by SPAN alone — a module specifier, an
/// import attribute, a variable declarator: the frozen verbatim slice when Rule A fires
/// ([`Self::list_item_frozen`]), else `build_ordinary()`. The freeze-aware twin for the
/// families whose item is not an `Expression`, so no list loop spells the dispatch itself.
///
/// The slice is the item's own node span; the list's `,` is parent-owned and stays
/// outside it — so the emission is the plain [`Self::build_frozen_node_doc`], which
/// claims the glued comment the item owns (docs/comments.md hazard 1). The
/// grouping-paren fact does not apply here: none of these item kinds prints parens of its
/// own around its span. A caller whose ordinary path is the rest of a loop body rather
/// than a closure (the variable declarator list) asks [`Self::list_item_frozen`] and that
/// emitter directly, so the two dispatch shapes still share one emitter.
///
/// `build_ordinary` is lazy so the common path builds the item doc exactly once;
/// [`Self::list_item_frozen`] already opens on the document-level flag, so a
/// directive-free document pays one predicted branch per item.
#[inline]
pub(in crate::printer) fn build_span_item_doc(
&self,
container_start: u32,
item_span: &impl Fn(usize) -> Span,
i: usize,
build_ordinary: impl FnOnce() -> DocId,
) -> DocId {
if self.list_item_frozen(container_start, item_span, i) {
self.build_frozen_node_doc(item_span(i))
} else {
build_ordinary()
}
}
/// Paren-transparent frozen doc for a union / intersection member — and, through
/// [`Self::build_frozen_head_doc`], for a single-child head's frozen value too (the
/// type-parameter constraint/default site passes a `member_parens` that keeps a
/// conditional value's clarity parens). Precedence parens are kept or dropped per
/// `member_parens`, and the freeze stays lossless:
///
/// - **paren dropped** (`member_parens(inner)` false — a redundant `(a1)` or a bare
/// `a1`) → freeze just the inner slice, so `(a1)` → `‹frozen a1›`;
/// - **paren kept, already parenthesized** (`(a1&a2)`, `(A | B)`, `(// c⏎ a | b)`) →
/// freeze the member's WHOLE span verbatim, parens and any inner comments included.
/// Byte-identical to re-synthesizing the paren around the frozen inner when the
/// shell is comment-free, but lossless when it holds a comment (slicing the inner
/// would drop a comment between `(` and the inner type);
/// - **paren kept, bare member** (`b1&b2` needing parens as a union member) →
/// re-synthesize the parens around the frozen slice.
///
/// Separators (`| ` / ` & `) are parent-owned and emitted by the loop; parent-owned
/// trailing punctuation stays out of the frozen slice (it is past the member span,
/// and `raw_source_range` trims trailing whitespace).
pub(in crate::printer) fn build_frozen_member_doc(
&self,
t: &TSType<'_>,
member_parens: TypeParenRule,
) -> DocId {
self.build_frozen_member_doc_at(
t,
member_parens,
self.frozen_member_slice_span(t, member_parens),
)
}
/// [`Self::build_frozen_member_doc`] over an already-resolved slice span, so a
/// caller that also needs the span (the heads' must-break test) resolves it once.
fn build_frozen_member_doc_at(
&self,
t: &TSType<'_>,
member_parens: TypeParenRule,
slice: Span,
) -> DocId {
let d = self.d();
let inner = unwrap_parenthesized(t);
let frozen = self.raw_source_range(slice.start, slice.end);
// Re-synthesize the parens only for a BARE member that needs them; a
// source-parenthesized member's slice already covers its own parens.
if member_parens(self, inner) && !matches!(t, TSType::Parenthesized(_)) {
d.concat(&[d.text("("), frozen, d.text(")")])
} else {
frozen
}
}
/// The span of the verbatim slice [`Self::build_frozen_member_doc`] emits for `t`:
///
/// - a parenthesized shell holding a comment (`(/* c */ a1)`, `(// c⏎ a1)`) freezes
/// the member's WHOLE span — slicing the inner would drop the shell comment
/// (overrides the redundant-paren drop below);
/// - a redundant paren (`member_parens(inner)` false) is dropped — the slice is the
/// paren-stripped inner;
/// - a kept, source-parenthesized member freezes whole-span (parens included); a
/// kept, bare member's slice is its own span (the caller re-synthesizes parens).
fn frozen_member_slice_span(&self, t: &TSType<'_>, member_parens: TypeParenRule) -> Span {
let inner = unwrap_parenthesized(t);
if self.frozen_paren_shell_has_comment(t) || member_parens(self, inner) {
t.span()
} else {
inner.span()
}
}
/// Whether the frozen slice for member `t` spans lines — the member-freeze
/// must-break trigger: a `verbatim_source_span` is `will_break`-opaque, so a caller
/// whose layout is width-decided forces the family broken explicitly when a frozen
/// member is multi-line (the leading-run analog is `FirstMember.multiline`).
fn frozen_member_multiline(&self, t: &TSType<'_>, member_parens: TypeParenRule) -> bool {
let slice = self.frozen_member_slice_span(t, member_parens);
!self.is_same_line(slice.start, slice.end)
}
/// The one spelling of the Rule A must-break OR-tracking at the width-decided call
/// sites: a `frozen` member whose slice spans lines forces the family's broken
/// layout.
pub(in crate::printer) fn frozen_member_forces_break(
&self,
frozen: bool,
t: &TSType<'_>,
member_parens: TypeParenRule,
) -> bool {
frozen && self.frozen_member_multiline(t, member_parens)
}
/// [`Self::build_frozen_member_doc`] for a LIST position whose element never needs
/// precedence parens (tuple elements, type arguments): a source paren there is
/// always redundant, so it drops under the freeze unless its shell holds a comment.
/// The single-child heads take [`Self::build_frozen_single_child_doc`], the same
/// slice plus their must-break.
pub(in crate::printer) fn build_frozen_list_member_doc(&self, t: &TSType<'_>) -> DocId {
self.build_frozen_member_doc(t, |_, _| false)
}
/// [`Self::frozen_member_multiline`] for the paren-free list positions of
/// [`Self::build_frozen_list_member_doc`]: whether the frozen slice spans lines.
/// Unlike [`Self::frozen_member_forces_break`] the frozen flag is NOT taken —
/// callers ask this only for an already-known-frozen member, so a width-decided
/// caller forces its broken layout explicitly on a `true` answer (a
/// `verbatim_source_span` is `will_break`-opaque).
pub(in crate::printer) fn frozen_list_member_multiline(&self, t: &TSType<'_>) -> bool {
self.frozen_member_multiline(t, |_, _| false)
}
/// [`Self::build_frozen_member_doc`] plus the single-child heads' must-break
/// ([`Self::with_frozen_must_break`]) — the one spelling every head freeze uses,
/// whatever its precedence-paren rule (`member_parens`: the type-parameter
/// constraint site keeps a conditional value's clarity parens, the conditional
/// `extends` site its own). Same catalog class as the multiline-member
/// divergence (`union_prettier_ignore_multiline_member`): prettier glues the container
/// flat around its printed-ignored slice.
pub(in crate::printer) fn build_frozen_head_doc(
&self,
child: &TSType<'_>,
member_parens: TypeParenRule,
) -> DocId {
let slice = self.frozen_member_slice_span(child, member_parens);
self.with_frozen_must_break(
self.build_frozen_member_doc_at(child, member_parens, slice),
slice,
)
}
/// [`Self::build_frozen_head_doc`] for the heads whose child never needs precedence
/// parens (annotation `:`, alias `=`, named-tuple `label:`, mapped value `]:`, a
/// cast's type, a `=>` return): a source paren there is always redundant, so it
/// drops under the freeze unless its shell holds a comment.
pub(in crate::printer) fn build_frozen_single_child_doc(&self, child: &TSType<'_>) -> DocId {
self.build_frozen_head_doc(child, |_, _| false)
}
/// The doc for the child of a head whose gap ROUTE has already fired (an
/// alone-on-line directive sits in the gap, or in a stripped shell's interior): the
/// frozen verbatim slice when the child is a freeze target, else the ordinarily-built
/// child — a composite declines the freeze here and applies Rule A inside via its own
/// leading-run walk, while the head still owns the directive's own-line emission.
pub(in crate::printer) fn build_routed_child_doc(&self, child: &TSType<'_>) -> DocId {
if is_freeze_target(child) {
self.build_frozen_single_child_doc(child)
} else {
self.build_type_doc(child)
}
}
/// The value doc for a head whose child is a paren SHELL that
/// [`Self::paren_interior_routed_inner`] routed: the routed inner, plus the comments the
/// shell's TRAILING gap holds lifted out after it.
///
/// The two halves are one question — a strip is lossless only if what the shell held on
/// BOTH sides still prints. The routing hands the leading run to the head's own emitter
/// (that is what widening the window to `inner.span().start` buys); this lifts the
/// trailing half, so every shell comment prints exactly once. Spelled here rather than at
/// each head because a head that remembered one half and forgot the other would drop a
/// comment with no gate firing (`docs/comments.md` hazard 1).
///
/// Every routed head uses it, and [`RoutedScope`] is the one axis they disagree on —
/// named at the call site rather than picked by choosing among near-identical function
/// names, so the lift above can never be the thing a new head forgets. The alias `=`
/// pre-filters its inner to a freeze target, which is exactly the arm
/// [`RoutedScope::Transparent`] takes there, so the shared spelling costs it nothing.
pub(in crate::printer) fn build_routed_inner_doc(
&self,
shell: &TSType<'_>,
inner: &TSType<'_>,
scope: RoutedScope,
) -> DocId {
let inner_doc = match scope {
RoutedScope::Transparent => self.build_routed_child_doc(inner),
RoutedScope::TransparentParens(member_parens) => {
if is_freeze_target(inner) {
self.build_frozen_head_doc(inner, member_parens)
} else {
self.build_type_doc_maybe_parens(inner, member_parens)
}
}
RoutedScope::Whole => self.build_frozen_single_child_doc(inner),
};
self.with_stripped_paren_trailing(inner_doc, shell, inner, TrailingBlock::Inline)
}
/// Whether a parenthesized member's shell — the bytes between `(` and the inner type,
/// or between the inner and `)` — physically holds a comment, in which case the
/// paren-transparent freeze must keep the WHOLE member span (slicing the inner drops
/// that comment). **In-source axis** (`comments_in_source_range`), not to-emit: a
/// glued shell comment (`(/* c */ a1)`) is owned, so the to-emit axis would miss it,
/// yet the frozen raw slice never routes it through `build_comment_doc` — it would be
/// dropped either way, so the physical-presence question is the correct one.
fn frozen_paren_shell_has_comment(&self, t: &TSType<'_>) -> bool {
let Some(shell) = outermost_paren(t) else {
return false;
};
// Both halves from [`paren_shell_gaps`]: a doubly-nested shell freezes as one
// slice, so the comment between its two `(`s belongs to it.
let (leading, trailing) = paren_shell_gaps(shell);
self.comments_in_source_between(leading.start, leading.end)
.next()
.is_some()
|| self
.comments_in_source_between(trailing.start, trailing.end)
.next()
.is_some()
}
/// [`Self::build_frozen_member_doc`] with the union's per-member `align(2)` offset
/// (mirroring `build_union_member_offset_doc`, so a frozen member aligns with its
/// reformatted siblings in the broken layout). An object-literal member supplies its
/// own layout, so — frozen verbatim (opaque) — it takes no offset.
pub(in crate::printer) fn build_frozen_union_member_offset_doc(
&self,
t: &TSType<'_>,
member_parens: TypeParenRule,
) -> DocId {
let d = self.d();
let inner = unwrap_parenthesized(t);
// A BARE object member supplies its own layout → verbatim, no offset. A
// parenthesized object with a shell comment routes through `build_frozen_member_doc`
// (whole-span freeze) so the shell comment is not dropped.
if matches!(inner, TSType::TypeLiteral(_)) && !self.frozen_paren_shell_has_comment(t) {
return self.raw_source_range(inner.span().start, inner.span().end);
}
d.align(2, self.build_frozen_member_doc(t, member_parens))
}
}