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