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 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
605                    .peek_comment()
606                    .and_then(|cmnt| (cmnt.span.hi() < block_lo).then_some((cmnt.span, cmnt.style)))
607                {
608                    Some((span, style)) => {
609                        if enabled {
610                            // Inline config is not disabled and span not handled
611                            if !self.inline_config.is_disabled(span) || style.is_isolated() {
612                                self.cursor.advance_to(span.lo(), true);
613                                self.break_offset(SIZE_INFINITY as usize, offset);
614                            }
615                            if let Some(cmnt) = self.print_comments(
616                                block_lo,
617                                CommentConfig::skip_leading_ws(false).offset(offset),
618                            ) && !cmnt.is_mixed()
619                                && !cmnt.is_blank()
620                            {
621                                self.s.offset(offset);
622                            }
623                        } else if style.is_isolated() {
624                            self.print_sep_unhandled(Separator::Hardbreak);
625                            self.s.offset(offset);
626                        }
627                    }
628                    None => {
629                        if enabled {
630                            self.zerobreak();
631                            self.s.offset(offset);
632                        } else if self.cursor.enabled {
633                            self.print_sep_unhandled(Separator::Space);
634                            self.s.offset(offset);
635                            self.cursor.advance_to(block_lo, true);
636                        }
637                    }
638                }
639                self.s.cbox(self.ind);
640            }
641            _ => {
642                self.print_word("{");
643                self.s.cbox(self.ind);
644                if !self.handle_span(self.cursor.span(block_lo), false)
645                    && self
646                        .print_comments(block_lo, CommentConfig::default())
647                        .is_none_or(|cmnt| cmnt.is_mixed())
648                {
649                    self.hardbreak_if_nonempty();
650                }
651            }
652        }
653
654        // Print multiline block statements.
655        for (i, stmt) in block.iter().enumerate() {
656            let is_last = i == block.len() - 1;
657            print(self, stmt);
658
659            let is_disabled = self.inline_config.is_disabled(get_block_span(stmt));
660            let mut next_enabled = false;
661            let mut next_lo = None;
662            if !is_last {
663                let next_span = get_block_span(&block[i + 1]);
664                next_enabled = !self.inline_config.is_disabled(next_span);
665                next_lo =
666                    self.peek_comment_before(next_span.lo()).is_none().then_some(next_span.lo());
667            }
668
669            // when this stmt and the next one are enabled, break normally (except if last stmt)
670            if !is_disabled
671                && next_enabled
672                && (!is_last
673                    || self.peek_comment_before(pos_hi).is_some_and(|cmnt| cmnt.style.is_mixed()))
674            {
675                self.hardbreak_if_not_bol();
676                continue;
677            }
678            // when this stmt is disabled and the next one is enabled, break if there is no
679            // enabled preceding comment. Otherwise the breakpoint is handled by the comment.
680            if is_disabled
681                && next_enabled
682                && let Some(next_lo) = next_lo
683                && self
684                    .peek_comment_before(next_lo)
685                    .is_none_or(|cmnt| self.inline_config.is_disabled(cmnt.span))
686            {
687                self.hardbreak_if_not_bol()
688            }
689        }
690
691        self.print_comments(
692            pos_hi,
693            CommentConfig::skip_trailing_ws().mixed_no_break().mixed_prev_space(),
694        );
695        if !block_format.breaks() {
696            if !self.last_token_is_break() {
697                self.hardbreak();
698            }
699            self.s.offset(-self.ind);
700        }
701        self.end();
702        if block_format.with_braces() {
703            self.print_word("}");
704        }
705
706        // restore block depth
707        self.block_depth -= 1;
708    }
709
710    fn print_single_line_block<T: Debug>(
711        &mut self,
712        block: &'ast [T],
713        block_format: BlockFormat,
714        mut print: impl FnMut(&mut Self, &'ast T),
715        mut get_block_span: impl FnMut(&'ast T) -> Span,
716    ) {
717        self.s.cbox(self.ind);
718
719        match block_format {
720            BlockFormat::Compact(true) => {
721                self.scan_break(BreakToken { pre_break: Some("{"), ..Default::default() });
722                print(self, &block[0]);
723                self.print_comments(get_block_span(&block[0]).hi(), CommentConfig::default());
724                self.s.scan_break(BreakToken { post_break: Some("}"), ..Default::default() });
725                self.s.offset(-self.ind);
726            }
727            _ => {
728                self.word("{");
729                self.space();
730                print(self, &block[0]);
731                self.print_comments(get_block_span(&block[0]).hi(), CommentConfig::default());
732                self.space_if_not_bol();
733                self.s.offset(-self.ind);
734                self.word("}");
735            }
736        }
737
738        self.end();
739    }
740
741    fn print_empty_block(&mut self, block_format: BlockFormat, pos_hi: BytePos) {
742        let has_braces = block_format.with_braces();
743
744        // Trailing comments are printed after the block
745        if self.peek_comment_before(pos_hi).is_none_or(|c| c.style.is_trailing()) {
746            if self.config.bracket_spacing {
747                if has_braces {
748                    self.word("{ }");
749                } else {
750                    self.nbsp();
751                }
752            } else if has_braces {
753                self.word("{}");
754            }
755            self.print_comments(pos_hi, CommentConfig::skip_ws());
756            return;
757        }
758
759        // Non-trailing or mixed comments - print inside block
760        if has_braces {
761            self.word("{");
762        }
763        let mut offset = 0;
764        if let BlockFormat::NoBraces(Some(off)) = block_format {
765            offset = off;
766        }
767        self.print_comments(
768            pos_hi,
769            self.cmnt_config().offset(offset).mixed_no_break().mixed_prev_space().mixed_post_nbsp(),
770        );
771        self.print_comments(
772            pos_hi,
773            CommentConfig::default().mixed_no_break().mixed_prev_space().mixed_post_nbsp(),
774        );
775        if has_braces {
776            self.word("}");
777        }
778    }
779}
780
781/// Formatting style for comma-separated lists.
782#[derive(Debug, Clone, Copy, PartialEq, Eq)]
783pub(crate) struct ListFormat {
784    /// The core formatting strategy.
785    kind: ListFormatKind,
786    /// If `true`, it means that the list already carries indentation.
787    no_ind: bool,
788    /// If `true`, a single-element list may break.
789    break_single: bool,
790    /// If `true`, a comment within the list forces a break.
791    breaks_cmnts: bool,
792    /// If `true`, a space is added after the opening delimiter and before the closing one.
793    with_space: bool,
794    /// If `true`, the list is enclosed in delimiters.
795    with_delimiters: bool,
796}
797
798/// The kind of formatting style for a list.
799#[derive(Debug, Clone, Copy, PartialEq, Eq)]
800pub(crate) enum ListFormatKind {
801    /// Always breaks for multiple elements.
802    AlwaysBreak,
803    /// Breaks all elements if any break.
804    Consistent,
805    /// Attempts to fit all elements in one line, before breaking consistently.
806    Compact,
807    /// The list is printed inline, without breaks.
808    Inline,
809    /// Special formatting for Yul return values.
810    Yul { sym_prev: Option<&'static str>, sym_post: Option<&'static str> },
811}
812
813impl Default for ListFormat {
814    fn default() -> Self {
815        Self {
816            kind: ListFormatKind::Consistent,
817            no_ind: false,
818            break_single: false,
819            breaks_cmnts: false,
820            with_space: false,
821            with_delimiters: true,
822        }
823    }
824}
825
826impl ListFormat {
827    // -- GETTER METHODS -------------------------------------------------------
828    pub(crate) fn prev_symbol(&self) -> Option<&'static str> {
829        if let ListFormatKind::Yul { sym_prev, .. } = self.kind { sym_prev } else { None }
830    }
831
832    pub(crate) fn post_symbol(&self) -> Option<&'static str> {
833        if let ListFormatKind::Yul { sym_post, .. } = self.kind { sym_post } else { None }
834    }
835
836    pub(crate) fn is_consistent(&self) -> bool {
837        matches!(self.kind, ListFormatKind::Consistent)
838    }
839
840    pub(crate) fn is_compact(&self) -> bool {
841        matches!(self.kind, ListFormatKind::Compact)
842    }
843
844    pub(crate) fn is_inline(&self) -> bool {
845        matches!(self.kind, ListFormatKind::Inline)
846    }
847
848    pub(crate) fn breaks_with_comments(&self) -> bool {
849        self.breaks_cmnts
850    }
851
852    // -- BUILDER METHODS ------------------------------------------------------
853    pub(crate) fn inline() -> Self {
854        Self { kind: ListFormatKind::Inline, ..Default::default() }
855    }
856
857    pub(crate) fn consistent() -> Self {
858        Self { kind: ListFormatKind::Consistent, ..Default::default() }
859    }
860
861    pub(crate) fn compact() -> Self {
862        Self { kind: ListFormatKind::Compact, ..Default::default() }
863    }
864
865    pub(crate) fn always_break() -> Self {
866        Self {
867            kind: ListFormatKind::AlwaysBreak,
868            breaks_cmnts: true,
869            break_single: true,
870            with_delimiters: true,
871            ..Default::default()
872        }
873    }
874
875    pub(crate) fn yul(sym_prev: Option<&'static str>, sym_post: Option<&'static str>) -> Self {
876        Self {
877            kind: ListFormatKind::Yul { sym_prev, sym_post },
878            breaks_cmnts: true,
879            with_delimiters: true,
880            ..Default::default()
881        }
882    }
883
884    pub(crate) fn without_ind(mut self, without: bool) -> Self {
885        if !matches!(self.kind, ListFormatKind::Inline) {
886            self.no_ind = without;
887        }
888        self
889    }
890
891    pub(crate) fn break_single(mut self, value: bool) -> Self {
892        if !matches!(self.kind, ListFormatKind::Inline) {
893            self.break_single = value;
894        }
895        self
896    }
897
898    pub(crate) fn break_cmnts(mut self) -> Self {
899        if !matches!(self.kind, ListFormatKind::Inline) {
900            self.breaks_cmnts = true;
901        }
902        self
903    }
904
905    pub(crate) fn with_space(mut self) -> Self {
906        if !matches!(self.kind, ListFormatKind::Inline) {
907            self.with_space = true;
908        }
909        self
910    }
911
912    pub(crate) fn with_delimiters(mut self, with: bool) -> Self {
913        if matches!(self.kind, ListFormatKind::Compact | ListFormatKind::Consistent) {
914            self.with_delimiters = with;
915        }
916        self
917    }
918
919    // -- PRINTER METHODS ------------------------------------------------------
920    pub(crate) fn print_break(&self, soft: bool, elems: usize, p: &mut Printer) {
921        match self.kind {
922            ListFormatKind::Inline => p.nbsp(), // CAREFUL: we can't use `pp.offset()` afterwards
923            ListFormatKind::AlwaysBreak if elems > 1 || (self.break_single && elems == 1) => {
924                p.hardbreak()
925            }
926            _ => {
927                if soft && !self.with_space {
928                    p.zerobreak();
929                } else {
930                    p.space();
931                }
932            }
933        }
934    }
935}
936
937/// Formatting style for code blocks
938#[derive(Debug, Clone, Copy, PartialEq, Eq)]
939#[expect(dead_code)]
940pub(crate) enum BlockFormat {
941    Regular,
942    /// Attempts to fit all elements in one line, before breaking consistently. Flags whether to
943    /// use braces or not.
944    Compact(bool),
945    /// Doesn't print braces. Flags the offset that should be applied before opening the block box.
946    /// Useful when the caller needs to manually handle the braces.
947    NoBraces(Option<isize>),
948}
949
950impl BlockFormat {
951    pub(crate) fn with_braces(&self) -> bool {
952        !matches!(self, Self::NoBraces(_))
953    }
954    pub(crate) fn breaks(&self) -> bool {
955        matches!(self, Self::NoBraces(None))
956    }
957
958    pub(crate) fn attempt_single_line(&self) -> bool {
959        matches!(self, Self::Compact(_))
960    }
961}