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