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