tsv_ts 0.8.0

TypeScript/JS formatter, type stripper and acorn-compatible parser — part of tsv
Documentation
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
// Array expression printing for TypeScript
//
// Handles printing of array expressions with:
// - Width-based wrapping
// - Fill mode for number-only arrays
// - Comment preservation

use super::objects::{LiteralRole, is_compound_target_value};
use crate::ast::{self, Expression, ExpressionKind, LiteralValue};
use crate::printer::comments::{
    ShareSite, block_is_before_comma, next_real_element_start, run_defers_line,
};
use crate::printer::{CommentVec, Printer};
use smallvec::{SmallVec, smallvec};
use tsv_lang::Span;
use tsv_lang::doc::DocBuf;
use tsv_lang::doc::arena::DocId;

/// The gap after array slot `i`, in this container's terms: [`next_real_element_start`]
/// with the literal's own fallback — just inside `]`, where a comment past the last real
/// element slides to. The array PATTERN's twin is `array_pattern_gap_end`, whose fallback
/// stops before a `: T` the pattern's span swallowed; each container names its own end
/// once and the walk past the holes is shared.
fn array_gap_end(arr: &ast::ArrayExpression<'_>, span: Span, i: usize) -> u32 {
    next_real_element_start(arr.elements, i).unwrap_or(span.end - 1)
}

/// The SHAPE half of prettier's `isConciselyPrintedArray` (`print/array.js`): a non-empty
/// array whose every element is a numeric literal, or a SIGNED one — prettier's
/// `isSignedNumericLiteral`, `+`/`-` over a numeric literal and deliberately NOT recursive,
/// so `- -1` is not a signed number and its array is not concise.
///
/// A hole is not an element, so any elision disqualifies the array — which is what keeps
/// the fill printer, whose slot builder has nothing to push for a hole, off a sparse one.
///
/// Free and shape-only so it can be read (and tested) without a printer; the comment clause
/// that completes the predicate needs the comment array and lives on
/// [`Printer::is_concisely_printed_array`], the one entry every caller asks.
fn array_elements_are_signed_numbers(arr: &ast::ArrayExpression<'_>) -> bool {
    !arr.elements.is_empty()
        && arr
            .elements
            .iter()
            .all(|elem| match elem.as_ref().map(|e| &e.kind) {
                Some(ExpressionKind::Literal(lit)) => matches!(lit.value, LiteralValue::Number(_)),
                Some(ExpressionKind::UnaryExpression(unary)) => {
                    matches!(
                        unary.operator,
                        ast::UnaryOperator::Minus | ast::UnaryOperator::Plus
                    ) && matches!(
                        &unary.argument.kind,
                        ExpressionKind::Literal(lit) if matches!(lit.value, LiteralValue::Number(_))
                    )
                }
                _ => false,
            })
}

impl<'a> Printer<'a> {
    /// Check if array should force break based on Prettier's heuristic
    ///
    /// Returns true when:
    /// - More than 1 element
    /// - ALL elements are arrays (or ALL are objects - no mixing)
    /// - EACH inner array/object has more than 1 item
    ///
    /// This matches prettier's shouldBreak logic in `printArray` (`print/array.js`)
    fn should_break_nested_array(&self, arr: &ast::ArrayExpression<'_>) -> bool {
        if arr.elements.len() <= 1 {
            return false;
        }

        let mut expect_arrays: Option<bool> = None;

        for elem in arr.elements {
            let Some(expr) = elem else { return false };

            let (is_array, inner_len) = match &expr.kind {
                ExpressionKind::ArrayExpression(inner) => (true, inner.elements.len()),
                ExpressionKind::ObjectExpression(inner) => (false, inner.properties.len()),
                _ => return false,
            };

            // All elements must be same type (all arrays or all objects)
            if expect_arrays.is_some_and(|expected| expected != is_array) {
                return false;
            }
            expect_arrays = Some(is_array);

            // Each inner must have more than 1 item
            if inner_len <= 1 {
                return false;
            }
        }

        true
    }

    /// Blank-line rule for the gap after array slot `i` — prettier's
    /// `node && isLineAfterElementEmpty(node)` (`print/array.js`, `printArrayElements`).
    ///
    /// Both terms are load-bearing for elisions:
    ///
    /// - A **hole carries no blank line after it** (prettier's `node &&`): it has no node
    ///   to anchor the scan on, and its own line break is structure, not authorship.
    /// - The scan stops at **slot `i + 1`**, not at the next real element. A hole is empty
    ///   — its slot is the point just before the comma that terminates it — so when slot
    ///   `i + 1` is a hole the next real element lies past that comma, and a scan running
    ///   that far reads the hole's own line break as an author's blank line.
    ///
    /// The scan stops where slot `i + 1`'s **printed content** begins, so a blank line the
    /// author left ahead of that content is inside the measured range. That position has
    /// three spellings, and this function owns all three so no caller can answer with a
    /// subset:
    ///
    /// 1. slot `i + 1`'s first **emitted** leading comment — the only one a caller can know,
    ///    since which comments a gap emits is the caller's own collection
    ///    (`emitted_leading_start`, supplied by the expanding printer; every other printer
    ///    passes `None`);
    /// 2. else slot `i + 1`'s **owned** comment, which prints ahead of the element's first
    ///    token from *inside* the element's own doc and so is invisible to the **to emit**
    ///    axis a caller's list rides — a bound taken from that list alone lands past the
    ///    comment, putting the author's blank line, which lies *before* it, outside the
    ///    range where it is silently dropped;
    /// 3. else the element itself.
    ///
    /// ⚠️ 2 is not the expanding printer's business either, though it once derived it
    /// alongside 1: a glued block comment is not an expansion trigger (neither a line, a
    /// multi-line, nor an own-line comment), so it reaches the width-wrapping and
    /// multiline-content printers too. Two copies of one derivation, and only the copy here
    /// carried the range guard.
    ///
    /// The hole guard runs first, so the scan always anchors on a real element's end —
    /// which also keeps a nested element's commas (`[[1, 2], , x]`) behind it.
    ///
    /// ⚠️ **There is a SECOND implementation of this rule**, in the array PATTERN's
    /// expanding builder (`expressions/patterns.rs`), and it is not foldable into this one:
    /// the pattern anchors on the claimed run's shell-peeled end and takes its bound from
    /// the first comment in SOURCE, where this one anchors on the element's span end and
    /// takes the caller's emitted-leading list. The two share what they *can* — the walk
    /// past the holes ([`next_real_element_start`]) and the hole ceiling
    /// ([`Printer::hole_slot_comma`]) — so a change to either of THOSE is already shared,
    /// but a change to the rule itself has to be made twice. The pattern lacking the hole
    /// arm entirely is what DROPPED an author blank line before an elision.
    fn has_blank_line_after_slot(
        &self,
        arr: &ast::ArrayExpression<'_>,
        arr_span: Span,
        i: usize,
        emitted_leading_start: Option<u32>,
    ) -> bool {
        let Some(elem) = arr.elements[i].as_ref() else {
            return false;
        };
        let elem_end = elem.span().end;
        // The next real element's start, or the closing `]`. It bounds the SEPARATOR
        // search, which runs past `upper`: the comma may sit below slot `i + 1`'s leading
        // comment, and `upper` stops at that comment's start
        // ([`Printer::has_blank_line_after_comma`]).
        let next_real = array_gap_end(arr, arr_span, i);

        // A HOLE's printed content is its comma alone, so none of the three spellings above
        // applies — whatever a caller emits in this gap prints *past* that comma, and
        // bounding there reads the hole's own line break as an author's blank line.
        if matches!(arr.elements.get(i + 1), Some(None)) {
            let hole_comma = self.hole_slot_comma(elem_end, next_real);
            // …but a comment BELOW this element's line does not print at that comma either —
            // it slides forward past it to lead the next real element — so the scan still
            // stops at the comment. Left to run the whole span,
            // `has_blank_line_after_comma`'s own comment-skip steps over it and takes the
            // author's blank line in FRONT of it along (`x,⏎⏎// c⏎,⏎y`).
            // The ceiling opens at the element-comma SPLIT, not at `elem_end`: a comment the
            // split claims TRAILS this element on its line, and prettier's
            // `isNextLineEmptyAfterIndex` skips exactly those before measuring, so bounding
            // at one would read the blank *after* it as absent (`x, // c⏎⏎,⏎y`). Both
            // spellings are live, one per side of the split.
            let split = self.element_gap_split(arr, arr_span, i, elem_end, next_real);
            let upper = self.blank_scan_end(split, hole_comma);
            return self.has_blank_line_after_comma(elem_end, upper, next_real);
        }

        let upper = emitted_leading_start
            .or_else(|| {
                // Guarded to this gap: the lookup is keyed on the element's own span start,
                // which says nothing about which gap the comment it finds sits in.
                arr.elements
                    .get(i + 1)
                    .and_then(|e| e.as_ref())
                    .and_then(|e| self.owned_leading_comment_start(e))
                    .filter(|&p| p > elem_end && p < next_real)
            })
            .unwrap_or(next_real);
        self.has_blank_line_after_comma(elem_end, upper, next_real)
    }

    /// Format a block comment for inline use (with appropriate spacing)
    ///
    /// - `leading: true` for comments before elements → space after: `/*c*/ elem`
    /// - `leading: false` for comments after elements → space before: `elem /*c*/`
    fn format_inline_block_comment(&self, comment: &tsv_lang::Comment, leading: bool) -> DocId {
        let d = self.d();
        // One text node either way (the full span is the verbatim `/*content*/`,
        // delimiters included) — array fill items must not gain a separate
        // space node.
        let mut w = d.pool_writer();
        if leading {
            w.push_str(comment.span.extract(self.source));
            w.push(' ');
        } else {
            w.push(' ');
            w.push_str(comment.span.extract(self.source));
        }
        let doc = w.finish_text();
        // A comment emission that can't route through `build_comment_doc` (the space must
        // share the node), so it tags its own ledger node.
        #[cfg(feature = "_internal_comment_check")]
        d.tag_comment_doc(doc, comment.span, self.source);
        doc
    }

    /// The last real element before slot `i` — its slot index and the element itself.
    ///
    /// A hole has no span, so this is the only anchor a scan across earlier slots can start
    /// from, and the slot index is what [`Self::element_gap_split`] derives `is_last` from.
    /// `None` when every earlier slot is a hole (or there are none): there is no element to
    /// have claimed anything, so the region opens at `[`.
    ///
    /// `i == arr.elements.len()` asks it of the whole array — "the last real element" — which
    /// is the form [`Self::end_scan_start`] and [`Self::last_element_trailing_split`] take, so
    /// the three sites share one walk rather than restating the search past holes.
    fn prev_real_slot<'e>(
        &self,
        arr: &'e ast::ArrayExpression<'e>,
        i: usize,
    ) -> Option<(usize, &'e Expression<'e>)> {
        arr.elements[..i]
            .iter()
            .enumerate()
            .rev()
            .find_map(|(idx, e)| e.map(|e| (idx, e)))
    }

    /// Does this **block** comment trail the element before it, rather than lead the one after?
    /// The separator decides, and is the whole rule: before the comma the comment trails
    /// (`[A /* c */, B]`); past it, it leads the next element (`[A, /* c */ B]`). On the
    /// LAST element (`is_last`) there is no next element to lead and the comma is never
    /// emitted (`trailingComma: 'none'`), so the whole same-line run trails — the shared
    /// comma arm, [`block_is_before_comma`], stated once with the collector's classifier.
    ///
    /// A newline after the comment does **not** carry it across the comma. Prettier classifies
    /// on newlines alone (`endOfLine`, `main/comments/attach.js` — a comma is not a node) and so
    /// rewrites `[A, /* c */⏎B]` to `[A /* c */, B]`, flipping the binding from `B` to `A`; tsv
    /// preserves the authored position. The comma is what carries the association, and unlike a
    /// `//` — which runs to end-of-line, so trailing it past the comma is the only rendering
    /// that exists (the sanctioned pure-separator trail) — a block comment renders fine either
    /// side, making the move unforced. See conformance_prettier_ts_comments.md §Comment relocation.
    ///
    /// The separator rule has one exception, and it is the run's **order**:
    /// `run_defers_line` — the run ends in a `//` (`A, /* c */ // x⏎B`), which defers
    /// through `line_suffix`, so a block left to lead `B` would render *after* it and the
    /// authored pair comes back reversed across two lines. Such a block trails `A`
    /// instead — emitted past the comma, in front of the suffix, exactly where it was
    /// written. Same rule, same reason as `Printer::push_trailing_comments_in_range`'s
    /// deferred-run block, and as the shared element-comma emitter's `after_comma` run.
    /// It is a **boolean, not a position**, because the deferring `//` can only be the
    /// run's last member, so every block being classified provably precedes it.
    ///
    /// Behind a spread's stripped-paren interior (`behind_interior`,
    /// [`Printer::paren_interior_separates_run`]) the comma is emitted against the spread
    /// itself, ahead of the interior and the run, so on a slot the array continues past no
    /// block is before it — and every block is claimed here, whichever side of the comma the
    /// author wrote it on. The expanding printer then either prints the run behind the
    /// interior or hands it to the next element to lead (`...(b⏎/* i */⏎) /* t */, c` →
    /// `...b,⏎/* i */⏎/* t */ c`, [`Printer::run_leads_next_item`]), as the shared
    /// collector does ([`Printer::collect_element_trailing_comments`]).
    ///
    /// ⚠️ **This decides the comma SIDE only. The positional half — is the comment in this
    /// element's run at all — is [`Printer::trailing_comment_run`]**, which every caller
    /// goes through first, so the source reading and the run's end are answered once for
    /// both this family and the shared collector. An own-line comment therefore never
    /// reaches here: the run stops before it.
    ///
    /// Only the expanding printer can reach the order exception, and the whole-array gate
    /// is what guarantees it: a `//` anywhere between `[` and `]` sets
    /// `has_expanding_comments`, so a gap holding one is never classified by the
    /// non-expanding callers, where the second arm is provably inert. That is now a
    /// statement about *reachability* only — the complement no longer rests on it, since
    /// [`Self::add_inline_leading_block_comments`] emits a range the split already ends.
    ///
    /// The `is_last` arm's double-print hazard is the end-of-array scans, which must not
    /// re-emit what it claims; [`Self::end_scan_emits_comment`] is the one statement of
    /// that exclusion, and it too keys on the split rather than re-deriving this question.
    fn block_comment_trails_prev_element(
        comment: &tsv_lang::Comment,
        comma_pos: Option<u32>,
        is_last: bool,
        run_defers_line: bool,
        behind_interior: bool,
    ) -> bool {
        block_is_before_comma(is_last, comma_pos, comment.span.start)
            || run_defers_line
            || behind_interior
    }

    /// The SPLIT POINT of the gap after the real element in slot `idx`: where that
    /// element's claimed trailing run ends and the next element's leading run begins.
    ///
    /// The one place the array's partition is decided, so its three readers — the trailing
    /// emitter, the next element's leading scan, and the end-of-array scan — cannot
    /// disagree about a comment. **Every emitter in this file goes through here**, each
    /// taking a *range* from the position rather than re-asking a predicate and hoping the
    /// readings stay complementary; they did not, in all three directions at once.
    ///
    /// ⚠️ **The claim is a PREFIX of the gap's comments**, which is why this is a walk and
    /// not a filter: it stops at the first comment that does not trail, so a later comment
    /// can never be claimed over an earlier one left to lead. Dropping that invariant
    /// REORDERS the authored run — `[a⏎/* c1 */ /* c2 */, b]` has c1 leading its line
    /// (so it leads `b`) while c2, with c1 before it on that line, reads as trailing `a`,
    /// and the pair comes back as `a /* c2 */, /* c1 */ b`. Same rule and same reason as
    /// [`Printer::collect_trailing_comments`]' `break`; see docs/comments.md §The
    /// element-comma seam.
    ///
    /// ⚠️ `is_last` is derived here, not passed in, and it is the **slot** question —
    /// whether `idx` is the final slot, not whether it is the final real element. It is
    /// load-bearing: on the last slot no comma is emitted (`trailingComma: 'none'`), so
    /// "before" and "after" the separator are one position and the whole run trails. Every
    /// hand-written copy of this rule has at some point got that half wrong.
    fn element_gap_split(
        &self,
        arr: &ast::ArrayExpression<'_>,
        span: Span,
        idx: usize,
        elem_end: u32,
        gap_end: u32,
    ) -> u32 {
        let comma_pos = self.find_comma_in_range(elem_end, gap_end);
        let is_last = idx + 1 == arr.elements.len();

        // The positional half is the shared run — the source reading, ending at the first
        // line comment — so this printer and the collector cannot disagree about which
        // comments are even candidates. A line comment in the run always trails (nothing
        // can follow it on its line); a block additionally has to keep the author's side
        // of the comma, which is this family's own arm. The run's deferring `//` is read
        // ONCE, above the loop: it is the run's last member, so the answer is the same for
        // every block being classified.
        let run: CommentVec<'_> = self.trailing_comment_run(elem_end, gap_end).collect();
        let defers_line = run_defers_line(&run);
        let behind_interior = self.slot_run_is_behind_interior(arr, span, idx);
        // Only a REAL next element has a leading run to hand a glued block to: with only
        // holes after the slot, `gap_end` is the array's own end, and the run travels with
        // the share past the elisions.
        let has_next = next_real_element_start(arr.elements, idx).is_some();

        let mut split = elem_end;
        for comment in run {
            if comment.is_block
                && !Self::block_comment_trails_prev_element(
                    comment,
                    comma_pos,
                    is_last,
                    defers_line,
                    // An after-comma block glued to the next element leads it, in every
                    // family (`Printer::comment_hugs_next_item`).
                    behind_interior
                        && !(has_next && self.comment_hugs_next_item(comment.span.end, gap_end)),
                )
            {
                break;
            }
            split = comment.span.end;
        }
        split
    }

    /// The stripped-paren interior of the spread in slot `idx` — `None` for anything else,
    /// and for a slot a format-ignore directive FROZE: its verbatim slice already printed
    /// the interior, so it has no share to emit and its run is an ordinary one.
    fn slot_paren_interior(
        &self,
        arr: &ast::ArrayExpression<'_>,
        span: Span,
        idx: usize,
    ) -> Option<Span> {
        let interior = arr.elements[idx].as_ref()?.paren_interior()?;
        self.element_frozen_span(span.start + 1, arr.elements, idx)
            .is_none()
            .then_some(interior)
    }

    /// Whether the run after the element in slot `idx` follows that element's
    /// stripped-paren interior ([`Printer::paren_interior_separates_run`]) — a spread whose
    /// parens held an own-line share or a same-line `//`. Asked by the split
    /// ([`Self::element_gap_split`]) and by the expanding printer, which emits such a run
    /// behind the interior ([`Printer::push_element_share_and_run`]), so the claim and the
    /// emission cannot disagree about it.
    fn slot_run_is_behind_interior(
        &self,
        arr: &ast::ArrayExpression<'_>,
        span: Span,
        idx: usize,
    ) -> bool {
        self.paren_interior_separates_run(self.slot_paren_interior(arr, span, idx))
    }

    /// Emit the block comments of a trailing-elision region that stays on the elision
    /// comma's line — the grouped array literal's and the grouped array pattern's run past
    /// the last hole (a line comment routes both to their expanded printers). The first
    /// hugs the comma (`[x, ,/* c */]`) and each later one takes a space ahead of it: the
    /// separator goes before each comment and is never left out, or two blocks weld into
    /// `/* c1 *//* c2 */` (`docs/comments.md` §Trailing and dangling runs).
    pub(in crate::printer) fn push_trailing_elision_blocks<'c>(
        &self,
        parts: &mut DocBuf,
        comments: impl IntoIterator<Item = &'c tsv_lang::Comment>,
    ) {
        let mut first = true;
        for comment in comments.into_iter().filter(|c| c.is_block) {
            if !first {
                parts.push(self.d().text(" "));
            }
            parts.push(self.build_comment_doc(comment));
            first = false;
        }
    }

    /// Emit block comments in `[search_start, elem_start)` as inline-leading
    /// (`/*c*/ elem`). Used by both the first-element and subsequent-element
    /// paths in the non-expanding array printers.
    ///
    /// `search_start` is the gap's split point — everything from there on leads this
    /// element, by [`Self::element_gap_split`] — so no trailing-side filter is
    /// applied or needed here.
    ///
    /// ⚠️ That split must be computed from the previous element's END, not from past the
    /// separator. A scan starting at `comma + 1` cannot see what the author wrote on the
    /// other side of a comma, and the trailing side claims only what precedes the comment
    /// on its line — so a block the author put on its own line before a glued comma
    /// (`[a⏎/* c */, b]`) had no emitter at all. Same rule and same drop as the four
    /// sites `Printer::collect_trailing_comments`' callers were converged onto; see
    /// docs/comments.md §The element-comma seam.
    fn add_inline_leading_block_comments(
        &self,
        search_start: u32,
        elem_start: u32,
        parts: &mut DocBuf,
    ) {
        for comment in self.comments_to_emit_between(search_start, elem_start) {
            if comment.is_block {
                parts.push(self.format_inline_block_comment(comment, true));
            }
        }
    }

    /// Where the array element at `i`'s leading-comment scan starts. `elem_start` is this
    /// element's own start, which bounds the scan (only a real element collects leading
    /// comments, so there is always one to bound it).
    ///
    /// **One rule, whatever sits in between**: the scan resumes at the last REAL element's
    /// gap SPLIT POINT ([`Self::element_gap_split`]) — or at `[` when there is no earlier
    /// element to have claimed anything. Everything from there on leads this element,
    /// which is the complement of that element's trailing claim, so the gap is partitioned
    /// exactly once however many elisions lie in it.
    ///
    /// ⚠️ **Intervening holes do not move the anchor forward.** They used to: the scan
    /// started past the hole's comma, located by *counting* commas from the last real
    /// element, on the reading that the region before that comma was "the hole seams'
    /// business". No hole seam prints, so it was nobody's — every comment in it was
    /// DROPPED (`[x⏎/* c */, , y]`, `[/* c */, y]`, `[x, /* c */ , y]`, and a run of two
    /// came back with only its tail). A hole is a **slot**, not an element: it prints
    /// nothing and its comma is re-emitted structure outside every element span, so a
    /// comment slides *forward* past it to the element it leads. Sliding backward across
    /// the previous element's own comma is the move tsv refuses — see
    /// conformance_prettier_ts_comments.md §Comment relocation (`across an elision`).
    fn leading_comment_search_start_for(
        &self,
        arr: &ast::ArrayExpression<'_>,
        span: Span,
        i: usize,
        elem_start: u32,
        role: LiteralRole,
    ) -> u32 {
        match self.prev_real_slot(arr, i) {
            Some((idx, prev)) => {
                let prev_end = self.element_printed_end(arr, span, idx, prev, role);
                self.element_gap_split(arr, span, idx, prev_end, elem_start)
            }
            None => span.start + 1,
        }
    }

    /// Add trailing block comments for an array element — the ones
    /// [`Self::block_comment_trails_prev_element`] binds to it.
    ///
    /// `is_last` is the SLOT question, not the real-element one: a trailing elision keeps
    /// its (syntactically significant) comma, and the comments past it belong to the
    /// trailing-hole seams — so a real element followed by holes must not claim them.
    fn add_trailing_array_comments(
        &self,
        arr: &ast::ArrayExpression<'_>,
        span: Span,
        elem_end: u32,
        current_index: usize,
        parts: &mut DocBuf,
    ) {
        // Bounded at `next_boundary`: this element's separator, if it has one, lies before the
        // next element. A SOURCE trailing comma past the last element is still found — the
        // `is_last` arm is what keeps the comments past it on this element.
        let next_boundary = array_gap_end(arr, span, current_index);
        let split = self.element_gap_split(arr, span, current_index, elem_end, next_boundary);

        // Everything below the split is this element's, by construction — the next
        // element's leading scan resumes exactly there.
        for comment in self.comments_to_emit_between(elem_end, split) {
            if comment.is_block {
                parts.push(self.format_inline_block_comment(comment, false));
            }
        }
    }

    /// Is this comment the end-of-array scan's to emit as an array sibling, rather than
    /// already printed by someone closer?
    ///
    /// The one statement of that exclusion, shared by [`Self::build_array_group_doc`]'s
    /// end-of-array scan and [`Self::build_array_doc_with_expanding_comments`]'s final
    /// scan (it drifted between them once — the group side learned the element anchor and
    /// the final scan kept re-emitting what the element claimed).
    ///
    /// It emits what BOTH closer emitters left, so the two exclusions compose rather than
    /// pick one:
    ///
    /// - **The last real element's trailing CLAIM** — everything below its gap SPLIT
    ///   ([`Self::last_element_trailing_split`]) is already printed. Re-deriving that from
    ///   an anchor plus a same-line test is what let the two drift: the claim's own gate is
    ///   a source reading (a block glued to a stripped `)`, or one written before a glued
    ///   comma, is claimed though it does not share the element's line), so a same-line
    ///   re-derivation DOUBLE-PRINTS every one of those. Taking the split makes the
    ///   exclusion exact by construction, including its prefix rule, which no per-comment
    ///   test can reproduce.
    /// - **The anchor's own line** — a comment glued there belongs to whoever owns that
    ///   line: the element's own trailing claim, and the hole seams past a **trailing
    ///   elision**, where the last real element's claim stops at its comma and the region
    ///   beyond is not this scan's to re-parent.
    ///
    /// `last_real` is `None` only when the array holds no real element at all.
    fn end_scan_emits_comment(
        &self,
        comment: &tsv_lang::Comment,
        scan_start: u32,
        last_real: Option<(u32, u32)>,
    ) -> bool {
        if let Some((elem_end, split)) = last_real
            && comment.span.start >= elem_end
            && comment.span.start < split
        {
            return false;
        }
        !self.is_same_line(scan_start, comment.span.start)
    }

    /// Where an end-of-array scan starts: the last REAL element's end.
    ///
    /// Never inside a last spread's stripped parens: that interior's own-line share is
    /// emitted by the expanding printer's loop, ahead of the run past the `)`
    /// (`Printer::push_element_share_and_run`) — and [`Self::spread_has_share`] is what
    /// routes every such array there. A scan that reached in as well would print the share
    /// twice.
    ///
    /// `None` when the array holds no real element: there is nothing to scan past.
    fn end_scan_start(
        &self,
        arr: &ast::ArrayExpression<'_>,
        span: Span,
        role: LiteralRole,
    ) -> Option<u32> {
        let (idx, elem) = self.prev_real_slot(arr, arr.elements.len())?;
        Some(self.element_printed_end(arr, span, idx, elem, role))
    }

    /// [`LiteralRole::printed_end`] for slot `idx`, read through a format-ignore freeze: a
    /// frozen element's verbatim slice prints its whole span, the value shell included
    /// ([`Printer::element_claim_anchor`]).
    fn element_printed_end(
        &self,
        arr: &ast::ArrayExpression<'_>,
        span: Span,
        idx: usize,
        elem: &Expression<'_>,
        role: LiteralRole,
    ) -> u32 {
        let frozen = self.element_frozen_span(span.start + 1, arr.elements, idx);
        Self::element_claim_anchor(frozen, role.printed_end(elem))
    }

    /// Whether any slot is a spread whose stripped parens hold an own-line share — an
    /// expansion trigger, so the one printer that orders that share against the run past
    /// the `)` is the only one to reach it. Its comments lie inside the spread's own span,
    /// out of every gap scan the other triggers make. A frozen slot has no share
    /// ([`Self::slot_paren_interior`]): its verbatim slice printed the interior.
    fn spread_has_share(&self, arr: &ast::ArrayExpression<'_>, span: Span) -> bool {
        (0..arr.elements.len()).any(|idx| {
            !self
                .paren_interior_own_line_comments(self.slot_paren_interior(arr, span, idx))
                .is_empty()
        })
    }

    /// Whether the end-of-array scan past the last real element finds a block comment no
    /// closer emitter prints ([`Self::end_scan_emits_comment`]) — the grouped printer's
    /// routing invariant, asserted in debug builds: `build_array_doc` sends every such array
    /// to the expanding printer.
    ///
    /// ⚠️ Keyed on the last SLOT, not the last real element: past a trailing elision the
    /// region belongs to the grouped printer's `has_trailing_elision` branch, which emits
    /// it. Keying it on `end_scan_start`'s last real element alone reads a spread's own-line
    /// interior there (`[...(b⏎/* i */⏎), , ]`) as unprinted.
    fn grouped_end_scan_finds_comment(
        &self,
        arr: &ast::ArrayExpression<'_>,
        arr_span: Span,
        role: LiteralRole,
    ) -> bool {
        let last_slot_real = arr.elements.last().is_some_and(Option::is_some);
        let Some(search_start) = last_slot_real
            .then(|| self.end_scan_start(arr, arr_span, role))
            .flatten()
        else {
            return false;
        };
        let last_real_split = self.last_element_trailing_split(arr, arr_span, role);
        self.comments_to_emit_between(search_start, arr_span.end - 1)
            .any(|c| c.is_block && self.end_scan_emits_comment(c, search_start, last_real_split))
    }

    /// The last REAL element's end paired with its gap SPLIT — the
    /// [`Self::end_scan_emits_comment`] key.
    ///
    /// Keyed on the last real element rather than the last SLOT, because a trailing
    /// elision does not stop that element from claiming: its gap still runs to the closing
    /// `]`. `is_last` is the slot question ([`Self::add_trailing_array_comments`]), so with
    /// holes after it the claim stops at its own comma and the hole seams keep the rest —
    /// which is exactly what this reproduces, by computing the split the same way that
    /// emitter does.
    fn last_element_trailing_split(
        &self,
        arr: &ast::ArrayExpression<'_>,
        span: Span,
        role: LiteralRole,
    ) -> Option<(u32, u32)> {
        let (idx, elem) = self.prev_real_slot(arr, arr.elements.len())?;
        let elem_end = self.element_printed_end(arr, span, idx, elem, role);
        // No real element follows, so the gap runs to the closing `]`.
        let gap_end = span.end - 1;
        Some((
            elem_end,
            self.element_gap_split(arr, span, idx, elem_end, gap_end),
        ))
    }

    /// Build a Doc for an array expression, wrapping on width.
    ///
    /// The single entry point for every array position — top-level and nested alike — so
    /// one layout decision serves every one of them.
    pub(in crate::printer) fn build_array_doc(
        &self,
        arr: &ast::ArrayExpression<'_>,
        arr_span: Span,
    ) -> DocId {
        self.build_array_doc_as(arr, arr_span, LiteralRole::Expression)
    }

    /// [`Self::build_array_doc`] in a [`LiteralRole`]. A compound target's array prints as
    /// prettier prints an array PATTERN: an element it reads as a nested pattern or a
    /// default takes the target's own print, and the nested-literal break
    /// ([`Self::should_break_nested_array`], an `ArrayExpression` rule) does not apply.
    pub(in crate::printer) fn build_array_doc_as(
        &self,
        arr: &ast::ArrayExpression<'_>,
        arr_span: Span,
        role: LiteralRole,
    ) -> DocId {
        self.mark_relational_sibling_authored_pairs(arr.elements.iter().copied());
        if arr.elements.is_empty() {
            return self.build_empty_brackets_inline_with_comments_doc(arr_span);
        }

        // Whole-array comment-presence flag (one binary search over the `[…]` span).
        // A false gate is exact: every per-element comment sub-range — the
        // expanding-comment checks below, and the inline leading/trailing lookups in
        // the fill/group builders — lies within [span.start, span.end], so when the
        // array holds no comment, none can lie in any of them (canonical reference:
        // build_params_doc_with_comments).
        let has_comments = self.has_comments_on_page_between(arr_span.start, arr_span.end);

        // Check for comments that force expansion: line comments (can't be inline),
        // multi-line block comments (contain hardlines that must propagate),
        // or own-line single-line block comments (on a separate line from adjacent tokens).
        // The gate skips all three sub-queries — and sub-query 3's eager element
        // collect — on the comment-free common case.
        let has_expanding_comments = has_comments
            && (self.has_line_comments_between(arr_span.start, arr_span.end)
                || self.has_multiline_block_comments_on_page_between(arr_span.start, arr_span.end)
                || self.has_own_line_block_comments_in_array(arr, arr_span, role)
                || self.spread_has_share(arr, arr_span));

        if has_expanding_comments {
            return self.build_array_doc_with_expanding_comments(arr, arr_span, role);
        }

        // A concisely printed array (numbers only) packs as a fill; everything else goes
        // one per line.
        //
        // An element carrying its own newline — a line-continuation string, a multiline
        // template — needs no arm of its own: its text node reports
        // `CachedWidth::HasNewline`, so `will_break` breaks the group below exactly as
        // prettier's `literalline`-borne `breakParent` does.
        if self.is_concisely_printed_array(arr) {
            // Use fill for greedy packing of numbers
            self.build_array_fill_doc(arr, arr_span, has_comments)
        } else {
            // Use group with one-per-line for other content
            self.build_array_group_doc(arr, arr_span, has_comments, role)
        }
    }

    /// An element's doc in a [`LiteralRole`]: the target's own print for a value a compound
    /// target reads as a nested pattern or a default ([`is_compound_target_value`]), the
    /// argument-position doc every array element takes otherwise.
    fn build_array_element_doc_as(&self, expr: &Expression<'_>, role: LiteralRole) -> DocId {
        if role == LiteralRole::CompoundTarget && is_compound_target_value(expr) {
            return self.build_list_element_doc(expr.span().start, || {
                self.build_compound_target_doc(expr)
            });
        }
        self.build_arg_expression_doc(expr)
    }

    /// Check if array contains own-line single-line block comments that force expansion.
    ///
    /// Delegates to the generic `has_own_line_block_comments_in_bracket_list` helper,
    /// filtering out holes (elisions) from the element list.
    ///
    /// The helper reads the SOURCE, the same reading the element→`,` seam takes
    /// ([`Self::block_comment_trails_prev_element`]): a comment with the comma or a
    /// stripped `)` before it on its line is not own-line, so it collapses onto the
    /// element it binds to instead of expanding the list. On an item-boundary reading
    /// those two spellings expanded — a third fixed point neither the bare authoring nor
    /// prettier produces, and, since the reprint puts the comment back on the element's
    /// line, one the next pass immediately collapsed (`[a⏎, /* c */⏎b]` was a 2-pass).
    fn has_own_line_block_comments_in_array(
        &self,
        arr: &ast::ArrayExpression<'_>,
        span: Span,
        role: LiteralRole,
    ) -> bool {
        let non_null: SmallVec<[_; 8]> = arr.elements.iter().flatten().collect();
        // The element's PRINTED span ([`LiteralRole::printed_end`]): a comment in a compound
        // target's erased value shell is not inside the element, its doc not printing it.
        self.has_own_line_block_comments_in_bracket_list(span, &non_null, |e| {
            Span::new(e.span().start, role.printed_end(e))
        })
    }

    /// Prettier's `isConciselyPrintedArray` (`print/array.js`) — the ONE reading of "this
    /// array packs as a fill", at all three sites prettier asks it: the array printer's own
    /// fill-vs-group choice below, and the two call-argument sites that refuse the
    /// expand-last hug over one (`call-arguments.js`'s `!(args.length > 1 &&
    /// isConciselyPrintedArray(lastArg, options))`), whose break characteristics the hug
    /// states can't express — reached through [`Self::arg_is_concisely_printed_array`].
    ///
    /// [`array_elements_are_signed_numbers`] is the shape half; the rest is prettier's
    /// comment clause, `!hasComment(element.argument)`. A comment INSIDE a signed literal
    /// binds to its argument and refuses the fill (`[-/* c */ 1, …]` prints one per line);
    /// one past the element binds to the element and does not (`[-1 /* c */, …]` still
    /// packs). The element's own span is exactly that line — it closes on the argument, or
    /// on a retained paren shell around it (`-(1 /* c */)`, which prettier also refuses).
    /// Only a signed literal has room for one: a bare numeric literal's span is the number.
    ///
    /// Prettier's other clause — a same-line trailing LINE comment refuses too — needs no
    /// arm here: a line comment anywhere in the array diverts it to
    /// [`Self::build_array_doc_with_expanding_comments`] before either caller asks.
    ///
    /// No array-wide zero-comment gate: the per-element ask is a signed literal's own span,
    /// which for all but a huge number is narrower than a comment can be, so
    /// `range_too_narrow_for_a_comment` answers it at the call site without a search.
    fn is_concisely_printed_array(&self, arr: &ast::ArrayExpression<'_>) -> bool {
        array_elements_are_signed_numbers(arr)
            && !arr.elements.iter().flatten().any(|elem| {
                matches!(elem.kind, ExpressionKind::UnaryExpression(_))
                    && self.has_comments_on_page_between(elem.span().start, elem.span().end)
            })
    }

    /// [`Self::is_concisely_printed_array`] read off a call ARGUMENT, which the caller holds
    /// as a bare expression: a non-array is never concisely printed.
    pub(in crate::printer) fn arg_is_concisely_printed_array(&self, expr: &Expression<'_>) -> bool {
        matches!(&expr.kind, ExpressionKind::ArrayExpression(arr) if self.is_concisely_printed_array(arr))
    }

    /// Build fill doc for numbers-only arrays (greedy packing)
    ///
    /// Includes inline block comments between elements.
    /// Uses binary search to find comments: O(log n + k)
    ///
    /// The fill's content/separator alternation is prettier's
    /// (`printArrayElementsConcisely`): each content is the slot's element, its glued
    /// comments AND — every slot but the last — its comma as ONE doc. The comma must sit
    /// in the content, not the separator, because the fill's pairwise measure is
    /// `[content, separator, next content]` — with a `comma_line` separator the next item
    /// is measured without ITS comma, so an item whose comma would land on column 101
    /// still "fits" and the break lands one item late (the numbers-fill 101-column line).
    ///
    /// The separator is a `line`, except across an author's blank line
    /// ([`Self::has_blank_line_after_slot`], prettier's `isLineAfterElementEmpty`), where
    /// it is the group builder's blank pair — `literalline` for the empty line, `hardline`
    /// for the indented one. Both are hard, so the pair also expands the array, exactly as
    /// the `breakParent` in prettier's `[hardline, hardline]` does. Prettier's third
    /// spelling — a `hardline` when the next element carries a leading LINE comment —
    /// cannot be reached here: a line comment anywhere in the array diverts it to
    /// [`Self::build_array_doc_with_expanding_comments`] before the fill is built.
    fn build_array_fill_doc(
        &self,
        arr: &ast::ArrayExpression<'_>,
        span: Span,
        has_comments: bool,
    ) -> DocId {
        let d = self.d();
        let last = arr.elements.len() - 1;
        let mut parts = DocBuf::with_capacity(arr.elements.len() * 2);
        // One slot's docs — element, glued comments, comma — assembled into a single
        // fill content; the buffer is reused across slots.
        let mut slot = DocBuf::new();

        for i in 0..arr.elements.len() {
            slot.clear();
            // Elements and their glued comments (a hole pushes nothing — though
            // `is_concisely_printed_array` already excludes elisions from this path).
            self.push_array_element_with_inline_comments(
                arr,
                span,
                i,
                has_comments,
                LiteralRole::Expression,
                &mut slot,
            );

            if i < last {
                slot.push(d.text(","));
            }
            parts.push(d.concat(&slot));
            if i < last {
                parts.push(if self.has_blank_line_after_slot(arr, span, i, None) {
                    d.concat(&[d.literalline(), d.hardline()])
                } else {
                    d.line()
                });
            }
        }

        let inner = d.concat(&[d.softline(), d.fill(&parts)]);

        d.group(d.concat(&[d.text("["), d.indent(inner), d.softline(), d.text("]")]))
    }

    /// Push slot `i`'s element doc plus its inline leading/trailing block comments.
    ///
    /// The one definition of "an array element and the comments glued around it",
    /// shared by [`Self::build_array_fill_doc`] and [`Self::build_array_group_doc`] — the
    /// two printers a *glued* block comment can reach, since gluing is not an expansion
    /// trigger and so does not divert the array to the expanding printer. Emitting the element alone DROPS the
    /// trailing side (the leading side is owned by the element and rides inside its own
    /// doc), so the pairing is an invariant worth having one home rather than three.
    ///
    /// In the fill printer the slot's pushed docs — comments included — fold into ONE fill
    /// content ([`Self::build_array_fill_doc`]), so a glued trailing block comment is
    /// measured with its element and wraps with it, rather than landing in the fill's
    /// content/separator alternation as an item of its own that nothing measures.
    ///
    /// Takes the slot INDEX rather than the element, and resolves it here: the comment
    /// lookups are keyed on `(arr, i)` while the doc comes from the element, so handing
    /// in both would let a caller pair them wrongly and bind an element's comments to its
    /// neighbour. A hole (elision) resolves to nothing and pushes nothing — it has no
    /// span to anchor a gap on.
    ///
    /// `has_comments` is the array-wide zero-comment fast gate: with no comment anywhere
    /// in the array none can lie in this element's leading/trailing gap, so both the
    /// block-comment collection and its comma scan are skipped. Blank-line detection is
    /// comment-independent and stays with the callers, outside the gate.
    fn push_array_element_with_inline_comments(
        &self,
        arr: &ast::ArrayExpression<'_>,
        span: Span,
        i: usize,
        has_comments: bool,
        role: LiteralRole,
        parts: &mut DocBuf,
    ) {
        let Some(expr) = arr.elements[i].as_ref() else {
            return;
        };

        if has_comments {
            let elem_start = expr.span().start;
            let search_start =
                self.leading_comment_search_start_for(arr, span, i, elem_start, role);
            self.add_inline_leading_block_comments(search_start, elem_start, parts);
        }

        parts.push(self.build_array_element_doc_as(expr, role));

        if has_comments {
            // Trailing block comments — the ones `block_comment_trails_prev_element`
            // binds to this element (before its comma; on the last slot the whole
            // same-line run, its comma never being emitted).
            let elem_end = self.element_printed_end(arr, span, i, expr, role);
            self.add_trailing_array_comments(arr, span, elem_end, i, parts);
        }
    }

    /// Build group doc for non-numeric arrays (one per line when broken)
    ///
    /// Includes inline block comments between elements.
    /// Uses binary search to find comments: O(log n + k)
    ///
    /// Note: Arrays with expanding comments use build_array_doc_with_expanding_comments instead.
    fn build_array_group_doc(
        &self,
        arr: &ast::ArrayExpression<'_>,
        arr_span: Span,
        has_comments: bool,
        role: LiteralRole,
    ) -> DocId {
        let d = self.d();
        let mut parts = d.pooled_docbuf();

        // Check if last element is an elision (requires mandatory trailing comma)
        let has_trailing_elision = arr.elements.last().is_some_and(Option::is_none);

        // Check Prettier's shouldBreak heuristic for nested arrays/objects
        let should_break = role == LiteralRole::Expression && self.should_break_nested_array(arr);

        for i in 0..arr.elements.len() {
            // Elements and their glued comments (a hole pushes nothing).
            self.push_array_element_with_inline_comments(
                arr,
                arr_span,
                i,
                has_comments,
                role,
                &mut parts,
            );

            let is_last = i == arr.elements.len() - 1;
            if !is_last {
                let has_blank_after = self.has_blank_line_after_slot(arr, arr_span, i, None);

                // Separator comma between elements
                parts.push(d.text(","));

                if has_blank_after {
                    // Blank line preservation: empty line (no indent) then content line (with indent)
                    // Flat mode: just a space (blank line collapses)
                    // Break mode: literalline (empty) + hardline (indented)
                    parts.push(d.if_break(d.concat(&[d.literalline(), d.hardline()]), d.text(" ")));
                } else {
                    parts.push(d.line());
                }
            } else if has_trailing_elision {
                // Trailing comma for elision - MUST be preserved (semantically significant)
                parts.push(d.text(","));

                // The trailing-hole region: everything past the last real element's own
                // claim (e.g. `[, , ,/* c */]`, `[x, /* c */ , ]`). No hole slot prints an
                // element, so add_leading/add_trailing never run for it and this is the
                // region's only emitter — it takes the SPLIT ([`Self::element_gap_split`],
                // via [`Self::last_element_trailing_split`]) so the two sides partition the
                // gap. Anchoring on the last comma instead dropped everything the author
                // wrote *before* one of the elision commas, which no other emitter sees.
                //
                // With no real element at all the region opens at `[`. The split is at or
                // past the element's end either way, so a LAST spread's stripped-paren
                // interior stays the spread doc's share and is never re-emitted here.
                let scan_start = self
                    .last_element_trailing_split(arr, arr_span, role)
                    .map_or(arr_span.start + 1, |(_, split)| split);
                self.push_trailing_elision_blocks(
                    &mut parts,
                    self.comments_to_emit_between(scan_start, arr_span.end - 1),
                );
            }
        }

        // Nothing past the last element is this path's to print: an own-line block there
        // lies outside every element span, so `has_own_line_block_comments_in_array`
        // routes the array to the expanding printer, and so does a spread's own-line share
        // (`spread_has_share`), which that printer orders against the run past the `)`.
        debug_assert!(
            !(has_comments && self.grouped_end_scan_finds_comment(arr, arr_span, role)),
            "an array with a comment past its last element must route to the expanding printer"
        );

        let inner_parts: DocBuf = smallvec![d.softline(), d.concat(&parts)];
        let inner = d.concat(&inner_parts);

        // Build group contents
        let group_contents = d.concat(&[d.text("["), d.indent(inner), d.softline(), d.text("]")]);

        // Use group_break() when shouldBreak heuristic matched or spread comments force it.
        // This sets shouldBreak on the GROUP ITSELF rather than using break_parent().
        // The difference: shouldBreak is local to this group, while break_parent()
        // propagates up and forces enclosing groups to break.
        // Prettier uses shouldBreak for this heuristic (array.js lines 89-106, 143).
        if should_break {
            d.group_break(group_contents)
        } else {
            d.group(group_contents)
        }
    }

    /// Build a Doc for an array with comments that force expansion.
    ///
    /// Used for arrays containing line comments (can't be inline) or multi-line
    /// block comments (hardlines must propagate). Always expands to multiline.
    fn build_array_doc_with_expanding_comments(
        &self,
        arr: &ast::ArrayExpression<'_>,
        span: Span,
        role: LiteralRole,
    ) -> DocId {
        let d = self.d();
        let mut parts = d.pooled_docbuf();

        // A comment trailing the opening `[` on its own line is kept on the `[`
        // line (divergence from prettier, which relocates it to its own line as the
        // first element's leading comment). See conformance_prettier_ts_comments.md
        // §Comment relocation (Array literal `[`).
        let first_elem_start = arr
            .elements
            .iter()
            .flatten()
            .next()
            .map_or(span.end - 1, |e| e.span().start);
        let (bracket_line_prefix, bracket_pull_pos) =
            self.delimiter_line_comment_prefix(span.start, first_elem_start);

        // End of the most recently emitted REAL element, and its slot. Holes don't advance
        // either; this lets the next real element's leading-comment range walk back across
        // any intervening hole commas to claim the comments between them and the
        // previous real element. The slot is what `element_gap_split` derives `is_last`
        // from, so the leading side asks the same question the trailing side answered.
        let mut last_real_emit_end = span.start + 1;
        let mut last_real_slot: Option<usize> = None;

        // End position of the last trailing-on-array comment emitted by the
        // trailing-hole iteration (when present). The post-loop scan starts here
        // to avoid re-emitting those comments.
        let mut trailing_hole_comments_end: Option<u32> = None;

        // A spread's stripped-paren interior and the trailing run that follows it, which
        // the array still owes the page, held across the elisions that follow the spread —
        // see the emission site below.
        let mut pending_behind_interior: Option<(Option<Span>, CommentVec<'_>)> = None;
        // A run displaced past a spread's comma that leads the next REAL element
        // (`Printer::run_leads_next_item`), held across any elisions until that element.
        let mut pending_leads_next: CommentVec<'_> = smallvec![];

        for (i, elem) in arr.elements.iter().enumerate() {
            // O(remaining elements) — compute once and reuse below.
            let next_boundary = array_gap_end(arr, span, i);
            let (elem_start, elem_end) = match elem {
                Some(e) => (
                    e.span().start,
                    self.element_printed_end(arr, span, i, e, role),
                ),
                None => (next_boundary, next_boundary),
            };

            // Hole at the LAST element index: its leading comments are trailing
            // on the array as a whole (no future real element to attach to).
            // Collect them and emit inline after the hole's comma below.
            let is_trailing_hole = elem.is_none() && i + 1 == arr.elements.len();

            // For real elements: comments in (last_real_emit_end, elem_start).
            // For a trailing hole: same range, but extended to the closing `]`.
            // Other holes contribute nothing — their comments belong to the next
            // real element's leading-comment range.
            //
            // Filter rule: comments same-line with the previous real element are
            // trailing on it, EXCEPT block comments past its comma, which are
            // leading on this element.
            let leading_upper = if elem.is_some() {
                Some(elem_start)
            } else if is_trailing_hole {
                Some(span.end - 1)
            } else {
                None
            };
            let leading_comments: CommentVec<'_> = if let Some(upper) = leading_upper {
                // Resume at the previous element's split point — the complement of its
                // trailing claim, stated once rather than re-derived as a filter here.
                let scan_start = match last_real_slot {
                    Some(prev) => {
                        self.element_gap_split(arr, span, prev, last_real_emit_end, upper)
                    }
                    None => last_real_emit_end,
                };
                self.comments_to_emit_between(scan_start, upper)
                    .filter(|c| {
                        // Bracket-line comments pulled onto the `[` line above are
                        // emitted as the prefix, not as leading on the first element.
                        // (Only the first element's gap can be same-line as `[`.)
                        !bracket_pull_pos
                            .is_some_and(|dpos| self.comment_on_delimiter_line(dpos, c))
                    })
                    .collect()
            } else {
                smallvec![]
            };

            // The separator for the gap BEFORE this slot. It is emitted here, by the slot
            // that follows the gap, because the ONE thing only this slot knows is which
            // comments its gap actually emits — and the blank-line scan must stop at the
            // first of them, so that a blank line the author left ahead of it is inside the
            // measured range. Everything else about that bound (the owned-comment case, the
            // hole case, the plain element case) is `has_blank_line_after_slot`'s, since
            // those reach printers that emit no leading run at all.
            //
            // ⚠️ A **hole** hands over nothing, whatever this slot's list holds. Its printed
            // content is the comma alone, and the trailing-hole branch below fills
            // `leading_comments` with the ARRAY's trailing run, which sits *past* that
            // comma: bounding the scan there reads the hole's own line break as an author's
            // blank line. That fabricated blank was an F1 break, since the reprint puts the
            // comment on its own line where the next pass measures a real blank
            // (`[x⏎/* c */, , ]` → `[x,⏎,⏎/* c */]` → `[x,⏎⏎,⏎/* c */]`).
            if i > 0 {
                let emitted_leading_start = elem
                    .as_ref()
                    .and_then(|_| leading_comments.first())
                    .map(|c| c.span.start);
                if self.has_blank_line_after_slot(arr, span, i - 1, emitted_leading_start) {
                    parts.push(d.literalline());
                }
                parts.push(d.hardline());
            }

            // The element's leading run and the element form one group — see
            // `build_list_element_group` for why. A hole takes neither. An own-line
            // format-ignore directive in the element's gap freezes it verbatim (Rule A);
            // this is the only element printer a directive reaches, since either spelling
            // of an own-line comment routes the whole array here.
            if let Some(e) = elem {
                let element_doc = match self.element_frozen_span(span.start + 1, arr.elements, i) {
                    // A compound target's default prints bare frozen too: the pair the
                    // argument position gives an assignment is not the target's.
                    Some(frozen)
                        if role == LiteralRole::CompoundTarget && is_compound_target_value(e) =>
                    {
                        self.build_frozen_expression_doc(e, frozen)
                    }
                    Some(frozen) => self.build_frozen_arg_doc(e, frozen),
                    None => self.build_array_element_doc_as(e, role),
                };
                self.push_run_leading_next_item(&mut parts, &pending_leads_next, elem_start);
                pending_leads_next.clear();
                parts.push(self.build_list_element_group_from_comments(
                    leading_comments.iter().copied(),
                    elem_start,
                    element_doc,
                ));
            }

            let is_last = i + 1 == arr.elements.len();

            // Same-line trailing comments (real elements only).
            let trailing_comma_pos = elem
                .is_some()
                .then(|| self.find_comma_in_range(elem_end, next_boundary))
                .flatten();
            let trailing: CommentVec<'_> = if elem.is_some() {
                // The claimed prefix of this gap; the next slot's leading scan resumes at
                // the same split, so every comment lands on exactly one side.
                let split = self.element_gap_split(arr, span, i, elem_end, next_boundary);
                self.comments_to_emit_between(elem_end, split).collect()
            } else {
                smallvec![]
            };
            let interior = self.slot_paren_interior(arr, span, i);
            if self.paren_interior_separates_run(interior) {
                // A spread whose stripped parens held an own-line share or a same-line `//`:
                // the comma moves up against the spread, and the share and the run past the
                // `)` follow it in source order (`Printer::push_element_share_and_run`) — the
                // order the same spread prints in without its parens. The LAST slot has no
                // comma, and its share is emitted here, so the final scan below never reaches
                // into the interior.
                if !is_last {
                    parts.push(d.text(","));
                }
                // A run that leads the next real element (`Printer::run_leads_next_item`) is
                // carried to it instead of following the interior.
                let leads = next_real_element_start(arr.elements, i).is_some_and(|next| {
                    self.run_leads_next_item(&trailing, trailing_comma_pos, next)
                });
                if leads {
                    pending_leads_next = trailing;
                    pending_behind_interior = Some((interior, smallvec![]));
                } else {
                    pending_behind_interior = Some((interior, trailing));
                }
            } else {
                // Which side of the comma each trailing block keeps — the author's side. A
                // block past a non-last element's comma is here only because a line comment
                // follows it (see `block_comment_trails_prev_element`), and it must render in
                // front of that deferred suffix, so the run comes out in source order. On the
                // LAST element the comma below is never emitted, so its after-comma blocks
                // render straight against the element — the only position left once the
                // separator the author wrote them against is gone (prettier agrees).
                let past_comma = |c: &tsv_lang::Comment| {
                    trailing_comma_pos.is_some_and(|pos| c.span.start > pos)
                };
                for comment in trailing.iter().filter(|c| c.is_block && !past_comma(c)) {
                    parts.push(d.text(" "));
                    parts.push(self.build_comment_doc(comment));
                }

                // Separator comma between elements; under `trailingComma: 'none'` the last
                // REAL element gets no trailing comma, but a trailing-elision hole keeps its
                // (syntactically significant) comma.
                if !is_last || elem.is_none() {
                    parts.push(d.text(","));
                }

                for comment in trailing.iter().filter(|c| c.is_block && past_comma(c)) {
                    parts.push(d.text(" "));
                    parts.push(self.build_comment_doc(comment));
                }
                for comment in trailing.iter().filter(|c| !c.is_block) {
                    parts.push(self.build_trailing_line_comment_doc(comment));
                }
            }

            // The array's share of a spread's stripped-paren interior and the run behind it,
            // past the comma — the own-line comments the spread's own doc leaves behind.
            //
            // Carried across any elisions that follow the spread, so it lands past their
            // commas too — the same forward slide every other hole-region comment takes. On
            // the next pass the parens are gone and the carried comments ARE hole-region
            // comments: leading the next real element, or — with only holes after the
            // spread — the trailing-hole region's run, printed by the emitter below, whose
            // separators the carried form matches.
            //
            // The drain slot says whether it was in fact carried: `elem` is the spread
            // itself when the next slot is real, and a HOLE when elisions intervened —
            // which is exactly when the authored blank inside the parens must be dropped,
            // the elision's line break being structure rather than authorship.
            if !matches!(arr.elements.get(i + 1), Some(None))
                && let Some((interior, run)) = pending_behind_interior.take()
            {
                let site = if arr.elements[i + 1..].iter().any(Option::is_some) {
                    ShareSite::NextElement
                } else {
                    ShareSite::Closer
                };
                self.push_element_share_and_run_with_blanks(
                    &mut parts,
                    interior,
                    site,
                    elem.is_some(),
                    run.iter().copied(),
                );
            }

            // Trailing-hole iter: emit collected trailing-on-array comments inline
            // after this hole's comma. First same-line block comment hugs the comma
            // (no separator); a later comment the author glued to the one before it keeps
            // that line behind a space — the separator goes before each comment, never
            // omitted (`/* i */ /* t */`, `/* i */ // h`) — and every other one takes a
            // hardline.
            if is_trailing_hole && !leading_comments.is_empty() {
                // Source position of the LAST comma before `]` (the comma we just
                // emitted for this hole). Used as the same-line anchor for the
                // first comment.
                let last_comma = self.find_last_comma_before(last_real_emit_end, span.end - 1);

                for (ci, comment) in leading_comments.iter().enumerate() {
                    if ci == 0 {
                        if comment.is_block
                            && last_comma.is_some_and(|c| self.is_same_line(c, comment.span.start))
                        {
                            parts.push(self.build_comment_doc(comment));
                            continue;
                        }
                    } else if self.trailing_run_hugs_previous(
                        Some(leading_comments[ci - 1]),
                        comment.span.start,
                    ) {
                        parts.push(d.text(" "));
                        parts.push(self.build_comment_doc(comment));
                        continue;
                    }
                    if ci > 0 {
                        let prev_comment_end = leading_comments[ci - 1].span.end;
                        if self.has_blank_line_between(prev_comment_end, comment.span.start) {
                            parts.push(d.literalline());
                        }
                    }
                    parts.push(d.hardline());
                    parts.push(self.build_comment_doc(comment));
                }
                trailing_hole_comments_end = leading_comments.last().map(|c| c.span.end);
            }

            // No separator is emitted here: the gap after this slot belongs to the slot that
            // follows it, which emits it on the way in. That slot is the only one that knows
            // where its own printed content begins — a fact this end-of-iteration position
            // could only *predict*, by re-deriving the next slot's leading comments before
            // they are collected. Two derivations of one fact drift: the prediction read the
            // **in source** axis while the collection read **to emit**, so a glued (owned)
            // comment bounded one and not the other, and the separator went missing on
            // exactly the arrays that had one.
            if elem.is_some() {
                last_real_emit_end = elem_end;
                last_real_slot = Some(i);
            }
        }

        // Final comments before closing bracket. Skip what trailing-hole emission
        // already handled; past that, only what no one closer prints — see
        // `end_scan_emits_comment` for the two exclusions it composes.
        //
        // The scan never reaches inside a LAST spread's stripped parens: the loop above
        // emitted that share, ordered against the run past the `)` (`end_scan_start`).
        let final_scan_start = trailing_hole_comments_end
            .or_else(|| self.end_scan_start(arr, span, role))
            .unwrap_or(span.start + 1);
        let last_real_split = self.last_element_trailing_split(arr, span, role);
        let mut prev_end = final_scan_start;
        // The comment this scan emitted last — `None` after one it skipped, which another
        // emitter put on a line of its own, so nothing here has it to glue to.
        let mut prev_comment: Option<&ast::Comment> = None;
        for comment in self.comments_to_emit_between(final_scan_start, span.end - 1) {
            if self.end_scan_emits_comment(comment, final_scan_start, last_real_split) {
                // A pair the author GLUED onto one line keeps that line
                // ([`Printer::trailing_run_hugs_previous`], the rule every end-of-container
                // run reads); otherwise the comment takes its own line, preserving an
                // author blank before it.
                self.push_trailing_run_separator(
                    &mut parts,
                    prev_comment,
                    prev_end,
                    comment.span.start,
                );
                parts.push(self.build_comment_doc(comment));
                prev_end = comment.span.end;
                prev_comment = Some(comment);
            } else {
                prev_comment = None;
            }
        }

        let inner = d.concat(&[d.hardline(), d.concat(&parts)]);

        self.build_delimited_doc(
            d.text("["),
            bracket_line_prefix,
            d.indent(inner),
            d.hardline(),
            d.text("]"),
        )
    }

    /// Build a Doc for an array expression with forced expansion (hardlines).
    ///
    /// Used by chain arg formatting when we need the array to expand internally
    /// with hardlines so fits() can correctly measure the first line.
    /// Produces: `[\n  elem,\n]` with actual hardlines.
    pub(in crate::printer) fn build_array_doc_expanded(
        &self,
        arr: &ast::ArrayExpression<'_>,
        span: Span,
    ) -> DocId {
        self.mark_relational_sibling_authored_pairs(arr.elements.iter().copied());
        let d = self.d();
        // A commented array hands off to `build_array_doc` wholesale — the element-doc-only
        // loop below would DROP every structural comment, the empty-`[]` dangling one
        // included. The object twin (`build_object_doc_expanded`) carries the same gate for
        // the same reason; the rationale lives there in full.
        if self.has_comments_on_page_between(span.start, span.end) {
            return self.build_array_doc(arr, span);
        }
        if arr.elements.is_empty() {
            return d.text("[]");
        }

        let mut parts = DocBuf::new();
        for (i, elem) in arr.elements.iter().enumerate() {
            // Elements are Option<Expression> where None = hole/elision
            if let Some(expr) = elem {
                parts.push(self.build_arg_expression_doc(expr));
            }
            // Holes are represented by just a comma (no element content)

            if i < arr.elements.len() - 1 {
                parts.push(d.text(","));
                parts.push(d.hardline());
            } else if elem.is_none() {
                // Trailing-elision hole keeps its (syntactically significant) comma;
                // a real last element gets no trailing comma under `trailingComma: 'none'`.
                parts.push(d.text(","));
            }
        }

        d.concat(&[
            d.text("["),
            d.indent_hardline(d.concat(&parts)),
            d.hardline(),
            d.text("]"),
        ])
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use bumpalo::Bump;

    /// Parse a bare array expression to its internal AST node (spans index into `src`),
    /// allocated in the caller-supplied `arena`.
    fn parse_array<'a>(arena: &'a Bump, src: &str) -> &'a ast::ArrayExpression<'a> {
        match crate::__private::parse_expression_with_comments(
            src,
            0,
            0,
            arena,
            "end of input",
            |_| Ok(()),
        )
        .expect("expression should parse")
        .0
        {
            Expression {
                kind: ExpressionKind::ArrayExpression(arr),
                ..
            } => arr,
            other => panic!("expected an array expression, got: {other:?}"),
        }
    }

    /// The shape half alone — the comment clause it composes with is
    /// `Printer::is_concisely_printed_array`, pinned by the `arrays/fill_signed_operand`
    /// fixture.
    #[test]
    fn concise_array_shape_detection() {
        let arena = Bump::new();
        assert!(array_elements_are_signed_numbers(parse_array(
            &arena,
            "[1, 2, 3]"
        )));
        // A single +/- prefix over a numeric literal is a signed number.
        assert!(array_elements_are_signed_numbers(parse_array(
            &arena, "[-1, +2]"
        )));
        // …but the rule is NOT recursive (prettier's `isSignedNumericLiteral`), so a
        // doubled sign is not one.
        assert!(!array_elements_are_signed_numbers(parse_array(
            &arena,
            "[- -1, -2]"
        )));
        // An empty array is not concisely printed.
        assert!(!array_elements_are_signed_numbers(parse_array(
            &arena, "[]"
        )));
        // A non-numeric element disqualifies it.
        assert!(!array_elements_are_signed_numbers(parse_array(
            &arena, "[1, 'x']"
        )));
        // A hole is not a numeric element (unlike is_simple_call_argument) — the fill
        // printer has nothing to push for one.
        assert!(!array_elements_are_signed_numbers(parse_array(
            &arena, "[1, , 2]"
        )));
    }
}