Skip to main content

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