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