Skip to main content

forge_fmt/state/
sol.rs

1#![allow(clippy::too_many_arguments)]
2
3use super::{
4    ChainedNamedCall, CommentConfig, Separator, State,
5    common::{BlockFormat, ListFormat},
6};
7use crate::{
8    pp::SIZE_INFINITY,
9    state::{CallContext, common::LitExt},
10};
11use foundry_common::{comments::Comment, iter::IterDelimited};
12use foundry_config::fmt::{self as config, MultilineFuncHeaderStyle};
13use solar::{
14    ast::BoxSlice,
15    interface::SpannedOption,
16    parse::{
17        ast::{self, Span},
18        interface::BytePos,
19    },
20};
21use std::{collections::HashMap, fmt::Debug};
22
23#[rustfmt::skip]
24macro_rules! get_span {
25    () => { |value| value.span };
26    (()) => { |value| value.span() };
27}
28
29/// Language-specific pretty printing: Solidity.
30impl<'ast> State<'_, 'ast> {
31    pub(crate) fn print_source_unit(&mut self, source_unit: &'ast ast::SourceUnit<'ast>) {
32        // Figure out if the cursor needs to check for CR (`\r`).
33        if let Some(item) = source_unit.items.first() {
34            self.check_crlf(item.span.to(source_unit.items.last().unwrap().span));
35        }
36
37        let mut items = source_unit.items.iter().peekable();
38        let mut is_first = true;
39        while let Some(item) = items.next() {
40            // If imports shouldn't be sorted, or if the item is not an import, print it directly.
41            if !self.config.sort_imports || !matches!(item.kind, ast::ItemKind::Import(_)) {
42                self.print_item(item, is_first);
43                is_first = false;
44                if let Some(next_item) = items.peek() {
45                    self.separate_items(next_item, false);
46                }
47                continue;
48            }
49
50            // Otherwise, collect a group of consecutive imports and sort them before printing.
51            let mut import_group = vec![item];
52            while let Some(next_item) = items.peek() {
53                // Groups end when the next item is not an import or when there is a blank line.
54                if !matches!(next_item.kind, ast::ItemKind::Import(_))
55                    || self.has_comment_between(item.span.hi(), next_item.span.lo())
56                {
57                    break;
58                }
59                import_group.push(items.next().unwrap());
60            }
61
62            import_group.sort_by_key(|item| {
63                if let ast::ItemKind::Import(import) = &item.kind {
64                    import.path.value.as_str()
65                } else {
66                    unreachable!("Expected an import item")
67                }
68            });
69
70            for (pos, group_item) in import_group.iter().delimited() {
71                self.print_item(group_item, is_first);
72                is_first = false;
73
74                if !pos.is_last {
75                    self.hardbreak_if_not_bol();
76                }
77            }
78            if let Some(next_item) = items.peek() {
79                self.separate_items(next_item, false);
80            }
81        }
82
83        self.print_remaining_comments(is_first);
84    }
85
86    /// Prints a hardbreak if the item needs an isolated line break.
87    fn separate_items(&mut self, next_item: &'ast ast::Item<'ast>, advance: bool) {
88        if !item_needs_iso(&next_item.kind) {
89            return;
90        }
91        let span = next_item.span;
92
93        let cmnts = self
94            .comments
95            .iter()
96            .filter_map(|c| (c.pos() < span.lo()).then_some(c.style))
97            .collect::<Vec<_>>();
98
99        if let Some(first) = cmnts.first()
100            && let Some(last) = cmnts.last()
101        {
102            if !(first.is_blank() || last.is_blank()) {
103                self.hardbreak();
104                return;
105            }
106            if advance {
107                if self.peek_comment_before(span.lo()).is_some() {
108                    self.print_comments(span.lo(), CommentConfig::default());
109                } else if self.inline_config.is_disabled(span.shrink_to_lo()) {
110                    self.hardbreak();
111                    self.cursor.advance_to(span.lo(), true);
112                }
113            }
114        } else {
115            self.hardbreak();
116        }
117    }
118
119    fn print_item(&mut self, item: &'ast ast::Item<'ast>, skip_ws: bool) {
120        let ast::Item { ref docs, span, ref kind } = *item;
121        self.print_docs(docs);
122
123        if self.handle_span(item.span, skip_ws) {
124            if !self.print_trailing_comment(span.hi(), None) {
125                self.print_sep(Separator::Hardbreak);
126            }
127            return;
128        }
129
130        if self
131            .print_comments(
132                span.lo(),
133                if skip_ws {
134                    CommentConfig::skip_leading_ws(false)
135                } else {
136                    CommentConfig::default()
137                },
138            )
139            .is_some_and(|cmnt| cmnt.is_mixed())
140        {
141            self.zerobreak();
142        }
143
144        match kind {
145            ast::ItemKind::Pragma(pragma) => self.print_pragma(pragma),
146            ast::ItemKind::Import(import) => self.print_import(import),
147            ast::ItemKind::Using(using) => self.print_using(using),
148            ast::ItemKind::Contract(contract) => self.print_contract(contract, span),
149            ast::ItemKind::Function(func) => self.print_function(func),
150            ast::ItemKind::Variable(var) => self.print_var_def(var),
151            ast::ItemKind::Struct(strukt) => self.print_struct(strukt, span),
152            ast::ItemKind::Enum(enm) => self.print_enum(enm, span),
153            ast::ItemKind::Udvt(udvt) => self.print_udvt(udvt),
154            ast::ItemKind::Error(err) => self.print_error(err),
155            ast::ItemKind::Event(event) => self.print_event(event),
156        }
157
158        self.cursor.advance_to(span.hi(), true);
159        self.print_comments(span.hi(), CommentConfig::default());
160        self.print_trailing_comment(span.hi(), None);
161        self.hardbreak_if_not_bol();
162        self.cursor.next_line(self.is_at_crlf());
163    }
164
165    fn print_pragma(&mut self, pragma: &'ast ast::PragmaDirective<'ast>) {
166        self.word("pragma ");
167        match &pragma.tokens {
168            ast::PragmaTokens::Version(ident, semver_req) => {
169                self.print_ident(ident);
170                self.nbsp();
171                self.word(semver_req.to_string());
172            }
173            ast::PragmaTokens::Custom(a, b) => {
174                self.print_ident_or_strlit(a);
175                if let Some(b) = b {
176                    self.nbsp();
177                    self.print_ident_or_strlit(b);
178                }
179            }
180            ast::PragmaTokens::Verbatim(tokens) => {
181                self.print_tokens(tokens);
182            }
183        }
184        self.word(";");
185    }
186
187    fn print_commasep_aliases<'a, I>(&mut self, aliases: I)
188    where
189        I: Iterator<Item = &'a (ast::Ident, Option<ast::Ident>)>,
190        'ast: 'a,
191    {
192        for (pos, (ident, alias)) in aliases.delimited() {
193            self.print_ident(ident);
194            if let Some(alias) = alias {
195                self.word(" as ");
196                self.print_ident(alias);
197            }
198            if !pos.is_last {
199                self.word(",");
200                self.space();
201            }
202        }
203    }
204
205    fn print_import(&mut self, import: &'ast ast::ImportDirective<'ast>) {
206        let ast::ImportDirective { path, items } = import;
207        self.word("import ");
208
209        use ast::ImportItems;
210        use config::NamespaceImportStyle as NIStyle;
211
212        match (items, self.config.namespace_import_style) {
213            (ImportItems::Plain(None), _) => {
214                self.print_ast_str_lit(path);
215            }
216
217            (ImportItems::Plain(Some(source_alias)), NIStyle::Preserve | NIStyle::PreferPlain)
218            | (ImportItems::Glob(source_alias), NIStyle::PreferPlain) => {
219                self.print_ast_str_lit(path);
220                self.word(" as ");
221                self.print_ident(source_alias);
222            }
223
224            (ImportItems::Glob(source_alias), NIStyle::Preserve | NIStyle::PreferGlob)
225            | (ImportItems::Plain(Some(source_alias)), NIStyle::PreferGlob) => {
226                self.word("*");
227                self.word(" as ");
228                self.print_ident(source_alias);
229                self.word(" from ");
230                self.print_ast_str_lit(path);
231            }
232
233            (ImportItems::Aliases(aliases), _) => {
234                // Check if we should keep single imports on one line
235                let use_single_line = self.config.single_line_imports && aliases.len() == 1;
236
237                if use_single_line {
238                    self.word("{");
239                    if self.config.bracket_spacing {
240                        self.nbsp();
241                    }
242                } else {
243                    self.s.cbox(self.ind);
244                    self.word("{");
245                    self.braces_break();
246                }
247
248                if self.config.sort_imports {
249                    let mut sorted: Vec<_> = aliases.iter().collect();
250                    sorted.sort_by_key(|(ident, _alias)| ident.name.as_str());
251                    self.print_commasep_aliases(sorted.into_iter());
252                } else {
253                    self.print_commasep_aliases(aliases.iter());
254                };
255
256                if use_single_line {
257                    if self.config.bracket_spacing {
258                        self.nbsp();
259                    }
260                    self.word("}");
261                } else {
262                    self.braces_break();
263                    self.s.offset(-self.ind);
264                    self.word("}");
265                    self.end();
266                }
267                self.word(" from ");
268                self.print_ast_str_lit(path);
269            }
270        }
271        self.word(";");
272    }
273
274    fn print_using(&mut self, using: &'ast ast::UsingDirective<'ast>) {
275        let ast::UsingDirective { list, ty, global } = using;
276        self.word("using ");
277        match list {
278            ast::UsingList::Single(path) => self.print_path(path, true),
279            ast::UsingList::Multiple(items) => {
280                self.s.cbox(self.ind);
281                self.word("{");
282                self.braces_break();
283                for (pos, (path, op)) in items.iter().delimited() {
284                    self.print_path(path, true);
285                    if let Some(op) = op {
286                        self.word(" as ");
287                        self.word(op.to_str());
288                    }
289                    if !pos.is_last {
290                        self.word(",");
291                        self.space();
292                    }
293                }
294                self.braces_break();
295                self.s.offset(-self.ind);
296                self.word("}");
297                self.end();
298            }
299        }
300        self.word(" for ");
301        if let Some(ty) = ty {
302            self.print_ty(ty);
303        } else {
304            self.word("*");
305        }
306        if *global {
307            self.word(" global");
308        }
309        self.word(";");
310    }
311
312    fn print_contract(&mut self, c: &'ast ast::ItemContract<'ast>, span: Span) {
313        let ast::ItemContract { kind, name, layout, bases, body } = c;
314        self.contract = Some(c);
315        self.cursor.advance_to(span.lo(), true);
316
317        self.s.cbox(self.ind);
318        self.ibox(0);
319        self.cbox(0);
320        self.word_nbsp(kind.to_str());
321        self.print_ident(name);
322        self.nbsp();
323
324        if let Some(layout) = layout
325            && !self.handle_span(layout.span, false)
326        {
327            self.word("layout at ");
328            self.print_expr(layout.slot);
329            self.print_sep(Separator::Space);
330        }
331
332        if let Some(first) = bases.first().map(|base| base.span())
333            && let Some(last) = bases.last().map(|base| base.span())
334            && self.inline_config.is_disabled(first.to(last))
335        {
336            _ = self.handle_span(first.until(last), false);
337        } else if !bases.is_empty() {
338            self.word("is");
339            self.space();
340            let last = bases.len() - 1;
341            for (i, base) in bases.iter().enumerate() {
342                if !self.handle_span(base.span(), false) {
343                    self.print_modifier_call(base, false);
344                    if i != last {
345                        self.word(",");
346                        if self
347                            .print_comments(
348                                bases[i + 1].span().lo(),
349                                CommentConfig::skip_ws().mixed_prev_space().mixed_post_nbsp(),
350                            )
351                            .is_none()
352                        {
353                            self.space();
354                        }
355                    }
356                }
357            }
358            if !self.print_trailing_comment(bases.last().unwrap().span().hi(), None) {
359                self.space();
360            }
361            self.s.offset(-self.ind);
362        }
363        self.end();
364
365        self.print_word("{");
366        self.end();
367        if body.is_empty() {
368            if self.print_comments(span.hi(), CommentConfig::skip_ws()).is_some() {
369                // Adjust the offset of the trailing break from comment printing
370                // so the closing brace is not indented
371                self.s.offset(-self.ind);
372            } else if self.config.bracket_spacing {
373                self.nbsp();
374            };
375            self.end();
376        } else {
377            // update block depth
378            self.block_depth += 1;
379
380            self.print_sep(Separator::Hardbreak);
381            if self.config.contract_new_lines {
382                self.hardbreak();
383            }
384            let body_lo = body[0].span.lo();
385            if self.peek_comment_before(body_lo).is_some() {
386                self.print_comments(body_lo, CommentConfig::skip_leading_ws(true));
387            }
388
389            let mut is_first = true;
390            let mut items = body.iter().peekable();
391            while let Some(item) = items.next() {
392                self.print_item(item, is_first);
393                is_first = false;
394                if let Some(next_item) = items.peek() {
395                    if self.inline_config.is_disabled(next_item.span) {
396                        _ = self.handle_span(next_item.span, false);
397                    } else {
398                        self.separate_items(next_item, true);
399                    }
400                }
401            }
402
403            if let Some(cmnt) = self.print_comments(span.hi(), CommentConfig::skip_trailing_ws())
404                && self.config.contract_new_lines
405                && !cmnt.is_blank()
406            {
407                self.print_sep(Separator::Hardbreak);
408            }
409            self.s.offset(-self.ind);
410            self.end();
411            if self.config.contract_new_lines {
412                self.hardbreak_if_nonempty();
413            }
414
415            // restore block depth
416            self.block_depth -= 1;
417        }
418        self.print_word("}");
419
420        self.cursor.advance_to(span.hi(), true);
421        self.contract = None;
422    }
423
424    fn print_struct(&mut self, strukt: &'ast ast::ItemStruct<'ast>, span: Span) {
425        let ast::ItemStruct { name, fields } = strukt;
426        let ind = if self.estimate_size(name.span) + 8 >= self.space_left() { self.ind } else { 0 };
427        self.s.ibox(self.ind);
428        self.word("struct");
429        self.space();
430        self.print_ident(name);
431        self.word(" {");
432        if !fields.is_empty() {
433            self.break_offset(SIZE_INFINITY as usize, ind);
434        }
435        self.s.ibox(0);
436        for var in fields.iter() {
437            self.print_var_def(var);
438            if !self.print_trailing_comment(var.span.hi(), None) {
439                self.hardbreak();
440            }
441        }
442        self.print_comments(span.hi(), CommentConfig::skip_ws());
443        if ind == 0 {
444            self.s.offset(-self.ind);
445        }
446        self.end();
447        self.end();
448        self.word("}");
449    }
450
451    fn print_enum(&mut self, enm: &'ast ast::ItemEnum<'ast>, span: Span) {
452        let ast::ItemEnum { name, variants } = enm;
453        self.s.cbox(self.ind);
454        self.word("enum ");
455        self.print_ident(name);
456        self.word(" {");
457        self.hardbreak_if_nonempty();
458        for (pos, ident) in variants.iter().delimited() {
459            self.print_comments(ident.span.lo(), CommentConfig::default());
460            self.print_ident(ident);
461            if !pos.is_last {
462                self.word(",");
463            }
464            if !self.print_trailing_comment(ident.span.hi(), None) {
465                self.hardbreak();
466            }
467        }
468        self.print_comments(span.hi(), CommentConfig::skip_ws());
469        self.s.offset(-self.ind);
470        self.end();
471        self.word("}");
472    }
473
474    fn print_udvt(&mut self, udvt: &'ast ast::ItemUdvt<'ast>) {
475        let ast::ItemUdvt { name, ty } = udvt;
476        self.word("type ");
477        self.print_ident(name);
478        self.word(" is ");
479        self.print_ty(ty);
480        self.word(";");
481    }
482
483    // NOTE(rusowsky): Functions are the only source unit item that handle inline (disabled) format
484    fn print_function(&mut self, func: &'ast ast::ItemFunction<'ast>) {
485        let ast::ItemFunction { kind, ref header, ref body, body_span } = *func;
486        let ast::FunctionHeader {
487            name,
488            ref parameters,
489            visibility,
490            state_mutability: sm,
491            virtual_,
492            ref override_,
493            ref returns,
494            ..
495        } = *header;
496
497        self.s.cbox(self.ind);
498
499        // Print fn name and params
500        _ = self.handle_span(self.cursor.span(header.span.lo()), false);
501        self.print_word(kind.to_str());
502        if let Some(name) = name {
503            self.print_sep(Separator::Nbsp);
504            self.print_ident(&name);
505            self.cursor.advance_to(name.span.hi(), true);
506        }
507        self.s.cbox(-self.ind);
508        let header_style = self.config.multiline_func_header;
509        let params_format = match header_style {
510            MultilineFuncHeaderStyle::ParamsAlways => ListFormat::always_break(),
511            MultilineFuncHeaderStyle::All
512                if header.parameters.len() > 1 && !self.can_header_be_inlined(func) =>
513            {
514                ListFormat::always_break()
515            }
516            MultilineFuncHeaderStyle::AllParams
517                if !header.parameters.is_empty() && !self.can_header_be_inlined(func) =>
518            {
519                ListFormat::always_break()
520            }
521            _ => ListFormat::consistent().break_cmnts().break_single(
522                // ensure fn params are always breakable when there is a single `Contract.Struct`
523                parameters.len() == 1
524                    && matches!(
525                        &parameters[0].ty,
526                        ast::Type { kind: ast::TypeKind::Custom(ty), .. } if ty.segments().len() > 1
527                    ),
528            ),
529        };
530        self.print_parameter_list(parameters, parameters.span, params_format);
531        self.end();
532
533        // Map attributes to their corresponding comments
534        let (mut map, attributes, first_attrib_pos) =
535            AttributeCommentMapper::new(returns.as_ref(), body_span.lo()).build(self, header);
536
537        let mut handle_pre_cmnts = |this: &mut Self, span: Span| -> bool {
538            if this.inline_config.is_disabled(span)
539                // Note: `map` is still captured from the outer scope, which is fine.
540                && let Some((pre_cmnts, ..)) = map.remove(&span.lo())
541            {
542                for (pos, cmnt) in pre_cmnts.into_iter().delimited() {
543                    if pos.is_first && cmnt.style.is_isolated() && !this.is_bol_or_only_ind() {
544                        this.print_sep(Separator::Hardbreak);
545                    }
546                    if let Some(cmnt) = this.handle_comment(cmnt, false) {
547                        this.print_comment(cmnt, CommentConfig::skip_ws().mixed_post_nbsp());
548                    }
549                    if pos.is_last {
550                        return true;
551                    }
552                }
553            }
554            false
555        };
556
557        let skip_attribs = returns.as_ref().is_some_and(|ret| {
558            let attrib_span = Span::new(first_attrib_pos, ret.span.lo());
559            handle_pre_cmnts(self, attrib_span);
560            self.handle_span(attrib_span, false)
561        });
562        let skip_returns = {
563            let pos = if skip_attribs { self.cursor.pos } else { first_attrib_pos };
564            let ret_span = Span::new(pos, body_span.lo());
565            handle_pre_cmnts(self, ret_span);
566            self.handle_span(ret_span, false)
567        };
568
569        let attrib_box = self.config.multiline_func_header.params_first()
570            || (self.config.multiline_func_header.attrib_first()
571                && !self.can_header_params_be_inlined(func));
572        if attrib_box {
573            self.s.cbox(0);
574        }
575        if !(skip_attribs || skip_returns) {
576            // Print fn attributes in correct order
577            if let Some(v) = visibility {
578                self.print_fn_attribute(v.span, &mut map, &mut |s| s.word(v.to_str()));
579            }
580            if let Some(sm) = sm
581                && !matches!(*sm, ast::StateMutability::NonPayable)
582            {
583                self.print_fn_attribute(sm.span, &mut map, &mut |s| s.word(sm.to_str()));
584            }
585            if let Some(v) = virtual_ {
586                self.print_fn_attribute(v, &mut map, &mut |s| s.word("virtual"));
587            }
588            if let Some(o) = override_ {
589                self.print_fn_attribute(o.span, &mut map, &mut |s| s.print_override(o));
590            }
591            for m in attributes.iter().filter(|a| matches!(a.kind, AttributeKind::Modifier(_))) {
592                if let AttributeKind::Modifier(modifier) = m.kind {
593                    let is_base = self.is_modifier_a_base_contract(kind, modifier);
594                    self.print_fn_attribute(m.span, &mut map, &mut |s| {
595                        s.print_modifier_call(modifier, is_base)
596                    });
597                }
598            }
599        }
600        if !skip_returns
601            && let Some(ret) = returns
602            && !ret.is_empty()
603        {
604            if !self.handle_span(self.cursor.span(ret.span.lo()), false) {
605                if !self.is_bol_or_only_ind() && !self.last_token_is_space() {
606                    self.print_sep(Separator::Space);
607                }
608                self.cursor.advance_to(ret.span.lo(), true);
609                self.print_word("returns ");
610            }
611            self.print_parameter_list(
612                ret,
613                ret.span,
614                ListFormat::consistent(), // .with_cmnts_break(false),
615            );
616        }
617
618        // Print fn body
619        if let Some(body) = body {
620            if self.handle_span(self.cursor.span(body_span.lo()), false) {
621                // Print spacing if necessary. Updates cursor.
622            } else {
623                if let Some(cmnt) = self.peek_comment_before(body_span.lo()) {
624                    if cmnt.style.is_mixed() {
625                        // These shouldn't update the cursor, as we've already dealt with it above
626                        self.space();
627                        self.s.offset(-self.ind);
628                        self.print_comments(body_span.lo(), CommentConfig::skip_ws());
629                    } else {
630                        self.zerobreak();
631                        self.s.offset(-self.ind);
632                        self.print_comments(body_span.lo(), CommentConfig::skip_ws());
633                        self.s.offset(-self.ind);
634                    }
635                } else {
636                    // If there are no modifiers, overrides, nor returns never break
637                    if header.modifiers.is_empty()
638                        && header.override_.is_none()
639                        && returns.as_ref().is_none_or(|r| r.is_empty())
640                        && (header.visibility().is_none() || body.is_empty())
641                    {
642                        self.nbsp();
643                    } else {
644                        self.space();
645                        self.s.offset(-self.ind);
646                    }
647                }
648                self.cursor.advance_to(body_span.lo(), true);
649            }
650            self.print_word("{");
651            self.end();
652            if attrib_box {
653                self.end();
654            }
655
656            self.print_block_without_braces(body, body_span.hi(), Some(self.ind));
657            if self.cursor.enabled || self.cursor.pos < body_span.hi() {
658                self.print_word("}");
659                self.cursor.advance_to(body_span.hi(), true);
660            }
661        } else {
662            self.print_comments(body_span.lo(), CommentConfig::skip_ws().mixed_prev_space());
663            self.end();
664            if attrib_box {
665                self.end();
666            }
667            self.neverbreak();
668            self.print_word(";");
669        }
670
671        if let Some(cmnt) = self.peek_trailing_comment(body_span.hi(), None) {
672            if cmnt.is_doc {
673                // trailing doc comments after the fn body are isolated
674                // these shouldn't update the cursor, as this is our own formatting
675                self.hardbreak();
676                self.hardbreak();
677            }
678            self.print_trailing_comment(body_span.hi(), None);
679        }
680    }
681
682    fn print_fn_attribute(
683        &mut self,
684        span: Span,
685        map: &mut AttributeCommentMap,
686        print_fn: &mut dyn FnMut(&mut Self),
687    ) {
688        match map.remove(&span.lo()) {
689            Some((pre_cmnts, inner_cmnts, post_cmnts)) => {
690                // Print preceding comments.
691                for cmnt in pre_cmnts {
692                    let Some(cmnt) = self.handle_comment(cmnt, false) else {
693                        continue;
694                    };
695                    self.print_comment(cmnt, CommentConfig::default());
696                }
697                // Push the inner comments back to the queue, so that they are printed in their
698                // intended place.
699                for cmnt in inner_cmnts.into_iter().rev() {
700                    self.comments.push_front(cmnt);
701                }
702                let enabled = if self.handle_span(span, false) {
703                    false
704                } else {
705                    if !self.is_bol_or_only_ind() {
706                        self.space();
707                    }
708                    self.ibox(0);
709                    print_fn(self);
710                    self.cursor.advance_to(span.hi(), true);
711                    true
712                };
713                // Print subsequent comments.
714                for cmnt in post_cmnts {
715                    let Some(cmnt) = self.handle_comment(cmnt, false) else {
716                        continue;
717                    };
718                    self.print_comment(cmnt, CommentConfig::default().mixed_prev_space());
719                }
720                if enabled {
721                    self.end();
722                }
723            }
724            // Fallback for attributes not in the map (should never happen)
725            None => {
726                if !self.is_bol_or_only_ind() {
727                    self.space();
728                }
729                print_fn(self);
730                self.cursor.advance_to(span.hi(), true);
731            }
732        }
733    }
734
735    fn is_modifier_a_base_contract(
736        &self,
737        kind: ast::FunctionKind,
738        modifier: &'ast ast::Modifier<'ast>,
739    ) -> bool {
740        // Add `()` in functions when the modifier is a base contract.
741        // HACK: heuristics:
742        // 1. exactly matches the name of a base contract as declared in the `contract is`;
743        // this does not account for inheritance;
744        let is_contract_base = self.contract.is_some_and(|contract| {
745            contract
746                .bases
747                .iter()
748                .any(|contract_base| contract_base.name.to_string() == modifier.name.to_string())
749        });
750        // 2. assume that title case names in constructors are bases.
751        // LEGACY: constructors used to also be `function NameOfContract...`; not checked.
752        let is_constructor = matches!(kind, ast::FunctionKind::Constructor);
753        // LEGACY: we are checking the beginning of the path, not the last segment.
754        is_contract_base
755            || (is_constructor
756                && modifier.name.first().name.as_str().starts_with(char::is_uppercase))
757    }
758
759    fn print_error(&mut self, err: &'ast ast::ItemError<'ast>) {
760        let ast::ItemError { name, parameters } = err;
761        self.word("error ");
762        self.print_ident(name);
763        self.print_parameter_list(
764            parameters,
765            parameters.span,
766            if self.config.prefer_compact.errors() {
767                ListFormat::compact()
768            } else {
769                ListFormat::consistent()
770            },
771        );
772        self.word(";");
773    }
774
775    fn print_event(&mut self, event: &'ast ast::ItemEvent<'ast>) {
776        let ast::ItemEvent { name, parameters, anonymous } = event;
777        self.word("event ");
778        self.print_ident(name);
779        self.print_parameter_list(
780            parameters,
781            parameters.span,
782            if self.config.prefer_compact.events() {
783                ListFormat::compact().break_cmnts()
784            } else {
785                ListFormat::consistent().break_cmnts()
786            },
787        );
788        if *anonymous {
789            self.word(" anonymous");
790        }
791        self.word(";");
792    }
793
794    fn print_var_def(&mut self, var: &'ast ast::VariableDefinition<'ast>) {
795        self.print_var(var, true);
796        self.word(";");
797    }
798
799    /// Prints the RHS of an assignment or variable initializer.
800    fn print_assign_rhs(
801        &mut self,
802        rhs: &'ast ast::Expr<'ast>,
803        lhs_size: usize,
804        space_left: usize,
805        ty: Option<&ast::TypeKind<'ast>>,
806        cache: bool,
807    ) {
808        // Check if the total expression overflows but the RHS would fit alone on a new line.
809        // This helps keep the RHS together on a single line when possible.
810        let rhs_size = self.estimate_size(rhs.span);
811        let overflows = lhs_size + rhs_size >= space_left;
812        let fits_alone = rhs_size + self.config.tab_width < space_left;
813        let fits_alone_no_cmnts =
814            fits_alone && !self.has_comment_between(rhs.span.lo(), rhs.span.hi());
815        let force_break = overflows && fits_alone_no_cmnts;
816
817        if lhs_size <= space_left {
818            self.neverbreak();
819        }
820
821        // Handle comments before the RHS expression
822        if let Some(cmnt) = self.peek_comment_before(rhs.span.lo())
823            && self.inline_config.is_disabled(cmnt.span)
824        {
825            self.print_sep(Separator::Nbsp);
826        }
827        if self
828            .print_comments(
829                rhs.span.lo(),
830                CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(),
831            )
832            .is_some_and(|cmnt| cmnt.is_trailing())
833        {
834            self.break_offset_if_not_bol(SIZE_INFINITY as usize, self.ind, false);
835        }
836
837        // Match on expression kind to determine formatting strategy
838        match &rhs.kind {
839            ast::ExprKind::Lit(lit, ..) if lit.is_str_concatenation() => {
840                // String concatenations stay on the same line with nbsp
841                self.print_sep(Separator::Nbsp);
842                self.neverbreak();
843                self.s.ibox(self.ind);
844                self.print_expr(rhs);
845                self.end();
846            }
847            ast::ExprKind::Lit(..) if ty.is_none() && !fits_alone => {
848                // Long string in assign expr goes on its own line
849                self.print_sep(Separator::Space);
850                self.s.offset(self.ind);
851                self.print_expr(rhs);
852            }
853            ast::ExprKind::Binary(lhs, op, _) => {
854                let print_inline = |this: &mut Self| {
855                    this.print_sep(Separator::Nbsp);
856                    this.neverbreak();
857                    this.print_expr(rhs);
858                };
859                let print_with_break = |this: &mut Self, force_break: bool| {
860                    if !this.is_bol_or_only_ind() {
861                        if force_break {
862                            this.print_sep(Separator::Hardbreak);
863                        } else {
864                            this.print_sep(Separator::Space);
865                        }
866                    }
867                    this.s.offset(this.ind);
868                    this.s.ibox(this.ind);
869                    this.print_expr(rhs);
870                    this.end();
871                };
872
873                // Binary expressions: check if we need to break and indent
874                if force_break {
875                    print_with_break(self, true);
876                } else if self.estimate_lhs_size(rhs, op) + lhs_size > space_left {
877                    if has_complex_successor(&rhs.kind, true)
878                        && get_callee_head_size(lhs) + lhs_size <= space_left
879                    {
880                        // Keep complex exprs (where callee fits) inline, as they will have breaks
881                        if matches!(lhs.kind, ast::ExprKind::Call(..)) {
882                            self.s.ibox(-self.ind);
883                            print_inline(self);
884                            self.end();
885                        } else {
886                            print_inline(self);
887                        }
888                    } else {
889                        print_with_break(self, false);
890                    }
891                }
892                // Otherwise, if expr fits, ensure no breaks
893                else {
894                    print_inline(self);
895                }
896            }
897            _ => {
898                // General case: handle calls, complex successors, and other expressions
899                let callee_doesnt_fit = if let ast::ExprKind::Call(call_expr, ..) = &rhs.kind {
900                    let callee_size = get_callee_head_size(call_expr);
901                    callee_size + lhs_size > space_left
902                        && callee_size + self.config.tab_width < space_left
903                } else {
904                    false
905                };
906
907                if (lhs_size + 1 >= space_left && !is_call_chain(&rhs.kind, false))
908                    || callee_doesnt_fit
909                {
910                    self.s.ibox(self.ind);
911                } else {
912                    self.s.ibox(0);
913                };
914
915                if has_complex_successor(&rhs.kind, true)
916                    && !matches!(&rhs.kind, ast::ExprKind::Member(..))
917                {
918                    // delegate breakpoints to `self.commasep(..)` for complex successors
919                    if !self.is_bol_or_only_ind() {
920                        let needs_offset = !callee_doesnt_fit
921                            && rhs_size + lhs_size + 1 >= space_left
922                            && fits_alone_no_cmnts;
923                        let separator = if callee_doesnt_fit || needs_offset {
924                            Separator::Space
925                        } else {
926                            Separator::Nbsp
927                        };
928                        self.print_sep(separator);
929                        if needs_offset {
930                            self.s.offset(self.ind);
931                        }
932                    }
933                } else {
934                    if !self.is_bol_or_only_ind() {
935                        self.print_sep_unhandled(Separator::Space);
936                    }
937                    // apply type-dependent indentation if type info is available
938                    if let Some(ty) = ty
939                        && matches!(ty, ast::TypeKind::Elementary(..) | ast::TypeKind::Mapping(..))
940                    {
941                        self.s.offset(self.ind);
942                    }
943                }
944                self.print_expr(rhs);
945                self.end();
946            }
947        }
948
949        self.var_init = cache;
950    }
951
952    fn print_var(&mut self, var: &'ast ast::VariableDefinition<'ast>, is_var_def: bool) {
953        let ast::VariableDefinition {
954            span,
955            ty,
956            visibility,
957            mutability,
958            data_location,
959            override_,
960            indexed,
961            name,
962            initializer,
963        } = var;
964
965        if self.handle_span(*span, false) {
966            return;
967        }
968
969        // NOTE(rusowsky): this is hacky but necessary to properly estimate if we figure out if we
970        // have double breaks (which should have double indentation) or not.
971        // Alternatively, we could achieve the same behavior with a new box group that supports
972        // "continuation" which would only increase indentation if its parent box broke.
973        let init_space_left = self.space_left();
974        let mut pre_init_size = self.estimate_size(ty.span);
975
976        // Non-elementary types use commasep which has its own padding.
977        self.s.ibox(0);
978        if override_.is_some() {
979            self.s.cbox(self.ind);
980        } else {
981            self.s.ibox(self.ind);
982        }
983        self.print_ty(ty);
984
985        self.print_attribute(visibility.map(|v| v.to_str()), is_var_def, &mut pre_init_size);
986        self.print_attribute(mutability.map(|m| m.to_str()), is_var_def, &mut pre_init_size);
987        self.print_attribute(data_location.map(|d| d.to_str()), is_var_def, &mut pre_init_size);
988
989        if let Some(override_) = override_ {
990            if self
991                .print_comments(override_.span.lo(), CommentConfig::skip_ws().mixed_prev_space())
992                .is_none()
993            {
994                self.print_sep(Separator::SpaceOrNbsp(is_var_def));
995            }
996            self.ibox(0);
997            self.print_override(override_);
998            pre_init_size += self.estimate_size(override_.span) + 1;
999        }
1000
1001        if *indexed {
1002            self.print_attribute(indexed.then_some("indexed"), is_var_def, &mut pre_init_size);
1003        }
1004
1005        if let Some(ident) = name {
1006            self.print_sep(Separator::SpaceOrNbsp(is_var_def && override_.is_none()));
1007            self.print_comments(
1008                ident.span.lo(),
1009                CommentConfig::skip_ws().mixed_no_break().mixed_post_nbsp(),
1010            );
1011            self.print_ident(ident);
1012            pre_init_size += self.estimate_size(ident.span) + 1;
1013        }
1014        if let Some(init) = initializer {
1015            let cache = self.var_init;
1016            self.var_init = true;
1017
1018            pre_init_size += 2;
1019            self.print_word(" =");
1020            if override_.is_some() {
1021                self.end();
1022            }
1023            self.end();
1024
1025            self.print_assign_rhs(init, pre_init_size, init_space_left, Some(&ty.kind), cache);
1026        } else {
1027            if override_.is_some() {
1028                self.end();
1029            }
1030            self.end();
1031        }
1032        self.end();
1033    }
1034
1035    fn print_attribute(
1036        &mut self,
1037        attribute: Option<&'static str>,
1038        is_var_def: bool,
1039        size: &mut usize,
1040    ) {
1041        if let Some(s) = attribute {
1042            self.print_sep(Separator::SpaceOrNbsp(is_var_def));
1043            self.print_word(s);
1044            *size += s.len() + 1;
1045        }
1046    }
1047
1048    fn print_parameter_list(
1049        &mut self,
1050        parameters: &'ast [ast::VariableDefinition<'ast>],
1051        span: Span,
1052        format: ListFormat,
1053    ) {
1054        if self.handle_span(span, false) {
1055            return;
1056        }
1057
1058        self.print_tuple(
1059            parameters,
1060            span.lo(),
1061            span.hi(),
1062            |fmt, var| fmt.print_var(var, false),
1063            get_span!(),
1064            format,
1065        );
1066    }
1067
1068    fn print_ident_or_strlit(&mut self, value: &'ast ast::IdentOrStrLit) {
1069        match value {
1070            ast::IdentOrStrLit::Ident(ident) => self.print_ident(ident),
1071            ast::IdentOrStrLit::StrLit(strlit) => self.print_ast_str_lit(strlit),
1072        }
1073    }
1074
1075    /// Prints a raw AST string literal, which is unescaped.
1076    fn print_ast_str_lit(&mut self, strlit: &'ast ast::StrLit) {
1077        self.print_str_lit(ast::StrKind::Str, strlit.span.lo(), strlit.value.as_str());
1078    }
1079
1080    fn print_lit(&mut self, lit: &'ast ast::Lit<'ast>) {
1081        self.print_lit_inner(lit, false);
1082    }
1083
1084    fn print_ty(&mut self, ty: &'ast ast::Type<'ast>) {
1085        if self.handle_span(ty.span, false) {
1086            return;
1087        }
1088
1089        match &ty.kind {
1090            &ast::TypeKind::Elementary(ty) => 'b: {
1091                match ty {
1092                    // `address payable` is normalized to `address`.
1093                    ast::ElementaryType::Address(true) => {
1094                        self.word("address payable");
1095                        break 'b;
1096                    }
1097                    // Integers are normalized to long form.
1098                    ast::ElementaryType::Int(size) | ast::ElementaryType::UInt(size) => {
1099                        match (self.config.int_types, size.bits_raw()) {
1100                            (config::IntTypes::Short, 0 | 256)
1101                            | (config::IntTypes::Preserve, 0) => {
1102                                let short = match ty {
1103                                    ast::ElementaryType::Int(_) => "int",
1104                                    ast::ElementaryType::UInt(_) => "uint",
1105                                    _ => unreachable!(),
1106                                };
1107                                self.word(short);
1108                                break 'b;
1109                            }
1110                            _ => {}
1111                        }
1112                    }
1113                    _ => {}
1114                }
1115                self.word(ty.to_abi_str());
1116            }
1117            ast::TypeKind::Array(ast::TypeArray { element, size }) => {
1118                self.print_ty(element);
1119                if let Some(size) = size {
1120                    self.word("[");
1121                    self.print_expr(size);
1122                    self.word("]");
1123                } else {
1124                    self.word("[]");
1125                }
1126            }
1127            ast::TypeKind::Function(ast::TypeFunction {
1128                parameters,
1129                visibility,
1130                state_mutability,
1131                returns,
1132            }) => {
1133                self.cbox(0);
1134                self.word("function");
1135                self.print_parameter_list(parameters, parameters.span, ListFormat::inline());
1136
1137                if let Some(v) = visibility {
1138                    self.space();
1139                    self.word(v.to_str());
1140                }
1141                if let Some(sm) = state_mutability
1142                    && !matches!(**sm, ast::StateMutability::NonPayable)
1143                {
1144                    self.space();
1145                    self.word(sm.to_str());
1146                }
1147                if let Some(ret) = returns
1148                    && !ret.is_empty()
1149                {
1150                    self.nbsp();
1151                    self.word("returns");
1152                    self.nbsp();
1153                    self.print_parameter_list(
1154                        ret,
1155                        ret.span,
1156                        ListFormat::consistent(), // .with_cmnts_break(false),
1157                    );
1158                }
1159                self.end();
1160            }
1161            ast::TypeKind::Mapping(ast::TypeMapping { key, key_name, value, value_name }) => {
1162                self.word("mapping(");
1163                self.s.cbox(0);
1164                if let Some(cmnt) = self.peek_comment_before(key.span.lo()) {
1165                    if cmnt.style.is_mixed() {
1166                        self.print_comments(
1167                            key.span.lo(),
1168                            CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(),
1169                        );
1170                        self.break_offset_if_not_bol(SIZE_INFINITY as usize, 0, false);
1171                    } else {
1172                        self.print_comments(key.span.lo(), CommentConfig::skip_ws());
1173                    }
1174                }
1175                // Fitting a mapping in one line takes, at least, 16 chars (one-char var name):
1176                // 'mapping(' + {key} + ' => ' {value} ') ' + {name} + ';'
1177                // To be more conservative, we use 18 to decide whether to force a break or not.
1178                else if 18
1179                    + self.estimate_size(key.span)
1180                    + key_name.map(|k| self.estimate_size(k.span)).unwrap_or(0)
1181                    + self.estimate_size(value.span)
1182                    + value_name.map(|v| self.estimate_size(v.span)).unwrap_or(0)
1183                    >= self.space_left()
1184                {
1185                    self.hardbreak();
1186                } else {
1187                    self.zerobreak();
1188                }
1189                self.s.cbox(0);
1190                self.print_ty(key);
1191                if let Some(ident) = key_name {
1192                    if self
1193                        .print_comments(
1194                            ident.span.lo(),
1195                            CommentConfig::skip_ws()
1196                                .mixed_no_break()
1197                                .mixed_prev_space()
1198                                .mixed_post_nbsp(),
1199                        )
1200                        .is_none()
1201                    {
1202                        self.nbsp();
1203                    }
1204                    self.print_ident(ident);
1205                }
1206                // NOTE(rusowsky): unless we add more spans to solar, using `value.span.lo()`
1207                // consumes "comment6" of which should be printed after the `=>`
1208                self.print_comments(
1209                    value.span.lo(),
1210                    CommentConfig::skip_ws()
1211                        .trailing_no_break()
1212                        .mixed_no_break()
1213                        .mixed_prev_space(),
1214                );
1215                self.space();
1216                self.s.offset(self.ind);
1217                self.word("=> ");
1218                self.s.ibox(self.ind);
1219                self.print_ty(value);
1220                if let Some(ident) = value_name {
1221                    self.neverbreak();
1222                    if self
1223                        .print_comments(
1224                            ident.span.lo(),
1225                            CommentConfig::skip_ws()
1226                                .mixed_no_break()
1227                                .mixed_prev_space()
1228                                .mixed_post_nbsp(),
1229                        )
1230                        .is_none()
1231                    {
1232                        self.nbsp();
1233                    }
1234                    self.print_ident(ident);
1235                    if self
1236                        .peek_comment_before(ty.span.hi())
1237                        .is_some_and(|cmnt| cmnt.style.is_mixed())
1238                    {
1239                        self.neverbreak();
1240                        self.print_comments(
1241                            value.span.lo(),
1242                            CommentConfig::skip_ws().mixed_no_break(),
1243                        );
1244                    }
1245                }
1246                self.end();
1247                self.end();
1248                if self
1249                    .print_comments(
1250                        ty.span.hi(),
1251                        CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(),
1252                    )
1253                    .is_some_and(|cmnt| !cmnt.is_mixed())
1254                {
1255                    self.break_offset_if_not_bol(0, -self.ind, false);
1256                } else {
1257                    self.zerobreak();
1258                    self.s.offset(-self.ind);
1259                }
1260                self.end();
1261                self.word(")");
1262            }
1263            ast::TypeKind::Custom(path) => self.print_path(path, false),
1264        }
1265    }
1266
1267    fn print_override(&mut self, override_: &'ast ast::Override<'ast>) {
1268        let ast::Override { span, paths } = override_;
1269        if self.handle_span(*span, false) {
1270            return;
1271        }
1272        self.word("override");
1273        if !paths.is_empty() {
1274            if self.config.override_spacing {
1275                self.nbsp();
1276            }
1277            self.print_tuple(
1278                paths,
1279                span.lo(),
1280                span.hi(),
1281                |this, path| this.print_path(path, false),
1282                get_span!(()),
1283                ListFormat::consistent(), // .with_cmnts_break(false),
1284            );
1285        }
1286    }
1287
1288    /* --- Expressions --- */
1289    /// Prints an expression by matching on its variant and delegating to the appropriate
1290    /// printer method, handling all Solidity expression kinds.
1291    fn print_expr(&mut self, expr: &'ast ast::Expr<'ast>) {
1292        let ast::Expr { span, ref kind } = *expr;
1293        if self.handle_span(span, false) {
1294            return;
1295        }
1296
1297        match kind {
1298            ast::ExprKind::Array(exprs) => {
1299                self.print_array(exprs, expr.span, |this, e| this.print_expr(e), get_span!())
1300            }
1301            ast::ExprKind::Assign(lhs, None, rhs) => self.print_assign_expr(lhs, rhs),
1302            ast::ExprKind::Assign(lhs, Some(op), rhs) => self.print_bin_expr(lhs, op, rhs, true),
1303            ast::ExprKind::Binary(lhs, op, rhs) => self.print_bin_expr(lhs, op, rhs, false),
1304            ast::ExprKind::Call(call_expr, call_args) => {
1305                let cache = self.call_with_opts_and_args;
1306                let chained_named_call_cache = self.chained_named_call;
1307                // Keep calls within a chained callee inline when they fit, so a multiline named
1308                // argument list does not force an earlier break inside the callee.
1309                let keep_inline = chained_named_call_cache
1310                    .is_some_and(|call| call.keep_inline && call.callee.contains(expr.span))
1311                    && !self.has_comments_between_elements(call_args.span, call_args.exprs());
1312                self.call_with_opts_and_args = is_call_with_opts_and_args(&expr.kind);
1313                let named_args_size = if call_args.is_empty() {
1314                    4 + usize::from(self.config.bracket_spacing)
1315                } else {
1316                    2
1317                };
1318                self.chained_named_call = (matches!(call_args.kind, ast::CallArgsKind::Named(_))
1319                    && is_call_chain(&call_expr.kind, true))
1320                .then(|| ChainedNamedCall {
1321                    callee: call_expr.span,
1322                    keep_inline: !call_chain_contains_options(call_expr)
1323                        && !self.has_comment_between(call_expr.span.lo(), call_expr.span.hi())
1324                        && self
1325                            .estimate_call_chain_size(call_expr)
1326                            .is_some_and(|size| size + named_args_size <= self.space_left()),
1327                })
1328                .or_else(|| {
1329                    chained_named_call_cache.filter(|call| call.callee.contains(expr.span))
1330                });
1331                let list_format = if keep_inline {
1332                    ListFormat::inline()
1333                } else {
1334                    ListFormat::compact().break_cmnts().break_single(true)
1335                };
1336                let terminal_callee = call_expr.peel_parens();
1337                let callee_has_breakable_comment = self
1338                    .has_breakable_comment_between(call_expr.span.lo(), terminal_callee.span.lo())
1339                    || self.has_breakable_comment_between(
1340                        terminal_callee.span.hi(),
1341                        call_expr.span.hi(),
1342                    )
1343                    || if let ast::ExprKind::Member(member_expr, ident) = &terminal_callee.kind {
1344                        self.has_breakable_comment_between(member_expr.span.hi(), ident.span.lo())
1345                    } else {
1346                        false
1347                    };
1348                self.print_member_or_call_chain(
1349                    call_expr,
1350                    MemberOrCallArgs::CallArgs(
1351                        self.estimate_size(call_args.span),
1352                        self.has_comments_between_elements(call_args.span, call_args.exprs()),
1353                    ),
1354                    |s| {
1355                        let callee_suffix_can_break = callee_has_breakable_comment
1356                            || match &terminal_callee.kind {
1357                                ast::ExprKind::Member(member_expr, _) => {
1358                                    s.member_suffix_emits_break(terminal_callee, member_expr)
1359                                }
1360                                ast::ExprKind::Index(..) => !s.skip_index_break,
1361                                _ => false,
1362                            };
1363                        s.print_call_args(
1364                            call_args,
1365                            list_format
1366                                .without_ind(s.return_bin_expr)
1367                                .with_delimiters(!s.call_with_opts_and_args),
1368                            get_callee_head_size(call_expr),
1369                            callee_suffix_can_break,
1370                        );
1371                    },
1372                );
1373                self.call_with_opts_and_args = cache;
1374                self.chained_named_call = chained_named_call_cache;
1375            }
1376            ast::ExprKind::CallOptions(expr, named_args) => {
1377                // the flag is only meant to be used to format the call args
1378                let cache = self.call_with_opts_and_args;
1379                self.call_with_opts_and_args = false;
1380
1381                self.print_expr(expr);
1382                self.print_named_args(named_args, span.hi(), false);
1383
1384                // restore cached value
1385                self.call_with_opts_and_args = cache;
1386            }
1387            ast::ExprKind::Delete(expr) => {
1388                self.word("delete ");
1389                self.print_expr(expr);
1390            }
1391            ast::ExprKind::Ident(ident) => self.print_ident(ident),
1392            ast::ExprKind::Index(expr, kind) => self.print_index_expr(span, expr, kind),
1393            ast::ExprKind::Lit(lit, unit) => {
1394                self.print_lit(lit);
1395                if let Some(unit) = unit {
1396                    self.nbsp();
1397                    self.word(unit.to_str());
1398                }
1399            }
1400            ast::ExprKind::Member(member_expr, ident) => {
1401                self.print_member_or_call_chain(
1402                    member_expr,
1403                    MemberOrCallArgs::Member(self.estimate_size(ident.span)),
1404                    |s| {
1405                        let has_mixed_comment = s
1406                            .peek_comment_between(member_expr.span.hi(), ident.span.lo())
1407                            .is_some_and(|comment| comment.style.is_mixed());
1408                        if has_mixed_comment {
1409                            s.print_comments(
1410                                ident.span.lo(),
1411                                CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(),
1412                            );
1413                        } else {
1414                            s.print_trailing_comment(member_expr.span.hi(), Some(ident.span.lo()));
1415                        }
1416                        if has_mixed_comment || s.member_suffix_emits_break(expr, member_expr) {
1417                            s.zerobreak();
1418                        }
1419                        s.word(".");
1420                        s.print_ident(ident);
1421                    },
1422                );
1423            }
1424            ast::ExprKind::New(ty) => {
1425                self.word("new ");
1426                self.print_ty(ty);
1427            }
1428            ast::ExprKind::Payable(args) => {
1429                self.word("payable");
1430                self.print_call_args(args, ListFormat::compact().break_cmnts(), 7, false);
1431            }
1432            ast::ExprKind::Ternary(cond, then, els) => self.print_ternary_expr(cond, then, els),
1433            ast::ExprKind::Tuple(exprs) => self.print_tuple(
1434                exprs,
1435                span.lo(),
1436                span.hi(),
1437                |this, expr| match expr.as_ref() {
1438                    SpannedOption::Some(expr) => this.print_expr(expr),
1439                    SpannedOption::None(span) => {
1440                        this.print_comments(span.hi(), CommentConfig::skip_ws().no_breaks());
1441                    }
1442                },
1443                |expr| match expr.as_ref() {
1444                    SpannedOption::Some(expr) => expr.span,
1445                    // Manually handled by printing the comment when `None`
1446                    SpannedOption::None(..) => Span::DUMMY,
1447                },
1448                ListFormat::compact().break_single(is_binary_expr(&expr.kind)),
1449            ),
1450            ast::ExprKind::TypeCall(ty) => {
1451                self.word("type");
1452                self.print_tuple(
1453                    std::slice::from_ref(ty),
1454                    span.lo(),
1455                    span.hi(),
1456                    Self::print_ty,
1457                    get_span!(),
1458                    ListFormat::consistent(),
1459                );
1460            }
1461            ast::ExprKind::Type(ty) => self.print_ty(ty),
1462            ast::ExprKind::Unary(un_op, expr) => {
1463                let prefix = un_op.kind.is_prefix();
1464                let op = un_op.kind.to_str();
1465                if prefix {
1466                    self.word(op);
1467                }
1468                self.print_expr(expr);
1469                if !prefix {
1470                    debug_assert!(un_op.kind.is_postfix());
1471                    self.word(op);
1472                }
1473            }
1474            ast::ExprKind::Err(_) => self.print_span(span),
1475        }
1476        self.cursor.advance_to(span.hi(), true);
1477    }
1478
1479    /// Prints a simple assignment expression of the form `lhs = rhs`.
1480    fn print_assign_expr(&mut self, lhs: &'ast ast::Expr<'ast>, rhs: &'ast ast::Expr<'ast>) {
1481        let cache = self.var_init;
1482        self.var_init = true;
1483
1484        let space_left = self.space_left();
1485        let lhs_size = self.estimate_size(lhs.span);
1486        self.print_expr(lhs);
1487        self.word(" =");
1488        self.print_assign_rhs(rhs, lhs_size + 2, space_left, None, cache);
1489    }
1490
1491    /// Prints a binary operator expression. Handles operator chains and formatting.
1492    fn print_bin_expr(
1493        &mut self,
1494        lhs: &'ast ast::Expr<'ast>,
1495        bin_op: &ast::BinOp,
1496        rhs: &'ast ast::Expr<'ast>,
1497        is_assign: bool,
1498    ) {
1499        let prev_chain = self.binary_expr;
1500        let is_chain = prev_chain.is_some_and(|prev| prev == bin_op.kind.group());
1501
1502        // Opening box if starting a new operator chain.
1503        if !is_chain {
1504            self.binary_expr = Some(bin_op.kind.group());
1505
1506            let indent = if (is_assign && has_complex_successor(&rhs.kind, true))
1507                || self.call_stack.is_nested()
1508                    && is_call_chain(&lhs.kind, false)
1509                    && self.estimate_size(lhs.span) >= self.space_left()
1510            {
1511                0
1512            } else {
1513                self.ind
1514            };
1515            self.s.ibox(indent);
1516        }
1517
1518        // Print LHS.
1519        self.print_expr(lhs);
1520
1521        // Handle assignment (`+=`, etc.) vs binary ops (`+`, `*`, etc.).
1522        let no_trailing_comment = !self.print_trailing_comment(lhs.span.hi(), Some(rhs.span.lo()));
1523        if is_assign {
1524            if no_trailing_comment {
1525                self.nbsp();
1526            }
1527            self.word(bin_op.kind.to_str());
1528            self.word("= ");
1529        } else {
1530            if no_trailing_comment
1531                && self
1532                    .print_comments(
1533                        bin_op.span.lo(),
1534                        CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(),
1535                    )
1536                    .is_none_or(|cmnt| cmnt.is_mixed())
1537            {
1538                if !self.config.pow_no_space || !matches!(bin_op.kind, ast::BinOpKind::Pow) {
1539                    self.space_if_not_bol();
1540                } else if !self.is_bol_or_only_ind() && !self.last_token_is_break() {
1541                    self.zerobreak();
1542                }
1543            }
1544
1545            self.word(bin_op.kind.to_str());
1546
1547            if !self.config.pow_no_space || !matches!(bin_op.kind, ast::BinOpKind::Pow) {
1548                self.nbsp();
1549            }
1550        }
1551
1552        // Print RHS with optional ibox if mixed comment precedes.
1553        let rhs_has_mixed_comment =
1554            self.peek_comment_before(rhs.span.lo()).is_some_and(|cmnt| cmnt.style.is_mixed());
1555        if rhs_has_mixed_comment {
1556            self.ibox(0);
1557            self.print_expr(rhs);
1558            self.end();
1559        } else {
1560            self.print_expr(rhs);
1561        }
1562
1563        // End current box if this was top-level in the chain.
1564        if !is_chain {
1565            self.binary_expr = prev_chain;
1566            self.end();
1567        }
1568    }
1569
1570    /// Prints an indexing expression.
1571    fn print_index_expr(
1572        &mut self,
1573        span: Span,
1574        expr: &'ast ast::Expr<'ast>,
1575        kind: &'ast ast::IndexKind<'ast>,
1576    ) {
1577        self.print_expr(expr);
1578        self.word("[");
1579        self.s.cbox(self.ind);
1580
1581        let mut skip_break = false;
1582        let mut zerobreak = |this: &mut Self| {
1583            if this.skip_index_break {
1584                skip_break = true;
1585            } else {
1586                this.zerobreak();
1587            }
1588        };
1589        match kind {
1590            ast::IndexKind::Index(Some(inner_expr)) => {
1591                zerobreak(self);
1592                self.print_expr(inner_expr);
1593            }
1594            ast::IndexKind::Index(None) => {}
1595            ast::IndexKind::Range(start, end) => {
1596                if let Some(start_expr) = start {
1597                    if self
1598                        .print_comments(start_expr.span.lo(), CommentConfig::skip_ws())
1599                        .is_none_or(|s| s.is_mixed())
1600                    {
1601                        zerobreak(self);
1602                    }
1603                    self.print_expr(start_expr);
1604                } else {
1605                    zerobreak(self);
1606                }
1607
1608                self.word(":");
1609
1610                if let Some(end_expr) = end {
1611                    self.s.ibox(self.ind);
1612                    if start.is_some() {
1613                        zerobreak(self);
1614                    }
1615                    self.print_comments(
1616                        end_expr.span.lo(),
1617                        CommentConfig::skip_ws()
1618                            .mixed_prev_space()
1619                            .mixed_no_break()
1620                            .mixed_post_nbsp(),
1621                    );
1622                    self.print_expr(end_expr);
1623                }
1624
1625                // Trailing comment handling.
1626                let is_trailing = if let Some(style) = self.print_comments(
1627                    span.hi(),
1628                    CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(),
1629                ) {
1630                    skip_break = true;
1631                    style.is_trailing()
1632                } else {
1633                    false
1634                };
1635
1636                // Adjust indentation and line breaks.
1637                match (skip_break, end.is_some()) {
1638                    (true, true) => {
1639                        self.break_offset_if_not_bol(0, -2 * self.ind, false);
1640                        self.end();
1641                        if !is_trailing {
1642                            self.break_offset_if_not_bol(0, -self.ind, false);
1643                        }
1644                    }
1645                    (true, false) => {
1646                        self.break_offset_if_not_bol(0, -self.ind, false);
1647                    }
1648                    (false, true) => {
1649                        self.end();
1650                    }
1651                    _ => {}
1652                }
1653            }
1654        }
1655
1656        if !skip_break {
1657            self.zerobreak();
1658            self.s.offset(-self.ind);
1659        }
1660
1661        self.end();
1662        self.word("]");
1663    }
1664
1665    /// Prints a ternary expression of the form `cond ? then : else`.
1666    fn print_ternary_expr(
1667        &mut self,
1668        cond: &'ast ast::Expr<'ast>,
1669        then: &'ast ast::Expr<'ast>,
1670        els: &'ast ast::Expr<'ast>,
1671    ) {
1672        self.s.cbox(self.ind);
1673        self.s.ibox(0);
1674
1675        let print_sub_expr = |this: &mut Self, span_lo, prefix, expr: &'ast ast::Expr<'ast>| {
1676            match prefix {
1677                Some(prefix) => {
1678                    if this.peek_comment_before(span_lo).is_some() {
1679                        this.space();
1680                    }
1681                    this.print_comments(span_lo, CommentConfig::skip_ws());
1682                    this.end();
1683                    if !this.is_bol_or_only_ind() {
1684                        this.space();
1685                    }
1686                    this.s.ibox(0);
1687                    this.word(prefix);
1688                }
1689                None => {
1690                    this.print_comments(expr.span.lo(), CommentConfig::skip_ws());
1691                }
1692            };
1693            this.print_expr(expr);
1694        };
1695
1696        // conditional expression
1697        self.s.ibox(-self.ind);
1698        print_sub_expr(self, then.span.lo(), None, cond);
1699        self.end();
1700        // then expression
1701        print_sub_expr(self, then.span.lo(), Some("? "), then);
1702        // else expression
1703        print_sub_expr(self, els.span.lo(), Some(": "), els);
1704
1705        self.end();
1706        self.neverbreak();
1707        self.s.offset(-self.ind);
1708        self.end();
1709    }
1710
1711    // If `add_parens_if_empty` is true, then add parentheses `()` even if there are no arguments.
1712    fn print_modifier_call(
1713        &mut self,
1714        modifier: &'ast ast::Modifier<'ast>,
1715        add_parens_if_empty: bool,
1716    ) {
1717        let ast::Modifier { name, arguments } = modifier;
1718        self.print_path(name, false);
1719        if !arguments.is_empty() || add_parens_if_empty {
1720            self.print_call_args(
1721                arguments,
1722                ListFormat::compact().break_cmnts(),
1723                name.to_string().len(),
1724                false,
1725            );
1726        }
1727    }
1728
1729    fn member_suffix_emits_break(&self, expr: &ast::Expr<'_>, member_expr: &ast::Expr<'_>) -> bool {
1730        match member_expr.kind {
1731            ast::ExprKind::Ident(_) | ast::ExprKind::Type(_) => false,
1732            ast::ExprKind::Index(..) if self.skip_index_break => false,
1733            _ if self
1734                .chained_named_call
1735                .is_some_and(|call| call.keep_inline && call.callee.contains(expr.span)) =>
1736            {
1737                false
1738            }
1739            // Don't add a break when accessing a field after a call with named args.
1740            // e.g., `_lzSend({_dstEid: x, ...}).guid` should keep `.guid`
1741            // on the same line as the closing `})`.
1742            // See: https://github.com/foundry-rs/foundry/issues/12399
1743            _ if is_call_with_named_args(&member_expr.kind) => false,
1744            _ => true,
1745        }
1746    }
1747
1748    fn print_member_or_call_chain<F>(
1749        &mut self,
1750        child_expr: &'ast ast::Expr<'ast>,
1751        member_or_args: MemberOrCallArgs,
1752        print_suffix: F,
1753    ) where
1754        F: FnOnce(&mut Self),
1755    {
1756        fn member_depth(depth: usize, expr: &ast::Expr<'_>) -> usize {
1757            if let ast::ExprKind::Member(child, ..) = &expr.kind {
1758                member_depth(depth + 1, child)
1759            } else {
1760                depth
1761            }
1762        }
1763
1764        let (mut extra_box, skip_cache) = (false, self.skip_index_break);
1765        let parent_is_chain = self.call_stack.last().copied().is_some_and(|call| call.is_chained());
1766        if !parent_is_chain {
1767            // Estimate sizes of callee and optional member
1768            let callee_size = get_callee_head_size(child_expr) + member_or_args.member_size();
1769            let expr_size = self.estimate_size(child_expr.span);
1770
1771            let callee_fits_line = self.space_left() > callee_size + 1;
1772            let total_fits_line = self.space_left() > expr_size + member_or_args.size() + 2;
1773            let no_cmnt_or_mixed =
1774                self.peek_comment_before(child_expr.span.hi()).is_none_or(|c| c.style.is_mixed());
1775
1776            // If call with options, add an extra box to prioritize breaking the call args.
1777            if self.call_with_opts_and_args {
1778                self.cbox(0);
1779                extra_box = true;
1780            }
1781
1782            // Determine if this chain will add its own indentation
1783            let keep_chain_inline = self
1784                .chained_named_call
1785                .is_some_and(|call| call.keep_inline && call.callee.contains(child_expr.span));
1786            let chain_has_indent = !keep_chain_inline
1787                && (is_call_chain(&child_expr.kind, true)
1788                    || !(no_cmnt_or_mixed
1789                        || matches!(&child_expr.kind, ast::ExprKind::CallOptions(..)))
1790                    || !callee_fits_line
1791                    || (member_depth(0, child_expr) >= 2
1792                        && (!total_fits_line || member_or_args.has_comments())));
1793
1794            // Start a new chain if needed
1795            if is_call_chain(&child_expr.kind, false) {
1796                self.call_stack.push(CallContext::chained(callee_size, chain_has_indent));
1797            }
1798
1799            if chain_has_indent {
1800                self.s.ibox(self.ind);
1801            } else {
1802                self.skip_index_break = true;
1803                self.cbox(0);
1804            }
1805        }
1806
1807        // Recursively print the child/prefix expression.
1808        self.print_expr(child_expr);
1809
1810        // If an extra box was opened, close it
1811        if extra_box {
1812            self.end();
1813        }
1814
1815        // Call the closure to print the suffix for the current link, with the calculated position.
1816        print_suffix(self);
1817
1818        // If a chain was started, clean up the state and end the box.
1819        if !parent_is_chain {
1820            if is_call_chain(&child_expr.kind, false) {
1821                self.call_stack.pop();
1822            }
1823            self.end();
1824        }
1825
1826        // Restore cache
1827        if self.skip_index_break {
1828            self.skip_index_break = skip_cache;
1829        }
1830    }
1831
1832    fn print_call_args(
1833        &mut self,
1834        args: &'ast ast::CallArgs<'ast>,
1835        format: ListFormat,
1836        callee_size: usize,
1837        callee_suffix_can_break: bool,
1838    ) {
1839        let ast::CallArgs { span, ref kind } = *args;
1840        if self.handle_span(span, true) {
1841            return;
1842        }
1843
1844        self.call_stack.push(CallContext::nested(callee_size));
1845
1846        // Clear the binary expression cache before the call.
1847        let cache = self.binary_expr.take();
1848
1849        match kind {
1850            ast::CallArgsKind::Unnamed(exprs) => {
1851                self.print_tuple(
1852                    exprs,
1853                    span.lo(),
1854                    span.hi(),
1855                    |this, e| this.print_expr(e),
1856                    get_span!(),
1857                    format,
1858                );
1859            }
1860            ast::CallArgsKind::Named(named_args) => {
1861                let without_ind =
1862                    self.call_stack.has_indented_parent_chain() && !callee_suffix_can_break;
1863                self.print_inside_parens(|state| {
1864                    state.print_named_args(named_args, span.hi(), without_ind)
1865                });
1866            }
1867        }
1868
1869        // Restore the cache to continue with the current chain.
1870        self.binary_expr = cache;
1871        self.call_stack.pop();
1872    }
1873
1874    fn print_named_args(
1875        &mut self,
1876        args: &'ast [ast::NamedArg<'ast>],
1877        pos_hi: BytePos,
1878        without_ind: bool,
1879    ) {
1880        let list_format = match (self.config.bracket_spacing, self.config.prefer_compact.calls()) {
1881            (false, true) => ListFormat::compact(),
1882            (false, false) => ListFormat::consistent(),
1883            (true, true) => ListFormat::compact().with_space(),
1884            (true, false) => ListFormat::consistent().with_space(),
1885        };
1886
1887        self.word("{");
1888        // Use the start position of the first argument's name for comment processing.
1889        if let Some(first_arg) = args.first() {
1890            let list_lo = first_arg.name.span.lo();
1891            self.commasep(
1892                args,
1893                list_lo,
1894                pos_hi,
1895                // Closure to print a single named argument (`name: value`)
1896                |s, arg| {
1897                    s.cbox(0);
1898                    s.print_ident(&arg.name);
1899                    s.word(":");
1900                    if s.same_source_line(arg.name.span.hi(), arg.value.span.hi())
1901                        || !s.print_trailing_comment(arg.name.span.hi(), None)
1902                    {
1903                        s.nbsp();
1904                    }
1905                    s.print_comments(
1906                        arg.value.span.lo(),
1907                        CommentConfig::skip_ws().mixed_no_break().mixed_post_nbsp(),
1908                    );
1909                    s.print_expr(arg.value);
1910                    s.end();
1911                },
1912                |arg| arg.name.span.until(arg.value.span),
1913                list_format
1914                    .break_cmnts()
1915                    .break_single(true)
1916                    .without_ind(without_ind)
1917                    .with_delimiters(!self.call_with_opts_and_args),
1918            );
1919        } else if self.config.bracket_spacing {
1920            self.nbsp();
1921        }
1922        self.word("}");
1923    }
1924
1925    /* --- Statements --- */
1926    /// Prints the given statement in the source code, handling formatting, inline documentation,
1927    /// trailing comments and layout logic for various statement kinds.
1928    fn print_stmt(&mut self, stmt: &'ast ast::Stmt<'ast>) {
1929        let ast::Stmt { ref docs, span, ref kind } = *stmt;
1930        self.print_docs(docs);
1931
1932        // Handle disabled statements.
1933        if self.handle_span(span, false) {
1934            self.print_trailing_comment_no_break(stmt.span.hi(), None);
1935            return;
1936        }
1937
1938        // return statements can't have a preceding comment in the same line.
1939        let force_break = matches!(kind, ast::StmtKind::Return(..))
1940            && self.peek_comment_before(span.lo()).is_some_and(|cmnt| cmnt.style.is_mixed());
1941
1942        match kind {
1943            ast::StmtKind::Assembly(ast::StmtAssembly { dialect, flags, block }) => {
1944                self.print_assembly_stmt(span, dialect, flags, block)
1945            }
1946            ast::StmtKind::DeclSingle(var) => self.print_var(var, true),
1947            ast::StmtKind::DeclMulti(vars, init_expr) => {
1948                self.print_multi_decl_stmt(span, vars, init_expr)
1949            }
1950            ast::StmtKind::Block(stmts) => self.print_block(stmts, span),
1951            ast::StmtKind::Break => self.word("break"),
1952            ast::StmtKind::Continue => self.word("continue"),
1953            ast::StmtKind::DoWhile(stmt, cond) => {
1954                self.word("do ");
1955                self.print_stmt_as_block(stmt, cond.span.lo(), false);
1956                self.nbsp();
1957                self.print_if_cond("while", cond, cond.span.hi());
1958            }
1959            ast::StmtKind::Emit(path, args) => self.print_emit_or_revert("emit", path, args),
1960            ast::StmtKind::Expr(expr) => self.print_expr(expr),
1961            ast::StmtKind::For { init, cond, next, body } => {
1962                self.print_for_stmt(span, init, cond, next, body)
1963            }
1964            ast::StmtKind::If(cond, then, els_opt) => self.print_if_stmt(span, cond, then, els_opt),
1965            ast::StmtKind::Return(expr) => self.print_return_stmt(force_break, expr),
1966            ast::StmtKind::Revert(path, args) => self.print_emit_or_revert("revert", path, args),
1967            ast::StmtKind::Try(ast::StmtTry { expr, clauses }) => {
1968                self.print_try_stmt(expr, clauses)
1969            }
1970            ast::StmtKind::UncheckedBlock(block) => {
1971                self.word("unchecked ");
1972                self.print_block(block, stmt.span);
1973            }
1974            ast::StmtKind::While(cond, stmt) => {
1975                // Check if blocks should be inlined and update cache if necessary
1976                let inline = self.is_single_line_block(span.lo(), cond, stmt, None);
1977                if !inline.is_cached && self.single_line_stmt.is_none() {
1978                    self.single_line_stmt = Some(inline.outcome);
1979                }
1980
1981                // Print while cond and its statement
1982                self.print_if_cond("while", cond, stmt.span.lo());
1983                self.nbsp();
1984                self.print_stmt_as_block(stmt, stmt.span.hi(), inline.outcome);
1985
1986                // Clear cache if necessary
1987                if !inline.is_cached && self.single_line_stmt.is_some() {
1988                    self.single_line_stmt = None;
1989                }
1990            }
1991            ast::StmtKind::Placeholder => self.word("_"),
1992        }
1993        if stmt_needs_semi(kind) {
1994            self.neverbreak(); // semicolon shouldn't account for linebreaks
1995            self.word(";");
1996            self.cursor.advance_to(span.hi(), true);
1997        }
1998        // print comments without breaks, as those are handled by the caller.
1999        self.print_comments(
2000            stmt.span.hi(),
2001            CommentConfig::default().trailing_no_break().mixed_no_break().mixed_prev_space(),
2002        );
2003        self.print_trailing_comment_no_break(stmt.span.hi(), None);
2004    }
2005
2006    /// Prints an `assembly` statement, including optional dialect and flags,
2007    /// followed by its Yul block.
2008    fn print_assembly_stmt(
2009        &mut self,
2010        span: Span,
2011        dialect: &'ast Option<ast::StrLit>,
2012        flags: &'ast [ast::StrLit],
2013        block: &'ast ast::yul::Block<'ast>,
2014    ) {
2015        _ = self.handle_span(self.cursor.span(span.lo()), false);
2016        if !self.handle_span(span.until(block.span), false) {
2017            self.cursor.advance_to(span.lo(), true);
2018            self.print_word("assembly "); // 9 chars
2019            if let Some(dialect) = dialect {
2020                self.print_ast_str_lit(dialect);
2021                self.print_sep(Separator::Nbsp);
2022            }
2023            if !flags.is_empty() {
2024                self.print_tuple(
2025                    flags,
2026                    span.lo(),
2027                    block.span.lo(),
2028                    Self::print_ast_str_lit,
2029                    get_span!(),
2030                    ListFormat::consistent(),
2031                );
2032                self.print_sep(Separator::Nbsp);
2033            }
2034        }
2035        self.print_yul_block(block, block.span, false, 9);
2036    }
2037
2038    /// Prints a multiple-variable declaration with a single initializer expression,
2039    /// formatted as a tuple-style assignment (e.g., `(a, b) = foo();`).
2040    fn print_multi_decl_stmt(
2041        &mut self,
2042        span: Span,
2043        vars: &'ast BoxSlice<'ast, SpannedOption<ast::VariableDefinition<'ast>>>,
2044        init_expr: &'ast ast::Expr<'ast>,
2045    ) {
2046        let space_left = self.space_left();
2047
2048        self.s.ibox(self.ind);
2049        self.s.ibox(-self.ind);
2050        self.print_tuple(
2051            vars,
2052            span.lo(),
2053            init_expr.span.lo(),
2054            |this, var| match var {
2055                SpannedOption::Some(var) => this.print_var(var, true),
2056                SpannedOption::None(span) => {
2057                    this.print_comments(span.hi(), CommentConfig::skip_ws().mixed_no_break_post());
2058                }
2059            },
2060            |var| match var {
2061                SpannedOption::Some(var) => var.span,
2062                // Manually handled by printing the comment when `None`
2063                SpannedOption::None(..) => Span::DUMMY,
2064            },
2065            ListFormat::consistent(),
2066        );
2067        self.end();
2068        self.word(" =");
2069
2070        if self.estimate_size(init_expr.span) + self.config.tab_width
2071            <= std::cmp::max(space_left, self.space_left())
2072        {
2073            self.print_sep(Separator::Space);
2074            self.ibox(0);
2075        } else {
2076            self.print_sep(Separator::Nbsp);
2077            self.neverbreak();
2078            self.s.ibox(-self.ind);
2079        }
2080        self.print_expr(init_expr);
2081        self.end();
2082        self.end();
2083    }
2084
2085    /// Prints a `for` loop statement, including its initializer, condition,
2086    /// increment expression, and loop body, with formatting and spacing.
2087    fn print_for_stmt(
2088        &mut self,
2089        span: Span,
2090        init: &'ast Option<&mut ast::Stmt<'ast>>,
2091        cond: &'ast Option<&mut ast::Expr<'ast>>,
2092        next: &'ast Option<&mut ast::Expr<'ast>>,
2093        body: &'ast ast::Stmt<'ast>,
2094    ) {
2095        self.cbox(0);
2096        self.s.ibox(self.ind);
2097        self.print_word("for (");
2098        self.zerobreak();
2099
2100        // Print init.
2101        self.s.cbox(0);
2102        match init {
2103            Some(init_stmt) => self.print_stmt(init_stmt),
2104            None => self.print_word(";"),
2105        }
2106
2107        // Print condition.
2108        match cond {
2109            Some(cond_expr) => {
2110                self.print_sep(Separator::Space);
2111                self.print_expr(cond_expr);
2112            }
2113            None => self.zerobreak(),
2114        }
2115        self.print_word(";");
2116
2117        // Print next clause.
2118        match next {
2119            Some(next_expr) => {
2120                self.space();
2121                self.print_expr(next_expr);
2122            }
2123            None => self.zerobreak(),
2124        }
2125
2126        // Close head.
2127        self.break_offset_if_not_bol(0, -self.ind, false);
2128        self.end();
2129        self.print_word(") ");
2130        self.neverbreak();
2131        self.end();
2132
2133        // Print comments and body.
2134        self.print_comments(body.span.lo(), CommentConfig::skip_ws());
2135        self.print_stmt_as_block(body, span.hi(), false);
2136        self.end();
2137    }
2138
2139    /// Prints an `if` statement, including its condition, `then` block, and any chained
2140    /// `else` or `else if` branches, handling inline formatting decisions and comments.
2141    fn print_if_stmt(
2142        &mut self,
2143        span: Span,
2144        cond: &'ast ast::Expr<'ast>,
2145        then: &'ast ast::Stmt<'ast>,
2146        els_opt: &'ast Option<&mut ast::Stmt<'ast>>,
2147    ) {
2148        // Check if blocks should be inlined and update cache if necessary
2149        let inline = self.is_single_line_block(span.lo(), cond, then, els_opt.as_ref());
2150        let set_inline_cache = !inline.is_cached && self.single_line_stmt.is_none();
2151        if set_inline_cache {
2152            self.single_line_stmt = Some(inline.outcome);
2153        }
2154
2155        self.cbox(0);
2156        self.ibox(0);
2157        // Print if stmt
2158        self.print_if_no_else(cond, then, inline.outcome);
2159
2160        // Print else (if) stmts, if any
2161        let mut current_else = els_opt.as_deref();
2162        while let Some(els) = current_else {
2163            if self.ends_with('}') {
2164                // If there are comments with line breaks, don't add spaces to mixed comments
2165                if self.has_comment_before_with(els.span.lo(), |cmnt| !cmnt.style.is_mixed()) {
2166                    // If last comment is miced, ensure line break
2167                    if self
2168                        .print_comments(els.span.lo(), CommentConfig::skip_ws().mixed_no_break())
2169                        .is_some_and(|cmnt| cmnt.is_mixed())
2170                    {
2171                        self.hardbreak();
2172                    }
2173                }
2174                // Otherwise, ensure a non-breaking space is added
2175                else if self
2176                    .print_comments(
2177                        els.span.lo(),
2178                        CommentConfig::skip_ws()
2179                            .mixed_no_break()
2180                            .mixed_prev_space()
2181                            .mixed_post_nbsp(),
2182                    )
2183                    .is_none()
2184                {
2185                    self.nbsp();
2186                }
2187            } else {
2188                self.hardbreak_if_not_bol();
2189                if self
2190                    .print_comments(els.span.lo(), CommentConfig::skip_ws())
2191                    .is_some_and(|cmnt| cmnt.is_mixed())
2192                {
2193                    self.hardbreak();
2194                };
2195            }
2196
2197            self.ibox(0);
2198            self.print_word("else ");
2199            match &els.kind {
2200                ast::StmtKind::If(cond, then, next_else) => {
2201                    self.print_if_no_else(cond, then, inline.outcome);
2202                    current_else = next_else.as_deref();
2203                }
2204                _ => {
2205                    self.print_stmt_as_block(els, span.hi(), inline.outcome);
2206                    self.end(); // end ibox for final else
2207                    break;
2208                }
2209            }
2210        }
2211        self.end();
2212
2213        // Clear inline cache if we set it earlier.
2214        if set_inline_cache {
2215            self.single_line_stmt = None;
2216        }
2217    }
2218
2219    /// Prints a `return` statement, optionally including a return expression.
2220    /// Handles spacing, line breaking, and formatting.
2221    fn print_return_stmt(&mut self, force_break: bool, expr: &'ast Option<&mut ast::Expr<'ast>>) {
2222        if force_break {
2223            self.hardbreak_if_not_bol();
2224        }
2225
2226        let space_left = self.space_left();
2227        let expr_size = expr.as_ref().map_or(0, |expr| self.estimate_size(expr.span));
2228
2229        // `return ' + expr + ';'
2230        let overflows = space_left < 8 + expr_size;
2231        let fits_alone = space_left > expr_size;
2232
2233        if let Some(expr) = expr {
2234            let is_simple = matches!(expr.kind, ast::ExprKind::Lit(..) | ast::ExprKind::Ident(..));
2235            let allow_break = overflows && fits_alone;
2236
2237            self.return_bin_expr = matches!(expr.kind, ast::ExprKind::Binary(..));
2238            self.s.ibox(if is_simple || allow_break { self.ind } else { 0 });
2239
2240            self.print_word("return");
2241
2242            match self.print_comments(
2243                expr.span.lo(),
2244                CommentConfig::skip_ws().mixed_no_break().mixed_prev_space().mixed_post_nbsp(),
2245            ) {
2246                Some(cmnt) if cmnt.is_trailing() && !is_simple => self.s.offset(self.ind),
2247                None => self.print_sep(Separator::SpaceOrNbsp(allow_break)),
2248                _ => {}
2249            }
2250
2251            self.print_expr(expr);
2252            self.end();
2253            self.return_bin_expr = false;
2254        } else {
2255            self.print_word("return");
2256        }
2257    }
2258
2259    /// Prints a `try` statement along with its associated `catch` clauses,
2260    /// following Solidity's `try ... returns (...) { ... } catch (...) { ... }` syntax.
2261    fn print_try_stmt(
2262        &mut self,
2263        expr: &'ast ast::Expr<'ast>,
2264        clauses: &'ast [ast::TryCatchClause<'ast>],
2265    ) {
2266        self.cbox(0);
2267        if let Some((first, other)) = clauses.split_first() {
2268            // Print the 'try' clause
2269            let ast::TryCatchClause { args, block, span: try_span, .. } = first;
2270            self.cbox(0);
2271            self.ibox(0);
2272            self.print_word("try ");
2273            self.print_comments(expr.span.lo(), CommentConfig::skip_ws());
2274            self.print_expr(expr);
2275
2276            // Print comments.
2277            self.print_comments(
2278                args.first().map(|p| p.span.lo()).unwrap_or_else(|| expr.span.lo()),
2279                CommentConfig::skip_ws(),
2280            );
2281            if !self.is_beginning_of_line() {
2282                self.nbsp();
2283            }
2284
2285            if args.is_empty() {
2286                self.end();
2287            } else {
2288                self.print_word("returns ");
2289                self.print_word("(");
2290                self.zerobreak();
2291                self.end();
2292                let span = args.span.with_hi(block.span.lo());
2293                self.commasep(
2294                    args,
2295                    span.lo(),
2296                    span.hi(),
2297                    |fmt, var| fmt.print_var(var, false),
2298                    get_span!(),
2299                    ListFormat::compact().with_delimiters(false),
2300                );
2301                self.print_word(")");
2302                self.nbsp();
2303            }
2304            if block.is_empty() {
2305                self.print_block(block, *try_span);
2306                self.end();
2307            } else {
2308                self.print_word("{");
2309                self.end();
2310                self.neverbreak();
2311                self.print_trailing_comment_no_break(try_span.lo(), None);
2312                self.print_block_without_braces(block, try_span.hi(), Some(self.ind));
2313                if self.cursor.enabled || self.cursor.pos < try_span.hi() {
2314                    self.print_word("}");
2315                    self.cursor.advance_to(try_span.hi(), true);
2316                }
2317            }
2318
2319            let mut skip_ind = false;
2320            if self.print_trailing_comment(try_span.hi(), other.first().map(|c| c.span.lo())) {
2321                // if a trailing comment is printed at the very end, we have to manually
2322                // adjust the offset to avoid having a double break.
2323                self.break_offset_if_not_bol(0, self.ind, false);
2324                skip_ind = true;
2325            };
2326
2327            let mut prev_block_multiline = self.is_multiline_block(block, false, true);
2328
2329            // Handle 'catch' clauses
2330            for (pos, ast::TryCatchClause { name, args, block, span: catch_span }) in
2331                other.iter().delimited()
2332            {
2333                let current_block_multiline = self.is_multiline_block(block, false, true);
2334                if !pos.is_first || !skip_ind {
2335                    if (pos.is_first && block.is_empty() && is_call_with_named_args(&expr.kind))
2336                        || (prev_block_multiline && (current_block_multiline || pos.is_last))
2337                    {
2338                        self.nbsp();
2339                    } else {
2340                        self.space();
2341                        if !current_block_multiline {
2342                            self.s.offset(self.ind);
2343                        }
2344                    }
2345                }
2346                self.s.ibox(self.ind);
2347                self.print_comments(
2348                    catch_span.lo(),
2349                    CommentConfig::skip_ws().mixed_no_break().mixed_post_nbsp(),
2350                );
2351
2352                self.print_word("catch ");
2353                if !args.is_empty() {
2354                    self.print_comments(
2355                        args[0].span.lo(),
2356                        CommentConfig::skip_ws().mixed_no_break().mixed_post_nbsp(),
2357                    );
2358                    if let Some(name) = name {
2359                        self.print_ident(name);
2360                    }
2361                    self.print_parameter_list(
2362                        args,
2363                        args.span.with_hi(block.span.lo()),
2364                        ListFormat::inline(),
2365                    );
2366                    self.nbsp();
2367                }
2368                self.print_word("{");
2369                self.end();
2370                if !block.is_empty() {
2371                    self.print_trailing_comment_no_break(catch_span.lo(), None);
2372                }
2373                self.print_block_without_braces(block, catch_span.hi(), Some(self.ind));
2374                if self.cursor.enabled || self.cursor.pos < try_span.hi() {
2375                    self.print_word("}");
2376                    self.cursor.advance_to(catch_span.hi(), true);
2377                }
2378
2379                prev_block_multiline = current_block_multiline;
2380            }
2381        }
2382        self.end();
2383    }
2384
2385    fn print_if_no_else(
2386        &mut self,
2387        cond: &'ast ast::Expr<'ast>,
2388        then: &'ast ast::Stmt<'ast>,
2389        inline: bool,
2390    ) {
2391        if !self.handle_span(cond.span.until(then.span), true) {
2392            self.print_if_cond("if", cond, then.span.lo());
2393            // if empty block without comments, ensure braces are inlined
2394            if let ast::StmtKind::Block(block) = &then.kind
2395                && block.is_empty()
2396                && self.peek_comment_before(then.span.hi()).is_none()
2397            {
2398                self.neverbreak();
2399                self.print_sep(Separator::Nbsp);
2400            } else {
2401                self.print_sep(Separator::Space);
2402            }
2403        }
2404        self.end();
2405        self.print_stmt_as_block(then, then.span.hi(), inline);
2406        self.cursor.advance_to(then.span.hi(), true);
2407    }
2408
2409    fn print_if_cond(&mut self, kw: &'static str, cond: &'ast ast::Expr<'ast>, pos_hi: BytePos) {
2410        self.print_word(kw);
2411        self.print_sep_unhandled(Separator::Nbsp);
2412        self.print_tuple(
2413            std::slice::from_ref(cond),
2414            cond.span.lo(),
2415            pos_hi,
2416            Self::print_expr,
2417            get_span!(),
2418            ListFormat::compact().break_cmnts().break_single(is_binary_expr(&cond.kind)),
2419        );
2420    }
2421
2422    fn print_emit_or_revert(
2423        &mut self,
2424        kw: &'static str,
2425        path: &'ast ast::PathSlice,
2426        args: &'ast ast::CallArgs<'ast>,
2427    ) {
2428        self.word(kw);
2429        if self
2430            .print_comments(
2431                path.span().lo(),
2432                CommentConfig::skip_ws().mixed_no_break().mixed_prev_space().mixed_post_nbsp(),
2433            )
2434            .is_none()
2435        {
2436            self.nbsp();
2437        };
2438        self.s.cbox(0);
2439        self.emit_or_revert = path.segments().len() > 1;
2440        self.print_path(path, false);
2441        let format = if self.config.prefer_compact.calls() {
2442            ListFormat::compact()
2443        } else {
2444            ListFormat::consistent()
2445        };
2446        self.print_call_args(args, format.break_cmnts(), path.to_string().len(), false);
2447        self.emit_or_revert = false;
2448        self.end();
2449    }
2450
2451    fn print_block(&mut self, block: &'ast [ast::Stmt<'ast>], span: Span) {
2452        self.print_block_inner(
2453            block,
2454            BlockFormat::Regular,
2455            Self::print_stmt,
2456            |b| b.span,
2457            span.hi(),
2458        );
2459    }
2460
2461    fn print_block_without_braces(
2462        &mut self,
2463        block: &'ast [ast::Stmt<'ast>],
2464        pos_hi: BytePos,
2465        offset: Option<isize>,
2466    ) {
2467        self.print_block_inner(
2468            block,
2469            BlockFormat::NoBraces(offset),
2470            Self::print_stmt,
2471            |b| b.span,
2472            pos_hi,
2473        );
2474    }
2475
2476    // Body of a if/loop.
2477    fn print_stmt_as_block(&mut self, stmt: &'ast ast::Stmt<'ast>, pos_hi: BytePos, inline: bool) {
2478        if self.handle_span(stmt.span, false) {
2479            return;
2480        }
2481
2482        let stmts = if let ast::StmtKind::Block(stmts) = &stmt.kind {
2483            stmts
2484        } else {
2485            std::slice::from_ref(stmt)
2486        };
2487
2488        if inline && stmts.len() == 1 {
2489            self.neverbreak();
2490            self.print_block_without_braces(stmts, pos_hi, None);
2491        } else {
2492            // Reset cache for nested (child) stmts within this (parent) block.
2493            let inline_parent = self.single_line_stmt.take();
2494
2495            self.print_word("{");
2496            self.print_block_without_braces(stmts, pos_hi, Some(self.ind));
2497            self.print_word("}");
2498
2499            // Restore cache for the rest of stmts within the same height.
2500            self.single_line_stmt = inline_parent;
2501        }
2502    }
2503
2504    /// Determines if an `if/else` block should be inlined.
2505    /// Also returns if the value was cached, so that it can be cleaned afterwards.
2506    ///
2507    /// # Returns
2508    ///
2509    /// A tuple `(should_inline, was_cached)`. The second boolean is `true` if the
2510    /// decision was retrieved from the cache or is a final decision based on config,
2511    /// preventing the caller from clearing a cache value that was never set.
2512    fn is_single_line_block(
2513        &mut self,
2514        stmt_span_lo: BytePos,
2515        cond: &'ast ast::Expr<'ast>,
2516        then: &'ast ast::Stmt<'ast>,
2517        els_opt: Option<&'ast &'ast mut ast::Stmt<'ast>>,
2518    ) -> Decision {
2519        // Dangling-else guard runs before the cache check so an inlined parent can't
2520        // coerce this `if` into dropping braces and rebinding its `else`.
2521        if Self::then_block_can_capture_trailing_else(then, els_opt.is_some()) {
2522            return Decision { outcome: false, is_cached: false };
2523        }
2524
2525        // If a decision is already cached from a parent, use it directly.
2526        if let Some(cached_decision) = self.single_line_stmt {
2527            return Decision { outcome: cached_decision, is_cached: true };
2528        }
2529
2530        // Empty statements are always printed as blocks.
2531        if std::slice::from_ref(then).is_empty() {
2532            return Decision { outcome: false, is_cached: false };
2533        }
2534
2535        // Comments near cond can break single-line layouts. Print as blocks in this case
2536        if self.peek_comment_between(stmt_span_lo, then.span.lo()).is_some() {
2537            return Decision { outcome: false, is_cached: false };
2538        }
2539
2540        // If possible, take an early decision based on the block style configuration.
2541        match self.config.single_line_statement_blocks {
2542            config::SingleLineBlockStyle::Preserve => {
2543                if self.is_stmt_in_new_line(cond, then) || self.is_multiline_block_stmt(then, true)
2544                {
2545                    return Decision { outcome: false, is_cached: false };
2546                }
2547            }
2548            config::SingleLineBlockStyle::Single => {
2549                if self.is_multiline_block_stmt(then, true) {
2550                    return Decision { outcome: false, is_cached: false };
2551                }
2552            }
2553            config::SingleLineBlockStyle::Multi => {
2554                return Decision { outcome: false, is_cached: false };
2555            }
2556        };
2557
2558        // If no decision was made, estimate the length to be formatted.
2559        // NOTE: conservative check -> worst-case scenario is formatting as multi-line block.
2560        if !self.can_stmts_be_inlined(cond, then, els_opt) {
2561            return Decision { outcome: false, is_cached: false };
2562        }
2563
2564        // If the parent would fit, check all of its children.
2565        if let ast::StmtKind::If(child_cond, child_then, child_els_opt) = &then.kind {
2566            let child_decision = self.is_single_line_block(
2567                then.span.lo(),
2568                child_cond,
2569                child_then,
2570                child_els_opt.as_ref(),
2571            );
2572            if !child_decision.outcome {
2573                return child_decision;
2574            }
2575        }
2576        if let Some(stmt) = els_opt {
2577            if let ast::StmtKind::If(child_cond, child_then, child_els_opt) = &stmt.kind {
2578                return self.is_single_line_block(
2579                    stmt.span.lo(),
2580                    child_cond,
2581                    child_then,
2582                    child_els_opt.as_ref(),
2583                );
2584            } else if self.is_multiline_block_stmt(stmt, true) {
2585                return Decision { outcome: false, is_cached: false };
2586            }
2587        }
2588
2589        // If all children can also fit, allow single-line block.
2590        Decision { outcome: true, is_cached: false }
2591    }
2592
2593    fn is_inline_stmt(&self, stmt: &'ast ast::Stmt<'ast>, cond_len: usize) -> bool {
2594        if let ast::StmtKind::If(cond, then, els_opt) = &stmt.kind {
2595            let if_span = cond.span.to(then.span);
2596            if self.sm.is_multiline(if_span)
2597                && matches!(
2598                    self.config.single_line_statement_blocks,
2599                    config::SingleLineBlockStyle::Preserve
2600                )
2601            {
2602                return false;
2603            }
2604            if cond_len + self.estimate_size(if_span) >= self.space_left() {
2605                return false;
2606            }
2607            if let Some(els) = els_opt
2608                && !self.is_inline_stmt(els, 6)
2609            {
2610                return false;
2611            }
2612        } else {
2613            if matches!(
2614                self.config.single_line_statement_blocks,
2615                config::SingleLineBlockStyle::Preserve
2616            ) && self.sm.is_multiline(stmt.span)
2617            {
2618                return false;
2619            }
2620            if cond_len + self.estimate_size(stmt.span) >= self.space_left() {
2621                return false;
2622            }
2623        }
2624        true
2625    }
2626
2627    /// Checks if a statement was explicitly written in a new line.
2628    fn is_stmt_in_new_line(
2629        &self,
2630        cond: &'ast ast::Expr<'ast>,
2631        then: &'ast ast::Stmt<'ast>,
2632    ) -> bool {
2633        let span_between = cond.span.between(then.span);
2634        if let Ok(snip) = self.sm.span_to_snippet(span_between) {
2635            // Check for newlines after the closing parenthesis of the `if (...)`.
2636            if let Some((_, after_paren)) = snip.split_once(')') {
2637                return after_paren.lines().count() > 1;
2638            }
2639        }
2640        false
2641    }
2642
2643    /// Returns true if eliding the braces of `then` would expose an inner `if` to a
2644    /// trailing `else` or change the AST shape on round-trip.
2645    fn then_block_can_capture_trailing_else(
2646        then: &'ast ast::Stmt<'ast>,
2647        has_outer_else: bool,
2648    ) -> bool {
2649        let ast::StmtKind::Block(block) = &then.kind else { return false };
2650        if block.stmts.len() != 1 {
2651            return false;
2652        }
2653        match &block.stmts[0].kind {
2654            ast::StmtKind::If(_, _, inner_else) => has_outer_else || inner_else.is_some(),
2655            ast::StmtKind::While(..) | ast::StmtKind::For { .. } => has_outer_else,
2656            _ => false,
2657        }
2658    }
2659
2660    /// Checks if a block statement `{ ... }` contains more than one line of actual code.
2661    fn is_multiline_block_stmt(
2662        &mut self,
2663        stmt: &'ast ast::Stmt<'ast>,
2664        empty_as_multiline: bool,
2665    ) -> bool {
2666        match &stmt.kind {
2667            ast::StmtKind::Block(block) => {
2668                self.is_multiline_block(block, empty_as_multiline, false)
2669            }
2670            ast::StmtKind::While(cond, body) => {
2671                !self.is_single_line_block(stmt.span.lo(), cond, body, None).outcome
2672            }
2673            ast::StmtKind::For { body, .. } => {
2674                // In `print_for_stmt`, `print_stmt_as_block(body, span.hi(), false)` is called with
2675                // `inline = false`. So only empty can be single-line.
2676                if let ast::StmtKind::Block(block) = &body.kind {
2677                    self.is_multiline_block(block, empty_as_multiline, true)
2678                } else {
2679                    true
2680                }
2681            }
2682
2683            ast::StmtKind::If(_, _, Some(_)) => true,
2684            ast::StmtKind::If(_, then, None) => {
2685                self.is_multiline_block_stmt(then, empty_as_multiline)
2686            }
2687
2688            // these ones always has an inner block, so we mark them as multiline
2689            ast::StmtKind::Assembly(_)
2690            | ast::StmtKind::DoWhile(_, _)
2691            | ast::StmtKind::Try(_)
2692            | ast::StmtKind::UncheckedBlock(_) => true,
2693
2694            ast::StmtKind::Break
2695            | ast::StmtKind::Continue
2696            | ast::StmtKind::DeclMulti(_, _)
2697            | ast::StmtKind::DeclSingle(_)
2698            | ast::StmtKind::Emit(_, _)
2699            | ast::StmtKind::Expr(_)
2700            | ast::StmtKind::Return(_)
2701            | ast::StmtKind::Revert(_, _)
2702            | ast::StmtKind::Placeholder => false,
2703        }
2704    }
2705
2706    /// Checks if a block statement `{ ... }` should be treated as multiline,
2707    /// either because it spans multiple lines or contains multiple statements.
2708    fn is_multiline_block(
2709        &mut self,
2710        block: &'ast ast::Block<'ast>,
2711        empty_as_multiline: bool,
2712        force_single_as_multiline: bool,
2713    ) -> bool {
2714        if block.stmts.is_empty() {
2715            return empty_as_multiline;
2716        }
2717        // A block with multiple statements should never be inlined, regardless of
2718        // whether it was written on a single line in the source.
2719        if block.stmts.len() > 1 {
2720            return true;
2721        }
2722
2723        if force_single_as_multiline {
2724            return true;
2725        }
2726
2727        // Check for multiline block.span first.
2728        // Block can spans multipline because of comments.
2729        if self.sm.is_multiline(block.span)
2730            && let Ok(snip) = self.sm.span_to_snippet(block.span)
2731        {
2732            let code_lines = snip.lines().filter(|line| {
2733                let trimmed = line.trim();
2734                // Ignore empty lines and lines with only '{' or '}'
2735                if empty_as_multiline {
2736                    !trimmed.is_empty() && trimmed != "{" && trimmed != "}"
2737                } else {
2738                    !trimmed.is_empty()
2739                }
2740            });
2741            if code_lines.count() > 1 {
2742                return true;
2743            }
2744        }
2745
2746        let stmt = &block.stmts[0];
2747
2748        // Comments can break single-line layout. Mark block as multiline if there is a comment at
2749        // the beginning.
2750        if self.peek_comment_between(block.span.lo(), stmt.span.lo()).is_some() {
2751            return true;
2752        }
2753
2754        self.is_multiline_block_stmt(stmt, empty_as_multiline)
2755    }
2756
2757    /// Performs a size estimation to see if the if/else can fit on one line.
2758    fn can_stmts_be_inlined(
2759        &mut self,
2760        cond: &'ast ast::Expr<'ast>,
2761        then: &'ast ast::Stmt<'ast>,
2762        els_opt: Option<&'ast &'ast mut ast::Stmt<'ast>>,
2763    ) -> bool {
2764        let cond_len = self.estimate_size(cond.span);
2765
2766        // If the condition fits in one line, 6 chars: 'if (' + {cond} + ') ' + {then}
2767        // Otherwise chars: ') ' + {then}
2768        let then_margin = if 6 + cond_len < self.space_left() { 6 + cond_len } else { 2 };
2769
2770        if !self.is_inline_stmt(then, then_margin) {
2771            return false;
2772        }
2773
2774        // Always 6 chars for the else: 'else '
2775        els_opt.is_none_or(|els| self.is_inline_stmt(els, 6))
2776    }
2777
2778    fn can_header_be_inlined(&mut self, func: &ast::ItemFunction<'_>) -> bool {
2779        self.estimate_header_size(func) <= self.space_left()
2780    }
2781
2782    fn can_header_params_be_inlined(&mut self, func: &ast::ItemFunction<'_>) -> bool {
2783        self.estimate_header_params_size(func) <= self.space_left()
2784    }
2785
2786    fn estimate_header_size(&mut self, func: &ast::ItemFunction<'_>) -> usize {
2787        let ast::ItemFunction { kind: _, ref header, ref body, body_span: _ } = *func;
2788
2789        // ' ' + visibility
2790        let visibility = header.visibility.map_or(0, |v| self.estimate_size(v.span) + 1);
2791        // ' ' + state mutability
2792        let mutability = header.state_mutability.map_or(0, |sm| self.estimate_size(sm.span) + 1);
2793        // ' ' + modifier + (' ' + modifier)
2794        let m = header.modifiers.iter().fold(0, |len, m| len + self.estimate_size(m.span()));
2795        let modifiers = if m != 0 { m + 1 } else { 0 };
2796        // ' ' + override
2797        let override_ = header.override_.as_ref().map_or(0, |o| self.estimate_size(o.span) + 1);
2798        // ' ' + virtual
2799        let virtual_ = if header.virtual_.is_none() { 0 } else { 8 };
2800        // ' returns(' + var + (', ' + var) + ')'
2801        let returns = header.returns.as_ref().map_or(0, |ret| {
2802            ret.vars
2803                .iter()
2804                .fold(0, |len, p| if len != 0 { len + 2 } else { 10 } + self.estimate_size(p.span))
2805        });
2806        // ' {' or ';'
2807        let end = if body.is_some() { 2 } else { 1 };
2808
2809        self.estimate_header_params_size(func)
2810            + visibility
2811            + mutability
2812            + modifiers
2813            + override_
2814            + virtual_
2815            + returns
2816            + end
2817    }
2818
2819    fn estimate_header_params_size(&mut self, func: &ast::ItemFunction<'_>) -> usize {
2820        let ast::ItemFunction { kind, ref header, body: _, body_span: _ } = *func;
2821
2822        let kw = match kind {
2823            ast::FunctionKind::Constructor => 11, // 'constructor'
2824            ast::FunctionKind::Function => 9,     // 'function '
2825            ast::FunctionKind::Modifier => 9,     // 'modifier '
2826            ast::FunctionKind::Fallback => 8,     // 'fallback'
2827            ast::FunctionKind::Receive => 7,      // 'receive'
2828        };
2829
2830        // '(' + param + (', ' + param) + ')'
2831        let params = header
2832            .parameters
2833            .vars
2834            .iter()
2835            .fold(0, |len, p| if len != 0 { len + 2 } else { 2 } + self.estimate_size(p.span));
2836
2837        kw + header.name.map_or(0, |name| self.estimate_size(name.span)) + std::cmp::max(2, params)
2838    }
2839
2840    fn estimate_lhs_size(&self, expr: &ast::Expr<'_>, parent_op: &ast::BinOp) -> usize {
2841        match &expr.kind {
2842            ast::ExprKind::Binary(lhs, op, _) if op.kind.group() == parent_op.kind.group() => {
2843                self.estimate_lhs_size(lhs, op)
2844            }
2845            _ => self.estimate_size(expr.span),
2846        }
2847    }
2848
2849    fn estimate_call_chain_size(&self, expr: &ast::Expr<'_>) -> Option<usize> {
2850        match &expr.kind {
2851            ast::ExprKind::Call(callee, args) => {
2852                let ast::CallArgsKind::Unnamed(args) = &args.kind else { return None };
2853                let mut size = self.estimate_call_chain_size(callee)? + 2;
2854                for arg in args.iter() {
2855                    size += self.estimate_call_chain_size(arg)?;
2856                }
2857                Some(size + args.len().saturating_sub(1) * 2)
2858            }
2859            ast::ExprKind::Ident(ident) => Some(ident.to_string().len()),
2860            ast::ExprKind::Index(expr, kind) => {
2861                let index_size = match kind {
2862                    ast::IndexKind::Index(Some(index)) => self.estimate_call_chain_size(index)?,
2863                    ast::IndexKind::Index(None) => 0,
2864                    ast::IndexKind::Range(start, end) => {
2865                        let start = match start {
2866                            Some(start) => self.estimate_call_chain_size(start)?,
2867                            None => 0,
2868                        };
2869                        let end = match end {
2870                            Some(end) => self.estimate_call_chain_size(end)?,
2871                            None => 0,
2872                        };
2873                        start + end + 1
2874                    }
2875                };
2876                Some(self.estimate_call_chain_size(expr)? + index_size + 2)
2877            }
2878            // Zero is invariant under all number underscore configurations.
2879            ast::ExprKind::Lit(lit, None)
2880                if matches!(lit.kind, ast::LitKind::Number(_)) && lit.symbol.as_str() == "0" =>
2881            {
2882                Some(1)
2883            }
2884            ast::ExprKind::Member(expr, ident) => {
2885                Some(self.estimate_call_chain_size(expr)? + ident.to_string().len() + 1)
2886            }
2887            ast::ExprKind::Tuple(exprs) if let [SpannedOption::Some(expr)] = exprs.as_ref() => {
2888                Some(self.estimate_call_chain_size(expr)? + 2)
2889            }
2890            _ => None,
2891        }
2892    }
2893
2894    fn has_comments_between_elements<I>(&self, limits: Span, elements: I) -> bool
2895    where
2896        I: IntoIterator<Item = &'ast ast::Expr<'ast>>,
2897    {
2898        let mut last_span_end = limits.lo();
2899        for expr in elements {
2900            if self.has_comment_between(last_span_end, expr.span.lo()) {
2901                return true;
2902            }
2903            last_span_end = expr.span.hi();
2904        }
2905
2906        if self.has_comment_between(last_span_end, limits.hi()) {
2907            return true;
2908        }
2909
2910        false
2911    }
2912}
2913
2914// -- HELPERS (language-specific) ----------------------------------------------
2915
2916#[derive(Debug)]
2917enum MemberOrCallArgs {
2918    Member(usize),
2919    CallArgs(usize, bool),
2920}
2921
2922impl MemberOrCallArgs {
2923    const fn size(&self) -> usize {
2924        match self {
2925            Self::CallArgs(size, ..) | Self::Member(size) => *size,
2926        }
2927    }
2928
2929    const fn member_size(&self) -> usize {
2930        match self {
2931            Self::CallArgs(..) => 0,
2932            Self::Member(size) => *size,
2933        }
2934    }
2935
2936    const fn has_comments(&self) -> bool {
2937        matches!(self, Self::CallArgs(.., true))
2938    }
2939}
2940
2941#[derive(Debug, Clone)]
2942#[expect(dead_code)]
2943enum AttributeKind<'ast> {
2944    Visibility(ast::Visibility),
2945    StateMutability(ast::StateMutability),
2946    Virtual,
2947    Override(&'ast ast::Override<'ast>),
2948    Modifier(&'ast ast::Modifier<'ast>),
2949}
2950
2951type AttributeCommentMap = HashMap<BytePos, (Vec<Comment>, Vec<Comment>, Vec<Comment>)>;
2952
2953#[derive(Debug, Clone)]
2954struct AttributeInfo<'ast> {
2955    kind: AttributeKind<'ast>,
2956    span: Span,
2957}
2958
2959/// Helper struct to map attributes to their associated comments in function headers.
2960struct AttributeCommentMapper<'ast> {
2961    limit_pos: BytePos,
2962    comments: Vec<Comment>,
2963    attributes: Vec<AttributeInfo<'ast>>,
2964}
2965
2966impl<'ast> AttributeCommentMapper<'ast> {
2967    fn new(returns: Option<&'ast ast::ParameterList<'ast>>, body_pos: BytePos) -> Self {
2968        Self {
2969            comments: Vec::new(),
2970            attributes: Vec::new(),
2971            limit_pos: returns.as_ref().map_or(body_pos, |ret| ret.span.lo()),
2972        }
2973    }
2974
2975    #[allow(clippy::type_complexity)]
2976    fn build(
2977        mut self,
2978        state: &mut State<'_, 'ast>,
2979        header: &'ast ast::FunctionHeader<'ast>,
2980    ) -> (AttributeCommentMap, Vec<AttributeInfo<'ast>>, BytePos) {
2981        let first_attr = self.collect_attributes(header);
2982        self.cache_comments(state);
2983        (self.map(), self.attributes, first_attr)
2984    }
2985
2986    fn map(&mut self) -> AttributeCommentMap {
2987        let mut map = HashMap::new();
2988        for a in 0..self.attributes.len() {
2989            let is_last = a == self.attributes.len() - 1;
2990            let (mut before, mut inner, mut after) = (Vec::new(), Vec::new(), Vec::new());
2991
2992            let before_limit = self.attributes[a].span.lo();
2993            let inner_limit = self.attributes[a].span.hi();
2994            let after_limit =
2995                if is_last { self.limit_pos } else { self.attributes[a + 1].span.lo() };
2996
2997            let mut c = 0;
2998            while c < self.comments.len() {
2999                if self.comments[c].pos() <= before_limit {
3000                    before.push(self.comments.remove(c));
3001                } else if self.comments[c].pos() <= inner_limit {
3002                    inner.push(self.comments.remove(c));
3003                } else if (after.is_empty() || is_last) && self.comments[c].pos() <= after_limit {
3004                    after.push(self.comments.remove(c));
3005                } else {
3006                    c += 1;
3007                }
3008            }
3009            map.insert(before_limit, (before, inner, after));
3010        }
3011        map
3012    }
3013
3014    fn collect_attributes(&mut self, header: &'ast ast::FunctionHeader<'ast>) -> BytePos {
3015        let mut first_pos = BytePos(u32::MAX);
3016        if let Some(v) = header.visibility {
3017            if v.span.lo() < first_pos {
3018                first_pos = v.span.lo()
3019            }
3020            self.attributes
3021                .push(AttributeInfo { kind: AttributeKind::Visibility(*v), span: v.span });
3022        }
3023        if let Some(sm) = header.state_mutability {
3024            if sm.span.lo() < first_pos {
3025                first_pos = sm.span.lo()
3026            }
3027            self.attributes
3028                .push(AttributeInfo { kind: AttributeKind::StateMutability(*sm), span: sm.span });
3029        }
3030        if let Some(span) = header.virtual_ {
3031            if span.lo() < first_pos {
3032                first_pos = span.lo()
3033            }
3034            self.attributes.push(AttributeInfo { kind: AttributeKind::Virtual, span });
3035        }
3036        if let Some(ref o) = header.override_ {
3037            if o.span.lo() < first_pos {
3038                first_pos = o.span.lo()
3039            }
3040            self.attributes.push(AttributeInfo { kind: AttributeKind::Override(o), span: o.span });
3041        }
3042        for m in header.modifiers.iter() {
3043            if m.span().lo() < first_pos {
3044                first_pos = m.span().lo()
3045            }
3046            self.attributes
3047                .push(AttributeInfo { kind: AttributeKind::Modifier(m), span: m.span() });
3048        }
3049        self.attributes.sort_by_key(|attr| attr.span.lo());
3050        first_pos
3051    }
3052
3053    fn cache_comments(&mut self, state: &mut State<'_, 'ast>) {
3054        let mut pending = None;
3055        for cmnt in state.comments.iter() {
3056            if cmnt.pos() >= self.limit_pos {
3057                break;
3058            }
3059            match pending {
3060                Some(ref p) => pending = Some(p + 1),
3061                None => pending = Some(0),
3062            }
3063        }
3064        while let Some(p) = pending {
3065            if p == 0 {
3066                pending = None;
3067            } else {
3068                pending = Some(p - 1);
3069            }
3070            let cmnt = state.next_comment().unwrap();
3071            if cmnt.style.is_blank() {
3072                continue;
3073            }
3074            self.comments.push(cmnt);
3075        }
3076    }
3077}
3078
3079const fn stmt_needs_semi(stmt: &ast::StmtKind<'_>) -> bool {
3080    match stmt {
3081        ast::StmtKind::Assembly { .. }
3082        | ast::StmtKind::Block { .. }
3083        | ast::StmtKind::For { .. }
3084        | ast::StmtKind::If { .. }
3085        | ast::StmtKind::Try { .. }
3086        | ast::StmtKind::UncheckedBlock { .. }
3087        | ast::StmtKind::While { .. } => false,
3088
3089        ast::StmtKind::DeclSingle { .. }
3090        | ast::StmtKind::DeclMulti { .. }
3091        | ast::StmtKind::Break { .. }
3092        | ast::StmtKind::Continue { .. }
3093        | ast::StmtKind::DoWhile { .. }
3094        | ast::StmtKind::Emit { .. }
3095        | ast::StmtKind::Expr { .. }
3096        | ast::StmtKind::Return { .. }
3097        | ast::StmtKind::Revert { .. }
3098        | ast::StmtKind::Placeholder { .. } => true,
3099    }
3100}
3101
3102/// Returns `true` if the item needs an isolated line break.
3103fn item_needs_iso(item: &ast::ItemKind<'_>) -> bool {
3104    match item {
3105        ast::ItemKind::Pragma(..)
3106        | ast::ItemKind::Import(..)
3107        | ast::ItemKind::Using(..)
3108        | ast::ItemKind::Variable(..)
3109        | ast::ItemKind::Udvt(..)
3110        | ast::ItemKind::Enum(..)
3111        | ast::ItemKind::Error(..)
3112        | ast::ItemKind::Event(..) => false,
3113
3114        ast::ItemKind::Contract(..) => true,
3115
3116        ast::ItemKind::Struct(strukt) => !strukt.fields.is_empty(),
3117        ast::ItemKind::Function(func) => {
3118            func.body.as_ref().is_some_and(|b| !b.is_empty())
3119                && !matches!(func.kind, ast::FunctionKind::Modifier)
3120        }
3121    }
3122}
3123
3124const fn is_binary_expr(expr_kind: &ast::ExprKind<'_>) -> bool {
3125    matches!(expr_kind, ast::ExprKind::Binary(..))
3126}
3127
3128fn has_complex_successor(expr_kind: &ast::ExprKind<'_>, left: bool) -> bool {
3129    match expr_kind {
3130        ast::ExprKind::Binary(lhs, _, rhs) => {
3131            if left {
3132                has_complex_successor(&lhs.kind, left)
3133            } else {
3134                has_complex_successor(&rhs.kind, left)
3135            }
3136        }
3137        ast::ExprKind::Unary(_, expr) => has_complex_successor(&expr.kind, left),
3138        ast::ExprKind::Lit(..) | ast::ExprKind::Ident(_) => false,
3139        ast::ExprKind::Tuple(..) => false,
3140        _ => true,
3141    }
3142}
3143
3144const fn is_call(expr_kind: &ast::ExprKind<'_>) -> bool {
3145    matches!(expr_kind, ast::ExprKind::Call(..))
3146}
3147
3148/// Returns true if this is a call with named arguments (struct-style syntax).
3149/// Used to determine if `.field` after such a call should avoid breaking.
3150/// E.g., `_lzSend({_dstEid: x, ...}).guid` → true (named args call)
3151/// E.g., `someFunc(a, b).field` → false (positional args)
3152const fn is_call_with_named_args(expr_kind: &ast::ExprKind<'_>) -> bool {
3153    if let ast::ExprKind::Call(_, args) = expr_kind {
3154        matches!(args.kind, ast::CallArgsKind::Named(_))
3155    } else {
3156        false
3157    }
3158}
3159
3160fn is_call_chain(expr_kind: &ast::ExprKind<'_>, must_have_child: bool) -> bool {
3161    match expr_kind {
3162        ast::ExprKind::Index(child, ..) | ast::ExprKind::Member(child, ..) => {
3163            is_call_chain(&child.kind, false)
3164        }
3165        ast::ExprKind::Tuple(exprs) if let [SpannedOption::Some(child)] = exprs.as_ref() => {
3166            is_call_chain(&child.kind, must_have_child)
3167        }
3168        _ => !must_have_child && is_call(expr_kind),
3169    }
3170}
3171
3172fn call_chain_contains_options(expr: &ast::Expr<'_>) -> bool {
3173    match &expr.peel_parens().kind {
3174        ast::ExprKind::CallOptions(..) => true,
3175        ast::ExprKind::Call(expr, ..)
3176        | ast::ExprKind::Index(expr, ..)
3177        | ast::ExprKind::Member(expr, ..) => call_chain_contains_options(expr),
3178        _ => false,
3179    }
3180}
3181
3182fn is_call_with_opts_and_args(expr_kind: &ast::ExprKind<'_>) -> bool {
3183    if let ast::ExprKind::Call(call_expr, call_args) = expr_kind {
3184        matches!(call_expr.kind, ast::ExprKind::CallOptions(..)) && !call_args.is_empty()
3185    } else {
3186        false
3187    }
3188}
3189
3190#[derive(Debug)]
3191struct Decision {
3192    outcome: bool,
3193    is_cached: bool,
3194}
3195
3196#[derive(Clone, Copy, PartialEq, Eq)]
3197pub(crate) enum BinOpGroup {
3198    Arithmetic,
3199    Bitwise,
3200    Comparison,
3201    Logical,
3202}
3203
3204trait BinOpExt {
3205    fn group(&self) -> BinOpGroup;
3206}
3207
3208impl BinOpExt for ast::BinOpKind {
3209    fn group(&self) -> BinOpGroup {
3210        match self {
3211            Self::Or | Self::And => BinOpGroup::Logical,
3212            Self::Eq | Self::Ne | Self::Lt | Self::Le | Self::Gt | Self::Ge => {
3213                BinOpGroup::Comparison
3214            }
3215            Self::BitOr | Self::BitXor | Self::BitAnd | Self::Shl | Self::Shr | Self::Sar => {
3216                BinOpGroup::Bitwise
3217            }
3218            Self::Add | Self::Sub | Self::Mul | Self::Div | Self::Rem | Self::Pow => {
3219                BinOpGroup::Arithmetic
3220            }
3221        }
3222    }
3223}
3224
3225/// Calculates the size the callee's "head," excluding its arguments.
3226///
3227/// # Examples
3228///
3229/// - `myFunction(..)`: 8 (length of `myFunction`)
3230/// - `uint256(..)`: 7 (length of `uint256`)
3231/// - `abi.encode(..)`: 10 (length of `abi.encode`)
3232/// - `foo(..).bar(..)`: 3 (length of `foo`)
3233pub(super) fn get_callee_head_size(callee: &ast::Expr<'_>) -> usize {
3234    match &callee.kind {
3235        ast::ExprKind::Ident(id) => id.as_str().len(),
3236        ast::ExprKind::Type(ast::Type { kind: ast::TypeKind::Elementary(ty), .. }) => {
3237            ty.to_abi_str().len()
3238        }
3239        ast::ExprKind::Index(base, idx) => {
3240            let idx_len = match idx {
3241                ast::IndexKind::Index(expr) => expr.as_ref().map_or(0, |e| get_callee_head_size(e)),
3242                ast::IndexKind::Range(e1, e2) => {
3243                    1 + e1.as_ref().map_or(0, |e| get_callee_head_size(e))
3244                        + e2.as_ref().map_or(0, |e| get_callee_head_size(e))
3245                }
3246            };
3247            get_callee_head_size(base) + 2 + idx_len
3248        }
3249        ast::ExprKind::Member(base, member_ident) => {
3250            match &base.kind {
3251                ast::ExprKind::Ident(..) | ast::ExprKind::Type(..) => {
3252                    get_callee_head_size(base) + 1 + member_ident.as_str().len()
3253                }
3254
3255                // Chainned calls are not traversed, and instead just the member identifier is used
3256                ast::ExprKind::Member(child, ..)
3257                    if !matches!(&child.kind, ast::ExprKind::Call(..)) =>
3258                {
3259                    get_callee_head_size(base) + 1 + member_ident.as_str().len()
3260                }
3261                _ => member_ident.as_str().len(),
3262            }
3263        }
3264        ast::ExprKind::Binary(lhs, _, _) => get_callee_head_size(lhs),
3265
3266        // If the callee is not an identifier or member access, it has no "head"
3267        _ => 0,
3268    }
3269}
3270
3271#[cfg(test)]
3272mod tests {
3273    use super::*;
3274    use crate::{FormatterConfig, InlineConfig};
3275    use foundry_common::comments::Comments;
3276    use solar::{
3277        interface::{Session, source_map::FileName},
3278        sema::Compiler,
3279    };
3280    use std::sync::Arc;
3281
3282    /// This helper extracts function headers from the AST and passes them to the test function.
3283    fn parse_and_test<F>(source: &str, test_fn: F)
3284    where
3285        F: FnOnce(&mut State<'_, '_>, &ast::ItemFunction<'_>) + Send,
3286    {
3287        let session = Session::builder().with_buffer_emitter(Default::default()).build();
3288        let mut compiler = Compiler::new(session);
3289
3290        compiler
3291            .enter_mut(|c| -> solar::interface::Result<()> {
3292                let mut pcx = c.parse();
3293                pcx.set_resolve_imports(false);
3294
3295                // Create a source file using stdin as the filename
3296                let file = c
3297                    .sess()
3298                    .source_map()
3299                    .new_source_file(FileName::Stdin, source)
3300                    .map_err(|e| c.sess().dcx.err(e.to_string()).emit())?;
3301
3302                pcx.add_file(file.clone());
3303                pcx.parse();
3304                c.dcx().has_errors()?;
3305
3306                // Get AST from parsed source and setup the formatter
3307                let gcx = c.gcx();
3308                let (_, source_obj) = gcx.get_ast_source(&file.name).expect("Failed to get AST");
3309                let ast = source_obj.ast.as_ref().expect("No AST found");
3310                let comments =
3311                    Comments::new(&source_obj.file, gcx.sess.source_map(), true, false, None);
3312                let config = Arc::new(FormatterConfig::default());
3313                let inline_config = InlineConfig::default();
3314                let mut state = State::new(gcx.sess.source_map(), config, inline_config, comments);
3315
3316                // Extract the first function header (either top-level or inside a contract)
3317                let func = ast
3318                    .items
3319                    .iter()
3320                    .find_map(|item| match &item.kind {
3321                        ast::ItemKind::Function(func) => Some(func),
3322                        ast::ItemKind::Contract(contract) => {
3323                            contract.body.iter().find_map(|contract_item| {
3324                                match &contract_item.kind {
3325                                    ast::ItemKind::Function(func) => Some(func),
3326                                    _ => None,
3327                                }
3328                            })
3329                        }
3330                        _ => None,
3331                    })
3332                    .expect("No function found in source");
3333
3334                // Run the closure
3335                test_fn(&mut state, func);
3336
3337                Ok(())
3338            })
3339            .expect("Test failed");
3340    }
3341
3342    #[test]
3343    fn test_estimate_header_sizes() {
3344        let test_cases = [
3345            ("function foo();", 14, 15),
3346            ("function foo() {}", 14, 16),
3347            ("function foo() public {}", 14, 23),
3348            ("function foo(uint256 a) public {}", 23, 32),
3349            ("function foo(uint256 a, address b, bool c) public {}", 42, 51),
3350            ("function foo() public pure {}", 14, 28),
3351            ("function foo() public virtual {}", 14, 31),
3352            ("function foo() public override {}", 14, 32),
3353            ("function foo() public onlyOwner {}", 14, 33),
3354            ("function foo() public returns(uint256) {}", 14, 40),
3355            ("function foo() public returns(uint256, address) {}", 14, 49),
3356            ("function foo(uint256 a) public virtual override returns(uint256) {}", 23, 66),
3357            ("function foo() external payable {}", 14, 33),
3358            // other function types
3359            ("contract C { constructor() {} }", 13, 15),
3360            ("contract C { constructor(uint256 a) {} }", 22, 24),
3361            ("contract C { modifier onlyOwner() {} }", 20, 22),
3362            ("contract C { modifier onlyRole(bytes32 role) {} }", 31, 33),
3363            ("contract C { fallback() external payable {} }", 10, 29),
3364            ("contract C { receive() external payable {} }", 9, 28),
3365        ];
3366
3367        for (source, expected_params, expected_header) in &test_cases {
3368            parse_and_test(source, |state, func| {
3369                let params_size = state.estimate_header_params_size(func);
3370                assert_eq!(
3371                    params_size, *expected_params,
3372                    "Failed params size: expected {expected_params}, got {params_size} for source: {source}",
3373                );
3374
3375                let header_size = state.estimate_header_size(func);
3376                assert_eq!(
3377                    header_size, *expected_header,
3378                    "Failed header size: expected {expected_header}, got {header_size} for source: {source}",
3379                );
3380            });
3381        }
3382    }
3383}