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