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