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                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                        self.neverbreak();
42                    } else {
43                        if !self.print_trailing_comment(span.hi(), None) {
44                            self.space_if_not_bol();
45                        }
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.split_once(['e', 'E']).unwrap_or((source, ""))
98        } else {
99            (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 skip_break = if state.peek_comment_before(span.hi()).is_some() {
247                state.hardbreak();
248                false
249            } else {
250                true
251            };
252
253            state.print_comments(span.lo(), CommentConfig::skip_ws().mixed_prev_space());
254            print(state, &values[0]);
255
256            if !state.print_trailing_comment(span.hi(), None) && skip_break {
257                state.neverbreak();
258            } else {
259                state.break_offset_if_not_bol(0, -state.ind, false);
260            }
261            state.end();
262        });
263    }
264
265    pub(super) fn print_array<'a, T, P, S>(
266        &mut self,
267        values: &'a [T],
268        span: Span,
269        print: P,
270        get_span: S,
271    ) where
272        P: FnMut(&mut Self, &'a T),
273        S: FnMut(&T) -> Span,
274    {
275        if self.handle_span(span, false) {
276            return;
277        }
278
279        self.print_word("[");
280        self.commasep(values, span.lo(), span.hi(), print, get_span, ListFormat::compact());
281        self.print_word("]");
282    }
283
284    pub(super) fn commasep_opening_logic<T, S>(
285        &mut self,
286        values: &[T],
287        mut get_span: S,
288        format: ListFormat,
289        manual_opening: bool,
290    ) -> bool
291    where
292        S: FnMut(&T) -> Span,
293    {
294        let Some(span) = values.first().map(&mut get_span) else {
295            return false;
296        };
297
298        // If first item is uninformed (just a comma), and it has its own comment, skip it.
299        // It will be dealt with when printing the item in the main loop of `commasep`.
300        if span.is_dummy()
301            && let Some(next_pos) = values.get(1).map(|v| get_span(v).lo())
302            && self.peek_comment_before(next_pos).is_some()
303        {
304            return true;
305        }
306
307        // Check for comments before the first item.
308        if let Some((cmnt_span, cmnt_style)) =
309            self.peek_comment_before(span.lo()).map(|c| (c.span, c.style))
310        {
311            let cmnt_disabled = self.inline_config.is_disabled(cmnt_span);
312            // Handle special formatting for disabled code with isolated comments.
313            if self.cursor.enabled && cmnt_disabled && cmnt_style.is_isolated() {
314                self.print_sep(Separator::Hardbreak);
315                if !format.with_delimiters {
316                    self.s.offset(self.ind);
317                }
318            };
319
320            // If manual opening flag is passed, we simply force the break, and skip the comment.
321            // It will be dealt with when printing the item in the main loop of `commasep`.
322            if manual_opening {
323                self.hardbreak();
324                self.s.offset(self.ind);
325                return true;
326            }
327
328            let cmnt_config = if format.with_delimiters {
329                CommentConfig::skip_ws().mixed_no_break().mixed_prev_space()
330            } else {
331                CommentConfig::skip_ws().no_breaks().mixed_prev_space().offset(self.ind)
332            };
333            // Apply spacing based on comment styles.
334            if let Some(last_style) = self.print_comments(span.lo(), cmnt_config) {
335                match (cmnt_style.is_mixed(), last_style.is_mixed()) {
336                    (true, true) => {
337                        if format.breaks_cmnts {
338                            self.hardbreak();
339                        } else {
340                            self.space();
341                        }
342                        if !format.with_delimiters && !cmnt_disabled {
343                            self.s.offset(self.ind);
344                        }
345                    }
346                    (false, true) => {
347                        self.nbsp();
348                    }
349                    (false, false) if !format.with_delimiters && !cmnt_disabled => {
350                        self.hardbreak();
351                        self.s.offset(self.ind);
352                    }
353                    _ => {}
354                }
355            }
356            if self.cursor.enabled {
357                self.cursor.advance_to(span.lo(), true);
358            }
359            return true;
360        }
361
362        if self.cursor.enabled {
363            self.cursor.advance_to(span.lo(), true);
364        }
365
366        if !values.is_empty() && !format.with_delimiters {
367            format.print_break(true, values.len(), &mut self.s);
368            self.s.offset(self.ind);
369            return true;
370        }
371
372        false
373    }
374
375    pub(super) fn commasep<'a, T, P, S>(
376        &mut self,
377        values: &'a [T],
378        _pos_lo: BytePos,
379        pos_hi: BytePos,
380        mut print: P,
381        mut get_span: S,
382        format: ListFormat,
383    ) where
384        P: FnMut(&mut Self, &'a T),
385        S: FnMut(&T) -> Span,
386    {
387        if values.is_empty() {
388            return;
389        }
390
391        // We can't simply check `peek_comment_before(pos_hi)` cause we would also account for
392        // comments in the child expression, and those don't matter.
393        let has_comments =
394            // check for comments before the first element
395            self.peek_comment_before(get_span(&values[0]).lo()).is_some() ||
396            // check for comments between elements
397            values.windows(2).any(|w| self.peek_comment_between(get_span(&w[0]).hi(), get_span(&w[1]).lo()).is_some()) ||
398            // check for comments after the last element
399            self.peek_comment_between(get_span(values.last().unwrap()).hi(), pos_hi).is_some();
400
401        // For calls with opts and args, which should break consistently, we need to skip the
402        // wrapping cbox to prioritize call args breaking before the call opts. Because of that, we
403        // must manually offset the breaks between args, so that they are properly indented.
404        let manual_opening =
405            format.is_consistent() && !format.with_delimiters && self.call_with_opts_and_args;
406        // When there are comments, we can preserve the cbox, as they will make it break
407        let manual_offset = !has_comments && manual_opening;
408
409        let is_single_without_cmnts = values.len() == 1 && !format.break_single && !has_comments;
410        let skip_first_break = if format.with_delimiters || format.is_inline() {
411            self.s.cbox(if format.no_ind { 0 } else { self.ind });
412            if is_single_without_cmnts {
413                true
414            } else {
415                self.commasep_opening_logic(values, &mut get_span, format, manual_opening)
416            }
417        } else {
418            let res = self.commasep_opening_logic(values, &mut get_span, format, manual_opening);
419            if !manual_offset {
420                self.s.cbox(if format.no_ind { 0 } else { self.ind });
421            }
422            res
423        };
424
425        if let Some(sym) = format.prev_symbol() {
426            self.word_space(sym);
427        } else if is_single_without_cmnts && format.with_space {
428            self.nbsp();
429        } else if !skip_first_break && !format.is_inline() {
430            format.print_break(true, values.len(), &mut self.s);
431            if manual_offset {
432                self.s.offset(self.ind);
433            }
434        }
435
436        if format.is_compact() && !(format.breaks_with_comments() && has_comments) {
437            self.s.cbox(0);
438        }
439
440        let mut last_delimiter_break = !format.with_delimiters;
441        let mut skip_last_break =
442            is_single_without_cmnts || !format.with_delimiters || format.is_inline();
443        for (i, value) in values.iter().enumerate() {
444            let is_last = i == values.len() - 1;
445            if self
446                .print_comments(get_span(value).lo(), CommentConfig::skip_ws().mixed_prev_space())
447                .is_some_and(|cmnt| cmnt.is_mixed())
448                && format.breaks_cmnts
449            {
450                self.hardbreak(); // trailing and isolated comments already hardbreak
451            }
452
453            // Avoid printing the last uninformed item, so that we can handle line breaks.
454            if !(is_last && get_span(value).is_dummy()) {
455                print(self, value);
456            }
457
458            let next_span = if is_last { None } else { Some(get_span(&values[i + 1])) };
459            let next_pos = next_span.map(Span::lo).unwrap_or(pos_hi);
460            let cmnt_before_next =
461                self.peek_comment_before(next_pos).map(|cmnt| (cmnt.span, cmnt.style));
462
463            if !is_last {
464                // Handle disabled lines with comments after the value, but before the comma.
465                if cmnt_before_next.is_some_and(|(cmnt_span, _)| {
466                    let span = self.cursor.span(cmnt_span.lo());
467                    self.inline_config.is_disabled(span)
468                        // NOTE: necessary workaround to patch this edgecase due to lack of spans for the commas.
469                        && self.sm.span_to_snippet(span).is_ok_and(|snip| !snip.contains(','))
470                }) {
471                    self.print_comments(
472                        next_pos,
473                        CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(),
474                    );
475                }
476                self.print_word(",");
477            }
478
479            if !is_last
480                && format.breaks_cmnts
481                && cmnt_before_next.is_some_and(|(cmnt_span, cmnt_style)| {
482                    let disabled = self.inline_config.is_disabled(cmnt_span);
483                    (cmnt_style.is_mixed() && !disabled) || (cmnt_style.is_isolated() && disabled)
484                })
485            {
486                self.hardbreak(); // trailing and isolated comments already hardbreak
487            }
488
489            // Print trailing comments.
490            let comment_config = if !is_last || format.with_delimiters {
491                CommentConfig::skip_ws().mixed_no_break().mixed_prev_space()
492            } else {
493                CommentConfig::skip_ws().no_breaks().mixed_prev_space()
494            };
495            let with_trailing = self.print_comments(next_pos, comment_config).is_some();
496
497            if is_last && with_trailing {
498                if self.is_bol_or_only_ind() {
499                    // if a trailing comment is printed at the very end, we have to manually adjust
500                    // the offset to avoid having a double break.
501                    self.break_offset_if_not_bol(0, -self.ind, false);
502                } else {
503                    self.s.break_offset(SIZE_INFINITY as usize, -self.ind);
504                }
505                skip_last_break = true;
506                last_delimiter_break = false;
507            }
508
509            // Final break if needed before the next value.
510            if let Some(next_span) = next_span
511                && !self.is_bol_or_only_ind()
512                && !self.inline_config.is_disabled(next_span)
513                && !next_span.is_dummy()
514            {
515                format.print_break(false, values.len(), &mut self.s);
516                if manual_offset {
517                    self.s.offset(self.ind);
518                }
519            }
520        }
521
522        if format.is_compact() && !(format.breaks_with_comments() && has_comments) {
523            self.end();
524        }
525        if !skip_last_break {
526            if let Some(sym) = format.post_symbol() {
527                format.print_break(false, values.len(), &mut self.s);
528                self.s.offset(-self.ind);
529                self.word(sym);
530            } else {
531                format.print_break(true, values.len(), &mut self.s);
532                self.s.offset(-self.ind);
533            }
534        } else if is_single_without_cmnts && format.with_space {
535            self.nbsp();
536        } else if let Some(sym) = format.post_symbol() {
537            self.nbsp();
538            self.word(sym);
539        }
540
541        if !manual_offset {
542            self.end();
543        }
544        self.cursor.advance_to(pos_hi, true);
545
546        if last_delimiter_break {
547            format.print_break(true, values.len(), &mut self.s);
548        }
549    }
550
551    pub(super) fn print_path(&mut self, path: &'ast ast::PathSlice, consistent_break: bool) {
552        if consistent_break {
553            self.s.cbox(self.ind);
554        } else {
555            self.s.ibox(self.ind);
556        }
557        for (pos, ident) in path.segments().iter().delimited() {
558            self.print_ident(ident);
559            if !pos.is_last {
560                if !self.emit_or_revert {
561                    self.zerobreak();
562                }
563                self.word(".");
564            }
565        }
566        self.end();
567    }
568
569    pub(super) fn print_block_inner<T: Debug>(
570        &mut self,
571        block: &'ast [T],
572        block_format: BlockFormat,
573        mut print: impl FnMut(&mut Self, &'ast T),
574        mut get_block_span: impl FnMut(&'ast T) -> Span,
575        pos_hi: BytePos,
576    ) {
577        // Attempt to print in a single line.
578        if block_format.attempt_single_line() && block.len() == 1 {
579            self.print_single_line_block(block, block_format, print, get_block_span);
580            return;
581        }
582
583        // Empty blocks with comments require special attention.
584        if block.is_empty() {
585            self.print_empty_block(block_format, pos_hi);
586            return;
587        }
588
589        // update block depth
590        self.block_depth += 1;
591
592        // Print multiline block comments.
593        let block_lo = get_block_span(&block[0]).lo();
594        match block_format {
595            BlockFormat::NoBraces(None) => {
596                if !self.handle_span(self.cursor.span(block_lo), false) {
597                    self.print_comments(block_lo, CommentConfig::default());
598                }
599                self.s.cbox(0);
600            }
601            BlockFormat::NoBraces(Some(offset)) => {
602                let enabled =
603                    !self.inline_config.is_disabled(Span::new(block_lo, block_lo + BytePos(1)))
604                        && !self.handle_span(self.cursor.span(block_lo), true);
605                match self
606                    .peek_comment()
607                    .and_then(|cmnt| (cmnt.span.hi() < block_lo).then_some((cmnt.span, cmnt.style)))
608                {
609                    Some((span, style)) => {
610                        if enabled {
611                            // Inline config is not disabled and span not handled
612                            if !self.inline_config.is_disabled(span) || style.is_isolated() {
613                                self.cursor.advance_to(span.lo(), true);
614                                self.break_offset(SIZE_INFINITY as usize, offset);
615                            }
616                            if let Some(cmnt) = self.print_comments(
617                                block_lo,
618                                CommentConfig::skip_leading_ws(false).offset(offset),
619                            ) && !cmnt.is_mixed()
620                                && !cmnt.is_blank()
621                            {
622                                self.s.offset(offset);
623                            }
624                        } else if style.is_isolated() {
625                            self.print_sep_unhandled(Separator::Hardbreak);
626                            self.s.offset(offset);
627                        }
628                    }
629                    None => {
630                        if enabled {
631                            self.zerobreak();
632                            self.s.offset(offset);
633                        } else if self.cursor.enabled {
634                            self.print_sep_unhandled(Separator::Space);
635                            self.s.offset(offset);
636                            self.cursor.advance_to(block_lo, true);
637                        }
638                    }
639                }
640                self.s.cbox(self.ind);
641            }
642            _ => {
643                self.print_word("{");
644                self.s.cbox(self.ind);
645                if !self.handle_span(self.cursor.span(block_lo), false)
646                    && self
647                        .print_comments(block_lo, CommentConfig::default())
648                        .is_none_or(|cmnt| cmnt.is_mixed())
649                {
650                    self.hardbreak_if_nonempty();
651                }
652            }
653        }
654
655        // Print multiline block statements.
656        for (i, stmt) in block.iter().enumerate() {
657            let is_last = i == block.len() - 1;
658            print(self, stmt);
659
660            let is_disabled = self.inline_config.is_disabled(get_block_span(stmt));
661            let (next_enabled, next_lo) = if is_last {
662                (false, None)
663            } else {
664                let next_span = get_block_span(&block[i + 1]);
665                (
666                    !self.inline_config.is_disabled(next_span),
667                    self.peek_comment_before(next_span.lo()).is_none().then_some(next_span.lo()),
668                )
669            };
670
671            // when this stmt and the next one are enabled, break normally (except if last stmt)
672            if !is_disabled
673                && next_enabled
674                && (!is_last
675                    || self.peek_comment_before(pos_hi).is_some_and(|cmnt| cmnt.style.is_mixed()))
676            {
677                self.hardbreak_if_not_bol();
678                continue;
679            }
680            // when this stmt is disabled and the next one is enabled, break if there is no
681            // enabled preceding comment. Otherwise the breakpoint is handled by the comment.
682            if is_disabled
683                && next_enabled
684                && let Some(next_lo) = next_lo
685                && self
686                    .peek_comment_before(next_lo)
687                    .is_none_or(|cmnt| self.inline_config.is_disabled(cmnt.span))
688            {
689                self.hardbreak_if_not_bol()
690            }
691        }
692
693        self.print_comments(
694            pos_hi,
695            CommentConfig::skip_trailing_ws().mixed_no_break().mixed_prev_space(),
696        );
697        if !block_format.breaks() {
698            if !self.last_token_is_break() {
699                self.hardbreak();
700            }
701            self.s.offset(-self.ind);
702        }
703        self.end();
704        if block_format.with_braces() {
705            self.print_word("}");
706        }
707
708        // restore block depth
709        self.block_depth -= 1;
710    }
711
712    fn print_single_line_block<T: Debug>(
713        &mut self,
714        block: &'ast [T],
715        block_format: BlockFormat,
716        mut print: impl FnMut(&mut Self, &'ast T),
717        mut get_block_span: impl FnMut(&'ast T) -> Span,
718    ) {
719        self.s.cbox(self.ind);
720
721        match block_format {
722            BlockFormat::Compact(true) => {
723                self.scan_break(BreakToken { pre_break: Some("{"), ..Default::default() });
724                print(self, &block[0]);
725                self.print_comments(get_block_span(&block[0]).hi(), CommentConfig::default());
726                self.s.scan_break(BreakToken { post_break: Some("}"), ..Default::default() });
727                self.s.offset(-self.ind);
728            }
729            _ => {
730                self.word("{");
731                self.space();
732                print(self, &block[0]);
733                self.print_comments(get_block_span(&block[0]).hi(), CommentConfig::default());
734                self.space_if_not_bol();
735                self.s.offset(-self.ind);
736                self.word("}");
737            }
738        }
739
740        self.end();
741    }
742
743    fn print_empty_block(&mut self, block_format: BlockFormat, pos_hi: BytePos) {
744        let has_braces = block_format.with_braces();
745
746        // Trailing comments are printed after the block
747        if self.peek_comment_before(pos_hi).is_none_or(|c| c.style.is_trailing()) {
748            if self.config.bracket_spacing {
749                if has_braces {
750                    self.word("{ }");
751                } else {
752                    self.nbsp();
753                }
754            } else if has_braces {
755                self.word("{}");
756            }
757            self.print_comments(pos_hi, CommentConfig::skip_ws());
758            return;
759        }
760
761        // Non-trailing or mixed comments - print inside block
762        if has_braces {
763            self.word("{");
764        }
765        let offset = if let BlockFormat::NoBraces(Some(off)) = block_format { off } else { 0 };
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) const 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) const 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) const fn is_consistent(&self) -> bool {
836        matches!(self.kind, ListFormatKind::Consistent)
837    }
838
839    pub(crate) const fn is_compact(&self) -> bool {
840        matches!(self.kind, ListFormatKind::Compact)
841    }
842
843    pub(crate) const fn is_inline(&self) -> bool {
844        matches!(self.kind, ListFormatKind::Inline)
845    }
846
847    pub(crate) const 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) const 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) const 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) const 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) const 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) const 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) const fn with_braces(&self) -> bool {
951        !matches!(self, Self::NoBraces(_))
952    }
953    pub(crate) const fn breaks(&self) -> bool {
954        matches!(self, Self::NoBraces(None))
955    }
956
957    pub(crate) const fn attempt_single_line(&self) -> bool {
958        matches!(self, Self::Compact(_))
959    }
960}