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