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
//! Plumbing for the sibling tsv crates — the Svelte embedding (`tsv_svelte`), the
//! bindings' shared format path (`tsv_arena`), and the dev tooling. Not public API:
//! nothing here is covered by semver, and any item may change or go away in any release.

use super::*;
use crate::ast::{
    Expression, ExpressionKind, Statement, TSTypeAnnotation, TSTypeParameterDeclaration,
    VariableDeclaration,
};
use tsv_lang::Span;

pub use crate::parser::TopLevelAs;
pub use crate::strip::strip_in;

/// The per-document environment shared by every formatting entry point: the
/// source the AST's spans index into, the comment buffer, the precomputed
/// line breaks and the three document-level flags computed beside them.
/// Bundling these keeps the printer constructor — and the
/// `tsv_svelte` embedding call sites — from re-threading the same values. The
/// [`EmbedContext`] and the expression/program being printed vary per call, so
/// they stay separate args.
pub struct PrinterInputs<'a> {
    /// Full source the AST's spans index into.
    pub source: &'a str,
    /// Detached comment buffer for the document.
    pub comments: &'a [ast::Comment],
    /// The document's line-break table with its verdict (`tsv_lang::printing::LineBreaks`):
    /// when every line terminator is a `\n` the printer's three line questions
    /// (`is_same_line` and siblings) read the source bytes and the table is never built;
    /// a document holding a bare `\r` or a U+2028 / U+2029 — which the format path's CR
    /// fold leaves only the latter of — builds the table on its first ask and searches it
    /// as before. A document-level fact like the two flags below: the verdict is one pass
    /// taken at construction, never per island.
    pub line_table: LineTable<'a>,
    /// Whether any comment in this document is owned by a node (`owned_by_node`).
    /// A document-level presence flag that short-circuits the owned-leading-comment
    /// path (`prepend_owned_leading_comment` & siblings), which otherwise runs a byte
    /// gate once per expression node — the highest-frequency comment path — to conclude
    /// "no owned comment" for the ~all documents that have none.
    ///
    /// **Compute this once per document, from the parsed comment list** (e.g.
    /// `comments.iter().any(|c| c.owned_by_node)`), and pass it in. It must NOT be
    /// derived inside `Printer::with_context` or `tsv_svelte`'s `ts_inputs()`: the latter
    /// is called per template `{expr}`, so an `any()` scan there is O(islands × comments)
    /// and regresses `.svelte`. `owned_by_node` is set at parse time (including for
    /// Svelte-embedded TS, parsed eagerly), so the flag is stable before any printing.
    pub has_owned_comments: bool,
    /// Whether any comment in this document is a `format-ignore` directive.
    /// A document-level presence flag that short-circuits every entry of the
    /// format-ignore seam (`printer/ignore.rs`), which otherwise runs a range binary
    /// search + directive-string match once per top-level statement / member expression /
    /// object property / list member / type head — concluding "no format-ignore" for the
    /// ~all documents that have none.
    ///
    /// **Compute this once per document, from the parsed comment list** (e.g.
    /// `comments.iter().any(|c| is_format_ignore_directive(c.content(source)))`), and pass
    /// it in — never inside `Printer::with_context` or `tsv_svelte`'s `ts_inputs()` (the
    /// per-`{expr}` O(islands × comments) trap the sibling `has_owned_comments` documents).
    pub has_format_ignore: bool,
    /// The comment-free window of the printer embedding this one, when there is one. An
    /// island printer (a template `{expr}`, a directive, a `<script>`) seeds its own
    /// window (`Printer::comment_free_gap`) from it at construction and hands its own back
    /// when it finishes (`with_doc_printer`), so the islands and the embedding printer
    /// read one window between them. Sound because a window is a fact about the comment
    /// ARRAY (`comments`), which the embedding printer shares with every island it
    /// constructs (`tsv_svelte`'s `ts_inputs()` hands over its own array); a printer over
    /// a different array passes `None`, as [`Self::for_document`] does. On a real Svelte
    /// corpus an island's first search was 45% of every search the islands made, and nine
    /// in ten of those lay inside a window already drawn.
    pub comment_free_window: Option<&'a CommentFreeWindow<'a>>,
}

impl<'a> PrinterInputs<'a> {
    /// Build the per-document environment, deriving the two presence flags from the
    /// comment list. The one place that scan lives — call it **once per document**
    /// (`tsv_svelte` copies the flags it computed at construction into its per-island
    /// `ts_inputs()`; see the field docs for why the scan must not move there).
    pub fn for_document(
        source: &'a str,
        comments: &'a [ast::Comment],
        line_table: LineTable<'a>,
    ) -> Self {
        PrinterInputs {
            source,
            comments,
            line_table,
            has_owned_comments: comments.iter().any(|c| c.owned_by_node),
            has_format_ignore: comments
                .iter()
                .any(|c| is_format_ignore_directive(c.content(source))),
            comment_free_window: None,
        }
    }
}

/// [`parse_with_goal_or_fallback`] over a document the format path folded
/// (`tsv_lang::printing::normalize_carriage_returns`) — the parse every TypeScript format
/// entry point runs (the CLI, the bindings' `parse_format!`, [`format()`]), its error
/// reported in the caller's own coordinates (`FoldedSource::parse_with`). The fold keeps
/// meaning everywhere in TypeScript, whose line rule ends a line at a `<CR>` already, so
/// this is the folded parse alone; `tsv_svelte::__private::parse_folded` is the sibling that is not.
pub fn parse_folded<'arena>(
    folded: &FoldedSource<'_>,
    goal: Option<Goal>,
    arena: &'arena bumpalo::Bump,
) -> Result<Program<'arena>> {
    folded.parse_with(|text| parse_with_goal_or_fallback(text, goal, arena))
}

/// [`format_ast_in`] over a document the caller folded ahead of the parse
/// (`tsv_lang::printing::normalize_carriage_returns`) — the format entry points that fold
/// (the CLI, the bindings, [`format()`]). Identical output; the document's line verdict
/// comes from the fold's own pass instead of a second walk of the source.
pub fn format_folded_in(
    program: &Program<'_>,
    folded: &FoldedSource<'_>,
    arena: &DocArena,
) -> String {
    let line_breaks = LineBreaks::of_folded(folded, arena.take_line_breaks_scratch());
    format_document_in(program, folded.text(), line_breaks, arena)
}

/// Format a program with newline-derived authoring intent **erased** — the
/// intent-erased *canonical* reprint.
///
/// Unlike [`fn@format_ast`], `source` is not required to be CR-free (no debug check): its one
/// consumer, the Svelte compiler's server output, formats a buffer that can carry the
/// component's own line endings. A CR there is NOT folded — a comment or template text
/// holding one reprints it as written.
///
/// [`fn@format_ast`] deliberately preserves authoring intent: an object literal, import
/// list, or call whose source carried a newline after the opening delimiter stays
/// expanded even when it would fit inline, and blank lines between statements are
/// kept. `format_canonical` turns all of that off — constructs collapse when they
/// fit the print width and break only by width, and blank lines are dropped — so
/// two programs that differ only in incidental whitespace reprint to the same
/// string. Comments are preserved (never dropped or merged); only their placement
/// is normalized deterministically (an own-line comment may become a trailing
/// comment of the preceding node). The output is idempotent:
/// `format_canonical(parse(format_canonical(x)))` reproduces its input.
///
/// # Residual: mapped-type multi-line-ness is NOT erased
///
/// One newline-derived read survives canonicalization: a `TSMappedType` whose
/// source breaks right after its `{` still force-breaks, so `Fn<{\n[K in keyof T]: T[K]\n}>`
/// and its inline authoring do **not** reach the same canonical string. It is not yet
/// erased — see `printer::types::composite::build_mapped_type_doc` for what erasing it
/// takes. The output stays idempotent (a force-broken mapped type reprints force-broken);
/// it is only the *authoring-independence* property that has this one hole.
///
/// The residual is unreachable for the current consumer (compiled JS carries no
/// TypeScript types, so a mapped type cannot appear in it). A caller that
/// canonicalizes hand-written TS must account for it.
///
/// This is a cold path used by tooling that needs a canonical form for comparison
/// (e.g. diffing two compilers' output); the [`fn@format_ast`] path is untouched and
/// byte-identical.
pub fn format_canonical(program: &Program<'_>, source: &str) -> String {
    let arena = DocArena::for_source(source);
    format_canonical_in(program, source, &arena)
}

/// [`format_canonical`] into a caller-provided doc arena (see [`format_ast_in`] for
/// the arena-reuse contract).
pub fn format_canonical_in(program: &Program<'_>, source: &str, arena: &DocArena) -> String {
    let line_breaks = LineBreaks::new(source, arena.take_line_breaks_scratch());
    // Deliberately the parser's own list: the nestled-pair merge is prettier's rule, read
    // through prettier's oracle, and this reprint answers to neither — its contract above
    // is that comments are never merged, only relocated.
    format_program_in(program, source, program.comments, line_breaks, arena, true)
}

/// Parse embedded TypeScript at a base offset into the full source
///
/// This is used when parsing embedded TypeScript in Svelte files.
///
/// # Arguments
///
/// * `source` - The TypeScript source code to parse
/// * `base_offset` - Offset in the full source file
/// * `host_nesting` - The nesting the host document has spent around the island, in
///   wire levels (`0` for a top-level island). The parse counts its own on top of it
///   against `tsv_lang::NESTING_LIMIT`, so the document's depth budget is shared with the
///   markup that holds the island. Every embedded entry point here takes it.
///
/// # Returns
///
/// * `Ok(Program)` - The parsed AST
/// * `Err(Error)` - If parsing fails
///
/// # Errors
///
/// An error is positioned in the host document (`base_offset` included) but carries no
/// point — [`Error::wire_point`] is `None` — until the host fills its context over
/// the whole document ([`Error::with_context`], as `tsv_svelte::__private::parse` does): a
/// context taken over `source`, a slice, would read that position against the wrong
/// text. The same holds for every embedded entry point here that takes a `base_offset`.
///
/// A text that gave a type-argument list up to tsc's comparison and then failed is read
/// once more as acorn-typescript reads it (`parser::parse_retrying_acorn_followers`).
/// Where that re-read fails too, its error is the one reported — Svelte reads every
/// embedded text with acorn-typescript, so it lands where Svelte's does — never the first
/// parse's, which stopped where tsc's reading did.
// The `Parser::parse` method-path form clippy suggests for the bare `parser.parse()`
// closure fails the higher-ranked lifetime check on `with_embedding_parser`'s `f`
// bound (the closure lets the compiler infer it), so allow the closure here.
#[expect(clippy::redundant_closure_for_method_calls)]
pub fn parse_embedded<'arena>(
    source: &str,
    base_offset: usize,
    host_nesting: u32,
    arena: &'arena bumpalo::Bump,
) -> Result<Program<'arena>> {
    with_embedding_parser(
        source,
        base_offset,
        host_nesting,
        arena,
        |parser| parser.parse(),
        |_| Ok(()),
    )
}

/// Parse embedded TypeScript with grouping parens preserved.
///
/// Like [`parse_embedded`] but keeps `(expr)` as an internal
/// `ParenthesizedExpression` node instead of discarding it. Used for the
/// `{#snippet}`-parameter sub-parse (`function f(PARAMS) {}`), where Svelte
/// parses with acorn's `preserveParens: true` and — unlike every other template
/// expression — skips `remove_parens`, so its public AST keeps the parens. All
/// other embedded parses ([`parse_embedded`], expression/pattern parses)
/// stay paren-free, matching acorn/Svelte.
///
/// The tree is a **wire** shape only: nothing prints or compiles a
/// `ParenthesizedExpression`. The returned `bool` reports whether any pair was
/// wrapped — when it is `true` the caller parses the same source again with
/// [`parse_embedded`] for every other consumer, and when it is `false` the two
/// parses would build the same tree, so this one serves both. It may over-report (a
/// pair a speculative parse built and then abandoned), which costs that second parse
/// and nothing else.
///
/// # Errors
///
/// As [`parse_embedded`]'s: positioned in the host document, with no point until the
/// host fills its context.
pub fn parse_embedded_preserve_parens<'arena>(
    source: &str,
    base_offset: usize,
    host_nesting: u32,
    arena: &'arena bumpalo::Bump,
) -> Result<(Program<'arena>, bool)> {
    with_embedding_parser(
        source,
        base_offset,
        host_nesting,
        arena,
        |parser| {
            parser.preserve_parens = true;
            let program = parser.parse()?;
            Ok((program, parser.preserved_a_paren))
        },
        |_| Ok(()),
    )
}

/// Parse a single TypeScript expression and return it with any comments.
///
/// This is used when parsing expressions in contexts where comments need to be
/// preserved (e.g., Svelte expression tags `{/* comment */ expr}`).
///
/// `host_check` is the host grammar's refusal of the parsed expression (`{@render}`
/// takes only a call, `{@const}` no bare second declarator), run as part of the parse so
/// a refusal of tsc's reading of a type-argument follower re-reads the text as
/// acorn-typescript does — see `with_embedding_parser`. `|_| Ok(())` refuses nothing.
///
/// The expression must fill `source`. `end` names where `source` ends, for the error a
/// token left after the expression gets (`Expected {end}, found …`): the host's delimiter
/// that bounds the slice — `'}'` for a tag, `')'` for an `{#each}` key — since the document
/// goes on past it, or `end of input` for a source that really ends there.
///
/// # Errors
///
/// As [`parse_embedded`]'s: positioned in the host document, with no point until the
/// host fills its context. A refusal `host_check` returns fails the parse it judged like
/// any other error, so it is reported only where no list was given up to tsc's comparison;
/// otherwise the acorn re-read decides, as at [`parse_embedded`].
pub fn parse_expression_with_comments<'arena>(
    source: &str,
    base_offset: usize,
    host_nesting: u32,
    arena: &'arena bumpalo::Bump,
    end: &str,
    host_check: impl Fn(&'arena Expression<'arena>) -> Result<()>,
) -> Result<(&'arena Expression<'arena>, &'arena [ast::Comment])> {
    with_embedding_parser(
        source,
        base_offset,
        host_nesting,
        arena,
        |parser| parser.parse_expression_with_comments(end),
        |&(expr, _)| host_check(expr),
    )
}

/// Parse a binding pattern — an expression converted to a pattern — and return it with
/// the comments it collected.
///
/// The expression-to-pattern conversion:
/// - ObjectExpression → ObjectPattern
/// - ArrayExpression → ArrayPattern
/// - SpreadElement → RestElement
/// - AssignmentExpression → AssignmentPattern
/// - Identifier → Identifier (unchanged)
///
/// Used for the Svelte block and tag patterns — `{#each xs as {a, b}}`,
/// `{@const {a, b} = expr}`, `{:then [a]}` — whose caller bounds the slice to the bare
/// pattern (a name or a matched bracket, as Svelte's `read_pattern` does) and reads any
/// `: T` itself with [`parse_type_annotation_partial`], attaching it through
/// [`attach_pattern_type_annotation`]. The pattern must fill the whole slice.
///
/// # Arguments
///
/// * `source` - The source code of the pattern
/// * `base_offset` - Offset in the full source file
///
/// # Returns
///
/// * `Ok((pattern, comments))` - The parsed pattern (ObjectPattern, ArrayPattern, …) and
///   the comments this sub-parse consumed — a caller that drops them drops the comment
///   outright (see [`parse_type_annotation_partial`])
/// * `Err(Error)` - If parsing or conversion fails
///
/// # Errors
///
/// As [`parse_embedded`]'s: positioned in the host document, with no point until the
/// host fills its context.
pub fn parse_pattern_with_comments<'arena>(
    source: &str,
    base_offset: usize,
    host_nesting: u32,
    arena: &'arena bumpalo::Bump,
) -> Result<(Expression<'arena>, &'arena [ast::Comment])> {
    with_embedding_parser(
        source,
        base_offset,
        host_nesting,
        arena,
        |parser| {
            let expr = parser.parse_expression_unbounded()?;
            let pattern = parser.expression_to_svelte_pattern(expr)?;
            // The pattern must fill the whole slice the caller bounded. Without this a trailing
            // token is silently dropped, losing content. The slice is the pattern itself (a name
            // or a matched bracket), so its end is the pattern's, not a delimiter's.
            parser.expect_end_of_input("end of pattern")?;
            Ok((pattern, parser.take_comments()))
        },
        |_| Ok(()),
    )
}

/// Attach a `: T` annotation to a **Svelte block-position** binding pattern
/// (`{#each xs as p: T}`, `{:then p: T}`, `{:catch p: T}`, `{@const p: T = v}`).
///
/// **The span stays on the bare pattern** — for every kind — and the wire writer
/// widens the emitted `end` to `type_annotation.end` (a `max`, so a signature
/// parameter, whose span already covers its annotation, is unaffected). That is the
/// canonical parser's byte range: Svelte's `read_pattern` (`1-parse/read/context.js`)
/// parses the bare pattern with acorn, then patches `expression.end =
/// typeAnnotation.end`. The same convention already holds for an `Identifier`, whose
/// span stays on the bare name with the annotation hanging off as a sibling.
///
/// `loc` follows the emitted `start`/`end`, as on every node (one definition — see
/// `docs/architecture.md` §`loc`), so it runs over the annotation too. Svelte's own
/// `loc` does not: its patch never touches `expression.loc`, which keeps the bare
/// pattern's — one of the canonical `loc` quirks tsv does not reproduce.
///
/// A kind with no place to put the annotation must never silently swallow it: a
/// dropped node is an **invisible** node — it vanishes from the wire AST, prints
/// nowhere (real content loss on a format round-trip), and is unreachable to every
/// consumer that reasons about TypeScript by walking the tree. So the fallback
/// **errors** rather than dropping.
pub fn attach_pattern_type_annotation<'arena>(
    pattern: &mut Expression<'arena>,
    mut ta: TSTypeAnnotation<'arena>,
    arena: &'arena bumpalo::Bump,
) -> Result<()> {
    // Re-anchor the annotation at the BINDING's end, which is where Svelte starts it:
    // `read_type_annotation` takes `const start = parser.index` *before* its
    // `allow_whitespace()`, so whatever the author put between the binding and the `:`
    // belongs to the annotation's span.
    //
    // This is a **Svelte-only** convention and it lives here, at the attach point only the
    // two block readers call, precisely because the shared type parser must NOT do it:
    // acorn-typescript anchors its own `TSTypeAnnotation` at the colon
    // (`function f(a : number)` opens the node at the `:`), and `parse_type_annotation`
    // matches it there for every ordinary TS position.
    //
    // The two anchors coincide whenever the colon is glued to the binding — which is every
    // fixture in the tree and essentially all real code — so this line is load-bearing only
    // on the spellings that separate them. `pattern` is still the bare binding here: both
    // callers parse the annotation separately and attach it after.
    ta.span.start = pattern.span().end;
    match &mut pattern.kind {
        ExpressionKind::Identifier(id) => {
            // Re-bind the identifier's binding extra with the parsed type
            // annotation (preserving any decorators already present).
            let decorators = id.decorators();
            id.extra = Some(arena.alloc(ast::IdentifierParamExtra {
                type_annotation: Some(ta),
                decorators,
            }));
        }
        ExpressionKind::ObjectPattern(pattern) => pattern.type_annotation = Some(ta),
        ExpressionKind::ArrayPattern(pattern) => pattern.type_annotation = Some(ta),
        // The Svelte block grammar admits an identifier or a destructuring
        // pattern, so nothing else is reachable — and no other `Expression`
        // variant has an annotation slot to hold it. Reject rather than drop.
        _ => {
            return Err(Error::lex(
                "type annotation is not valid on this binding pattern",
                pattern.span.start as usize,
            ));
        }
    }
    Ok(())
}

/// The read counterpart of [`attach_pattern_type_annotation`] — the annotation a binding
/// pattern carries, or `None`.
///
/// One definition of "where does this binding's annotation live", because the three kinds
/// hold it in three different places (an `Identifier` behind its `extra`, the two
/// destructuring patterns in a field of their own). Callers wanting the binding's **end**
/// take [`pattern_binding_end`] rather than reading `span.end` themselves.
///
/// Its **span** also marks the boundary between the two acorn parses Svelte's
/// `read_pattern` runs — the pattern, then `read_type_annotation` for the `: T` — so the
/// annotation is its own comment island. That is why the wire writer and the Svelte
/// parser both ask *this* function rather than matching the three kinds again; a block
/// pattern's root is always an identifier or a destructure, so no other root can carry one.
pub fn pattern_type_annotation<'a, 'arena>(
    pattern: &'a Expression<'arena>,
) -> Option<&'a TSTypeAnnotation<'arena>> {
    match &pattern.kind {
        ExpressionKind::Identifier(id) => id.type_annotation(),
        ExpressionKind::ObjectPattern(obj) => obj.type_annotation.as_ref(),
        ExpressionKind::ArrayPattern(arr) => arr.type_annotation.as_ref(),
        _ => None,
    }
}

/// Where a binding pattern **ends** — past its `: T` when it has one.
///
/// Not the same as `pattern.span().end`: a block pattern's span stops at the bare pattern
/// and the annotation hangs off as a sibling (see [`attach_pattern_type_annotation`] for
/// why the span cannot encode both), so the bare end is the binding's end only in the
/// un-annotated case. Anything scanning the gap *after* a binding — a printer looking for
/// the init's leading comments, a comment-attach window splitting a declarator — must
/// anchor here, or it starts inside the annotation and claims text some other emitter has
/// already taken. That double claim is a real bug class, not a hypothetical: the printer
/// and the wire attach each read the bare end, and each duplicated an annotation's comment
/// (once onto a `{@const}` init, once inside an `ArrayPattern`'s brackets). One function so
/// the two cannot answer it differently again.
pub fn pattern_binding_end(pattern: &Expression<'_>) -> u32 {
    let bare_end = pattern.span().end;
    pattern_type_annotation(pattern).map_or(bare_end, |t| t.span.end)
}

/// Parse a type annotation (`: Type`) and return it with the comments it collected.
///
/// Used in Svelte block contexts where patterns may have type annotations
/// after simple identifiers (e.g., `{#each items as x: number}`).
/// The source must start with `:`.
///
/// The comments come back for the same reason [`parse_pattern_with_comments`]'s do: this is
/// a **sub-parse of the host document**, so a comment it consumes exists nowhere else. A
/// caller that drops them drops the comment outright — it never reaches the root `comments`
/// array, so the print-once ledger cannot see it either (a comment that was never
/// registered never existed, as far as the ledger knows).
///
/// ⚠️ No "where parsing stopped" position is returned, deliberately: the lexer's one-token
/// lookahead has already skipped the trailing trivia by then, so that position is *past* a
/// trailing comment and using it as a consumed extent silently eats one — which is exactly
/// how `{#each xs as x: T /* c */}` came to be accepted where canonical Svelte rejects it.
/// The annotation's own `span.end` is the consumed extent; take it from the returned node.
///
/// # Errors
///
/// As [`parse_embedded`]'s: positioned in the host document, with no point until the
/// host fills its context.
pub fn parse_type_annotation_partial<'arena>(
    source: &str,
    base_offset: usize,
    host_nesting: u32,
    arena: &'arena bumpalo::Bump,
) -> Result<(TSTypeAnnotation<'arena>, &'arena [ast::Comment])> {
    with_embedding_parser(
        source,
        base_offset,
        host_nesting,
        arena,
        |parser| {
            let ta = parser.parse_type_annotation()?;
            Ok((ta, parser.take_comments()))
        },
        |_| Ok(()),
    )
}

/// Parse a partial expression, stopping at top-level commas, and return it
/// with any collected comments.
///
/// Used when parsing patterns in contexts where commas have other meanings,
/// such as `{#each items as pattern, index}` where the comma separates the
/// pattern from the index variable. Uses assignment-expression parsing which
/// stops at top-level commas (but handles commas inside objects/arrays/calls
/// correctly).
///
/// `top_level_as` says whether a top-level `as` is this parse's or the host grammar's —
/// see [`TopLevelAs`]. It is not a formality: a head that gives the keyword away owes the
/// assertion run its own reader, and one that passes the wrong policy either swallows its
/// binding or rejects every assertion in the head. `satisfies` is never in question.
///
/// `host_check` judges the expression and where the parse stopped — whether the head
/// continues there as its grammar requires (`as`, `then`, `, index`, the tag's end) —
/// as part of the parse, as [`parse_expression_with_comments`]'s does.
///
/// # Errors
///
/// As [`parse_embedded`]'s: positioned in the host document, with no point until the
/// host fills its context. A refusal `host_check` returns fails the parse it judged like
/// any other error, so it is reported only where no list was given up to tsc's comparison;
/// otherwise the acorn re-read decides, as at [`parse_embedded`].
pub fn parse_expression_partial_with_comments<'arena>(
    source: &str,
    base_offset: usize,
    host_nesting: u32,
    arena: &'arena bumpalo::Bump,
    top_level_as: TopLevelAs,
    host_check: impl Fn(&'arena Expression<'arena>, usize) -> Result<()>,
) -> Result<(&'arena Expression<'arena>, usize, &'arena [ast::Comment])> {
    with_embedding_parser(
        source,
        base_offset,
        host_nesting,
        arena,
        |parser| {
            let (expr, end_pos) = parser.parse_assignment_expression_partial(top_level_as)?;
            Ok((expr, end_pos, parser.take_comments()))
        },
        |&(expr, end_pos, _)| host_check(expr, end_pos),
    )
}

/// How far a **bare** type (no leading `:`) reaches from the start of `source`, as an
/// absolute offset — or an error if no type parses there.
///
/// The probe behind Svelte's `{#each}` head split, where `as` is both TypeScript's
/// assertion operator and the block's binding separator: walking the run one type at a
/// time is what tells the two apart, and a failure here is the answer "the `as` before
/// this is Svelte's" (`{#each xs as A[] as { a = 1 }}` — a default value is a pattern,
/// never a type).
///
/// Deliberately hands back a position and nothing else. The node is dropped, and so are
/// the comments the probe collected: the region is re-read by the real iterable and
/// binding parses, so returning either would register every comment in the head twice.
///
/// # Errors
///
/// As [`parse_embedded`]'s: positioned in the host document, with no point until the
/// host fills its context.
// The method-path form clippy suggests fails the higher-ranked lifetime check on
// `with_embedding_parser`'s `f` bound — same reason as [`parse_embedded`]'s closure.
#[expect(clippy::redundant_closure_for_method_calls)]
pub fn parse_type_extent(
    source: &str,
    base_offset: usize,
    host_nesting: u32,
    arena: &bumpalo::Bump,
) -> Result<usize> {
    with_embedding_parser(
        source,
        base_offset,
        host_nesting,
        arena,
        |parser| parser.type_extent(),
        |_| Ok(()),
    )
}

/// Parse a single statement and return it with any collected comments.
///
/// Used by embedders whose host syntax wraps a statement — e.g. Svelte's
/// `{const …}` / `{let …}` tags, which are a `VariableDeclaration` (no trailing
/// `;`). The statement's `span().end` is the byte offset just past it.
///
/// `host_check` judges the statement — the declaration tags' "a declaration, every
/// `const` initialized, nothing after it" — as part of the parse, as
/// [`parse_expression_with_comments`]'s does.
///
/// # Errors
///
/// As [`parse_embedded`]'s: positioned in the host document, with no point until the
/// host fills its context. A refusal `host_check` returns fails the parse it judged like
/// any other error, so it is reported only where no list was given up to tsc's comparison;
/// otherwise the acorn re-read decides, as at [`parse_embedded`].
pub fn parse_statement_with_comments<'arena>(
    source: &str,
    base_offset: usize,
    host_nesting: u32,
    arena: &'arena bumpalo::Bump,
    host_check: impl Fn(&Statement<'arena>) -> Result<()>,
) -> Result<(Statement<'arena>, &'arena [ast::Comment])> {
    with_embedding_parser(
        source,
        base_offset,
        host_nesting,
        arena,
        |parser| {
            let stmt = parser.parse_statement()?;
            Ok((stmt, parser.take_comments()))
        },
        |(stmt, _)| host_check(stmt),
    )
}

/// A variable declaration's **terminator gap**: from the end of its last declarator to the
/// end of the declaration itself — everything the declaration's own doc does not print.
///
/// One definition because two crates claim this range, and which of them does depends on
/// where the declaration sits. In a statement list `tsv_ts` claims it, around the `;` it
/// emits (`Printer::push_statement_semicolon`); in a Svelte `{const …}` / `{let …}` tag the
/// HOST claims it, before a `;` of its own, because [`build_variable_declaration_doc`]
/// emits no terminator there at all. An anchor the two spelled differently would drop a
/// comment or print it twice, so neither spells it itself.
///
/// The `;` the span may reach past is deliberately **included**: no comment can start at a
/// `;`, so the byte costs a reader nothing and the range needs no second look at the source
/// to find the terminator.
///
/// A declaration with no declarators — which the grammar does not admit — yields the empty
/// span at the declaration's end, so a caller emits its bare terminator through the same
/// path rather than a defensive arm of its own.
pub fn declaration_terminator_gap(decl: &VariableDeclaration<'_>) -> Span {
    Span {
        start: decl
            .declarations
            .last()
            .map_or(decl.span.end, |last| last.span.end),
        end: decl.span.end,
    }
}

/// Build a DocId for a variable declaration in the caller's arena.
///
/// The declaration alone — **no terminator and no terminator gap**. The embedder that
/// calls this (Svelte's `{const …}` / `{let …}` tags) closes with `}`, and what may stand
/// between the declaration and that `}` is its own grammar's question, not this crate's:
/// a statement's `;` gap trails a same-line block *after* the `;` and defers a `//` past
/// it, both of which put a comment where the host's closer cannot follow it. So the host
/// owns the gap and the `;` together. Set `inputs.comments` to `&[]` when no comments
/// need to be preserved.
pub fn build_variable_declaration_doc(
    arena: &DocArena,
    decl: &VariableDeclaration<'_>,
    inputs: &PrinterInputs<'_>,
    embed: EmbedContext,
) -> DocId {
    with_doc_printer(arena, inputs, embed, |printer| {
        printer.build_embedded_variable_declaration_doc(decl)
    })
}

/// Build a DocId for a TypeScript expression with comments in the caller's arena.
///
/// Set `inputs.comments` to `&[]` when no comments need to be preserved.
pub fn build_expression_doc(
    arena: &DocArena,
    expression: &Expression<'_>,
    inputs: &PrinterInputs<'_>,
    embed: EmbedContext,
) -> DocId {
    with_doc_printer(arena, inputs, embed, |printer| {
        printer.build_root_expression_doc(expression)
    })
}

/// Build the docs of a **bare** comma list's operands — a Svelte function binding's
/// `bind:value={get, set}`, which the host prints without the sequence's pair and with
/// its own separators — in the caller's arena.
///
/// Every operand is built by ONE printer, after the comma-list region rules have marked
/// the list as a sequence's own printer marks it: between an operand holding a `<` / `<<`
/// and a later one holding a `>` that may close the type-argument region the `<` opens,
/// the pairs the printer would otherwise decide inside an operand print as the author wrote
/// them, and an operand the author wrapped in a pair the rules keep — around a comparison
/// (`(a < b), c > (d, e)`), or around an assignment operand (`(x = a < b), c > (d, e)`),
/// which a sequence prints in its style's pair — is built inside it. An operand built on a
/// printer of its own sees no list, so a pair it adds or strips there (`(v) =>`,
/// `typeof c`, `(a < b)`) turns the two comparisons into one generic call.
///
/// `build(i)` answers whether this call builds operand `i`; where it answers `false` the
/// slot is `None` and the host builds that operand itself (a frozen operand, one printed
/// as its own source bytes). The marks read only the tree and the source, so an operand
/// the host builds is still counted in the list.
pub fn build_bare_sequence_operand_docs(
    arena: &DocArena,
    sequence: &ast::SequenceExpression<'_>,
    inputs: &PrinterInputs<'_>,
    embed: EmbedContext,
    mut build: impl FnMut(usize) -> bool,
) -> Vec<Option<DocId>> {
    with_doc_printer(arena, inputs, embed, |printer| {
        printer
            .mark_relational_sibling_authored_pairs(sequence.expressions.iter().copied().map(Some));
        sequence
            .expressions
            .iter()
            .enumerate()
            .map(|(i, expr)| {
                build(i).then(|| {
                    let doc = printer.build_root_expression_doc(expr);
                    printer.wrap_bare_list_operand(expr, doc)
                })
            })
            .collect()
    })
}

/// The comment-broken layout of a Svelte **block binding pattern** (`{#each … as P}`,
/// `{#await … then P}`, `{:then P}`, `{:catch P}`), in the caller's arena — `Some` exactly when
/// a comment in `pattern` breaks its TypeScript twin (`const P = x`) open: then the twin's
/// expanded object or array pattern, `: T` tail included. `None` for every other pattern, which
/// the host keeps on one line — at any width, and whatever it nests, as prettier-plugin-svelte
/// keeps these heads.
pub fn build_block_pattern_broken_doc(
    arena: &DocArena,
    pattern: &Expression<'_>,
    inputs: &PrinterInputs<'_>,
    embed: EmbedContext,
) -> Option<DocId> {
    // The gate may build a default value's doc to ask whether it breaks, so it runs in a
    // printer of its own: the doc the host keeps is built fresh.
    with_doc_printer(arena, inputs, embed, |printer| {
        printer.block_pattern_comment_breaks(pattern)
    })
    .then(|| {
        with_doc_printer(arena, inputs, embed, |printer| {
            printer.build_block_pattern_broken_doc(pattern)
        })
    })
}

/// Where `expression`'s doc STARTS PRINTING — its span start advanced past every grouping
/// paren on its left spine that this crate's printer strips.
///
/// The start twin of [`Expression::printed_end`], for a host whose own syntax opens a gap
/// **before** the expression and must decide a layout from what that gap holds. A comment the
/// author wrote inside a stripped shell (`{#if ( // c⏎a).b}`) sits *inside* the expression's
/// span, yet the printer hoists it ahead of the node and prints it exactly where a comment in
/// the host's gap would land — so a host that bounds its scan at `span().start` cannot see it
/// and answers one layout question two ways by authoring. Bound it here instead.
///
/// Unlike `printed_end` this cannot be a pure AST query: whether a pair is stripped or
/// RETAINED depends on the parenthesization rules and on what the pair holds, which is why it
/// takes the printer's inputs. A retained pair prints its own parens and keeps its comments
/// inside them, and this returns the span start unchanged there — correctly, since such a run
/// never reaches the host's gap.
///
/// ⚠️ **Layout questions only.** The hoisted run is printed by the expression's own doc, so a
/// host that also widened its *emitter* scan to here would print it twice.
pub fn expression_printed_start(
    arena: &DocArena,
    expression: &Expression<'_>,
    inputs: &PrinterInputs<'_>,
) -> u32 {
    with_doc_printer(arena, inputs, EmbedContext::default(), |printer| {
        printer.left_spine_printed_start(expression)
    })
}

/// Build a DocId for an expression the host is printing as an **assignment value**, in
/// the caller's arena — `build_expression_doc` plus the position mark
/// (`Printer::mark_assignment_value`).
///
/// The one position-keyed layout rule a host would otherwise miss: an *inlining* logical
/// chain (`a ?? b ?? [1]`) whose earlier operators need a home takes prettier's
/// `samePrecedenceSubExpression` arm, `group([head, indent(rest)])`, and reaches it only
/// where the value's parent is one of prettier's `shouldIndentIfInlining` nodes. tsv_svelte's
/// `{@const}` tag is exactly that parent — prettier-plugin-svelte prints the tag's body as a
/// VariableDeclarator — while `{const}`/`{let}` get the mark for free through
/// [`build_variable_declaration_doc`]. Without it the two sibling tags disagree on one shape.
///
/// Three obligations on the caller, none of which the type system states:
///
/// - `expression` must be the value of a real assignment position in the host's syntax, not
///   merely a value the host places after an `=`-like token. The mark is what tells the chain
///   builder the enclosing layout has *withheld* its break-after-operator for this value; a
///   host that breaks after its own operator anyway would stack a second indent.
/// - `embed.mode` should be `LayoutMode::Standalone`. An Embedded root already takes
///   continuation indent from the root question itself, so the mark is inert there — passing
///   Embedded is not wrong, but it is not what this seam is for.
/// - Nothing else is marked: the target is keyed by `expression`'s own span and the printer
///   is fresh per call, so a nested binary (a ternary's test, an operand's own sub-chain)
///   reads `false` exactly as it does inside a `<script>`.
///
/// `value_gap_start` is the host's layout verdict for a **curried arrow chain**, the one
/// value whose shape depends on it. `Some(start)`: the host leaves the break after its
/// operator to the value AND hands it the operator→value gap `[start, value start)` — the
/// host prints no comment in that gap, the value prints them all — so the chain takes the
/// assignment-RHS shape and owns that break itself (`=⏎(a) =>⏎(b) =>`, the heads at one
/// indent), a comment run in the gap leading its first head behind the break. `None`: the
/// host has placed the value itself — it broke after its operator for a comment in the gap,
/// or keeps a commented value on the operator's line — and the chain takes the default
/// shape. A chain whose heads force the break ([`curried_chain_breaks_after_operator`]) owns
/// no break of its own either way: the host breaks after its operator for it, and on `Some`
/// the gap's run leads the first head below that break, as the `<script>` declarator
/// prints it.
pub fn build_assignment_value_expression_doc(
    arena: &DocArena,
    expression: &Expression<'_>,
    inputs: &PrinterInputs<'_>,
    embed: EmbedContext,
    value_gap_start: Option<u32>,
) -> DocId {
    with_doc_printer(arena, inputs, embed, |printer| {
        printer.build_assignment_value_root_doc(expression, value_gap_start)
    })
}

/// The whole `" =" …` right-hand side of a host's assignment seam — operator, the
/// operator→value comment run, and the value — for a **curried arrow chain** whose gap
/// stacks it under the break the comment forces, exactly as a `<script>` declarator does:
/// a `//` on the operator's line (kept there), or an indentable block leading the value.
/// The chain's heads then stand at one indent under the operator, as with no comment,
/// whether the chain breaks for width or because its heads force it. `None` for every other
/// value and gap, and the host keeps its own layout.
///
/// `operator_pos` is the `=`'s byte offset; the gap runs from past it to the value. On
/// `Some` the returned doc prints every comment in that gap, so the host must print none
/// of them. `build_suffix` (the host's trailing run) is handed the value's doc, only on
/// `Some` — the run may have to flush what the value defers to its end — and its doc is
/// placed after the value inside the indent the value stands at; the host's closer follows
/// the returned doc directly, one level out.
pub fn build_stacked_curried_chain_rhs_doc(
    arena: &DocArena,
    expression: &Expression<'_>,
    inputs: &PrinterInputs<'_>,
    embed: EmbedContext,
    operator_pos: u32,
    build_suffix: impl Fn(DocId) -> DocId,
) -> Option<DocId> {
    with_doc_printer(arena, inputs, embed, |printer| {
        printer.build_stacked_curried_chain_rhs_doc(expression, operator_pos, build_suffix)
    })
}

/// The whole `" =" …` right-hand side of a host's assignment seam whose operator→value gap
/// holds a block run the author broke AFTER (`= /* c */⏎value`) — the `<script>`
/// declarator's broke-after arm (`Printer::broke_after_operator_rhs_doc`), so the two hosts
/// cannot answer one comment two ways: a value that fits behind the run collapses onto the
/// operator's line (`= /* c */ value`), one that does not keeps the run on its own line with
/// the value below it. `None` for every other gap (an empty one, a line comment, a run glued
/// through to the value, a run holding an own-line comment before a value with no hard
/// break), and the host keeps its own layout.
///
/// `operator_pos` is the `=`'s byte offset; the gap runs from past it to the value's start.
/// `build_value` builds the value WITHOUT the gap's run — on `Some` the returned doc prints
/// that run, so the host must print none of it — but with any comment the value owns, which
/// the run leaves to it. It is called at most once, and its doc is discarded on `None`.
pub fn build_broke_after_operator_rhs_doc(
    arena: &DocArena,
    expression: &Expression<'_>,
    inputs: &PrinterInputs<'_>,
    embed: EmbedContext,
    operator_pos: u32,
    build_value: impl FnOnce() -> DocId,
) -> Option<DocId> {
    with_doc_printer(arena, inputs, embed, |printer| {
        let rhs = printer.broke_after_operator_rhs_doc(
            operator_pos + 1,
            expression.span().start,
            build_value,
        )?;
        Some(arena.concat(&[arena.text(" ="), rhs]))
    })
}

/// Whether `expression` is a curried arrow chain (an arrow whose body is another arrow) —
/// the value that can take the operator→value gap's comment run in behind the break after
/// its operator ([`build_assignment_value_expression_doc`]'s `value_gap_start`), whether
/// the chain owns that break or its heads force it
/// ([`curried_chain_breaks_after_operator`]).
pub fn is_curried_arrow_chain(expression: &Expression<'_>) -> bool {
    printer::is_curried_arrow_chain(expression)
}

/// Whether `expression` is a curried arrow chain whose heads force the break after the
/// operator of the assignment it is the value of — prettier's `shouldBreakChain` read at
/// its `chooseLayout` site (`chain-tail-arrow-chain`): a head with type parameters, a
/// return type over parameters, or a non-identifier parameter. The host owes such a value a
/// mandatory break after its operator; the heads then stack under it.
pub fn curried_chain_breaks_after_operator(expression: &Expression<'_>) -> bool {
    printer::is_curried_arrow_chain_that_breaks(expression)
}

/// Build a DocId for a single comment (`/* … */` / `// …`) in the caller's arena,
/// through the same rendering the standalone TS printer uses.
///
/// A multi-line block comment reindents to context and propagates its break via a
/// `MultilineText` node, identically to an owned leading comment
/// (`prepend_owned_leading_comment`). `tsv_svelte` uses it for a *non-owned* leading
/// comment in a directive-value gap (`bind:value={/* c⏎*/ (a > b)}`): a discarded
/// grouping `(` leaves the comment positional rather than owned, so the gap emits it —
/// and a multi-line block there must force the same expansion the bare (owned)
/// authoring takes, which the gap's plain source-span emitter cannot. `EmbedContext`
/// is irrelevant to comment rendering, so it is not exposed (a default is used).
pub fn build_comment_doc(
    arena: &DocArena,
    comment: &ast::Comment,
    inputs: &PrinterInputs<'_>,
) -> DocId {
    with_doc_printer(arena, inputs, EmbedContext::default(), |printer| {
        printer.build_comment_doc(comment)
    })
}

/// Build a DocId for a function parameter list (`(…)`) with comments, in the caller's arena.
///
/// Routes each parameter through the same comment-aware, `FunctionParameter`-context
/// printer a real function signature uses, so interior comments (`{ a = /* c */ 1 }`),
/// boundary comments (`a /* c */, b`), the single-pattern hug, and nesting-depth
/// expansion all match a standalone parameter list. `params_start` / `trailing_comments_end`
/// are the source positions of the `(` and `)` (for leading / dangling / trailing comment
/// lookup). Emits no group of its own — the caller's surrounding group controls breaking.
/// Used by `tsv_svelte` for `{#snippet}` parameters.
pub fn build_function_params_doc(
    arena: &DocArena,
    params: &[Expression<'_>],
    params_start: Option<u32>,
    trailing_comments_end: Option<u32>,
    inputs: &PrinterInputs<'_>,
    embed: EmbedContext,
) -> DocId {
    with_doc_printer(arena, inputs, embed, |printer| {
        printer.build_params_doc_with_comments(params, params_start, trailing_comments_end)
    })
}

/// Build a DocId for a type-parameter declaration (`<…>`) with comments, in the caller's arena.
///
/// Routes the generics through the same comment-aware, width-wrapping type-parameter printer a
/// real function/class signature uses, so constraints (`<T extends X>`), defaults (`<T = X>`),
/// modifiers (`<const T>`), interior comments (`<T /* c */>`), and per-param wrapping of a long
/// generic list all match a standalone declaration. The emitted doc includes its own group and
/// the surrounding `<` / `>`, breaking independently of the parameter list. Used by `tsv_svelte`
/// for `{#snippet}` generics.
pub fn build_type_parameters_doc(
    arena: &DocArena,
    type_parameters: &TSTypeParameterDeclaration<'_>,
    inputs: &PrinterInputs<'_>,
    embed: EmbedContext,
) -> DocId {
    with_doc_printer(arena, inputs, embed, |printer| {
        printer.build_type_parameter_declaration_doc_wrapping(type_parameters)
    })
}

/// Build a DocId for a type annotation (`: T`) with comments, in the caller's arena.
///
/// Routes the annotation through the same comment-aware printer a real declaration
/// uses, so a comment between the `:` and the type keeps its place. Used by
/// `tsv_svelte` for a **destructuring** block binding pattern
/// (`{#each xs as { a }: T}`, `{:then { a }: T}`), whose braces the Svelte printer
/// builds itself (its own comment-preserving path) and which therefore has to
/// append the annotation tail explicitly — an identifier binding gets it for free
/// from the TypeScript pattern printer.
pub fn build_type_annotation_doc(
    arena: &DocArena,
    annotation: &TSTypeAnnotation<'_>,
    inputs: &PrinterInputs<'_>,
    embed: EmbedContext,
) -> DocId {
    with_doc_printer(arena, inputs, embed, |printer| {
        printer.build_type_annotation_doc_public(annotation)
    })
}

/// Build a DocId for a TypeScript program in the caller's arena.
///
/// Returns a DocId that can be rendered with the arena.
/// Used when embedding TypeScript in other formats like Svelte's `<script>`.
///
/// `line_table` must be the host document's whole-source line table (spans are absolute,
/// so a table over an island slice is wrong); `comments` stay **island-local** (taken from
/// `program`), which is load-bearing rather than incidental — `push_program_trailing_comments`
/// scans to `source.len()` and the statement walk opens at byte 0, so an array holding the
/// host's template comments would emit every one of them into this `<script>`. That is why
/// this island builds its own environment instead of borrowing the host's `ts_inputs()`
/// like every template island, and therefore why it applies the comment-view merge
/// (`tsv_lang::merge_nestled_block_comments`) a second time. The two agree by construction:
/// the merge is a function of the source bytes, and an island's comments are a contiguous
/// run of the host's — the print-once ledger, which the host registers from ITS merge,
/// grades that agreement on every Svelte fixture.
pub fn build_program_doc(
    arena: &DocArena,
    program: &Program<'_>,
    source: &str,
    line_table: LineTable<'_>,
    embed: EmbedContext,
) -> DocId {
    with_program_printer(arena, program, source, line_table, embed, |printer| {
        printer.build_program_doc(program)
    })
}

/// [`build_program_doc`] without the program's trailing newline, or `None` when the program
/// prints nothing (every statement a dropped `;`, no comment).
///
/// For an embedder that places the body between delimiters of its own, inside its own
/// document's doc rather than rendering it separately: a `<script>` nested in Svelte markup,
/// whose closing tag takes the line break the program would otherwise end with. The body
/// renders at whatever indentation the host's doc gives it, so verbatim text — a template
/// literal's quasis, a non-indentable block comment's interior, a `prettier-ignore` slice —
/// keeps its columns while every break the printer owns is indented. The contract is
/// [`build_program_doc`]'s: `source` is the whole host document, `program` was parsed at
/// host-absolute offsets ([`parse_embedded`]), and `line_table` is the host's.
pub fn build_program_body_doc(
    arena: &DocArena,
    program: &Program<'_>,
    source: &str,
    line_table: LineTable<'_>,
    embed: EmbedContext,
) -> Option<DocId> {
    let (body, has_output) =
        with_program_printer(arena, program, source, line_table, embed, |printer| {
            printer.build_program_body_doc(program)
        });
    has_output.then_some(body)
}

// Assignment-layout predicates for embedders: tsv_svelte's {@const} tag
// mirrors Prettier's assignment layout selection and must apply the same
// break-after-operator rules as our own assignment printer.
pub use crate::printer::{conditional_should_break_after_op, should_inline_logical_expression};

/// Whether `expression`, printed at a position that adds no pair of its own, ENDS on the
/// closing `>` of an instantiation expression's type argument list — `f<T>`, or a node
/// whose rightmost printed child does (`-f<T>`, `a ?? f<T>`, `c ? a : f<T>`,
/// `() => f<T>`, `await f<T>`).
///
/// An embedder asks it where its own syntax puts a WORD right after the expression:
/// acorn-typescript, the parser Svelte hands a block head to, reads a type argument list
/// only where the next token cannot start an expression on the same line, so a bare
/// `{#each f<T> as item}` is the comparison `f < T > as` and does not parse. `tsv_svelte`
/// wraps an `{#each}` head and a shorthand `{#await}` head in a pair when this answers
/// `true`, the block-head twin of the pair the left of `as` / `satisfies` keeps.
#[must_use]
pub fn prints_ending_on_instantiation_close(expression: &Expression<'_>) -> bool {
    printer::ends_with_instantiation_close(expression, false)
}

/// Where the regex literal `expression` PRINTS FIRST begins — when its first printed byte,
/// at a position that adds no pair of its own, is a regex literal's opening `/` (`/a/.test(s)`,
/// `(/a/) ? b : c` once the author's pair is stripped); `None` when a kept pair or any other
/// token comes first.
///
/// An embedder asks it where its own syntax reads a leading `/` another way: Svelte reads
/// `{/` in text position as a block close, so `{/a/.test(s)}` does not parse. `tsv_svelte`
/// wraps such a tag's expression in a pair, the text-position twin of the pair
/// [`prints_ending_on_instantiation_close`] answers for a block head.
#[must_use]
pub fn leading_regex_start(expression: &Expression<'_>) -> Option<u32> {
    printer::leading_regex_start(expression)
}

/// The TypeScript grammar's answers to the operand ends a raw byte scan cannot read
/// ([`closes_type_arguments`], [`closes_statement_header`]).
///
/// The raw scans that find where an expression ENDS before this crate's parser reads it
/// (the arrow-head lookahead and the printer's paren scan) take it, so each reads a `/` the
/// way that parser will. It leaves a `}` to the bytes — after every one a `/` opens a regex
/// — since the reader that tells an object literal's `}` from a block's is acorn's
/// tokenizer, and these scans have no acorn parse behind them
/// ([`ACORN_ISLAND_GRAMMAR`]).
// TODO: after a `}` these scans read a regex where the grammar divides, so an arrow head
// whose default divides a braced operand ahead of a `)` is rejected:
// `f((a = {} / (2 / 1)) => a);`.
pub const OPERAND_GRAMMAR: tsv_lang::source_scan::OperandGrammar =
    tsv_lang::source_scan::OperandGrammar {
        closes_type_arguments,
        closes_statement_header,
        ..tsv_lang::source_scan::OperandGrammar::BYTES_ONLY
    };

/// [`OPERAND_GRAMMAR`] plus acorn's reading of a `}` — the grammar for a scan that finds
/// where a Svelte island ends.
///
/// Svelte hands each island's expression to acorn as a parse of its own, so the island
/// ends where acorn's tokenizer says it does: a `/` after an object literal's `}`, or after
/// a function or class body acorn takes for an expression's, divides, and the scan must not
/// read a regex there and run past the island's own `}`. The `}` question is answered by a
/// walk that keeps acorn's token contexts, which are a tokenizer's heuristic and not the
/// grammar's reading (`parser/expression_braced_operand.rs`) — which is why only a scan
/// whose region acorn will parse takes this grammar. It scans a whole expression
/// (`ScanStart::Expression`); a template literal's interpolations are scanned as their own
/// kind by the scan itself.
pub const ACORN_ISLAND_GRAMMAR: tsv_lang::source_scan::OperandGrammar =
    tsv_lang::source_scan::OperandGrammar {
        closes_braced_operand: parser::closes_braced_operand,
        start: tsv_lang::source_scan::ScanStart::Expression,
        ..OPERAND_GRAMMAR
    };

// The ECMAScript identifier grammar, for embedders that read an identifier out of
// their OWN syntax rather than through this crate's lexer: a `{#snippet}` name and
// an `{#each}` index are JS identifiers that Svelte reads with acorn's
// `isIdentifierStart` / `isIdentifierChar`, so tsv_svelte must ask the same
// question. Exported rather than re-derived — `char::is_alphabetic` /
// `is_alphanumeric` is not this class in either direction (U+2118 is `ID_Start`
// and not alphabetic; U+00B2 is alphanumeric and not `ID_Continue`).
pub use crate::lexer::ident::{is_id_continue, is_id_start};

#[cfg(feature = "json")]
pub use crate::ast::convert::{
    CommentAttach, CommentMode, EmbedWriter, IslandComments, write_expression_embedded,
    write_identifier_expression_with_character, write_pattern_embedded, write_program_embedded,
    write_variable_declaration_embedded,
};

/// Format a TypeScript AST back to source code — the hidden, AST-taking primitive beneath [`format()`], which
/// is the public one-shot (parse and format a `&str`). Unlike [`format()`] it does no fold.
///
/// `source` must hold **no carriage return**: the printers split lines on `\n` alone, so a
/// `\r` would reach the output verbatim and the same document could format two ways on two
/// passes. Parse a CR-folded text (`tsv_lang::printing::normalize_carriage_returns`, via
/// `parse_folded`) and format it with `format_folded_in`, or call [`format()`], which
/// does both. Debug builds assert it.
///
/// # Arguments
///
/// * `program` - The TypeScript AST to format
///
/// # Returns
///
/// The formatted TypeScript source code as a String
///
/// # Example
///
/// ```
/// # fn main() -> tsv_ts::Result<()> {
/// let source = "const x=42;";
/// let arena = bumpalo::Bump::new();
/// let ast = tsv_ts::__private::parse(source, &arena)?;
/// let formatted = tsv_ts::__private::format_ast(&ast, source);
/// assert_eq!(formatted, "const x = 42;\n");
/// # Ok(())
/// # }
/// ```
pub fn format_ast(program: &Program<'_>, source: &str) -> String {
    let arena = DocArena::for_source(source);
    format_ast_in(program, source, &arena)
}

/// Format into a caller-provided doc arena.
///
/// The same precondition on `source` as [`fn@format_ast`]: no carriage return.
///
/// Identical output to [`fn@format_ast`], but the doc IR is built into `arena` instead
/// of a freshly allocated one, so a driver that formats many files can reuse one
/// arena across them (`arena.reset()` between files retains the buffers). Nothing
/// borrowed from `arena` escapes — the result is an owned `String` — so the
/// caller may reset and reuse it the moment this returns.
pub fn format_ast_in(program: &Program<'_>, source: &str, arena: &DocArena) -> String {
    let line_breaks = LineBreaks::new(source, arena.take_line_breaks_scratch());
    format_document_in(program, source, line_breaks, arena)
}

/// Parse TypeScript source code into an internal AST
///
/// # Arguments
///
/// * `source` - The TypeScript source code to parse
///
/// # Returns
///
/// * `Ok(Program)` - The parsed AST
/// * `Err(Error)` - If parsing fails
///
/// # Example
///
/// ```
/// # fn main() -> tsv_ts::Result<()> {
/// let arena = bumpalo::Bump::new();
/// let ast = tsv_ts::__private::parse("const x = 42;", &arena)?;
/// # Ok(())
/// # }
/// ```
pub fn parse<'arena>(source: &str, arena: &'arena bumpalo::Bump) -> Result<Program<'arena>> {
    Error::ensure_source_fits(source)?;
    parser::parse_typescript(source, arena).map_err(|e| e.with_context(source, WIRE_COORDINATES))
}

/// Parse TypeScript source against an explicit [`Goal`] (`Script` vs `Module`).
///
/// [`parse`] is the `Goal::Module` form (the default, correct for Svelte
/// `<script>` and ~all real TS). Pass `Goal::Script` to parse a standalone
/// script, where `await` is an ordinary identifier and top-level `import`/`export`
/// declarations, `import.meta`, and top-level `await` expressions are syntax
/// errors — a TypeScript namespace body holds `import`/`export` at either goal, since
/// tsc decides module-ness from top-level statements alone. The goal carries no
/// strictness of its own past `Module`: a script is
/// sloppy unless its directive prologue holds a `"use strict"`, so the strict-mode
/// rules the parser enforces parse only under a directive-free `Script`. Which
/// constructs those are is enumerated once, on the flag that gates them
/// (`Parser::strict`).
pub fn parse_with_goal<'arena>(
    source: &str,
    goal: Goal,
    arena: &'arena bumpalo::Bump,
) -> Result<Program<'arena>> {
    Error::ensure_source_fits(source)?;
    parser::parse_typescript_with_goal(source, goal, arena)
        .map_err(|e| e.with_context(source, WIRE_COORDINATES))
}

/// Parse TypeScript against `goal`, or — when the goal is unset — at `Module`
/// with a `Script` retry.
///
/// The **format** path's parse, and the type strip's ([`strip_in`]). `Some(goal)` is
/// exact, the same parse [`parse_with_goal`] runs. `None` means the caller never named a
/// source type (`tsv format <path>`, an editor's `format_typescript(source)`): the bare-name
/// tier of the crate docs' [Goals](crate#goals), which also states which error is reported
/// when both attempts fail ([`Error::is_goal_gated`] marks a goal gate). Pinned by
/// `tests/format_fallback_error_attribution.rs`.
///
/// **A module attempt the depth limit refused takes no retry** ([`ErrorKind::TooDeep`]):
/// the limit is no verdict on the grammar, and the script grammar reads every construct the
/// module one accepts as the same tree but `await`, which it reads as a name — so a script
/// attempt could only succeed by reading the source differently
/// (`await <T>a` as `await < T > a`) — a reading chosen by the limit rather than by the
/// source, which the same text one level shallower would never have been given.
///
/// [`parse`] and [`parse_with_goal`] have no fallback: a parse hands back an AST
/// whose `sourceType` is a claim about which grammar produced it, and a retry would
/// make that claim depend on the input. Formatting exposes neither the AST nor the
/// source type, and the printer never reads the goal, so the retry changes no output
/// for anything the module grammar already accepts — the strip's reason too: it hands
/// back JS text, and no verdict of its walk reads the goal. The retry parses into the same
/// arena with no reset between the two attempts, so a script's arena high-water is the
/// failed module attempt plus the script parse — an error-path cost only.
pub fn parse_with_goal_or_fallback<'arena>(
    source: &str,
    goal: Option<Goal>,
    arena: &'arena bumpalo::Bump,
) -> Result<Program<'arena>> {
    if let Some(goal) = goal {
        return parse_with_goal(source, goal, arena);
    }
    parse_with_goal(source, Goal::Module, arena).or_else(|module_error| {
        if module_error.kind() == ErrorKind::TooDeep {
            return Err(module_error);
        }
        parse_with_goal(source, Goal::Script, arena).map_err(|script_error| {
            // A goal gate proves the source a module; past that, the further error is
            // the file's own and a tie keeps the module's. The one positionless error a
            // TypeScript parse raises (`FileTooLarge`) is raised by both attempts alike, so
            // the comparison never sees a `None` against a `Some`.
            if !script_error.is_goal_gated() && script_error.position() > module_error.position() {
                script_error
            } else {
                module_error
            }
        })
    })
}

/// Convert internal AST to compact JSON wire bytes with character-based positions
///
/// The span-only wire every binding ships, and `tsv parse`'s default: each node carries
/// `start` / `end` as UTF-16 offsets and no `loc`. Line/column is a pure function of those
/// offsets plus the source, so a consumer holding the source loses nothing — it derives
/// `loc` on demand (the npm packages' `{locations: true}`), and emission skips the line
/// table entirely, mirroring acorn's own `locations: false`.
///
/// The **sole emission path** for its wire: emits the wire JSON directly during a single
/// walk of the internal AST (the writer in `ast/convert/write/`), never materializing a
/// typed public tree or an intermediate `Value`, and fuses the byte→UTF-16 offset
/// translation into that walk: the writer receives the byte→UTF-16 map via
/// `WirePositions` and emits final char-space positions directly, so no post-conversion
/// translation walk runs. For ASCII sources the map is empty and emission is byte-space
/// passthrough. Returning bytes lets a consumer that never needs `&str` skip the
/// O(output) UTF-8 validation `convert_ast_json_string` pays (the output is several times
/// the source).
#[cfg(feature = "json")]
pub fn convert_ast_json_bytes(program: &Program<'_>, source: &str) -> Vec<u8> {
    ast::convert::write_program_bytes(program, source, tsv_lang::Wire::Span)
}

/// Convert internal AST to compact JSON wire bytes **with** per-node `loc`.
///
/// The `convert_ast_json_bytes` wire plus a `loc` on every node — the line (1-based) and
/// column (0-based, UTF-16 code units) of its own `start` / `end` under ECMAScript's line
/// terminators (CR, LF, CRLF, LS, PS), which is acorn's. `tsv parse --locations` writes it.
#[cfg(feature = "locations")]
pub fn convert_ast_json_bytes_with_locations(program: &Program<'_>, source: &str) -> Vec<u8> {
    ast::convert::write_program_bytes(program, source, tsv_lang::Wire::Loc)
}

/// The `String` form of `convert_ast_json_bytes` for `&str` boundaries (the WASM
/// binding's `JSON.parse`, N-API strings): same wire bytes plus one UTF-8 validation of
/// the output. Byte-oriented consumers should prefer the bytes variant.
///
/// There is no `_with_locations` `String` twin: the bindings this form serves ship the
/// span-only wire alone, and `parse_to_json_with` validates the loc-bearing bytes itself.
#[cfg(feature = "json")]
#[expect(clippy::expect_used)]
pub fn convert_ast_json_string(program: &Program<'_>, source: &str) -> String {
    String::from_utf8(convert_ast_json_bytes(program, source))
        .expect("writer emits valid UTF-8 (source slices + ASCII fragments)")
}

/// Parse standalone TypeScript with grouping parens preserved.
///
/// Like [`parse`] but keeps `(expr)` as a `ParenthesizedExpression` node (acorn's
/// `preserveParens: true`), so the paren structure is
/// present in the AST and its wire JSON. The binding audit uses this to reparse
/// formatted output and see which parenthesized subtree a glued comment binds to
/// — a re-binding is invisible in the paren-free public AST.
///
/// ⚠️ The tree is for the **wire** (`convert_ast_json_*`) and for walks that read it,
/// never for [`crate::format()`] or any other printer entry point: a `ParenthesizedExpression`
/// answers no paren question about its position, so the printer refuses one in a debug
/// build and prints its contents bare in a release one — dropping every pair the
/// position needs (`(x + y) * 2` → `x + y * 2`). Format the [`parse`] tree instead.
pub fn parse_preserve_parens<'arena>(
    source: &str,
    arena: &'arena bumpalo::Bump,
) -> Result<Program<'arena>> {
    Error::ensure_source_fits(source)?;
    parser::parse_typescript_preserve_parens(source, arena)
        .map_err(|e| e.with_context(source, WIRE_COORDINATES))
}

/// Whether a block comment's content (delimiters excluded) is a JSDoc type cast —
/// the `/**`-form comment carrying `@type`/`@satisfies` that binds forward to the
/// `(` it precedes (prettier's `isTypeCastComment`).
///
/// Exposed for the binding audit, which classifies a glued comment's anchor rule
/// by whether it is a cast (its parens are expected — the audit looks *inside*
/// them) or an ordinary glued/annotation comment (its next token is what it binds).
pub fn is_jsdoc_type_cast_comment(content: &str) -> bool {
    parser::is_jsdoc_type_cast_comment(content)
}