Skip to main content

forge_fmt/state/
common.rs

1use super::{CommentConfig, Separator, State};
2use crate::pp::{BreakToken, Printer, SIZE_INFINITY};
3use foundry_common::iter::IterDelimited;
4use foundry_config::fmt as config;
5use itertools::{Either, Itertools};
6use solar::parse::{
7    Cursor,
8    ast::{self, Span},
9    interface::BytePos,
10};
11use std::{borrow::Cow, fmt::Debug};
12
13pub(crate) trait LitExt<'ast> {
14    fn is_str_concatenation(&self) -> bool;
15}
16
17impl<'ast> LitExt<'ast> for ast::Lit<'ast> {
18    /// Checks if a the input literal is a string literal with multiple parts.
19    fn is_str_concatenation(&self) -> bool {
20        if let ast::LitKind::Str(_, _, parts) = &self.kind { !parts.is_empty() } else { false }
21    }
22}
23
24/// Language-specific pretty printing. Common for both: Solidity + Yul.
25impl<'ast> State<'_, 'ast> {
26    pub(super) fn print_lit_inner(&mut self, lit: &'ast ast::Lit<'ast>, is_yul: bool) {
27        let ast::Lit { span, symbol, ref kind } = *lit;
28        if self.handle_span(span, false) {
29            return;
30        }
31
32        match *kind {
33            ast::LitKind::Str(kind, ..) => {
34                self.s.ibox(0);
35                let mut literals = lit.literals().peekable();
36                while let Some((span, symbol)) = literals.next() {
37                    if !self.handle_span(span, false) {
38                        let quote_pos = span.lo() + kind.prefix().len() as u32;
39                        self.print_str_lit(kind, quote_pos, symbol.as_str());
40                    }
41                    if let Some((next_span, _)) = literals.peek() {
42                        if !self.print_trailing_comment(span.hi(), Some(next_span.lo())) {
43                            self.space_if_not_bol();
44                        }
45                    } else {
46                        self.neverbreak();
47                    }
48                }
49                self.end();
50            }
51            ast::LitKind::Number(_) | ast::LitKind::Rational(_) => {
52                self.print_num_literal(symbol.as_str(), is_yul);
53            }
54            ast::LitKind::Address(value) => self.word(value.to_string()),
55            ast::LitKind::Bool(value) => self.word(if value { "true" } else { "false" }),
56            ast::LitKind::Err(_) => self.word(symbol.to_string()),
57        }
58    }
59
60    fn print_num_literal(&mut self, source: &str, is_yul: bool) {
61        fn strip_underscores_if(b: bool, s: &str) -> Cow<'_, str> {
62            if b && s.contains('_') { Cow::Owned(s.replace('_', "")) } else { Cow::Borrowed(s) }
63        }
64
65        fn add_underscores(
66            out: &mut String,
67            config: config::NumberUnderscore,
68            string: &str,
69            is_dec: bool,
70            is_yul: bool,
71            reversed: bool,
72        ) {
73            // The underscore thousand separator is only valid in Solidity decimal numbers.
74            // It is not supported by hex numbers, nor Yul literals.
75            // https://github.com/foundry-rs/foundry/issues/12111
76            if !config.is_thousands() || !is_dec || is_yul || string.len() < 5 {
77                out.push_str(string);
78                return;
79            }
80
81            let chunks = if reversed {
82                Either::Left(string.as_bytes().chunks(3))
83            } else {
84                Either::Right(string.as_bytes().rchunks(3).rev())
85            }
86            .map(|chunk| std::str::from_utf8(chunk).unwrap());
87            for chunk in Itertools::intersperse(chunks, "_") {
88                out.push_str(chunk);
89            }
90        }
91
92        debug_assert!(source.is_ascii(), "{source:?}");
93
94        let config = self.config.number_underscore;
95        let is_dec = !["0x", "0b", "0o"].iter().any(|prefix| source.starts_with(prefix));
96
97        let (val, exp) = if is_dec {
98            source.split_once(['e', 'E']).unwrap_or((source, ""))
99        } else {
100            (source, "")
101        };
102        let (val, fract) = val.split_once('.').unwrap_or((val, ""));
103
104        let strip_underscores = !config.is_preserve() || is_yul;
105        let mut val = &strip_underscores_if(strip_underscores, val)[..];
106        let mut exp = &strip_underscores_if(strip_underscores, exp)[..];
107        let mut fract = &strip_underscores_if(strip_underscores, fract)[..];
108
109        // strip any padded 0's
110        let mut exp_sign = "";
111        if is_dec {
112            val = val.trim_start_matches('0');
113            fract = fract.trim_end_matches('0');
114            (exp_sign, exp) =
115                if let Some(exp) = exp.strip_prefix('-') { ("-", exp) } else { ("", exp) };
116            exp = exp.trim_start_matches('0');
117        }
118
119        let mut out = String::with_capacity(source.len() * 2);
120        if val.is_empty() {
121            out.push('0');
122        } else {
123            add_underscores(&mut out, config, val, is_dec, is_yul, false);
124        }
125        if source.contains('.') {
126            out.push('.');
127            match (fract.is_empty(), exp.is_empty()) {
128                // `X.YeZ`: keep as is
129                (false, false) => out.push_str(fract),
130                // `X.Y`
131                (false, true) => add_underscores(&mut out, config, fract, is_dec, is_yul, true),
132                // `X.` -> `X.0`
133                (true, _) => out.push('0'),
134            };
135        }
136        if !exp.is_empty() {
137            out.push('e');
138            out.push_str(exp_sign);
139            add_underscores(&mut out, config, exp, is_dec, is_yul, false);
140        }
141
142        self.word(out);
143    }
144
145    /// `s` should be the *unescaped contents of the string literal*.
146    pub(super) fn print_str_lit(&mut self, kind: ast::StrKind, quote_pos: BytePos, s: &str) {
147        self.print_comments(quote_pos, CommentConfig::default());
148        let s = self.str_lit_to_string(kind, quote_pos, s);
149        self.word(s);
150    }
151
152    /// `s` should be the *unescaped contents of the string literal*.
153    fn str_lit_to_string(&self, kind: ast::StrKind, quote_pos: BytePos, s: &str) -> String {
154        let prefix = kind.prefix();
155        let quote = match self.config.quote_style {
156            config::QuoteStyle::Double => '\"',
157            config::QuoteStyle::Single => '\'',
158            config::QuoteStyle::Preserve => self.char_at(quote_pos).unwrap_or_default(),
159        };
160        debug_assert!(matches!(quote, '\"' | '\''), "{quote:?}");
161        let s = solar::parse::interface::data_structures::fmt::from_fn(move |f| {
162            if matches!(kind, ast::StrKind::Hex) {
163                match self.config.hex_underscore {
164                    config::HexUnderscore::Preserve => {}
165                    config::HexUnderscore::Remove | config::HexUnderscore::Bytes => {
166                        let mut clean = s.to_string().replace('_', "");
167                        if matches!(self.config.hex_underscore, config::HexUnderscore::Bytes) {
168                            clean =
169                                clean.chars().chunks(2).into_iter().map(|c| c.format("")).join("_");
170                        }
171                        return f.write_str(&clean);
172                    }
173                };
174            }
175            f.write_str(s)
176        });
177        let mut s = format!("{prefix}{quote}{s}{quote}");
178
179        // If the output is not a single token then revert to the original quote.
180        #[allow(unstable_name_collisions)]
181        if Cursor::new(&s).exactly_one().is_err() {
182            let other_quote = if quote == '\"' { '\'' } else { '\"' };
183            {
184                let s = unsafe { s.as_bytes_mut() };
185                s[prefix.len()] = other_quote as u8;
186                s[s.len() - 1] = other_quote as u8;
187            }
188            debug_assert!(Cursor::new(&s).exactly_one().map(|_| true).unwrap());
189        }
190
191        s
192    }
193
194    pub(super) fn print_tuple_empty(&mut self, pos_lo: BytePos, pos_hi: BytePos) {
195        if self.handle_span(Span::new(pos_lo, pos_hi), true) {
196            return;
197        }
198
199        self.print_inside_parens(|state| {
200            state.s.cbox(state.ind);
201            if let Some(cmnt) =
202                state.print_comments(pos_hi, CommentConfig::skip_ws().mixed_prev_space())
203            {
204                if cmnt.is_mixed() {
205                    state.s.offset(-state.ind);
206                } else {
207                    state.break_offset_if_not_bol(0, -state.ind, false);
208                }
209            }
210            state.end();
211        });
212    }
213
214    pub(super) fn print_tuple<'a, T, P, S>(
215        &mut self,
216        values: &'a [T],
217        pos_lo: BytePos,
218        pos_hi: BytePos,
219        mut print: P,
220        mut get_span: S,
221        format: ListFormat,
222    ) where
223        P: FnMut(&mut Self, &'a T),
224        S: FnMut(&T) -> Span,
225    {
226        if self.handle_span(Span::new(pos_lo, pos_hi), true) {
227            return;
228        }
229
230        if values.is_empty() {
231            self.print_tuple_empty(pos_lo, pos_hi);
232            return;
233        }
234
235        if !(values.len() == 1 && format.is_inline()) {
236            // Use commasep
237            self.print_inside_parens(|state| {
238                state.commasep(values, pos_lo, pos_hi, print, get_span, format)
239            });
240            return;
241        }
242
243        // Format single-item inline lists directly without boxes
244        self.print_inside_parens(|state| {
245            let span = get_span(&values[0]);
246            state.s.cbox(state.ind);
247            let skip_break = if state.peek_comment_before(span.hi()).is_some() {
248                state.hardbreak();
249                false
250            } else {
251                true
252            };
253
254            state.print_comments(span.lo(), CommentConfig::skip_ws().mixed_prev_space());
255            print(state, &values[0]);
256
257            // Bound the scan to the closing paren. Unbounded, it reaches past the list and claims a
258            // comment that trails whatever follows it, such as the modifiers of a function type.
259            if !state.print_trailing_comment(span.hi(), Some(pos_hi)) && skip_break {
260                state.neverbreak();
261            } else {
262                state.break_offset_if_not_bol(0, -state.ind, false);
263            }
264            state.end();
265        });
266    }
267
268    pub(super) fn print_array<'a, T, P, S>(
269        &mut self,
270        values: &'a [T],
271        span: Span,
272        print: P,
273        get_span: S,
274    ) where
275        P: FnMut(&mut Self, &'a T),
276        S: FnMut(&T) -> Span,
277    {
278        if self.handle_span(span, false) {
279            return;
280        }
281
282        self.print_word("[");
283        self.commasep(values, span.lo(), span.hi(), print, get_span, ListFormat::compact());
284        self.print_word("]");
285    }
286
287    pub(super) fn commasep_opening_logic<T, S>(
288        &mut self,
289        values: &[T],
290        mut get_span: S,
291        format: ListFormat,
292        manual_opening: bool,
293    ) -> bool
294    where
295        S: FnMut(&T) -> Span,
296    {
297        let Some(span) = values.first().map(&mut get_span) else {
298            return false;
299        };
300
301        // If first item is uninformed (just a comma), and it has its own comment, skip it.
302        // It will be dealt with when printing the item in the main loop of `commasep`.
303        if span.is_dummy()
304            && let Some(next_pos) = values.get(1).map(|v| get_span(v).lo())
305            && self.peek_comment_before(next_pos).is_some()
306        {
307            return true;
308        }
309
310        // Check for comments before the first item.
311        if let Some((cmnt_span, cmnt_style)) =
312            self.peek_comment_before(span.lo()).map(|c| (c.span, c.style))
313        {
314            let cmnt_disabled = self.inline_config.is_disabled(cmnt_span);
315            // Handle special formatting for disabled code with isolated comments.
316            if self.cursor.enabled && cmnt_disabled && cmnt_style.is_isolated() {
317                self.print_sep(Separator::Hardbreak);
318                if !format.with_delimiters {
319                    self.s.offset(self.ind);
320                }
321            };
322
323            // If manual opening flag is passed, we simply force the break, and skip the comment.
324            // It will be dealt with when printing the item in the main loop of `commasep`.
325            if manual_opening {
326                self.hardbreak();
327                self.s.offset(self.ind);
328                return true;
329            }
330
331            let cmnt_config = if format.with_delimiters {
332                CommentConfig::skip_ws().mixed_no_break().mixed_prev_space()
333            } else {
334                CommentConfig::skip_ws().no_breaks().mixed_prev_space().offset(self.ind)
335            };
336            // Apply spacing based on comment styles.
337            if let Some(last_style) = self.print_comments(span.lo(), cmnt_config) {
338                match (cmnt_style.is_mixed(), last_style.is_mixed()) {
339                    (true, true) => {
340                        if format.breaks_with_comments() {
341                            self.hardbreak();
342                        } else {
343                            self.space();
344                        }
345                        if !format.with_delimiters && !cmnt_disabled {
346                            self.s.offset(self.ind);
347                        }
348                    }
349                    (false, true) => {
350                        self.nbsp();
351                    }
352                    (false, false) if !format.with_delimiters && !cmnt_disabled => {
353                        self.hardbreak();
354                        self.s.offset(self.ind);
355                    }
356                    _ => {}
357                }
358            }
359            if self.cursor.enabled {
360                self.cursor.advance_to(span.lo(), true);
361            }
362            return true;
363        }
364
365        if self.cursor.enabled {
366            self.cursor.advance_to(span.lo(), true);
367        }
368
369        if !values.is_empty() && !format.with_delimiters {
370            format.print_break(true, values.len(), &mut self.s);
371            self.s.offset(self.ind);
372            return true;
373        }
374
375        false
376    }
377
378    pub(super) fn commasep<'a, T, P, S>(
379        &mut self,
380        values: &'a [T],
381        _pos_lo: BytePos,
382        pos_hi: BytePos,
383        mut print: P,
384        mut get_span: S,
385        format: ListFormat,
386    ) where
387        P: FnMut(&mut Self, &'a T),
388        S: FnMut(&T) -> Span,
389    {
390        if values.is_empty() {
391            return;
392        }
393
394        // We can't simply check `peek_comment_before(pos_hi)` cause we would also account for
395        // comments in the child expression, and those don't matter.
396        let has_comments =
397            // check for comments before the first element
398            self.peek_comment_before(get_span(&values[0]).lo()).is_some() ||
399            // check for comments between elements
400            values.windows(2).any(|w| self.peek_comment_between(get_span(&w[0]).hi(), get_span(&w[1]).lo()).is_some()) ||
401            // check for comments after the last element
402            self.peek_comment_between(get_span(values.last().unwrap()).hi(), pos_hi).is_some();
403
404        // For calls with opts and args, which should break consistently, we need to skip the
405        // wrapping cbox to prioritize call args breaking before the call opts. Because of that, we
406        // must manually offset the breaks between args, so that they are properly indented.
407        let manual_opening =
408            format.is_consistent() && !format.with_delimiters && self.call_with_opts_and_args;
409        // When there are comments, we can preserve the cbox, as they will make it break
410        let manual_offset = !has_comments && manual_opening;
411
412        let is_single_without_cmnts = values.len() == 1 && !format.break_single && !has_comments;
413        let skip_first_break = if format.with_delimiters || format.is_inline() {
414            self.s.cbox(if format.no_ind { 0 } else { self.ind });
415            if is_single_without_cmnts {
416                true
417            } else {
418                self.commasep_opening_logic(values, &mut get_span, format, manual_opening)
419            }
420        } else {
421            let res = self.commasep_opening_logic(values, &mut get_span, format, manual_opening);
422            if !manual_offset {
423                self.s.cbox(if format.no_ind { 0 } else { self.ind });
424            }
425            res
426        };
427
428        if let Some(sym) = format.prev_symbol() {
429            self.word_space(sym);
430        } else if is_single_without_cmnts && format.with_space {
431            self.nbsp();
432        } else if !skip_first_break && !format.is_inline() {
433            format.print_break(true, values.len(), &mut self.s);
434            if manual_offset {
435                self.s.offset(self.ind);
436            }
437        }
438
439        if format.is_compact() && !(format.breaks_with_comments() && has_comments) {
440            self.s.cbox(0);
441        }
442
443        let mut last_delimiter_break = !format.with_delimiters;
444        let mut skip_last_break =
445            is_single_without_cmnts || !format.with_delimiters || format.is_inline();
446        for (i, value) in values.iter().enumerate() {
447            let is_last = i == values.len() - 1;
448            if self
449                .print_comments(get_span(value).lo(), CommentConfig::skip_ws().mixed_prev_space())
450                .is_some_and(|cmnt| cmnt.is_mixed())
451                && format.breaks_with_comments()
452            {
453                self.hardbreak(); // trailing and isolated comments already hardbreak
454            }
455
456            // Avoid printing the last uninformed item, so that we can handle line breaks.
457            if !(is_last && get_span(value).is_dummy()) {
458                print(self, value);
459            }
460
461            let next_span = if is_last { None } else { Some(get_span(&values[i + 1])) };
462            let next_pos = next_span.map(Span::lo).unwrap_or(pos_hi);
463            let cmnt_before_next =
464                self.peek_comment_before(next_pos).map(|cmnt| (cmnt.span, cmnt.style));
465
466            if !is_last {
467                // Handle disabled lines with comments after the value, but before the comma.
468                if cmnt_before_next.is_some_and(|(cmnt_span, _)| {
469                    let span = self.cursor.span(cmnt_span.lo());
470                    self.inline_config.is_disabled(span)
471                        // NOTE: necessary workaround to patch this edgecase due to lack of spans for the commas.
472                        && self.snippet(span).is_some_and(|snip| !snip.contains(','))
473                }) {
474                    self.print_comments(
475                        next_pos,
476                        CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(),
477                    );
478                }
479                self.print_word(",");
480            }
481
482            if !is_last
483                && format.breaks_with_comments()
484                && cmnt_before_next.is_some_and(|(cmnt_span, cmnt_style)| {
485                    let disabled = self.inline_config.is_disabled(cmnt_span);
486                    (cmnt_style.is_mixed() && !disabled) || (cmnt_style.is_isolated() && disabled)
487                })
488            {
489                self.hardbreak(); // trailing and isolated comments already hardbreak
490            }
491
492            // Print trailing comments.
493            let comment_config = if !is_last || format.with_delimiters {
494                CommentConfig::skip_ws().mixed_no_break().mixed_prev_space()
495            } else {
496                CommentConfig::skip_ws().no_breaks().mixed_prev_space()
497            };
498            let with_trailing = self.print_comments(next_pos, comment_config).is_some();
499
500            if is_last && with_trailing {
501                if self.is_bol_or_only_ind() {
502                    // if a trailing comment is printed at the very end, we have to manually adjust
503                    // the offset to avoid having a double break.
504                    self.break_offset_if_not_bol(0, -self.ind, false);
505                } else {
506                    self.s.break_offset(SIZE_INFINITY as usize, -self.ind);
507                }
508                skip_last_break = true;
509                last_delimiter_break = false;
510            }
511
512            // Final break if needed before the next value.
513            if let Some(next_span) = next_span
514                && !self.is_bol_or_only_ind()
515                && !self.inline_config.is_disabled(next_span)
516                && !next_span.is_dummy()
517            {
518                format.print_break(false, values.len(), &mut self.s);
519                if manual_offset {
520                    self.s.offset(self.ind);
521                }
522            }
523        }
524
525        if format.is_compact() && !(format.breaks_with_comments() && has_comments) {
526            self.end();
527        }
528        if !skip_last_break {
529            if let Some(sym) = format.post_symbol() {
530                format.print_break(false, values.len(), &mut self.s);
531                self.s.offset(-self.ind);
532                self.word(sym);
533            } else {
534                format.print_break(true, values.len(), &mut self.s);
535                self.s.offset(-self.ind);
536            }
537        } else if is_single_without_cmnts && format.with_space {
538            self.nbsp();
539        } else if let Some(sym) = format.post_symbol() {
540            self.nbsp();
541            self.word(sym);
542        }
543
544        if !manual_offset {
545            self.end();
546        }
547        self.cursor.advance_to(pos_hi, true);
548
549        if last_delimiter_break {
550            format.print_break(true, values.len(), &mut self.s);
551        }
552    }
553
554    pub(super) fn print_path(&mut self, path: &'ast ast::PathSlice, consistent_break: bool) {
555        if consistent_break {
556            self.s.cbox(self.ind);
557        } else {
558            self.s.ibox(self.ind);
559        }
560        for (pos, ident) in path.segments().iter().delimited() {
561            self.print_ident(ident);
562            if !pos.is_last {
563                if !self.emit_or_revert {
564                    self.zerobreak();
565                }
566                self.word(".");
567            }
568        }
569        self.end();
570    }
571
572    pub(super) fn print_block_inner<T: Debug>(
573        &mut self,
574        block: &'ast [T],
575        block_format: BlockFormat,
576        mut print: impl FnMut(&mut Self, &'ast T),
577        mut get_block_span: impl FnMut(&'ast T) -> Span,
578        pos_hi: BytePos,
579    ) {
580        // Attempt to print in a single line.
581        if block_format.attempt_single_line() && block.len() == 1 {
582            self.print_single_line_block(block, block_format, print, get_block_span);
583            return;
584        }
585
586        // Empty blocks with comments require special attention.
587        if block.is_empty() {
588            self.print_empty_block(block_format, pos_hi);
589            return;
590        }
591
592        // update block depth
593        self.block_depth += 1;
594
595        // Print multiline block comments.
596        let block_lo = get_block_span(&block[0]).lo();
597        match block_format {
598            BlockFormat::NoBraces(None) => {
599                if !self.handle_span(self.cursor.span(block_lo), false) {
600                    self.print_comments(block_lo, CommentConfig::default());
601                }
602                self.s.cbox(0);
603            }
604            BlockFormat::NoBraces(Some(offset)) => {
605                let enabled =
606                    !self.inline_config.is_disabled(Span::new(block_lo, block_lo + BytePos(1)))
607                        && !self.handle_span(self.cursor.span(block_lo), true);
608                match self
609                    .peek_comment()
610                    .and_then(|cmnt| (cmnt.span.hi() < block_lo).then_some((cmnt.span, cmnt.style)))
611                {
612                    Some((span, style)) => {
613                        if enabled {
614                            // Inline config is not disabled and span not handled
615                            if !self.inline_config.is_disabled(span) || style.is_isolated() {
616                                self.cursor.advance_to(span.lo(), true);
617                                self.break_offset(SIZE_INFINITY as usize, offset);
618                            }
619                            if let Some(cmnt) = self.print_comments(
620                                block_lo,
621                                CommentConfig::skip_leading_ws(false).offset(offset),
622                            ) && !cmnt.is_mixed()
623                                && !cmnt.is_blank()
624                            {
625                                self.s.offset(offset);
626                            }
627                        } else if style.is_isolated() {
628                            self.print_sep_unhandled(Separator::Hardbreak);
629                            self.s.offset(offset);
630                        }
631                    }
632                    None => {
633                        if enabled {
634                            self.zerobreak();
635                            self.s.offset(offset);
636                        } else if self.cursor.enabled {
637                            self.print_sep_unhandled(Separator::Space);
638                            self.s.offset(offset);
639                            self.cursor.advance_to(block_lo, true);
640                        }
641                    }
642                }
643                self.s.cbox(self.ind);
644            }
645            _ => {
646                self.print_word("{");
647                self.s.cbox(self.ind);
648                if !self.handle_span(self.cursor.span(block_lo), false)
649                    && self
650                        .print_comments(block_lo, CommentConfig::default())
651                        .is_none_or(|cmnt| cmnt.is_mixed())
652                {
653                    self.hardbreak_if_nonempty();
654                }
655            }
656        }
657
658        // Print multiline block statements.
659        for (i, stmt) in block.iter().enumerate() {
660            let is_last = i == block.len() - 1;
661            print(self, stmt);
662
663            let is_disabled = self.inline_config.is_disabled(get_block_span(stmt));
664            let (next_enabled, next_lo) = if is_last {
665                (false, None)
666            } else {
667                let next_span = get_block_span(&block[i + 1]);
668                (
669                    !self.inline_config.is_disabled(next_span),
670                    self.peek_comment_before(next_span.lo()).is_none().then_some(next_span.lo()),
671                )
672            };
673
674            // when this stmt and the next one are enabled, break normally (except if last stmt)
675            if !is_disabled
676                && next_enabled
677                && (!is_last
678                    || self.peek_comment_before(pos_hi).is_some_and(|cmnt| cmnt.style.is_mixed()))
679            {
680                self.hardbreak_if_not_bol();
681                continue;
682            }
683            // when this stmt is disabled and the next one is enabled, break if there is no
684            // enabled preceding comment. Otherwise the breakpoint is handled by the comment.
685            if is_disabled
686                && next_enabled
687                && let Some(next_lo) = next_lo
688                && self
689                    .peek_comment_before(next_lo)
690                    .is_none_or(|cmnt| self.inline_config.is_disabled(cmnt.span))
691            {
692                self.hardbreak_if_not_bol()
693            }
694        }
695
696        self.print_comments(
697            pos_hi,
698            CommentConfig::skip_trailing_ws().mixed_no_break().mixed_prev_space(),
699        );
700        if !block_format.breaks() {
701            if !self.last_token_is_break() && !self.is_beginning_of_line() {
702                self.hardbreak();
703            }
704            self.s.offset(-self.ind);
705        }
706        self.end();
707        if block_format.with_braces() {
708            self.print_word("}");
709        }
710
711        // restore block depth
712        self.block_depth -= 1;
713    }
714
715    fn print_single_line_block<T: Debug>(
716        &mut self,
717        block: &'ast [T],
718        block_format: BlockFormat,
719        mut print: impl FnMut(&mut Self, &'ast T),
720        mut get_block_span: impl FnMut(&'ast T) -> Span,
721    ) {
722        self.s.cbox(self.ind);
723
724        match block_format {
725            BlockFormat::Compact(true) => {
726                self.scan_break(BreakToken { pre_break: Some("{"), ..Default::default() });
727                print(self, &block[0]);
728                self.print_comments(get_block_span(&block[0]).hi(), CommentConfig::default());
729                self.s.scan_break(BreakToken { post_break: Some("}"), ..Default::default() });
730                self.s.offset(-self.ind);
731            }
732            _ => {
733                self.word("{");
734                self.space();
735                print(self, &block[0]);
736                self.print_comments(get_block_span(&block[0]).hi(), CommentConfig::default());
737                self.space_if_not_bol();
738                self.s.offset(-self.ind);
739                self.word("}");
740            }
741        }
742
743        self.end();
744    }
745
746    fn print_empty_block(&mut self, block_format: BlockFormat, pos_hi: BytePos) {
747        let has_braces = block_format.with_braces();
748
749        // Trailing comments are printed after the block
750        if self.peek_comment_before(pos_hi).is_none_or(|c| c.style.is_trailing()) {
751            if self.config.bracket_spacing {
752                if has_braces {
753                    self.word("{ }");
754                } else {
755                    self.nbsp();
756                }
757            } else if has_braces {
758                self.word("{}");
759            }
760            self.print_comments(pos_hi, CommentConfig::skip_ws());
761            return;
762        }
763
764        // Non-trailing or mixed comments - print inside block
765        if has_braces {
766            self.word("{");
767        }
768        let offset = if let BlockFormat::NoBraces(Some(off)) = block_format { off } else { 0 };
769        self.print_comments(
770            pos_hi,
771            self.cmnt_config()
772                .offset(offset)
773                .mixed_no_break()
774                .mixed_prev_space()
775                .mixed_post_glued(),
776        );
777        self.print_comments(
778            pos_hi,
779            CommentConfig::default().mixed_no_break().mixed_prev_space().mixed_post_glued(),
780        );
781        if has_braces {
782            self.word("}");
783        }
784    }
785}
786
787/// Formatting style for comma-separated lists.
788#[derive(Debug, Clone, Copy, PartialEq, Eq)]
789pub(crate) struct ListFormat {
790    /// The core formatting strategy.
791    kind: ListFormatKind,
792    /// If `true`, it means that the list already carries indentation.
793    no_ind: bool,
794    /// If `true`, a single-element list may break.
795    break_single: bool,
796    /// If `true`, a comment within the list forces a break.
797    breaks_cmnts: bool,
798    /// If `true`, a space is added after the opening delimiter and before the closing one.
799    with_space: bool,
800    /// If `true`, the list is enclosed in delimiters.
801    with_delimiters: bool,
802}
803
804/// The kind of formatting style for a list.
805#[derive(Debug, Clone, Copy, PartialEq, Eq)]
806pub(crate) enum ListFormatKind {
807    /// Always breaks for multiple elements.
808    AlwaysBreak,
809    /// Breaks all elements if any break.
810    Consistent,
811    /// Attempts to fit all elements in one line, before breaking consistently.
812    Compact,
813    /// The list is printed inline, without breaks.
814    Inline,
815    /// Special formatting for Yul return values.
816    Yul { sym_prev: Option<&'static str>, sym_post: Option<&'static str> },
817}
818
819impl Default for ListFormat {
820    fn default() -> Self {
821        Self {
822            kind: ListFormatKind::Consistent,
823            no_ind: false,
824            break_single: false,
825            breaks_cmnts: false,
826            with_space: false,
827            with_delimiters: true,
828        }
829    }
830}
831
832impl ListFormat {
833    // -- GETTER METHODS -------------------------------------------------------
834    pub(crate) const fn prev_symbol(&self) -> Option<&'static str> {
835        if let ListFormatKind::Yul { sym_prev, .. } = self.kind { sym_prev } else { None }
836    }
837
838    pub(crate) const fn post_symbol(&self) -> Option<&'static str> {
839        if let ListFormatKind::Yul { sym_post, .. } = self.kind { sym_post } else { None }
840    }
841
842    pub(crate) const fn is_consistent(&self) -> bool {
843        matches!(self.kind, ListFormatKind::Consistent)
844    }
845
846    pub(crate) const fn is_compact(&self) -> bool {
847        matches!(self.kind, ListFormatKind::Compact)
848    }
849
850    pub(crate) const fn is_inline(&self) -> bool {
851        matches!(self.kind, ListFormatKind::Inline)
852    }
853
854    pub(crate) const fn breaks_with_comments(&self) -> bool {
855        self.breaks_cmnts
856    }
857
858    // -- BUILDER METHODS ------------------------------------------------------
859    pub(crate) fn inline() -> Self {
860        Self { kind: ListFormatKind::Inline, ..Default::default() }
861    }
862
863    pub(crate) fn consistent() -> Self {
864        Self { kind: ListFormatKind::Consistent, ..Default::default() }
865    }
866
867    pub(crate) fn compact() -> Self {
868        Self { kind: ListFormatKind::Compact, ..Default::default() }
869    }
870
871    pub(crate) fn always_break() -> Self {
872        Self {
873            kind: ListFormatKind::AlwaysBreak,
874            breaks_cmnts: true,
875            break_single: true,
876            with_delimiters: true,
877            ..Default::default()
878        }
879    }
880
881    pub(crate) fn yul(sym_prev: Option<&'static str>, sym_post: Option<&'static str>) -> Self {
882        Self {
883            kind: ListFormatKind::Yul { sym_prev, sym_post },
884            breaks_cmnts: true,
885            with_delimiters: true,
886            ..Default::default()
887        }
888    }
889
890    pub(crate) const fn without_ind(mut self, without: bool) -> Self {
891        if !self.is_inline() {
892            self.no_ind = without;
893        }
894        self
895    }
896
897    pub(crate) const fn break_single(mut self, value: bool) -> Self {
898        if !self.is_inline() {
899            self.break_single = value;
900        }
901        self
902    }
903
904    pub(crate) const fn break_cmnts(mut self) -> Self {
905        if !self.is_inline() {
906            self.breaks_cmnts = true;
907        }
908        self
909    }
910
911    pub(crate) const fn with_space(mut self) -> Self {
912        if !self.is_inline() {
913            self.with_space = true;
914        }
915        self
916    }
917
918    pub(crate) const fn with_delimiters(mut self, with: bool) -> Self {
919        if matches!(self.kind, ListFormatKind::Compact | ListFormatKind::Consistent) {
920            self.with_delimiters = with;
921        }
922        self
923    }
924
925    // -- PRINTER METHODS ------------------------------------------------------
926    pub(crate) fn print_break(&self, soft: bool, elems: usize, p: &mut Printer) {
927        match self.kind {
928            ListFormatKind::Inline => p.nbsp(), // CAREFUL: we can't use `pp.offset()` afterwards
929            ListFormatKind::AlwaysBreak if elems > 1 || (self.break_single && elems == 1) => {
930                p.hardbreak()
931            }
932            _ => {
933                if soft && !self.with_space {
934                    p.zerobreak();
935                } else {
936                    p.space();
937                }
938            }
939        }
940    }
941}
942
943/// Formatting style for code blocks
944#[derive(Debug, Clone, Copy, PartialEq, Eq)]
945#[expect(dead_code)]
946pub(crate) enum BlockFormat {
947    Regular,
948    /// Attempts to fit all elements in one line, before breaking consistently. Flags whether to
949    /// use braces or not.
950    Compact(bool),
951    /// Doesn't print braces. Flags the offset that should be applied before opening the block box.
952    /// Useful when the caller needs to manually handle the braces.
953    NoBraces(Option<isize>),
954}
955
956impl BlockFormat {
957    pub(crate) const fn with_braces(&self) -> bool {
958        !matches!(self, Self::NoBraces(_))
959    }
960    pub(crate) const fn breaks(&self) -> bool {
961        matches!(self, Self::NoBraces(None))
962    }
963
964    pub(crate) const fn attempt_single_line(&self) -> bool {
965        matches!(self, Self::Compact(_))
966    }
967}