forge_fmt/state/
mod.rs

1#![allow(clippy::too_many_arguments)]
2use crate::{
3    FormatterConfig, InlineConfig,
4    pp::{self, BreakToken, SIZE_INFINITY, Token},
5    state::sol::BinOpGroup,
6};
7use foundry_common::{
8    comments::{Comment, CommentStyle, Comments, estimate_line_width, line_with_tabs},
9    iter::IterDelimited,
10};
11use foundry_config::fmt::{DocCommentStyle, IndentStyle};
12use solar::parse::{
13    ast::{self, Span},
14    interface::{BytePos, SourceMap},
15    token,
16};
17use std::{borrow::Cow, ops::Deref, sync::Arc};
18
19mod common;
20mod sol;
21mod yul;
22
23/// Specifies the nature of a complex call.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub(super) enum CallContextKind {
26    /// A chained method call, `a().b()`.
27    Chained,
28
29    /// A nested function call, `a(b())`.
30    Nested,
31}
32
33/// Formatting context for a call expression.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub(super) struct CallContext {
36    /// The kind call.
37    pub(super) kind: CallContextKind,
38
39    /// The size of the callee's head, excluding its arguments.
40    pub(super) size: usize,
41}
42
43impl CallContext {
44    pub(super) fn nested(size: usize) -> Self {
45        Self { kind: CallContextKind::Nested, size }
46    }
47
48    pub(super) fn chained(size: usize) -> Self {
49        Self { kind: CallContextKind::Chained, size }
50    }
51
52    pub(super) fn is_nested(&self) -> bool {
53        matches!(self.kind, CallContextKind::Nested)
54    }
55
56    pub(super) fn is_chained(&self) -> bool {
57        matches!(self.kind, CallContextKind::Chained)
58    }
59}
60
61#[derive(Debug, Default)]
62pub(super) struct CallStack {
63    stack: Vec<CallContext>,
64    precall_size: usize,
65}
66
67impl Deref for CallStack {
68    type Target = [CallContext];
69    fn deref(&self) -> &Self::Target {
70        &self.stack
71    }
72}
73
74impl CallStack {
75    pub(crate) fn push(&mut self, call: CallContext) {
76        self.stack.push(call);
77    }
78
79    pub(crate) fn pop(&mut self) -> Option<CallContext> {
80        self.stack.pop()
81    }
82
83    pub(crate) fn add_precall(&mut self, size: usize) {
84        self.precall_size += size;
85    }
86
87    pub(crate) fn reset_precall(&mut self) {
88        self.precall_size = 0;
89    }
90
91    pub(crate) fn is_nested(&self) -> bool {
92        self.last().is_some_and(|call| call.is_nested())
93    }
94
95    pub(crate) fn is_chain(&self) -> bool {
96        self.last().is_some_and(|call| call.is_chained())
97    }
98}
99
100pub(super) struct State<'sess, 'ast> {
101    // CORE COMPONENTS
102    pub(super) s: pp::Printer,
103    ind: isize,
104
105    sm: &'sess SourceMap,
106    pub(super) comments: Comments,
107    config: Arc<FormatterConfig>,
108    inline_config: InlineConfig<()>,
109    cursor: SourcePos,
110
111    // FORMATTING CONTEXT:
112    // Whether the source file uses CRLF (`\r\n`) line endings.
113    has_crlf: bool,
114    // The current contract being formatted, if inside a contract definition.
115    contract: Option<&'ast ast::ItemContract<'ast>>,
116    // Current block nesting depth (incremented for each `{...}` block entered).
117    block_depth: usize,
118    // Stack tracking nested and chained function calls.
119    call_stack: CallStack,
120
121    // Whether the current statement should be formatted as a single line, or not.
122    single_line_stmt: Option<bool>,
123    // The current binary expression chain context, if inside one.
124    binary_expr: Option<BinOpGroup>,
125    // Whether inside a `return` statement that contains a binary expression, or not.
126    return_bin_expr: bool,
127    // Whether inside a call with call options and at least one argument.
128    call_with_opts_and_args: bool,
129    // Whether to skip the index soft breaks because the callee fits inline.
130    skip_index_break: bool,
131    // Whether inside an `emit` or `revert` call with a qualified path, or not.
132    emit_or_revert: bool,
133    // Whether inside a variable initialization expression, or not.
134    var_init: bool,
135}
136
137impl std::ops::Deref for State<'_, '_> {
138    type Target = pp::Printer;
139
140    #[inline(always)]
141    fn deref(&self) -> &Self::Target {
142        &self.s
143    }
144}
145
146impl std::ops::DerefMut for State<'_, '_> {
147    #[inline(always)]
148    fn deref_mut(&mut self) -> &mut Self::Target {
149        &mut self.s
150    }
151}
152
153struct SourcePos {
154    pos: BytePos,
155    enabled: bool,
156}
157
158impl SourcePos {
159    pub(super) fn advance(&mut self, bytes: u32) {
160        self.pos += BytePos(bytes);
161    }
162
163    pub(super) fn advance_to(&mut self, pos: BytePos, enabled: bool) {
164        self.pos = std::cmp::max(pos, self.pos);
165        self.enabled = enabled;
166    }
167
168    pub(super) fn next_line(&mut self, is_at_crlf: bool) {
169        self.pos += if is_at_crlf { 2 } else { 1 };
170    }
171
172    pub(super) fn span(&self, to: BytePos) -> Span {
173        Span::new(self.pos, to)
174    }
175}
176
177pub(super) enum Separator {
178    Nbsp,
179    Space,
180    Hardbreak,
181    SpaceOrNbsp(bool),
182}
183
184impl Separator {
185    fn print(&self, p: &mut pp::Printer, cursor: &mut SourcePos, is_at_crlf: bool) {
186        match self {
187            Self::Nbsp => p.nbsp(),
188            Self::Space => p.space(),
189            Self::Hardbreak => p.hardbreak(),
190            Self::SpaceOrNbsp(breaks) => p.space_or_nbsp(*breaks),
191        }
192
193        cursor.next_line(is_at_crlf);
194    }
195}
196
197/// Generic methods
198impl<'sess> State<'sess, '_> {
199    pub(super) fn new(
200        sm: &'sess SourceMap,
201        config: Arc<FormatterConfig>,
202        inline_config: InlineConfig<()>,
203        comments: Comments,
204    ) -> Self {
205        Self {
206            s: pp::Printer::new(
207                config.line_length,
208                if matches!(config.style, IndentStyle::Tab) {
209                    Some(config.tab_width)
210                } else {
211                    None
212                },
213            ),
214            ind: config.tab_width as isize,
215            sm,
216            comments,
217            config,
218            inline_config,
219            cursor: SourcePos { pos: BytePos::from_u32(0), enabled: true },
220            has_crlf: false,
221            contract: None,
222            single_line_stmt: None,
223            call_with_opts_and_args: false,
224            skip_index_break: false,
225            binary_expr: None,
226            return_bin_expr: false,
227            emit_or_revert: false,
228            var_init: false,
229            block_depth: 0,
230            call_stack: CallStack::default(),
231        }
232    }
233
234    /// Checks a span of the source for a carriage return (`\r`) to determine if the file
235    /// uses CRLF line endings.
236    ///
237    /// If a `\r` is found, `self.has_crlf` is set to `true`. This is intended to be
238    /// called once at the beginning of the formatting process for efficiency.
239    fn check_crlf(&mut self, span: Span) {
240        if let Ok(snip) = self.sm.span_to_snippet(span)
241            && snip.contains('\r')
242        {
243            self.has_crlf = true;
244        }
245    }
246
247    /// Checks if the cursor is currently positioned at the start of a CRLF sequence (`\r\n`).
248    /// The check is only meaningful if `self.has_crlf` is true.
249    fn is_at_crlf(&self) -> bool {
250        self.has_crlf && self.char_at(self.cursor.pos) == Some('\r')
251    }
252
253    /// Computes the space left, bounded by the max space left.
254    fn space_left(&self) -> usize {
255        std::cmp::min(self.s.space_left(), self.max_space_left(0))
256    }
257
258    /// Computes the maximum space left given the context information available:
259    /// `block_depth`, `tab_width`, and a user-defined unavailable size `prefix_len`.
260    fn max_space_left(&self, prefix_len: usize) -> usize {
261        self.config
262            .line_length
263            .saturating_sub(self.block_depth * self.config.tab_width + prefix_len)
264    }
265
266    fn break_offset_if_not_bol(&mut self, n: usize, off: isize, search: bool) {
267        // When searching, the break token is expected to be inside a closed box. Thus, we will
268        // traverse the buffer and evaluate the first non-end token.
269        if search {
270            // We do something pretty sketchy here: tuck the nonzero offset-adjustment we
271            // were going to deposit along with the break into the previous hardbreak.
272            self.find_and_replace_last_token_still_buffered(
273                pp::Printer::hardbreak_tok_offset(off),
274                |token| token.is_hardbreak(),
275            );
276            return;
277        }
278
279        // When not explicitly searching, the break token is expected to be the last token.
280        if !self.is_beginning_of_line() {
281            self.break_offset(n, off)
282        } else if off != 0
283            && let Some(last_token) = self.last_token_still_buffered()
284            && last_token.is_hardbreak()
285        {
286            // We do something pretty sketchy here: tuck the nonzero offset-adjustment we
287            // were going to deposit along with the break into the previous hardbreak.
288            self.replace_last_token_still_buffered(pp::Printer::hardbreak_tok_offset(off));
289        }
290    }
291
292    fn braces_break(&mut self) {
293        if self.config.bracket_spacing {
294            self.space();
295        } else {
296            self.zerobreak();
297        }
298    }
299}
300
301/// Span to source.
302impl State<'_, '_> {
303    fn char_at(&self, pos: BytePos) -> Option<char> {
304        let res = self.sm.lookup_byte_offset(pos);
305        res.sf.src.get(res.pos.to_usize()..)?.chars().next()
306    }
307
308    fn print_span(&mut self, span: Span) {
309        match self.sm.span_to_snippet(span) {
310            Ok(s) => self.s.word(if matches!(self.config.style, IndentStyle::Tab) {
311                snippet_with_tabs(s, self.config.tab_width)
312            } else {
313                s
314            }),
315            Err(e) => panic!("failed to print {span:?}: {e:#?}"),
316        }
317        // Drop comments that are included in the span.
318        while let Some(cmnt) = self.peek_comment() {
319            if cmnt.pos() >= span.hi() {
320                break;
321            }
322            let _ = self.next_comment().unwrap();
323        }
324        // Update cursor
325        self.cursor.advance_to(span.hi(), false);
326    }
327
328    /// Returns `true` if the span is disabled and has been printed as-is.
329    #[must_use]
330    fn handle_span(&mut self, span: Span, skip_prev_cmnts: bool) -> bool {
331        if !skip_prev_cmnts {
332            self.print_comments(span.lo(), CommentConfig::default());
333        }
334        self.print_span_if_disabled(span)
335    }
336
337    /// Returns `true` if the span is disabled and has been printed as-is.
338    #[inline]
339    #[must_use]
340    fn print_span_if_disabled(&mut self, span: Span) -> bool {
341        let cursor_span = self.cursor.span(span.hi());
342        if self.inline_config.is_disabled(cursor_span) {
343            self.print_span_cold(cursor_span);
344            return true;
345        }
346        if self.inline_config.is_disabled(span) {
347            self.print_span_cold(span);
348            return true;
349        }
350        false
351    }
352
353    #[cold]
354    fn print_span_cold(&mut self, span: Span) {
355        self.print_span(span);
356    }
357
358    fn print_tokens(&mut self, tokens: &[token::Token]) {
359        // Leave unchanged.
360        let span = Span::join_first_last(tokens.iter().map(|t| t.span));
361        self.print_span(span);
362    }
363
364    fn print_word(&mut self, w: impl Into<Cow<'static, str>>) {
365        let cow = w.into();
366        self.cursor.advance(cow.len() as u32);
367        self.word(cow);
368    }
369
370    fn print_sep(&mut self, sep: Separator) {
371        if self.handle_span(
372            self.cursor.span(self.cursor.pos + if self.is_at_crlf() { 2 } else { 1 }),
373            true,
374        ) {
375            return;
376        }
377
378        self.print_sep_unhandled(sep);
379    }
380
381    fn print_sep_unhandled(&mut self, sep: Separator) {
382        let is_at_crlf = self.is_at_crlf();
383        sep.print(&mut self.s, &mut self.cursor, is_at_crlf);
384    }
385
386    fn print_ident(&mut self, ident: &ast::Ident) {
387        if self.handle_span(ident.span, true) {
388            return;
389        }
390
391        self.print_comments(ident.span.lo(), CommentConfig::skip_ws());
392        self.word(ident.to_string());
393    }
394
395    fn print_inside_parens<F>(&mut self, f: F)
396    where
397        F: FnOnce(&mut Self),
398    {
399        self.print_word("(");
400        f(self);
401        self.print_word(")");
402    }
403
404    fn estimate_size(&self, span: Span) -> usize {
405        if let Ok(snip) = self.sm.span_to_snippet(span) {
406            let (mut size, mut first, mut prev_needs_space) = (0, true, false);
407
408            for line in snip.lines() {
409                let line = line.trim();
410
411                if prev_needs_space {
412                    size += 1;
413                } else if !first && let Some(char) = line.chars().next() {
414                    // A line break or a space are required if this line:
415                    // - starts with an operator.
416                    // - starts with one of the ternary operators
417                    // - starts with a bracket and fmt config forces bracket spacing.
418                    match char {
419                        '&' | '|' | '=' | '>' | '<' | '+' | '-' | '*' | '/' | '%' | '^' | '?'
420                        | ':' => size += 1,
421                        '}' | ')' | ']' if self.config.bracket_spacing => size += 1,
422                        _ => (),
423                    }
424                }
425                first = false;
426
427                // trim spaces before and after mixed comments
428                let mut search = line;
429                loop {
430                    if let Some((lhs, comment)) = search.split_once(r#"/*"#) {
431                        size += lhs.trim_end().len() + 2;
432                        search = comment;
433                    } else if let Some((comment, rhs)) = search.split_once(r#"*/"#) {
434                        size += comment.len() + 2;
435                        search = rhs;
436                    } else {
437                        size += search.trim().len();
438                        break;
439                    }
440                }
441
442                // Next line requires a line break if this one:
443                // - ends with a bracket and fmt config forces bracket spacing.
444                // - ends with ',' a line break or a space are required.
445                // - ends with ';' a line break is required.
446                prev_needs_space = match line.chars().next_back() {
447                    Some('[') | Some('(') | Some('{') => self.config.bracket_spacing,
448                    Some(',') | Some(';') => true,
449                    _ => false,
450                };
451            }
452            return size;
453        }
454
455        span.to_range().len()
456    }
457
458    fn same_source_line(&self, a: BytePos, b: BytePos) -> bool {
459        self.sm.lookup_char_pos(a).line == self.sm.lookup_char_pos(b).line
460    }
461}
462
463/// Comment-related methods.
464impl<'sess> State<'sess, '_> {
465    /// Returns `None` if the span is disabled and has been printed as-is.
466    #[must_use]
467    fn handle_comment(&mut self, cmnt: Comment, skip_break: bool) -> Option<Comment> {
468        if self.cursor.enabled {
469            if self.inline_config.is_disabled(cmnt.span) {
470                if cmnt.style.is_trailing() && !self.last_token_is_space() {
471                    self.nbsp();
472                }
473                self.print_span_cold(cmnt.span);
474                if !skip_break && (cmnt.style.is_isolated() || cmnt.style.is_trailing()) {
475                    self.print_sep(Separator::Hardbreak);
476                }
477                return None;
478            }
479        } else if self.print_span_if_disabled(cmnt.span) {
480            if !skip_break && (cmnt.style.is_isolated() || cmnt.style.is_trailing()) {
481                self.print_sep(Separator::Hardbreak);
482            }
483            return None;
484        }
485        Some(cmnt)
486    }
487
488    fn cmnt_config(&self) -> CommentConfig {
489        CommentConfig { ..Default::default() }
490    }
491
492    fn print_docs(&mut self, docs: &'_ ast::DocComments<'_>) {
493        // Intetionally no-op. Handled with `self.comments`.
494        let _ = docs;
495    }
496
497    /// Prints comments that are before the given position.
498    ///
499    /// Returns `Some` with the style of the last comment printed, or `None` if no comment was
500    /// printed.
501    fn print_comments(&mut self, pos: BytePos, mut config: CommentConfig) -> Option<CommentStyle> {
502        let mut last_style: Option<CommentStyle> = None;
503        let mut is_leading = true;
504        let config_cache = config;
505        let mut buffered_blank = None;
506        while self.peek_comment().is_some_and(|c| c.pos() < pos) {
507            let mut cmnt = self.next_comment().unwrap();
508            let style_cache = cmnt.style;
509
510            // Merge consecutive line doc comments when converting to block style
511            if self.config.docs_style == foundry_config::fmt::DocCommentStyle::Block
512                && cmnt.is_doc
513                && cmnt.kind == ast::CommentKind::Line
514            {
515                let mut ref_line = self.sm.lookup_char_pos(cmnt.span.hi()).line;
516                while let Some(next_cmnt) = self.peek_comment() {
517                    if !next_cmnt.is_doc
518                        || next_cmnt.kind != ast::CommentKind::Line
519                        || ref_line + 1 != self.sm.lookup_char_pos(next_cmnt.span.lo()).line
520                    {
521                        break;
522                    }
523
524                    let next_to_merge = self.next_comment().unwrap();
525                    cmnt.lines.extend(next_to_merge.lines);
526                    cmnt.span = cmnt.span.to(next_to_merge.span);
527                    ref_line += 1;
528                }
529            }
530
531            // Ensure breaks are never skipped when there are multiple comments
532            if self.peek_comment_before(pos).is_some() {
533                config.iso_no_break = false;
534                config.trailing_no_break = false;
535            }
536
537            // Handle disabled comments
538            let Some(cmnt) = self.handle_comment(
539                cmnt,
540                if style_cache.is_isolated() {
541                    config.iso_no_break
542                } else {
543                    config.trailing_no_break
544                },
545            ) else {
546                last_style = Some(style_cache);
547                continue;
548            };
549
550            if cmnt.style.is_blank() {
551                match config.skip_blanks {
552                    Some(Skip::All) => continue,
553                    Some(Skip::Leading { resettable: true }) if is_leading => continue,
554                    Some(Skip::Leading { resettable: false }) if last_style.is_none() => continue,
555                    Some(Skip::Trailing) => {
556                        buffered_blank = Some(cmnt);
557                        continue;
558                    }
559                    _ => (),
560                }
561            // Never print blank lines after docs comments
562            } else if !cmnt.is_doc {
563                is_leading = false;
564            }
565
566            if let Some(blank) = buffered_blank.take() {
567                self.print_comment(blank, config);
568            }
569
570            // Handle mixed with follow-up comment
571            if cmnt.style.is_mixed() {
572                if let Some(cmnt) = self.peek_comment_before(pos) {
573                    config.mixed_no_break_prev = true;
574                    config.mixed_no_break_post = true;
575                    config.mixed_post_nbsp = cmnt.style.is_mixed();
576                }
577
578                // Ensure consecutive mixed comments don't have a double-space
579                if last_style.is_some_and(|s| s.is_mixed()) {
580                    config.mixed_no_break_prev = true;
581                    config.mixed_no_break_post = true;
582                    config.mixed_prev_space = false;
583                }
584            } else if config.offset != 0
585                && cmnt.style.is_isolated()
586                && last_style.is_some_and(|s| s.is_isolated())
587            {
588                self.offset(config.offset);
589            }
590
591            last_style = Some(cmnt.style);
592            self.print_comment(cmnt, config);
593            config = config_cache;
594        }
595        last_style
596    }
597
598    /// Prints a line, wrapping it if it starts with the given prefix.
599    fn print_wrapped_line(
600        &mut self,
601        line: &str,
602        prefix: &'static str,
603        break_offset: isize,
604        is_doc: bool,
605    ) {
606        if !line.starts_with(prefix) {
607            self.word(line.to_owned());
608            return;
609        }
610
611        fn post_break_prefix(prefix: &'static str, has_content: bool) -> &'static str {
612            if !has_content {
613                return prefix;
614            }
615            match prefix {
616                "///" => "/// ",
617                "//" => "// ",
618                "/*" => "/* ",
619                " *" => " * ",
620                _ => prefix,
621            }
622        }
623
624        self.ibox(0);
625        self.word(prefix);
626
627        let content = &line[prefix.len()..];
628        let content = if is_doc {
629            // Doc comments preserve leading whitespaces (right after the prefix) as nbps.
630            let ws_len = content
631                .char_indices()
632                .take_while(|(_, c)| c.is_whitespace())
633                .last()
634                .map_or(0, |(idx, c)| idx + c.len_utf8());
635            let (leading_ws, rest) = content.split_at(ws_len);
636            if !leading_ws.is_empty() {
637                self.word(leading_ws.to_owned());
638            }
639            rest
640        } else {
641            // Non-doc comments: replace first whitespace with nbsp, rest of content continues
642            if let Some(first_char) = content.chars().next() {
643                if first_char.is_whitespace() {
644                    self.nbsp();
645                    &content[first_char.len_utf8()..]
646                } else {
647                    content
648                }
649            } else {
650                ""
651            }
652        };
653
654        let post_break = post_break_prefix(prefix, !content.is_empty());
655
656        // Process content character by character to preserve consecutive whitespaces
657        let (mut chars, mut current_word) = (content.chars().peekable(), String::new());
658        while let Some(ch) = chars.next() {
659            if ch.is_whitespace() {
660                // Print current word
661                if !current_word.is_empty() {
662                    self.word(std::mem::take(&mut current_word));
663                }
664
665                // Preserve multiple spaces while adding a single break
666                let mut ws_count = 1;
667                while chars.peek().is_some_and(|c| c.is_whitespace()) {
668                    ws_count += 1;
669                    chars.next();
670                }
671                self.s.scan_break(BreakToken {
672                    offset: break_offset,
673                    blank_space: ws_count,
674                    post_break: if post_break.starts_with("/*") { None } else { Some(post_break) },
675                    ..Default::default()
676                });
677                continue;
678            }
679
680            current_word.push(ch);
681        }
682
683        // Print final word
684        if !current_word.is_empty() {
685            self.word(current_word);
686        }
687
688        self.end();
689    }
690
691    /// Merges consecutive line comments to avoid orphan words.
692    fn merge_comment_lines(&self, lines: &[String], prefix: &str) -> Vec<String> {
693        // Do not apply smart merging to block comments
694        if lines.is_empty() || lines.len() < 2 || !prefix.starts_with("//") {
695            return lines.to_vec();
696        }
697
698        let mut result = Vec::new();
699        let mut i = 0;
700
701        while i < lines.len() {
702            let current_line = &lines[i];
703
704            // Keep empty lines, and non-prefixed lines, untouched
705            if current_line.trim().is_empty() || !current_line.starts_with(prefix) {
706                result.push(current_line.clone());
707                i += 1;
708                continue;
709            }
710
711            if i + 1 < lines.len() {
712                let next_line = &lines[i + 1];
713
714                // Check if next line is has the same prefix and is not empty
715                if next_line.starts_with(prefix) && !next_line.trim().is_empty() {
716                    // Only merge if the current line doesn't fit within available width
717                    if estimate_line_width(current_line, self.config.tab_width) > self.space_left()
718                    {
719                        // Merge the lines and let the wrapper handle breaking if needed
720                        let merged_line = format!(
721                            "{current_line} {next_content}",
722                            next_content = &next_line[prefix.len()..].trim_start()
723                        );
724                        result.push(merged_line);
725
726                        // Skip both lines since they are merged
727                        i += 2;
728                        continue;
729                    }
730                }
731            }
732
733            // No merge possible, keep the line as-is
734            result.push(current_line.clone());
735            i += 1;
736        }
737
738        result
739    }
740
741    fn print_comment(&mut self, mut cmnt: Comment, mut config: CommentConfig) {
742        self.cursor.advance_to(cmnt.span.hi(), true);
743
744        if cmnt.is_doc {
745            cmnt = style_doc_comment(self.config.docs_style, cmnt);
746        }
747
748        match cmnt.style {
749            CommentStyle::Mixed => {
750                let Some(prefix) = cmnt.prefix() else { return };
751                let never_break = self.last_token_is_neverbreak();
752                if !self.is_bol_or_only_ind() {
753                    match (never_break || config.mixed_no_break_prev, config.mixed_prev_space) {
754                        (false, true) => config.space(&mut self.s),
755                        (false, false) => config.zerobreak(&mut self.s),
756                        (true, true) => self.nbsp(),
757                        (true, false) => (),
758                    };
759                }
760                if self.config.wrap_comments {
761                    // Merge and wrap comments
762                    let merged_lines = self.merge_comment_lines(&cmnt.lines, prefix);
763                    for (pos, line) in merged_lines.into_iter().delimited() {
764                        self.print_wrapped_line(&line, prefix, 0, cmnt.is_doc);
765                        if !pos.is_last {
766                            self.hardbreak();
767                        }
768                    }
769                } else {
770                    // No wrapping, print as-is
771                    for (pos, line) in cmnt.lines.into_iter().delimited() {
772                        self.word(line);
773                        if !pos.is_last {
774                            self.hardbreak();
775                        }
776                    }
777                }
778                if config.mixed_post_nbsp {
779                    config.nbsp_or_space(self.config.wrap_comments, &mut self.s);
780                    self.cursor.advance(1);
781                } else if !config.mixed_no_break_post {
782                    config.space(&mut self.s);
783                    self.cursor.advance(1);
784                }
785            }
786            CommentStyle::Isolated => {
787                let Some(mut prefix) = cmnt.prefix() else { return };
788                if !config.iso_no_break {
789                    config.hardbreak_if_not_bol(self.is_bol_or_only_ind(), &mut self.s);
790                }
791
792                if self.config.wrap_comments {
793                    // Merge and wrap comments
794                    let merged_lines = self.merge_comment_lines(&cmnt.lines, prefix);
795                    for (pos, line) in merged_lines.into_iter().delimited() {
796                        let hb = |this: &mut Self| {
797                            this.hardbreak();
798                            if pos.is_last {
799                                this.cursor.next_line(this.is_at_crlf());
800                            }
801                        };
802                        if line.is_empty() {
803                            hb(self);
804                            continue;
805                        }
806                        if pos.is_first {
807                            self.ibox(config.offset);
808                            if cmnt.is_doc && matches!(prefix, "/**") {
809                                self.word(prefix);
810                                hb(self);
811                                prefix = " * ";
812                                continue;
813                            }
814                        }
815
816                        self.print_wrapped_line(&line, prefix, 0, cmnt.is_doc);
817
818                        if pos.is_last {
819                            self.end();
820                            if !config.iso_no_break {
821                                hb(self);
822                            }
823                        } else {
824                            hb(self);
825                        }
826                    }
827                } else {
828                    // No wrapping, print as-is
829                    for (pos, line) in cmnt.lines.into_iter().delimited() {
830                        let hb = |this: &mut Self| {
831                            this.hardbreak();
832                            if pos.is_last {
833                                this.cursor.next_line(this.is_at_crlf());
834                            }
835                        };
836                        if line.is_empty() {
837                            hb(self);
838                            continue;
839                        }
840                        if pos.is_first {
841                            self.ibox(config.offset);
842                            if cmnt.is_doc && matches!(prefix, "/**") {
843                                self.word(prefix);
844                                hb(self);
845                                prefix = " * ";
846                                continue;
847                            }
848                        }
849
850                        self.word(line);
851
852                        if pos.is_last {
853                            self.end();
854                            if !config.iso_no_break {
855                                hb(self);
856                            }
857                        } else {
858                            hb(self);
859                        }
860                    }
861                }
862            }
863            CommentStyle::Trailing => {
864                let Some(prefix) = cmnt.prefix() else { return };
865                self.neverbreak();
866                if !self.is_bol_or_only_ind() {
867                    self.nbsp();
868                }
869
870                if !self.config.wrap_comments && cmnt.lines.len() == 1 {
871                    self.word(cmnt.lines.pop().unwrap());
872                } else if self.config.wrap_comments {
873                    if cmnt.is_doc || matches!(cmnt.kind, ast::CommentKind::Line) {
874                        config.offset = 0;
875                    } else {
876                        config.offset = self.ind;
877                    }
878                    for (lpos, line) in cmnt.lines.into_iter().delimited() {
879                        if !line.is_empty() {
880                            self.print_wrapped_line(&line, prefix, config.offset, cmnt.is_doc);
881                        }
882                        if !lpos.is_last {
883                            config.hardbreak(&mut self.s);
884                        }
885                    }
886                } else {
887                    self.visual_align();
888                    for (pos, line) in cmnt.lines.into_iter().delimited() {
889                        if !line.is_empty() {
890                            self.word(line);
891                            if !pos.is_last {
892                                self.hardbreak();
893                            }
894                        }
895                    }
896                    self.end();
897                }
898
899                if !config.trailing_no_break {
900                    self.print_sep(Separator::Hardbreak);
901                }
902            }
903
904            CommentStyle::BlankLine => {
905                // Pre-requisite: ensure that blank links are printed at the beginning of new line.
906                if !self.last_token_is_break() && !self.is_bol_or_only_ind() {
907                    config.hardbreak(&mut self.s);
908                    self.cursor.next_line(self.is_at_crlf());
909                }
910
911                // We need to do at least one, possibly two hardbreaks.
912                let twice = match self.last_token() {
913                    Some(Token::String(s)) => ";" == s,
914                    Some(Token::Begin(_)) => true,
915                    Some(Token::End) => true,
916                    _ => false,
917                };
918                if twice {
919                    config.hardbreak(&mut self.s);
920                    self.cursor.next_line(self.is_at_crlf());
921                }
922                config.hardbreak(&mut self.s);
923                self.cursor.next_line(self.is_at_crlf());
924            }
925        }
926    }
927
928    fn peek_comment<'b>(&'b self) -> Option<&'b Comment>
929    where
930        'sess: 'b,
931    {
932        self.comments.peek()
933    }
934
935    fn peek_comment_before<'b>(&'b self, pos: BytePos) -> Option<&'b Comment>
936    where
937        'sess: 'b,
938    {
939        self.comments.iter().take_while(|c| c.pos() < pos).find(|c| !c.style.is_blank())
940    }
941
942    fn has_comment_before_with<F>(&self, pos: BytePos, f: F) -> bool
943    where
944        F: FnMut(&Comment) -> bool,
945    {
946        self.comments.iter().take_while(|c| c.pos() < pos).any(f)
947    }
948
949    fn peek_comment_between<'b>(&'b self, pos_lo: BytePos, pos_hi: BytePos) -> Option<&'b Comment>
950    where
951        'sess: 'b,
952    {
953        self.comments
954            .iter()
955            .take_while(|c| pos_lo < c.pos() && c.pos() < pos_hi)
956            .find(|c| !c.style.is_blank())
957    }
958
959    fn has_comment_between(&self, start_pos: BytePos, end_pos: BytePos) -> bool {
960        self.comments.iter().filter(|c| c.pos() > start_pos && c.pos() < end_pos).any(|_| true)
961    }
962
963    pub(crate) fn next_comment(&mut self) -> Option<Comment> {
964        self.comments.next()
965    }
966
967    fn peek_trailing_comment<'b>(
968        &'b self,
969        span_pos: BytePos,
970        next_pos: Option<BytePos>,
971    ) -> Option<&'b Comment>
972    where
973        'sess: 'b,
974    {
975        self.comments.peek_trailing(self.sm, span_pos, next_pos).map(|(cmnt, _)| cmnt)
976    }
977
978    fn print_trailing_comment_inner(
979        &mut self,
980        span_pos: BytePos,
981        next_pos: Option<BytePos>,
982        config: Option<CommentConfig>,
983    ) -> bool {
984        let mut printed = 0;
985        if let Some((_, n)) = self.comments.peek_trailing(self.sm, span_pos, next_pos) {
986            let config =
987                config.unwrap_or(CommentConfig::skip_ws().mixed_no_break().mixed_prev_space());
988            while printed <= n {
989                let cmnt = self.comments.next().unwrap();
990                if let Some(cmnt) = self.handle_comment(cmnt, config.trailing_no_break) {
991                    self.print_comment(cmnt, config);
992                };
993                printed += 1;
994            }
995        }
996        printed != 0
997    }
998
999    fn print_trailing_comment(&mut self, span_pos: BytePos, next_pos: Option<BytePos>) -> bool {
1000        self.print_trailing_comment_inner(span_pos, next_pos, None)
1001    }
1002
1003    fn print_trailing_comment_no_break(&mut self, span_pos: BytePos, next_pos: Option<BytePos>) {
1004        self.print_trailing_comment_inner(
1005            span_pos,
1006            next_pos,
1007            Some(CommentConfig::skip_ws().trailing_no_break().mixed_no_break().mixed_prev_space()),
1008        );
1009    }
1010
1011    fn print_remaining_comments(&mut self, skip_leading_ws: bool) {
1012        // If there aren't any remaining comments, then we need to manually
1013        // make sure there is a line break at the end.
1014        if self.peek_comment().is_none() && !self.is_bol_or_only_ind() {
1015            self.hardbreak();
1016            return;
1017        }
1018
1019        let mut is_leading = true;
1020        while let Some(cmnt) = self.next_comment() {
1021            if cmnt.style.is_blank() && skip_leading_ws && is_leading {
1022                continue;
1023            }
1024
1025            is_leading = false;
1026            if let Some(cmnt) = self.handle_comment(cmnt, false) {
1027                self.print_comment(cmnt, CommentConfig::default());
1028            } else if self.peek_comment().is_none() && !self.is_bol_or_only_ind() {
1029                self.hardbreak();
1030            }
1031        }
1032    }
1033}
1034
1035#[derive(Clone, Copy)]
1036enum Skip {
1037    All,
1038    Leading { resettable: bool },
1039    Trailing,
1040}
1041
1042#[derive(Default, Clone, Copy)]
1043pub(crate) struct CommentConfig {
1044    // Config: all
1045    skip_blanks: Option<Skip>,
1046    offset: isize,
1047
1048    // Config: isolated comments
1049    iso_no_break: bool,
1050    // Config: trailing comments
1051    trailing_no_break: bool,
1052    // Config: mixed comments
1053    mixed_prev_space: bool,
1054    mixed_post_nbsp: bool,
1055    mixed_no_break_prev: bool,
1056    mixed_no_break_post: bool,
1057}
1058
1059impl CommentConfig {
1060    pub(crate) fn skip_ws() -> Self {
1061        Self { skip_blanks: Some(Skip::All), ..Default::default() }
1062    }
1063
1064    pub(crate) fn skip_leading_ws(resettable: bool) -> Self {
1065        Self { skip_blanks: Some(Skip::Leading { resettable }), ..Default::default() }
1066    }
1067
1068    pub(crate) fn skip_trailing_ws() -> Self {
1069        Self { skip_blanks: Some(Skip::Trailing), ..Default::default() }
1070    }
1071
1072    pub(crate) fn offset(mut self, off: isize) -> Self {
1073        self.offset = off;
1074        self
1075    }
1076
1077    pub(crate) fn no_breaks(mut self) -> Self {
1078        self.iso_no_break = true;
1079        self.trailing_no_break = true;
1080        self.mixed_no_break_prev = true;
1081        self.mixed_no_break_post = true;
1082        self
1083    }
1084
1085    pub(crate) fn trailing_no_break(mut self) -> Self {
1086        self.trailing_no_break = true;
1087        self
1088    }
1089
1090    pub(crate) fn mixed_no_break(mut self) -> Self {
1091        self.mixed_no_break_prev = true;
1092        self.mixed_no_break_post = true;
1093        self
1094    }
1095
1096    pub(crate) fn mixed_no_break_post(mut self) -> Self {
1097        self.mixed_no_break_post = true;
1098        self
1099    }
1100
1101    pub(crate) fn mixed_prev_space(mut self) -> Self {
1102        self.mixed_prev_space = true;
1103        self
1104    }
1105
1106    pub(crate) fn mixed_post_nbsp(mut self) -> Self {
1107        self.mixed_post_nbsp = true;
1108        self
1109    }
1110
1111    pub(crate) fn hardbreak_if_not_bol(&self, is_bol: bool, p: &mut pp::Printer) {
1112        if self.offset != 0 && !is_bol {
1113            self.hardbreak(p);
1114        } else {
1115            p.hardbreak_if_not_bol();
1116        }
1117    }
1118
1119    pub(crate) fn hardbreak(&self, p: &mut pp::Printer) {
1120        p.break_offset(SIZE_INFINITY as usize, self.offset);
1121    }
1122
1123    pub(crate) fn space(&self, p: &mut pp::Printer) {
1124        p.break_offset(1, self.offset);
1125    }
1126
1127    pub(crate) fn nbsp_or_space(&self, breaks: bool, p: &mut pp::Printer) {
1128        if breaks {
1129            self.space(p);
1130        } else {
1131            p.nbsp();
1132        }
1133    }
1134
1135    pub(crate) fn zerobreak(&self, p: &mut pp::Printer) {
1136        p.break_offset(0, self.offset);
1137    }
1138}
1139
1140fn snippet_with_tabs(s: String, tab_width: usize) -> String {
1141    // process leading breaks
1142    let trimmed = s.trim_start_matches('\n');
1143    let num_breaks = s.len() - trimmed.len();
1144    let mut formatted = std::iter::repeat_n('\n', num_breaks).collect::<String>();
1145
1146    // process lines
1147    for (pos, line) in trimmed.lines().delimited() {
1148        line_with_tabs(&mut formatted, line, tab_width, None);
1149        if !pos.is_last {
1150            formatted.push('\n');
1151        }
1152    }
1153
1154    formatted
1155}
1156
1157/// Formats a doc comment with the requested style.
1158///
1159/// NOTE: assumes comments have already been normalized.
1160fn style_doc_comment(style: DocCommentStyle, mut cmnt: Comment) -> Comment {
1161    match style {
1162        DocCommentStyle::Line if cmnt.kind == ast::CommentKind::Block => {
1163            let mut new_lines = Vec::new();
1164            for (pos, line) in cmnt.lines.iter().delimited() {
1165                if pos.is_first || pos.is_last {
1166                    // Skip the opening '/**' and closing '*/' lines
1167                    continue;
1168                }
1169
1170                // Convert ' * {content}' to '/// {content}'
1171                let trimmed = line.trim_start();
1172                if let Some(content) = trimmed.strip_prefix('*') {
1173                    new_lines.push(format!("///{content}"));
1174                } else if !trimmed.is_empty() {
1175                    new_lines.push(format!("/// {trimmed}"));
1176                }
1177            }
1178
1179            cmnt.lines = new_lines;
1180            cmnt.kind = ast::CommentKind::Line;
1181            cmnt
1182        }
1183        DocCommentStyle::Block if cmnt.kind == ast::CommentKind::Line => {
1184            let mut new_lines = vec!["/**".to_string()];
1185
1186            for line in &cmnt.lines {
1187                // Convert '/// {content}' to ' * {content}'
1188                new_lines.push(format!(" *{content}", content = &line[3..]))
1189            }
1190
1191            new_lines.push(" */".to_string());
1192            cmnt.lines = new_lines;
1193            cmnt.kind = ast::CommentKind::Block;
1194            cmnt
1195        }
1196        // Otherwise, no conversion needed.
1197        _ => cmnt,
1198    }
1199}