forge_fmt/state/
mod.rs

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