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
// New expression printing for TypeScript
//
// Handles: new Foo(), new Foo(arg1, arg2), new Foo<T>()

use super::arg_comments::{
    PartitionedComments, any_arg_empty_line, first_arg_has_any_comments,
    has_inter_argument_comments_slice, has_trailing_comments_slice,
    has_trailing_line_comments_slice,
};
use super::arg_predicates::{
    arrow_body_is_call_through_non_null, is_array_or_object_unwrapped,
    is_function_composition_args, is_ternary_arrow_body,
};
use super::arg_wrapping::{
    ArgOpener, ArgsJoin, append_type_args_with_gap_comments, arrow_hug_refused_by_comments,
    build_args_joined_with_comments, build_arrow_call_body_states,
    build_arrow_gap_break_single_arg_doc, build_arrow_hug_printed_doc, build_arrow_sig_doc,
    build_call_args_expanded, build_call_args_with_blank_lines, build_empty_args_doc,
    build_expand_first_arg_doc, build_printed_argument_doc, build_single_arrow_hug_doc,
    build_single_container_arg_doc, build_ternary_arrow_hug_ladder, could_expand_arrow_chain,
    first_arg_signature_refuses_expand_first, last_arg_arrow_gap_break,
    prebuild_expand_last_break_body, should_expand_first_arg, try_hook_deps_args_doc,
    try_hug_multiline_template_arg, wrap_call_with_hard_breaks_paren_line,
    wrap_call_with_soft_breaks, wrap_call_with_will_break_guard,
};
use super::expand_last::{ArgOwner, try_expand_last_arg};
use crate::ast;
use crate::printer::chain::{child_stops_optional_chain, tag_paren_leading_start};
use crate::printer::expressions::functions::{arrow_signature_refuses_hug, prepend_leading};
use crate::printer::needs_parens::instantiation_operand_is_sealed_chain;
use crate::printer::{
    ParenContext, Printer, SecondTypeArgs, instantiation_keeps_pair_before_type_args,
};
use tsv_lang::Span;
use tsv_lang::doc::DocBuf;
use tsv_lang::doc::arena::DocId;

impl<'a> Printer<'a> {
    /// Build a Doc for a new expression with argument wrapping.
    ///
    /// The `new`→callee gap is one of the two expression-level keyword→**value** gaps
    /// (`await`'s is the other), so it takes the family's shared seam,
    /// [`Printer::append_keyword_value_line_comments`]: a `//` there runs to end-of-line,
    /// so the callee cannot stay on the keyword's line, and the whole tail — callee, type
    /// arguments **and** argument list — drops one level rather than landing flush, where
    /// it would read as a sibling statement. The tail is built keyword-less for that
    /// reason: indenting the callee alone would leave a broken argument list rendering at
    /// the outer column.
    ///
    /// The seam, not [`Printer::build_continuation_indent`], because a comment here
    /// **leads the callee**: own-line-ness is authoring signal for a leading position
    /// (conformance_prettier.md §Comment Position Philosophy), so an own-line comment
    /// keeps its line instead of being pulled up to trail `new`.
    pub(super) fn build_new_doc_with_wrapping(
        &self,
        new_expr: &ast::NewExpression<'_>,
        new_expr_span: Span,
    ) -> DocId {
        let d = self.d();
        let keyword_end = new_expr_span.start + "new".len() as u32;
        let callee_start = new_expr.callee.span().start;
        // The `new`→callee value head ([`Printer::value_head_frozen_span`]): an own-line
        // directive in the gap freezes the CALLEE alone — the type arguments and the argument
        // list follow the callee's span and stay parent-owned, so they keep normalizing.
        let frozen = self.value_head_frozen_span(keyword_end, new_expr.callee.span());
        // A comment in a shell below the pair the callee prints in leads the whole callee
        // from the keyword gap, ahead of that pair, which is where the reparse reads it
        // (`Printer::left_spine_run_claim`): the gap runs to the claimed region's end. So
        // does a run the callee's doc would OPEN with on a line of its own, pair or no pair —
        // the `await`→operand rule (`Printer::build_await_doc`).
        let claim = self.left_spine_run_claim(new_expr_span, keyword_end, new_expr.callee, || {
            let new_list = new_expr
                .type_arguments
                .as_ref()
                .map(|_| SecondTypeArgs::Expression);
            super::CalleeParens::of(self, new_expr.callee, ParenContext::NewCallee, new_list)
                .is_some()
                || self.left_spine_shell_has_own_line_comment(new_expr.callee)
        });
        let gap_end = claim.map_or(callee_start, |claim| claim.region.end.max(callee_start));
        self.with_left_spine_claim(claim, || {
            // The keyword→value gap, shared with `await`→operand: the tail hangs whole below
            // the run ([`Printer::keyword_value_hang_doc`]), and otherwise the run trails `new`
            // inline. A resolved freeze always hangs — an honored directive is a comment that
            // forces its own line — so the inline arm, where the directive would be inert,
            // never meets one.
            if let Some(tail) = self.keyword_value_hang_doc(keyword_end, gap_end, || {
                self.build_new_doc_after_keyword(new_expr, new_expr_span, d.empty(), frozen)
            }) {
                return self.build_keyword_hang_doc("new", keyword_end, gap_end, tail);
            }
            let keyword = match self
                .build_inline_comments_between_doc_trailing_space_opt(keyword_end, gap_end)
            {
                Some(run) => d.concat(&[d.text("new "), run]),
                None => d.text("new "),
            };
            self.build_new_doc_after_keyword(new_expr, new_expr_span, keyword, frozen)
        })
    }

    /// The frozen `new`→callee doc: the position's own pair
    /// ([`Printer::build_frozen_value_doc`]), plus the one pair the SLICE requires that the
    /// position does not.
    ///
    /// A `new` callee written without an argument list (`new (new X)()`) is the case. The
    /// printed form of a `new` expression always carries one — `build_new_doc` emits `()`
    /// even where the author wrote none — so the unfrozen callee is unambiguous and
    /// `needs_parens` leaves it bare (`new new X()()` binds the outer `()` to the outer
    /// `new`). A verbatim slice carries only what the author wrote: bare, `new new X()`
    /// hands the `()` to the INNER `new` instead. The meaning survives (an empty argument
    /// list and none are the same construction), but the next pass reads a callee that now
    /// HAS an argument list and appends a second pair, so the output has no fixed point.
    ///
    /// The pair is a property of the slice, not of the position, which is why it is
    /// answered by [`Printer::frozen_new_slice_absorbs_tail`] rather than in
    /// [`fn@crate::printer::needs_parens`] — the unfrozen callee must keep printing bare.
    /// The left-spine positions ask that same predicate for the suffixes THEY print past a
    /// frozen slice (`.k`, `[0]`, `(1)`, `!`, `` `t` ``).
    fn build_frozen_new_callee_doc(&self, callee: &ast::Expression<'_>, frozen: Span) -> DocId {
        // The position's pair also covers a call the callee holds bare
        // ([`Self::new_callee_holds_bare_call`]) — one pair either way.
        let doc = if self.new_callee_holds_bare_call(callee) {
            self.d()
                .parens(self.build_frozen_expression_doc(callee, frozen))
        } else {
            self.build_frozen_value_doc(callee, frozen, ParenContext::NewCallee)
        };
        if self.frozen_new_slice_absorbs_tail(callee) {
            self.d().parens(doc)
        } else {
            doc
        }
    }

    /// [`Self::build_new_doc_with_wrapping`] past the keyword, which it emits as
    /// `keyword` — `new ` on the ordinary path, and `empty()` where the caller has
    /// already emitted `new` and the gap's continuation supplies the separator.
    fn build_new_doc_after_keyword(
        &self,
        new_expr: &ast::NewExpression<'_>,
        span: Span,
        keyword: DocId,
        frozen: Option<Span>,
    ) -> DocId {
        let d = self.d();
        // Wrap callee in parens if needed (e.g., `new (a || b)()`, `new (a ? b : c)()`,
        // an optional chain `new (a?.b)()` — a chain can't be a `new` callee per spec).
        // A non-null assertion sealing a parenthesized chain (`new (a?.b)!()`) keeps the
        // parens via the sealed-base rendering (checked first; the `!`-outside form is
        // not stripped here even though the standalone path would).
        //
        // A FROZEN callee short-circuits the cascade: the slice is its own bytes and the
        // pair the `NewCallee` position supplies rides outside it
        // ([`Printer::build_frozen_value_doc`]), so the arms below — each of which exists to
        // lay out the callee's *contents* — have nothing left to decide.
        //
        // A member chain in callee position prints its `.prop` lookups with no break
        // point, so the constructor name stays one unit and the width falls to the
        // argument list ([`Printer::mark_new_callee_member_lookups`]). Marked ahead of
        // the whole cascade rather than in one arm: the chain reaches this position
        // through three of them — bare (`new a.b.C()`), parenthesized where the callee
        // holds a call (`new (fn(k).a.B)()`), and frozen, where the mark is simply inert.
        self.mark_new_callee_member_lookups(new_expr.callee);
        let new_list = new_expr
            .type_arguments
            .as_ref()
            .map(|_| SecondTypeArgs::Expression);
        let callee = if let Some(frozen) = frozen {
            self.build_frozen_new_callee_doc(new_expr.callee, frozen)
        } else if let Some(sealed) = self.build_sealed_non_null_paren_doc(new_expr.callee) {
            sealed
        } else if let Some(parens) =
            super::CalleeParens::of(self, new_expr.callee, ParenContext::NewCallee, new_list)
        {
            // The pair's shape and the operand's builder off ONE derivation of the callee's
            // kind — a `new` callee and a call callee are the same position to prettier, so
            // both read it from [`super::CalleeParens`].
            let callee_span = new_expr.callee.span();
            let pair_is_comment_free = self.pair_gaps_are_comment_free(
                (span.start, callee_span.start),
                self.paren_shell_close_after(callee_span.end)
                    .map(|close| (callee_span.end, close)),
            );
            parens.build_doc(self, parens.build_body_doc(self, pair_is_comment_free))
        } else {
            self.build_expression_doc(new_expr.callee)
        };

        // Combine callee with type arguments (`new Foo<K, V>`), preserving comments
        // in the gap, e.g. `new Foo/* c */ <string>()` — comment between callee and `<`
        let callee_with_types_base = append_type_args_with_gap_comments(
            self,
            callee,
            new_expr.callee.span().end,
            new_expr.type_arguments.as_ref(),
        );

        // Position just before `(` (after the callee and any type arguments); in
        // the empty-args case it doubles as the dangling-comment boundary.
        let paren_open = new_expr
            .type_arguments
            .as_ref()
            .map_or_else(|| new_expr.callee.span().end, |ta| ta.span.end);

        // Empty args: just `new Foo()` or `new Foo<K, V>()`, preserving dangling comments
        if new_expr.arguments.is_empty() {
            return build_empty_args_doc(
                self,
                d.concat(&[keyword, callee_with_types_base]),
                paren_open,
                span.end,
                false,
            );
        }

        // Build callee with type args: `new Foo<K, V>`
        let callee_with_types = d.concat(&[keyword, callee_with_types_base]);

        // The callee→`(` gap's pre-paren half ([`super::CalleeGap::paren_split`], which the
        // call family reads off its own derivation): a `//` written there trails the callee
        // and the whole argument list hangs under it. The list must be its own doc for that,
        // which is what the head/args split below is for — and on the unsplit path, every
        // `new` without a comment in that gap, the head is handed straight through.
        let Some((split_start, paren)) =
            super::paren_split_for(self, paren_open, new_expr.arguments)
        else {
            return self.build_new_args_doc(new_expr, span, callee_with_types, paren_open);
        };
        // The argument side opens AT the `(`, so its scans no longer see the half the head
        // has taken (`CalleeGap::paren_open` states the same rule for a call).
        let args = self.build_new_args_doc(new_expr, span, d.empty(), paren);
        super::hang_args_under_split(self, callee_with_types, (split_start, paren), args)
    }

    /// The argument-list half of [`Self::build_new_doc_after_keyword`] — `callee_with_types`
    /// is the head it hangs off, an empty doc when the callee→`(` gap's pre-paren half has
    /// taken it so the caller can hang this whole doc under the run.
    fn build_new_args_doc(
        &self,
        new_expr: &ast::NewExpression<'_>,
        new_expr_span: Span,
        callee_with_types: DocId,
        paren_open: u32,
    ) -> DocId {
        self.mark_relational_sibling_authored_pairs(new_expr.arguments.iter().copied().map(Some));
        let d = self.d();

        // Single template literal argument with embedded newlines on the same line as `(` —
        // hug it. A template on its own line declines and falls through to the layouts
        // below, whose `will_break` guards see the template's own newline.
        //
        // Prettier's own position, shared with the plain call and dynamic `import()`:
        // `isTemplateLiteralSingleArg` is the first thing `printCallExpression` asks, above
        // `printCallArguments` and every layout inside it. `new` would reach the same ANSWER from
        // further down only because the two gates it would sit under — `anyArgEmptyLine`
        // and the composition check — are vacuous for a single argument; stating the position
        // once is what keeps the four spellings one rule rather than four that happen to
        // agree.
        if let Some(doc) = try_hug_multiline_template_arg(
            self,
            callee_with_types,
            new_expr.arguments,
            paren_open,
            new_expr_span.end,
        ) {
            return doc;
        }

        // Zero-comment fast gate: one binary search over the whole argument window
        // (`(` … `)`) short-circuits every per-branch argument-comment predicate
        // below (trailing-line / trailing-block / inter-argument / leading), each of
        // which scans a sub-range within it. The callee and type-argument gap
        // comments live before `paren_open` and are handled unconditionally above.
        // Canonical reference: build_params_doc_with_comments.
        //
        // **on page**: this is the master gate for the whole `new` cascade — including
        // the layout predicates (`first_arg_has_any_comments`, the huggable-argument
        // refusal). Skipping an owned annotation here would short-circuit them all and
        // silently hug an argument prettier expands. Its analogs (`call_has_comments`)
        // in `calls/mod.rs`, `call_formatting.rs` and `chain_args.rs` count too.
        let new_has_comments = self.has_comments_on_page_between(paren_open, new_expr_span.end);

        // Prettier's React-hook deps-array layout — the FIRST thing `printCallArguments`
        // asks, and `new` shares that printer, so a `new` written in the shape takes it too.
        // Above `anyArgEmptyLine`, so an author blank between the callback and the deps
        // array collapses here rather than forcing the arguments out.
        if let Some(doc) = try_hook_deps_args_doc(
            self,
            new_expr.arguments,
            paren_open,
            new_expr_span.end,
            new_has_comments,
            d.concat(&[callee_with_types, d.text("(")]),
        ) {
            return doc;
        }

        // Prettier's `anyArgEmptyLine` — `new` shares `printCallArguments` with a plain call,
        // so the rule and its POSITION are the same: an author blank in any inter-argument gap
        // forces `allArgsBrokenOut()`, ABOVE every specialized layout. See the twin in
        // `call_formatting.rs` for the full argument; the single-argument hug arms between here
        // and the first use are vacuously safe (a blank needs two arguments to sit between).
        let any_arg_empty_line = any_arg_empty_line(new_expr.arguments, self);

        // Single huggable argument: object literal or function
        // These stay on the same line as the opening paren: `new Cls({...})` not `new Cls(\n{...})`
        // Skip hugging if there are trailing comments (line OR block) - let the comment handling below handle it
        let single_arg_has_trailing_comment = new_expr.arguments.len() == 1
            && new_has_comments
            && has_trailing_comments_slice(new_expr.arguments, new_expr_span.end, self);

        // A comment in the `(`→argument gap that this expression would have to EMIT
        // disqualifies the whole single-argument block below: none of its arms has a
        // gap emitter, so the comment would be DROPPED (the hazard-4 shape in
        // docs/comments.md). Such a call falls through to the comment-aware paths
        // and expands, matching prettier.
        //
        // The three plain hug arms (object / array / function expression) decline on
        // the wider ON-PAGE question inside the match, like the arrow arm: an OWNED
        // glued comment rides inside the argument's doc — the hug does print it —
        // but it still defeats the hug, exactly as prettier's `couldExpandArg`
        // refuses to hug an argument whose leading comment sits before it
        // (prettier shares one `printCallArguments` for Call and New, and the plain
        // call already expands `fn(/* c */ { breaking })`). A to-emit gate went
        // blind to it and kept the hug, which collapsed prettier's expanded form
        // back to `new A(/* c */ {` on every pass. The declined argument falls to
        // the default soft-wrapped join at the bottom: flat when it fits — the
        // same bytes the hug rendered — expanded when the argument breaks.
        let single_arg_leading_emit_comment = new_expr.arguments.len() == 1
            && new_has_comments
            && self.has_comments_to_emit_between(paren_open, new_expr.arguments[0].span().start);
        let single_arg_leading_on_page_comment = new_expr.arguments.len() == 1
            && new_has_comments
            && self.has_comments_on_page_between(paren_open, new_expr.arguments[0].span().start);

        if new_expr.arguments.len() == 1
            && !single_arg_has_trailing_comment
            && !single_arg_leading_emit_comment
        {
            // A broken `=>`→body gap keeps the arrow start on the `new A(` line and breaks the
            // closing paren onto its own — **above the match**, the plain call's order
            // (`try_single_arg_hug` in `call_formatting.rs` gives the full argument): the rule
            // is the gap's and not the arms' body-kind split, and the one body question it does
            // owe, prettier's `couldExpandArg`, the helper applies itself
            // ([`last_arg_arrow_gap_break`]). Asked inside the block-arrow arm alone, it left a
            // ternary body to the reassembling ladder, whose broken-out state printed the
            // signature flat past the print width (`calls/new_arrow_body_own_line_comment_long`).
            // It asks the leading gap **on page**, as the block-arrow arm's refusal below does:
            // an OWNED comment before the arrow (glued `/* l */`, a JSDoc) is not to emit, so
            // the guard above passes it, yet it still breaks the argument out — the plain
            // call's leading-comment branch returns before its hoist for the same reason.
            let arg0 = new_expr.arguments[0];
            if new_has_comments
                && !single_arg_leading_on_page_comment
                && let ast::ExpressionKind::ArrowFunctionExpression(_) = &arg0.kind
                && let Some(gap_break) = last_arg_arrow_gap_break(self, arg0)
            {
                return build_arrow_gap_break_single_arg_doc(
                    self,
                    ArgOpener::callee(d, callee_with_types),
                    arg0,
                    &gap_break,
                    None,
                    || self.build_expression_doc(arg0),
                );
            }

            match &new_expr.arguments[0] {
                // Object / array container argument, cast or not: the shared hug, which also
                // owns the truly-empty softline case ([`build_single_container_arg_doc`]). An
                // on-page leading comment declines it (see the guard's comment above);
                // prettier expands the same way.
                arg if is_array_or_object_unwrapped(arg, self)
                    && !single_arg_leading_on_page_comment =>
                {
                    return build_single_container_arg_doc(self, callee_with_types, arg);
                }
                // Function-expression argument: the shared lone-argument ladder
                // ([`ArgOpener::lone_hug_ladder`]), always with its middle state — prettier's
                // flat-parameter rule gates on `isCallExpression(parent)`, and a `new` is not
                // one, so a `new`'s callback keeps a breakable parameter list at every width.
                ast::Expression {
                    kind: ast::ExpressionKind::FunctionExpression(_),
                    ..
                } if !single_arg_leading_on_page_comment => {
                    let arg_doc = self.build_expression_doc(new_expr.arguments[0]);
                    return ArgOpener::callee(d, callee_with_types)
                        .lone_hug_ladder(d, arg_doc, false);
                }
                // Block arrow (or expandable arrow chain): use conditional_group to let Doc decide hug vs wrap
                ast::Expression {
                    kind: ast::ExpressionKind::ArrowFunctionExpression(arrow),
                    ..
                } if !arrow.body.is_expression() || could_expand_arrow_chain(arrow) => {
                    let arg0 = &new_expr.arguments[0];
                    let build = || self.build_expression_doc(arg0);

                    // Prepend leading comments (e.g., /** @param {any} x */ before arrow)
                    // and force wrapped state when present (prettier expands args with leading comments)
                    let arg_start = new_expr.arguments[0].span().start;
                    // Glued like the regular-call leading-arg paths (prettier shares
                    // one `printCallArguments` for Call and New): a single-line block
                    // hugged to `(` stays with the argument across a source newline.
                    let glued = if new_has_comments {
                        self.build_rhs_comments_glued_opt(paren_open, arg_start)
                    } else {
                        None
                    };
                    // **on page**: a leading comment forces the wrapped (expanded) state,
                    // owned or not — an owned comment rides inside the argument's doc (so
                    // it's not in `glued`) but still defeats the hug, exactly as prettier
                    // expands a block-arrow arg whose leading comment precedes it. A
                    // to-emit gate here would go blind to it and wrongly hug.
                    let has_leading_comment = new_has_comments
                        && self.has_comments_on_page_between(paren_open, arg_start);

                    // A signature that will break, or a leading comment, forces the wrapped
                    // state.
                    let signature_refuses =
                        arrow_signature_refuses_hug(self, arrow, new_expr.arguments[0].span);

                    // ⚠️ Both refusals are asked BEFORE the printing pair below and read only
                    // the `printedArguments` one — the pair is a second build of the argument
                    // ([`build_arrow_hug_printed_doc`]).
                    //
                    // The refused form is prettier's `allArgsBrokenOut` fallback,
                    // `group(contents, {shouldBreak: printedArguments.some(willBreak)})`: the
                    // argument breaks out only when its doc will break, and otherwise stays
                    // on the `new` line while it fits. A bare concat has no group of its own,
                    // so its softlines would take whatever mode the enclosing group renders in —
                    // broken at a statement root, whatever the argument's width.
                    if signature_refuses || has_leading_comment {
                        let printed = prepend_leading(
                            d,
                            glued,
                            build_arrow_hug_printed_doc(self, arg0, arrow, build),
                        );
                        return wrap_call_with_will_break_guard(d, callee_with_types, printed);
                    }

                    // The same two printings and state ladder the plain call's and the member
                    // chain's single-argument hugs take ([`build_single_arrow_hug_doc`]) — an
                    // object/array terminal takes the middle state, which expands the body
                    // internally while the arrow stays hugged to `new A(`. The hand-rolled
                    // two-state copy here had no such state, so a FLAT-written body broke the
                    // argument out where prettier hugs.
                    return build_single_arrow_hug_doc(
                        self,
                        ArgOpener::callee(d, callee_with_types),
                        arg0,
                        arrow,
                        glued,
                        build,
                    );
                }
                // Expression-body arrow: break at => not at (
                // Mirrors call_formatting.rs expression arrow handling
                ast::Expression {
                    kind: ast::ExpressionKind::ArrowFunctionExpression(arrow),
                    ..
                } if arrow.body.is_expression() => {
                    if let ast::ArrowFunctionBody::Expression(body_expr) = &arrow.body {
                        // Expandable body (ternary): conditional parens
                        // Flat: `new Xy((x) => (x ? y : z))`
                        // Break: `new Xy((x) =>\n  x ? y : z,\n)`
                        // couldExpandArg keys on the body type, looking through the
                        // return-type annotation, so typed-return arrows are eligible.
                        // The reassembling arm's refusal pair — a break forced inside the
                        // signature, or a comment on the body's own tail, either of which
                        // these states cannot honor (`arrow_hug_refused_by_comments`) — and a
                        // signature that will break, which every state prints flattened.
                        if is_ternary_arrow_body(body_expr)
                            && !(new_has_comments
                                && arrow_hug_refused_by_comments(
                                    self,
                                    arrow,
                                    new_expr.arguments[0].span,
                                    body_expr,
                                ))
                            && !arrow_signature_refuses_hug(self, arrow, new_expr.arguments[0].span)
                        {
                            let sig_doc =
                                build_arrow_sig_doc(self, arrow, new_expr.arguments[0].span);
                            let body_doc = self.build_arrow_arg_body_doc(body_expr);

                            // The ladder, shared with the plain call's
                            // `build_ternary_arrow_hug_states` and the member chain's arm.
                            return build_ternary_arrow_hug_ladder(
                                self,
                                ArgOpener::callee(d, callee_with_types),
                                arrow,
                                body_expr,
                                sig_doc,
                                body_doc,
                                || self.build_expression_doc(new_expr.arguments[0]),
                            );
                        }

                        // Call body: the expand-last ladder (hug, break at `=>`, break out)
                        // couldExpandArg keys on the body type, looking through the
                        // return-type annotation and a trailing non-null `!`.
                        if arrow_body_is_call_through_non_null(body_expr)
                            // Same refusal pair as the ternary arm above.
                            && !(new_has_comments
                                && arrow_hug_refused_by_comments(self, arrow, new_expr.arguments[0].span, body_expr))
                            // The body is built once and shared by every state (see the
                            // ladder's doc) — last, because it builds.
                            && let Some(body_reuse) =
                                prebuild_expand_last_break_body(self, Some(new_expr.arguments[0]))
                        {
                            let arg0 = new_expr.arguments[0];
                            return build_arrow_call_body_states(
                                self,
                                ArgOpener::callee(d, callee_with_types),
                                arg0,
                                arrow,
                                body_expr,
                                body_reuse,
                                || self.build_expression_doc(arg0),
                            );
                        }
                    }
                    // Non-call/non-expandable expression body or typed arrows: fall through
                }
                _ => {}
            }
        }

        // Prettier's POSITION for `anyArgEmptyLine` — above every specialized layout, the
        // plain call's twin (see `call_formatting.rs` for the full argument). ONE site rather
        // than a conjunct per arm; the arms above are all inside the single-argument block, so
        // the question is vacuous there and lifting the gate over them changes nothing.
        //
        // Unlike the plain call, no comment path preempts this builder, so both edge gaps are
        // live here: it puts a `(`-line run on the `(` line and the last argument's trailing
        // comments after that argument.
        if any_arg_empty_line {
            return build_call_args_with_blank_lines(
                self,
                callee_with_types,
                new_expr.arguments,
                paren_open,
                new_expr_span.end,
            );
        }

        // Function composition pattern: when any argument is a call containing a callback
        // OR when there are multiple function arguments
        // e.g., new Cls(arr.map((x) => x), b) → new Cls(\n\t...,\n)
        // e.g., new Cls(() => a, () => b) → new Cls(\n\t...,\n)
        // Skip this path if there are trailing comments - let the comment handling paths handle it
        if is_function_composition_args(new_expr.arguments)
            && !(new_has_comments
                && has_trailing_comments_slice(new_expr.arguments, new_expr_span.end, self))
        {
            return build_call_args_expanded(
                self,
                ArgOpener::callee(d, callee_with_types),
                new_expr.arguments,
                paren_open,
                new_expr_span.end,
            );
        }

        // No multiline-content arm here, for the reason the plain-call twin states: an
        // argument's own newline breaks the layouts below through their `will_break`
        // guards, which is prettier's only mechanism too.

        // "Expand first arg" pattern: callback first, short/empty container last
        // e.g., new Proxy((x) => { ... }, {}) - callback hugs, empty obj stays inline.
        // Block for comments the inline tail can't carry (matching the plain-call path):
        // a line comment anywhere in the args, or any comment on the first arg — those
        // break all args instead (a before-comma trailing block, a leading first-arg
        // comment). An after-comma inline block leading the second arg is carried below.
        // Named once, like the plain call's twin — and factored on `new_has_comments`, the
        // cascade's zero-comment fast gate, so a comment-free `new` asks neither predicate.
        // The first argument's own signature refusing the hug is asked last, behind the
        // shape test: it builds that signature.
        let expand_first_blocked = new_has_comments
            && (has_trailing_line_comments_slice(new_expr.arguments, new_expr_span.end, self)
                || first_arg_has_any_comments(new_expr.arguments, self, paren_open));
        // One `printCallArguments` prints both spellings, so the layout is the plain call's
        // (`build_expand_first_arg_doc`), not a copy of it.
        if should_expand_first_arg(self, new_expr.arguments)
            && !expand_first_blocked
            && !first_arg_signature_refuses_expand_first(self, new_expr.arguments)
        {
            return build_expand_first_arg_doc(
                self,
                ArgOpener::callee(d, callee_with_types),
                new_expr.arguments,
                paren_open,
                new_expr_span.end,
            );
        }

        // Check for trailing LINE comments on arguments (forces hardline expansion)
        // Must check this BEFORE the "last arg is array/object" pattern below,
        // otherwise trailing comments on the last arg cause it to be hugged incorrectly.
        // e.g., new Class(arg1, // comment\n  arg2)
        if new_has_comments
            && has_trailing_line_comments_slice(new_expr.arguments, new_expr_span.end, self)
        {
            // The shared joined-argument builder owns every gap in the list — the
            // `(`→first-argument run (into `paren_line`), each inter-argument gap,
            // and the last argument's trailing region. Hardline-joined throughout,
            // so each gap's `forces_expansion` obligation is already met. The
            // hand-rolled loop this replaced re-spelled the builder's Hardline join
            // exactly (its unguarded `open_inter_arg_gap` on a comment-free gap
            // emits just the comma the builder's guarded arm bakes into
            // `comma_hardline`), and a copied loop is where the last-arg and
            // inter-arg emitters have drifted before.
            let mut paren_line = DocBuf::new();
            let arg_doc = build_args_joined_with_comments(
                self,
                new_expr.arguments,
                paren_open,
                new_expr_span.end,
                ArgsJoin::Hardline,
                &mut paren_line,
            );
            return wrap_call_with_hard_breaks_paren_line(
                d,
                callee_with_types,
                &paren_line,
                arg_doc,
            );
        }

        // The last argument trails comments, and no LINE comment sits in the gap after it
        // (one there ends the `)` line, which the expand-last / hug paths below cannot
        // express). A block stays inline for simple args (`new A(a, b /* comment */)`);
        // function composition expands (`new A(() => {}, () => {} /* comment */,)`).
        //
        // An argument whose stripped grouping parens hid a comment
        // (`new A(a, ...(b⏎/* c */))`) routes here too: it lies *before* the argument's
        // own end, so the `[arg_end, )` scan cannot see it and every collapsing path
        // below would drop it. That interior may hold a `//` — hence "no line comment in
        // the GAP" rather than "block-only": the spread's own doc defers its line
        // comments through `line_suffix`, and `hard` below forces the break they need.
        // `new_has_comments` (on page over `[paren_open, span.end)`, a superset of every
        // interior) gates the per-argument scan off the comment-free path, like the
        // call/chain entry gates.
        let spread_paren_comments_expand =
            new_has_comments && self.any_paren_interior_forces_expansion(new_expr.arguments);
        let has_trailing_comments_no_gap_line = new_has_comments
            && new_expr.arguments.last().is_some_and(|last_arg| {
                let arg_end = last_arg.span().end;
                let paren_close = new_expr_span.end;
                (spread_paren_comments_expand
                    || self.has_comments_to_emit_between(arg_end, paren_close))
                    && !self.has_line_comments_between(arg_end, paren_close)
            });

        if has_trailing_comments_no_gap_line {
            // The shared joined-argument builder owns every gap in the list — the
            // `(`→first-argument run (into `paren_line`), each inter-argument gap, and
            // the last argument's trailing region (its spread interior included). The
            // hand-rolled loop this replaced built the argument docs and joined them with
            // commas, so it had no inter-argument emitter at all: a comment between two
            // arguments was DROPPED whenever the argument itself didn't own it (the
            // hazard-4 shape in docs/comments.md).
            let last_arg = &new_expr.arguments[new_expr.arguments.len() - 1];
            let arg_end = last_arg.span().end;
            let paren_close = new_expr_span.end;

            // An own-line comment — from the spread interior or from the `[arg_end, )`
            // gap — is a sibling of the last argument rather than a trailer on its line,
            // so the argument list must hard-break around it. So must a `(`-line run,
            // which ends in a `//` nothing may share a line with; asked here rather than
            // read off `paren_line` afterwards, since the join is chosen before the build.
            let paren_line_run =
                PartitionedComments::new(self, paren_open, new_expr.arguments[0].span().start)
                    .has_trailing_line();
            // The last-argument arm reads own-line-ness from the SOURCE
            // ([`Printer::has_own_line_block_comment_before_closer`]), the same predicate
            // the call and parameter lists ask of this position: the gap holds the list's
            // own comma, which `trailingComma: 'none'` deletes, so an `is_same_line(arg_end,
            // …)` reading called a comment glued to it own-line and hard-broke a `new` that
            // fits (`docs/comments.md` §Own-line-ness is a SOURCE question).
            let hard = paren_line_run
                || spread_paren_comments_expand
                || is_function_composition_args(new_expr.arguments)
                || self.has_own_line_block_comment_before_closer(arg_end, paren_close);

            let mut paren_line = DocBuf::new();
            let arg_parts = build_args_joined_with_comments(
                self,
                new_expr.arguments,
                paren_open,
                paren_close,
                if hard {
                    ArgsJoin::Hardline
                } else {
                    ArgsJoin::SoftLine
                },
                &mut paren_line,
            );
            if hard {
                return wrap_call_with_hard_breaks_paren_line(
                    d,
                    callee_with_types,
                    &paren_line,
                    arg_parts,
                );
            }
            return wrap_call_with_soft_breaks(d, callee_with_types, arg_parts);
        }

        // Prettier's `shouldExpandLastArg`, shared with the plain call — one
        // `printCallArguments` prints both spellings, so the layout is not a copy of the
        // call's, it IS the call's (`expand_last.rs`). Must come AFTER the trailing-comment
        // paths above, which own the gaps these states cannot express.
        if let Some(doc) = try_expand_last_arg(
            self,
            new_expr.arguments,
            callee_with_types,
            paren_open,
            new_expr_span.end,
            new_has_comments,
            ArgOwner::New,
        ) {
            return doc;
        }

        // Check for leading comments or inter-argument block comments
        // These need explicit handling that the simple join_doc path doesn't provide
        let has_leading_comments = new_has_comments
            && !new_expr.arguments.is_empty()
            && self.has_comments_to_emit_between(paren_open, new_expr.arguments[0].span().start);
        let has_inter_arg_comments =
            new_has_comments && has_inter_argument_comments_slice(new_expr.arguments, self);

        // Comments trailing the `(` on the same line stay on the `(` line, with
        // own-line comments on their own lines before the first arg — preserving
        // the author's placement and source order (divergence from prettier,
        // which floats a line comment past the statement and relocates a block
        // before `(`). Also content-preserving: a line comment trailing `(` is
        // kept. See conformance_prettier_ts_comments.md §Comment relocation.
        if has_leading_comments {
            let first_arg_start = new_expr.arguments[0].span().start;
            let gap_pc = PartitionedComments::new(self, paren_open, first_arg_start);
            if gap_pc.pulls_to_delimiter_line(self) {
                let mut paren_line_prefix = DocBuf::new();
                let mut inner = DocBuf::new();
                gap_pc.emit_pulled_gap(&mut paren_line_prefix, &mut inner, self);
                // The `(`→first-argument gap is emitted above (the paren-line prefix and
                // the leading run), so the builder must not print it a second time — hence
                // its `paren_line` out-param stays unused here and must come back empty.
                let mut unused_paren_line = DocBuf::new();
                inner.push(build_args_joined_with_comments(
                    self,
                    new_expr.arguments,
                    paren_open,
                    new_expr_span.end,
                    ArgsJoin::HardlineLeadingGapEmitted,
                    &mut unused_paren_line,
                ));
                debug_assert!(unused_paren_line.is_empty());

                return d.concat(&[
                    callee_with_types,
                    d.text("("),
                    d.concat(&paren_line_prefix),
                    d.indent_hardline(d.concat(&inner)),
                    d.hardline(),
                    d.text(")"),
                ]);
            }
        }

        if has_leading_comments || has_inter_arg_comments {
            let mut paren_line = DocBuf::new();
            let arg_parts = build_args_joined_with_comments(
                self,
                new_expr.arguments,
                paren_open,
                new_expr_span.end,
                ArgsJoin::SoftLine,
                &mut paren_line,
            );
            // Both trailing-comment arms above return before this one, so the last
            // argument's gap is empty here and the soft wrap can still collapse. A
            // `(`-line run can't: its `//` ends the line.
            if !paren_line.is_empty() {
                return wrap_call_with_hard_breaks_paren_line(
                    d,
                    callee_with_types,
                    &paren_line,
                    arg_parts,
                );
            }
            return wrap_call_with_will_break_guard(d, callee_with_types, arg_parts);
        }

        // Build args with line separators (one per line when broken). Prettier shares one
        // `printCallArguments` for Call and New, so this is its `printedArguments` — printed
        // with no `expandLastArg` (`build_printed_argument_doc`), exactly as the plain call's
        // twin does.
        let arg_parts = d.join_doc(
            new_expr.arguments.iter().map(|arg| {
                build_printed_argument_doc(self, arg, || self.build_arg_expression_doc(arg))
            }),
            d.comma_line(),
        );

        // Wrap in group with parens, forcing break when args contain hardlines
        wrap_call_with_will_break_guard(d, callee_with_types, arg_parts)
    }

    /// Build a Doc for a new expression (for nested contexts)
    pub(crate) fn build_new_doc(&self, new_expr: &ast::NewExpression<'_>, span: Span) -> DocId {
        self.build_new_doc_with_wrapping(new_expr, span)
    }

    /// Whether a `new` callee holds a call on its left spine that no pair the printer keeps
    /// encloses. Such a callee takes a pair so the `new`'s arguments bind to the `new` rather
    /// than to the inner call: `new (f())()`, `new (a.b())()`, `new (f().C)()`,
    /// `new (a?.b())()` — bare, `new f()()` is `(new f())()`. The spine is prettier's
    /// `NewExpression` callee rule (`parentheses/needs-parentheses.js`): a member's object, a
    /// `!`'s operand, a tag. The call must be to the left of the `new`'s argument list to be
    /// captured, so `new a[b]()` — no inner call — stays bare.
    ///
    /// The walk goes on through an instantiation expression's operand, where prettier's
    /// stops: `new (f()<T>)()` printed bare is `new f()<T>()`, a call — with the type
    /// arguments — on the constructed `f`.
    ///
    /// It stops at a node the printer prints INSIDE a pair of its own, since the call in
    /// there can no longer take the `new`'s argument list — and it asks each position the
    /// question that position's own builder asks, so the two cannot disagree: an operand
    /// that takes a pair by kind (`new (f()<T>).k()`, `new (a + b()).k()`), an instantiation
    /// head ahead of a second type argument list
    /// ([`instantiation_keeps_pair_before_type_args`]), and an optional chain an authored
    /// pair seals, which every position on this spine keeps (`new (a?.b()).c()`,
    /// `new ((a?.b())!<T>)()`). A missed pair costs a second one outside it — never a
    /// harmless redundancy.
    pub(in crate::printer) fn new_callee_holds_bare_call(
        &self,
        mut callee: &ast::Expression<'_>,
    ) -> bool {
        use ast::ExpressionKind as E;
        // A loop down the spine, not a recursion: a chain is as long as the spine limit lets
        // it be, and a frame per link is what the stack contract prices
        loop {
            let (holds, next) = match &callee.kind {
                E::CallExpression(_) => return true,
                E::MemberExpression(member) => (
                    !self.needs_parens(member.object, ParenContext::ChainBase)
                        && !child_stops_optional_chain(
                            callee.span.start,
                            member.optional,
                            member.object,
                        ),
                    member.object,
                ),
                E::TSNonNullExpression(non_null) => (
                    !self.needs_parens(non_null.expression, ParenContext::NonNull)
                        && !non_null.seals_optional_chain(callee.span),
                    non_null.expression,
                ),
                E::TaggedTemplateExpression(tagged) => {
                    let list = tagged.type_arguments.as_ref();
                    let keeps_pair = self.needs_parens(tagged.tag, ParenContext::TaggedTemplateTag)
                        || tag_paren_leading_start(tagged, callee.span).is_some()
                        || tagged.tag.is_sealing_non_null()
                        || list.is_some_and(|_| {
                            instantiation_keeps_pair_before_type_args(
                                self.source,
                                tagged.tag,
                                SecondTypeArgs::Expression,
                            )
                        });
                    (!keeps_pair, tagged.tag)
                }
                E::TSInstantiationExpression(inst) => {
                    let operand = inst.expression;
                    let second_list_pair = instantiation_keeps_pair_before_type_args(
                        self.source,
                        operand,
                        SecondTypeArgs::Expression,
                    );
                    let source_pair = second_list_pair
                        || instantiation_operand_is_sealed_chain(callee.span.start, operand);
                    (
                        !self.needs_parens(
                            operand,
                            ParenContext::InstantiationExpression { source_pair },
                        ),
                        operand,
                    )
                }
                _ => return false,
            };
            if !holds {
                return false;
            }
            callee = next;
        }
    }
}