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. The separator before the attribute is printed below,
768                // so a mixed comment must not add one of its own or the two become a blank line.
769                let mut previous_mixed = false;
770                for cmnt in pre_cmnts {
771                    // A line comment after a mixed comment becomes isolated once the header wraps.
772                    if previous_mixed
773                        && cmnt.style.is_trailing()
774                        && matches!(cmnt.kind, ast::CommentKind::Line)
775                    {
776                        self.hardbreak_if_not_bol();
777                    }
778                    previous_mixed = cmnt.style.is_mixed();
779                    let Some(cmnt) = self.handle_comment(cmnt, false) else {
780                        continue;
781                    };
782                    self.print_comment(
783                        cmnt,
784                        CommentConfig::default().mixed_no_break_post().mixed_prev_space(),
785                    );
786                }
787                // Push the inner comments back to the queue, so that they are printed in their
788                // intended place.
789                for cmnt in inner_cmnts.into_iter().rev() {
790                    self.comments.push_front(cmnt);
791                }
792                let enabled = if self.handle_span(span, false) {
793                    false
794                } else {
795                    if !self.is_bol_or_only_ind() {
796                        self.space();
797                    }
798                    self.ibox(0);
799                    print_fn(self);
800                    self.cursor.advance_to(span.hi(), true);
801                    true
802                };
803                // Print subsequent comments.
804                for cmnt in post_cmnts {
805                    let Some(cmnt) = self.handle_comment(cmnt, false) else {
806                        continue;
807                    };
808                    self.print_comment(cmnt, CommentConfig::default().mixed_prev_space());
809                }
810                if enabled {
811                    self.end();
812                }
813            }
814            // Fallback for attributes not in the map (should never happen)
815            None => {
816                if !self.is_bol_or_only_ind() {
817                    self.space();
818                }
819                print_fn(self);
820                self.cursor.advance_to(span.hi(), true);
821            }
822        }
823    }
824
825    fn is_modifier_a_base_contract(
826        &self,
827        kind: ast::FunctionKind,
828        modifier: &'ast ast::Modifier<'ast>,
829    ) -> bool {
830        // Add `()` in functions when the modifier is a base contract.
831        // HACK: heuristics:
832        // 1. exactly matches the name of a base contract as declared in the `contract is`;
833        // this does not account for inheritance;
834        let is_contract_base = self.contract.is_some_and(|contract| {
835            contract
836                .bases
837                .iter()
838                .any(|contract_base| contract_base.name.to_string() == modifier.name.to_string())
839        });
840        // 2. assume that title case names in constructors are bases.
841        // LEGACY: constructors used to also be `function NameOfContract...`; not checked.
842        let is_constructor = matches!(kind, ast::FunctionKind::Constructor);
843        // LEGACY: we are checking the beginning of the path, not the last segment.
844        is_contract_base
845            || (is_constructor
846                && modifier.name.first().name.as_str().starts_with(char::is_uppercase))
847    }
848
849    fn print_error(&mut self, err: &'ast ast::ItemError<'ast>) {
850        let ast::ItemError { name, parameters } = err;
851        self.word("error ");
852        self.print_ident(name);
853        self.print_parameter_list(
854            parameters,
855            parameters.span,
856            if self.config.prefer_compact.errors() {
857                ListFormat::compact()
858            } else {
859                ListFormat::consistent()
860            },
861        );
862        self.word(";");
863    }
864
865    fn print_event(&mut self, event: &'ast ast::ItemEvent<'ast>) {
866        let ast::ItemEvent { name, parameters, anonymous } = event;
867        self.word("event ");
868        self.print_ident(name);
869        self.print_parameter_list(
870            parameters,
871            parameters.span,
872            if self.config.prefer_compact.events() {
873                ListFormat::compact().break_cmnts()
874            } else {
875                ListFormat::consistent().break_cmnts()
876            },
877        );
878        if *anonymous {
879            self.word(" anonymous");
880        }
881        self.word(";");
882    }
883
884    fn print_var_def(&mut self, var: &'ast ast::VariableDefinition<'ast>) {
885        self.print_var(var, true);
886        self.word(";");
887    }
888
889    /// Prints the RHS of an assignment or variable initializer.
890    fn print_assign_rhs(
891        &mut self,
892        rhs: &'ast ast::Expr<'ast>,
893        lhs_size: usize,
894        space_left: usize,
895        ty: Option<&ast::TypeKind<'ast>>,
896        cache: bool,
897    ) {
898        // Check if the total expression overflows but the RHS would fit alone on a new line.
899        // This helps keep the RHS together on a single line when possible.
900        let rhs_size = if is_binary_expr(&rhs.kind)
901            && !self.has_comment_between(rhs.span.lo(), rhs.span.hi())
902        {
903            self.estimate_binary_size(rhs)
904        } else if assignment_member_depth(rhs) >= 2
905            && self.peek_comment_before(rhs.span.lo()).is_none()
906        {
907            // These chains can break before a member after collapsing their terminal delimiters.
908            // Size that collapsed form before choosing the assignment break instead.
909            self.estimate_assignment_size(rhs.span)
910        } else {
911            self.estimate_size(rhs.span)
912        };
913        let overflows = lhs_size + rhs_size >= space_left;
914        let fits_alone = rhs_size + self.config.tab_width < space_left;
915        let fits_alone_no_cmnts =
916            fits_alone && !self.has_comment_between(rhs.span.lo(), rhs.span.hi());
917        let force_break = overflows && fits_alone_no_cmnts;
918
919        if lhs_size <= space_left {
920            self.neverbreak();
921        }
922
923        // Handle comments before the RHS expression
924        if let Some(cmnt) = self.peek_comment_before(rhs.span.lo())
925            && self.inline_config.is_disabled(cmnt.span)
926        {
927            self.print_sep(Separator::Nbsp);
928        }
929        if self
930            .print_comments(
931                rhs.span.lo(),
932                CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(),
933            )
934            .is_some_and(|cmnt| cmnt.is_trailing())
935        {
936            self.break_offset_if_not_bol(SIZE_INFINITY as usize, self.ind, false);
937        }
938
939        // Match on expression kind to determine formatting strategy
940        match &rhs.kind {
941            ast::ExprKind::Lit(lit, ..) if lit.is_str_concatenation() => {
942                // String concatenations stay on the same line with nbsp
943                self.print_sep(Separator::Nbsp);
944                self.neverbreak();
945                self.s.ibox(self.ind);
946                self.print_expr(rhs);
947                self.end();
948            }
949            ast::ExprKind::Lit(..) if ty.is_none() && !fits_alone => {
950                // Long string in assign expr goes on its own line
951                self.print_sep(Separator::Space);
952                self.s.offset(self.ind);
953                self.print_expr(rhs);
954            }
955            ast::ExprKind::Binary(lhs, op, _) => {
956                let print_inline = |this: &mut Self| {
957                    this.print_sep(Separator::Nbsp);
958                    this.neverbreak();
959                    this.print_expr(rhs);
960                };
961                let print_with_break = |this: &mut Self, force_break: bool| {
962                    if !this.is_bol_or_only_ind() {
963                        if force_break {
964                            this.print_sep(Separator::Hardbreak);
965                        } else {
966                            this.print_sep(Separator::Space);
967                        }
968                    }
969                    this.s.offset(this.ind);
970                    this.s.ibox(this.ind);
971                    this.print_expr(rhs);
972                    this.end();
973                };
974
975                // Binary expressions: check if we need to break and indent
976                if force_break {
977                    print_with_break(self, true);
978                } else if self.estimate_lhs_size(rhs, op) + lhs_size > space_left {
979                    if has_complex_successor(&rhs.kind, true)
980                        && get_callee_head_size(lhs) + lhs_size <= space_left
981                    {
982                        // Keep complex exprs (where callee fits) inline, as they will have breaks
983                        if is_call(&lhs.kind) {
984                            self.s.ibox(-self.ind);
985                            print_inline(self);
986                            self.end();
987                        } else {
988                            print_inline(self);
989                        }
990                    } else {
991                        print_with_break(self, false);
992                    }
993                }
994                // Otherwise, if expr fits, ensure no breaks
995                else {
996                    print_inline(self);
997                }
998            }
999            _ => {
1000                // General case: handle calls, complex successors, and other expressions
1001                let callee_doesnt_fit = if let ast::ExprKind::Call(call_expr, ..) = &rhs.kind {
1002                    let callee_size = get_callee_head_size(call_expr);
1003                    callee_size + lhs_size > space_left
1004                        && callee_size + self.config.tab_width < space_left
1005                } else {
1006                    false
1007                };
1008
1009                if (lhs_size + 1 >= space_left && !is_call_chain(&rhs.kind, false))
1010                    || callee_doesnt_fit
1011                {
1012                    self.s.ibox(self.ind);
1013                } else {
1014                    self.s.ibox(0);
1015                };
1016
1017                if has_complex_successor(&rhs.kind, true)
1018                    && !matches!(&rhs.kind, ast::ExprKind::Member(..))
1019                {
1020                    // delegate breakpoints to `self.commasep(..)` for complex successors
1021                    if !self.is_bol_or_only_ind() {
1022                        let needs_offset = !callee_doesnt_fit
1023                            && rhs_size + lhs_size + 1 >= space_left
1024                            && fits_alone_no_cmnts;
1025                        let separator = if callee_doesnt_fit || needs_offset {
1026                            Separator::Space
1027                        } else {
1028                            Separator::Nbsp
1029                        };
1030                        self.print_sep(separator);
1031                        if needs_offset {
1032                            self.s.offset(self.ind);
1033                        }
1034                    }
1035                } else {
1036                    if !self.is_bol_or_only_ind() {
1037                        self.print_sep_unhandled(Separator::Space);
1038                    }
1039                    // apply type-dependent indentation if type info is available
1040                    if let Some(ty) = ty
1041                        && matches!(ty, ast::TypeKind::Elementary(..) | ast::TypeKind::Mapping(..))
1042                    {
1043                        self.s.offset(self.ind);
1044                    }
1045                }
1046                self.print_expr(rhs);
1047                self.end();
1048            }
1049        }
1050
1051        self.var_init = cache;
1052    }
1053
1054    fn print_var(&mut self, var: &'ast ast::VariableDefinition<'ast>, is_var_def: bool) {
1055        let ast::VariableDefinition {
1056            span,
1057            ty,
1058            visibility,
1059            mutability,
1060            data_location,
1061            override_,
1062            indexed,
1063            name,
1064            initializer,
1065        } = var;
1066
1067        if self.handle_span(*span, false) {
1068            return;
1069        }
1070
1071        // NOTE(rusowsky): this is hacky but necessary to properly estimate if we figure out if we
1072        // have double breaks (which should have double indentation) or not.
1073        // Alternatively, we could achieve the same behavior with a new box group that supports
1074        // "continuation" which would only increase indentation if its parent box broke.
1075        let init_space_left = self.space_left();
1076        let mut pre_init_size = self.estimate_size(ty.span);
1077
1078        // Non-elementary types use commasep which has its own padding.
1079        self.s.ibox(0);
1080        if override_.is_some() {
1081            self.s.cbox(self.ind);
1082        } else {
1083            self.s.ibox(self.ind);
1084        }
1085        self.print_ty(ty);
1086
1087        self.print_attribute(visibility.map(|v| v.to_str()), is_var_def, &mut pre_init_size);
1088        self.print_attribute(mutability.map(|m| m.to_str()), is_var_def, &mut pre_init_size);
1089        self.print_attribute(data_location.map(|d| d.to_str()), is_var_def, &mut pre_init_size);
1090
1091        if let Some(override_) = override_ {
1092            if self
1093                .print_comments(override_.span.lo(), CommentConfig::skip_ws().mixed_prev_space())
1094                .is_none()
1095            {
1096                self.print_sep(Separator::SpaceOrNbsp(is_var_def));
1097            }
1098            self.ibox(0);
1099            self.print_override(override_);
1100            pre_init_size += self.estimate_size(override_.span) + 1;
1101        }
1102
1103        if *indexed {
1104            self.print_attribute(indexed.then_some("indexed"), is_var_def, &mut pre_init_size);
1105        }
1106
1107        if let Some(ident) = name {
1108            self.print_sep(Separator::SpaceOrNbsp(is_var_def && override_.is_none()));
1109            self.print_comments(
1110                ident.span.lo(),
1111                CommentConfig::skip_ws().mixed_no_break().mixed_post_nbsp(),
1112            );
1113            self.print_ident(ident);
1114            pre_init_size += self.estimate_size(ident.span) + 1;
1115        }
1116        if let Some(init) = initializer {
1117            let cache = self.var_init;
1118            self.var_init = true;
1119
1120            pre_init_size += 2;
1121            self.print_word(" =");
1122            if override_.is_some() {
1123                self.end();
1124            }
1125            self.end();
1126
1127            self.print_assign_rhs(init, pre_init_size, init_space_left, Some(&ty.kind), cache);
1128        } else {
1129            if override_.is_some() {
1130                self.end();
1131            }
1132            self.end();
1133        }
1134        self.end();
1135    }
1136
1137    fn print_attribute(
1138        &mut self,
1139        attribute: Option<&'static str>,
1140        is_var_def: bool,
1141        size: &mut usize,
1142    ) {
1143        if let Some(s) = attribute {
1144            self.print_sep(Separator::SpaceOrNbsp(is_var_def));
1145            self.print_word(s);
1146            *size += s.len() + 1;
1147        }
1148    }
1149
1150    fn print_parameter_list(
1151        &mut self,
1152        parameters: &'ast [ast::VariableDefinition<'ast>],
1153        span: Span,
1154        format: ListFormat,
1155    ) {
1156        if self.handle_span(span, false) {
1157            return;
1158        }
1159
1160        self.print_tuple(
1161            parameters,
1162            span.lo(),
1163            span.hi(),
1164            |fmt, var| fmt.print_var(var, false),
1165            get_span!(),
1166            format,
1167        );
1168    }
1169
1170    fn print_ident_or_strlit(&mut self, value: &'ast ast::IdentOrStrLit) {
1171        match value {
1172            ast::IdentOrStrLit::Ident(ident) => self.print_ident(ident),
1173            ast::IdentOrStrLit::StrLit(strlit) => self.print_ast_str_lit(strlit),
1174        }
1175    }
1176
1177    /// Prints a raw AST string literal, which is unescaped.
1178    fn print_ast_str_lit(&mut self, strlit: &'ast ast::StrLit) {
1179        self.print_str_lit(ast::StrKind::Str, strlit.span.lo(), strlit.value.as_str());
1180    }
1181
1182    fn print_ty(&mut self, ty: &'ast ast::Type<'ast>) {
1183        if self.handle_span(ty.span, false) {
1184            return;
1185        }
1186
1187        match &ty.kind {
1188            &ast::TypeKind::Elementary(ty) => 'b: {
1189                match ty {
1190                    // `address payable` is normalized to `address`.
1191                    ast::ElementaryType::Address(true) => {
1192                        self.word("address payable");
1193                        break 'b;
1194                    }
1195                    // Integers are normalized to long form.
1196                    ast::ElementaryType::Int(size) | ast::ElementaryType::UInt(size) => {
1197                        match (self.config.int_types, size.bits_raw()) {
1198                            (config::IntTypes::Short, 0 | 256)
1199                            | (config::IntTypes::Preserve, 0) => {
1200                                let short = match ty {
1201                                    ast::ElementaryType::Int(_) => "int",
1202                                    ast::ElementaryType::UInt(_) => "uint",
1203                                    _ => unreachable!(),
1204                                };
1205                                self.word(short);
1206                                break 'b;
1207                            }
1208                            _ => {}
1209                        }
1210                    }
1211                    _ => {}
1212                }
1213                self.word(ty.to_abi_str());
1214            }
1215            ast::TypeKind::Array(ast::TypeArray { element, size }) => {
1216                self.print_ty(element);
1217                let open_bracket = self
1218                    .find_uncommented_char(Span::new(element.span.hi(), ty.span.hi()), '[')
1219                    .unwrap();
1220                self.print_comments(
1221                    open_bracket,
1222                    CommentConfig::skip_ws().mixed_prev_space().mixed_post_nbsp(),
1223                );
1224                if let Some(size) = size {
1225                    self.word("[");
1226                    self.print_expr(size);
1227                    self.word("]");
1228                } else {
1229                    self.word("[]");
1230                }
1231            }
1232            ast::TypeKind::Function(ast::TypeFunction {
1233                parameters,
1234                visibility,
1235                state_mutability,
1236                returns,
1237            }) => {
1238                self.cbox(0);
1239                self.word("function");
1240                self.print_parameter_list(parameters, parameters.span, ListFormat::inline());
1241
1242                if let Some(v) = visibility {
1243                    self.space();
1244                    self.word(v.to_str());
1245                }
1246                if let Some(sm) = state_mutability
1247                    && !matches!(**sm, ast::StateMutability::NonPayable)
1248                {
1249                    self.space();
1250                    self.word(sm.to_str());
1251                }
1252                if let Some(ret) = returns
1253                    && !ret.is_empty()
1254                {
1255                    self.nbsp();
1256                    self.word("returns");
1257                    self.nbsp();
1258                    self.print_parameter_list(
1259                        ret,
1260                        ret.span,
1261                        ListFormat::consistent(), // .with_cmnts_break(false),
1262                    );
1263                }
1264                self.end();
1265            }
1266            ast::TypeKind::Mapping(ast::TypeMapping { key, key_name, value, value_name }) => {
1267                self.word("mapping(");
1268                self.s.cbox(0);
1269                if let Some(cmnt) = self.peek_comment_before(key.span.lo()) {
1270                    if cmnt.style.is_mixed() {
1271                        self.print_comments(
1272                            key.span.lo(),
1273                            CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(),
1274                        );
1275                        self.break_offset_if_not_bol(SIZE_INFINITY as usize, 0, false);
1276                    } else {
1277                        self.print_comments(key.span.lo(), CommentConfig::skip_ws());
1278                    }
1279                }
1280                // Fitting a mapping in one line takes, at least, 16 chars (one-char var name):
1281                // 'mapping(' + {key} + ' => ' {value} ') ' + {name} + ';'
1282                // To be more conservative, we use 18 to decide whether to force a break or not.
1283                else if 18
1284                    + self.estimate_type_size(key)
1285                    + key_name.map(|k| self.estimate_size(k.span)).unwrap_or(0)
1286                    + self.estimate_type_size(value)
1287                    + value_name.map(|v| self.estimate_size(v.span)).unwrap_or(0)
1288                    >= self.space_left()
1289                {
1290                    self.hardbreak();
1291                } else {
1292                    self.zerobreak();
1293                }
1294                self.s.cbox(0);
1295                self.print_ty(key);
1296                if let Some(ident) = key_name {
1297                    if self
1298                        .print_comments(
1299                            ident.span.lo(),
1300                            CommentConfig::skip_ws()
1301                                .mixed_no_break()
1302                                .mixed_prev_space()
1303                                .mixed_post_nbsp(),
1304                        )
1305                        .is_none()
1306                    {
1307                        self.nbsp();
1308                    }
1309                    self.print_ident(ident);
1310                }
1311                // NOTE(rusowsky): unless we add more spans to solar, using `value.span.lo()`
1312                // consumes "comment6" of which should be printed after the `=>`
1313                self.print_comments(
1314                    value.span.lo(),
1315                    CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(),
1316                );
1317                if !self.is_bol_or_only_ind() {
1318                    self.space();
1319                }
1320                self.s.offset(self.ind);
1321                self.word("=> ");
1322                self.s.ibox(self.ind);
1323                self.print_ty(value);
1324                if let Some(ident) = value_name {
1325                    self.neverbreak();
1326                    if self
1327                        .print_comments(
1328                            ident.span.lo(),
1329                            CommentConfig::skip_ws()
1330                                .mixed_no_break()
1331                                .mixed_prev_space()
1332                                .mixed_post_nbsp(),
1333                        )
1334                        .is_none()
1335                    {
1336                        self.nbsp();
1337                    }
1338                    self.print_ident(ident);
1339                    if self
1340                        .peek_comment_before(ty.span.hi())
1341                        .is_some_and(|cmnt| cmnt.style.is_mixed())
1342                    {
1343                        self.neverbreak();
1344                        self.print_comments(
1345                            value.span.lo(),
1346                            CommentConfig::skip_ws().mixed_no_break(),
1347                        );
1348                    }
1349                }
1350                self.end();
1351                self.end();
1352                if self
1353                    .print_comments(
1354                        ty.span.hi(),
1355                        CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(),
1356                    )
1357                    .is_some_and(|cmnt| !cmnt.is_mixed())
1358                {
1359                    self.break_offset_if_not_bol(0, -self.ind, false);
1360                } else {
1361                    self.zerobreak();
1362                    self.s.offset(-self.ind);
1363                }
1364                self.end();
1365                self.word(")");
1366            }
1367            ast::TypeKind::Custom(path) => self.print_path(path, false),
1368        }
1369    }
1370
1371    fn print_override(&mut self, override_: &'ast ast::Override<'ast>) {
1372        let ast::Override { span, paths } = override_;
1373        if self.handle_span(*span, false) {
1374            return;
1375        }
1376        self.word("override");
1377        if !paths.is_empty() {
1378            if self.config.override_spacing {
1379                self.nbsp();
1380            }
1381            self.print_tuple(
1382                paths,
1383                span.lo(),
1384                span.hi(),
1385                |this, path| this.print_path(path, false),
1386                get_span!(()),
1387                ListFormat::consistent(), // .with_cmnts_break(false),
1388            );
1389        }
1390    }
1391
1392    /* --- Expressions --- */
1393    /// Prints an expression by matching on its variant and delegating to the appropriate
1394    /// printer method, handling all Solidity expression kinds.
1395    fn print_expr(&mut self, expr: &'ast ast::Expr<'ast>) {
1396        let ast::Expr { span, ref kind } = *expr;
1397        if self.handle_span(span, false) {
1398            return;
1399        }
1400
1401        match kind {
1402            ast::ExprKind::Array(exprs) => {
1403                self.print_array(exprs, expr.span, |this, e| this.print_expr(e), get_span!())
1404            }
1405            ast::ExprKind::Assign(lhs, None, rhs) => self.print_assign_expr(lhs, rhs),
1406            ast::ExprKind::Assign(lhs, Some(op), rhs) => self.print_bin_expr(lhs, op, rhs, true),
1407            ast::ExprKind::Binary(lhs, op, rhs) => self.print_bin_expr(lhs, op, rhs, false),
1408            ast::ExprKind::Call(call_expr, call_args) => {
1409                let cache = self.call_with_opts_and_args;
1410                let chained_named_call_cache = self.chained_named_call;
1411                // Keep calls within a chained callee inline when they fit, so a multiline named
1412                // argument list does not force an earlier break inside the callee.
1413                let keep_inline = chained_named_call_cache
1414                    .is_some_and(|call| call.keep_inline && call.callee.contains(expr.span))
1415                    && !self.has_comments_between_elements(call_args.span, call_args.exprs());
1416                self.call_with_opts_and_args = is_call_with_opts_and_args(&expr.kind);
1417                let named_args_size = if call_args.is_empty() {
1418                    4 + usize::from(self.config.bracket_spacing)
1419                } else {
1420                    2
1421                };
1422                self.chained_named_call = (matches!(call_args.kind, ast::CallArgsKind::Named(_))
1423                    && is_call_chain(&call_expr.kind, true))
1424                .then(|| ChainedNamedCall {
1425                    callee: call_expr.span,
1426                    keep_inline: !call_chain_contains_options(call_expr)
1427                        && !self.has_comment_between(call_expr.span.lo(), call_expr.span.hi())
1428                        && self
1429                            .estimate_call_chain_size(call_expr)
1430                            .is_some_and(|size| size + named_args_size <= self.space_left()),
1431                })
1432                .or_else(|| {
1433                    chained_named_call_cache.filter(|call| call.callee.contains(expr.span))
1434                });
1435                let list_format = if keep_inline {
1436                    ListFormat::inline()
1437                } else {
1438                    ListFormat::compact().break_cmnts().break_single(true)
1439                };
1440                let terminal_callee = call_expr.peel_parens();
1441                let callee_has_breakable_comment = self
1442                    .has_breakable_comment_between(call_expr.span.lo(), terminal_callee.span.lo())
1443                    || self.has_breakable_comment_between(
1444                        terminal_callee.span.hi(),
1445                        call_expr.span.hi(),
1446                    )
1447                    || if let ast::ExprKind::Member(member_expr, ident) = &terminal_callee.kind {
1448                        self.has_breakable_comment_between(member_expr.span.hi(), ident.span.lo())
1449                    } else {
1450                        false
1451                    };
1452                self.print_member_or_call_chain(
1453                    call_expr,
1454                    MemberOrCallArgs::CallArgs(
1455                        self.estimate_call_args_size(call_args.span),
1456                        self.has_comments_between_elements(call_args.span, call_args.exprs()),
1457                    ),
1458                    |s| {
1459                        let callee_suffix_can_break = callee_has_breakable_comment
1460                            || match &terminal_callee.kind {
1461                                ast::ExprKind::Member(member_expr, _) => {
1462                                    s.member_suffix_emits_break(terminal_callee, member_expr)
1463                                }
1464                                ast::ExprKind::Index(..) => !s.skip_index_break,
1465                                _ => false,
1466                            };
1467                        s.print_call_args(
1468                            call_args,
1469                            list_format.without_ind(s.return_bin_expr).with_delimiters(
1470                                !s.call_with_opts_and_args
1471                                    || s.call_stack
1472                                        .last()
1473                                        .is_some_and(|call| call.is_chained() && call.has_indent),
1474                            ),
1475                            get_callee_head_size(call_expr),
1476                            callee_suffix_can_break,
1477                        );
1478                    },
1479                );
1480                self.call_with_opts_and_args = cache;
1481                self.chained_named_call = chained_named_call_cache;
1482            }
1483            ast::ExprKind::CallOptions(expr, named_args) => {
1484                // the flag is only meant to be used to format the call args
1485                let cache = self.call_with_opts_and_args;
1486                self.call_with_opts_and_args = false;
1487
1488                self.print_expr(expr);
1489                self.print_named_args(named_args, span.hi(), false);
1490
1491                // restore cached value
1492                self.call_with_opts_and_args = cache;
1493            }
1494            ast::ExprKind::Delete(expr) => {
1495                self.word("delete ");
1496                self.print_expr(expr);
1497            }
1498            ast::ExprKind::Ident(ident) => self.print_ident(ident),
1499            ast::ExprKind::Index(expr, kind) => self.print_index_expr(span, expr, kind),
1500            ast::ExprKind::Lit(lit, unit) => {
1501                self.print_lit_inner(lit, false);
1502                if let Some(unit) = unit {
1503                    self.nbsp();
1504                    self.word(unit.to_str());
1505                }
1506            }
1507            ast::ExprKind::Member(member_expr, ident) => {
1508                self.print_member_or_call_chain(
1509                    member_expr,
1510                    MemberOrCallArgs::Member(self.estimate_size(ident.span)),
1511                    |s| {
1512                        let has_mixed_comment = s
1513                            .peek_comment_between(member_expr.span.hi(), ident.span.lo())
1514                            .is_some_and(|comment| comment.style.is_mixed());
1515                        let break_before_suffix = if has_mixed_comment {
1516                            s.print_comments(
1517                                ident.span.lo(),
1518                                CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(),
1519                            );
1520                            true
1521                        } else {
1522                            !s.print_trailing_comment(member_expr.span.hi(), Some(ident.span.lo()))
1523                                && s.peek_comment_between(member_expr.span.hi(), ident.span.lo())
1524                                    .is_none()
1525                                && s.member_suffix_emits_break(expr, member_expr)
1526                        };
1527                        if break_before_suffix {
1528                            s.zerobreak();
1529                        }
1530                        s.word(".");
1531                        s.print_ident(ident);
1532                    },
1533                );
1534            }
1535            ast::ExprKind::New(ty) => {
1536                self.word("new ");
1537                self.print_ty(ty);
1538            }
1539            ast::ExprKind::Payable(args) => {
1540                self.word("payable");
1541                self.print_call_args(args, ListFormat::compact().break_cmnts(), 7, false);
1542            }
1543            ast::ExprKind::Ternary(cond, then, els) => self.print_ternary_expr(cond, then, els),
1544            ast::ExprKind::Tuple(exprs) => self.print_tuple(
1545                exprs,
1546                span.lo(),
1547                span.hi(),
1548                |this, expr| match expr.as_ref() {
1549                    SpannedOption::Some(expr) => this.print_expr(expr),
1550                    SpannedOption::None(span) => {
1551                        this.print_comments(
1552                            span.hi(),
1553                            CommentConfig::skip_ws().mixed_no_break_post(),
1554                        );
1555                    }
1556                },
1557                |expr| match expr.as_ref() {
1558                    SpannedOption::Some(expr) => expr.span,
1559                    // Manually handled by printing the comment when `None`
1560                    SpannedOption::None(..) => Span::DUMMY,
1561                },
1562                ListFormat::compact().break_single(is_binary_expr(&expr.kind)),
1563            ),
1564            ast::ExprKind::TypeCall(ty) => {
1565                self.word("type");
1566                self.print_tuple(
1567                    std::slice::from_ref(ty),
1568                    span.lo(),
1569                    span.hi(),
1570                    Self::print_ty,
1571                    get_span!(),
1572                    ListFormat::consistent(),
1573                );
1574            }
1575            ast::ExprKind::Type(ty) => self.print_ty(ty),
1576            ast::ExprKind::Unary(un_op, expr) => {
1577                let prefix = un_op.kind.is_prefix();
1578                let op = un_op.kind.to_str();
1579                if prefix {
1580                    self.word(op);
1581                }
1582                self.print_expr(expr);
1583                if !prefix {
1584                    debug_assert!(un_op.kind.is_postfix());
1585                    self.word(op);
1586                }
1587            }
1588            ast::ExprKind::Err(_) => self.print_span(span),
1589        }
1590        self.cursor.advance_to(span.hi(), true);
1591    }
1592
1593    /// Prints a simple assignment expression of the form `lhs = rhs`.
1594    fn print_assign_expr(&mut self, lhs: &'ast ast::Expr<'ast>, rhs: &'ast ast::Expr<'ast>) {
1595        let cache = self.var_init;
1596        self.var_init = true;
1597
1598        let space_left = self.space_left();
1599        let lhs_size = self.estimate_size(lhs.span);
1600        // Normalize only indexes that the existing layout keeps together. Longer indexes keep
1601        // their own breaks, so their source delimiter padding remains relevant to that layout.
1602        let lhs_size = if matches!(lhs.kind, ast::ExprKind::Index(..))
1603            && lhs_size + 2 <= space_left
1604            && !self.has_comment_between(lhs.span.lo(), rhs.span.lo())
1605        {
1606            self.estimate_assignment_size(lhs.span)
1607        } else {
1608            lhs_size
1609        };
1610        self.print_expr(lhs);
1611        self.word(" =");
1612        self.print_assign_rhs(rhs, lhs_size + 2, space_left, None, cache);
1613    }
1614
1615    /// Prints a binary operator expression. Handles operator chains and formatting.
1616    fn print_bin_expr(
1617        &mut self,
1618        lhs: &'ast ast::Expr<'ast>,
1619        bin_op: &ast::BinOp,
1620        rhs: &'ast ast::Expr<'ast>,
1621        is_assign: bool,
1622    ) {
1623        let prev_chain = self.binary_expr;
1624        let is_chain = prev_chain.is_some_and(|prev| prev == bin_op.kind.group());
1625
1626        // Opening box if starting a new operator chain.
1627        if !is_chain {
1628            self.binary_expr = Some(bin_op.kind.group());
1629
1630            let indent = if (is_assign && has_complex_successor(&rhs.kind, true))
1631                || self.call_stack.is_nested()
1632                    && is_call_chain(&lhs.kind, false)
1633                    && self.estimate_size(lhs.span) >= self.space_left()
1634            {
1635                0
1636            } else {
1637                self.ind
1638            };
1639            self.s.ibox(indent);
1640        }
1641
1642        // Print LHS.
1643        self.print_expr(lhs);
1644
1645        // Handle assignment (`+=`, etc.) vs binary ops (`+`, `*`, etc.).
1646        let no_trailing_comment = !self.print_trailing_comment(lhs.span.hi(), Some(rhs.span.lo()));
1647        if is_assign {
1648            if no_trailing_comment {
1649                self.nbsp();
1650            }
1651            self.word(bin_op.kind.to_str());
1652            self.word("= ");
1653        } else {
1654            if no_trailing_comment
1655                && self
1656                    .print_comments(
1657                        bin_op.span.lo(),
1658                        CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(),
1659                    )
1660                    .is_none_or(|cmnt| cmnt.is_mixed())
1661            {
1662                if !self.config.pow_no_space || !matches!(bin_op.kind, ast::BinOpKind::Pow) {
1663                    self.space_if_not_bol();
1664                } else if !self.is_bol_or_only_ind() && !self.last_token_is_break() {
1665                    self.zerobreak();
1666                }
1667            }
1668
1669            self.word(bin_op.kind.to_str());
1670
1671            if !self.config.pow_no_space || !matches!(bin_op.kind, ast::BinOpKind::Pow) {
1672                self.nbsp();
1673            }
1674        }
1675
1676        // Print RHS with optional ibox if mixed comment precedes.
1677        let rhs_has_mixed_comment =
1678            self.peek_comment_before(rhs.span.lo()).is_some_and(|cmnt| cmnt.style.is_mixed());
1679        if rhs_has_mixed_comment {
1680            self.ibox(0);
1681            self.print_expr(rhs);
1682            self.end();
1683        } else {
1684            self.print_expr(rhs);
1685        }
1686
1687        // End current box if this was top-level in the chain.
1688        if !is_chain {
1689            self.binary_expr = prev_chain;
1690            self.end();
1691        }
1692    }
1693
1694    /// Prints an indexing expression.
1695    fn print_index_expr(
1696        &mut self,
1697        span: Span,
1698        expr: &'ast ast::Expr<'ast>,
1699        kind: &'ast ast::IndexKind<'ast>,
1700    ) {
1701        self.print_expr(expr);
1702        self.word("[");
1703        self.s.cbox(self.ind);
1704
1705        let mut skip_break = false;
1706        let mut zerobreak = |this: &mut Self| {
1707            if this.skip_index_break {
1708                skip_break = true;
1709            } else {
1710                this.zerobreak();
1711            }
1712        };
1713        match kind {
1714            ast::IndexKind::Index(Some(inner_expr)) => {
1715                zerobreak(self);
1716                self.print_expr(inner_expr);
1717            }
1718            ast::IndexKind::Index(None) => {}
1719            ast::IndexKind::Range(start, end) => {
1720                if let Some(start_expr) = start {
1721                    if self
1722                        .print_comments(start_expr.span.lo(), CommentConfig::skip_ws())
1723                        .is_none_or(|s| s.is_mixed())
1724                    {
1725                        zerobreak(self);
1726                    }
1727                    self.print_expr(start_expr);
1728                } else {
1729                    zerobreak(self);
1730                }
1731
1732                self.word(":");
1733
1734                if let Some(end_expr) = end {
1735                    self.s.ibox(self.ind);
1736                    if start.is_some() {
1737                        zerobreak(self);
1738                    }
1739                    self.print_comments(
1740                        end_expr.span.lo(),
1741                        CommentConfig::skip_ws()
1742                            .mixed_prev_space()
1743                            .mixed_no_break()
1744                            .mixed_post_nbsp(),
1745                    );
1746                    self.print_expr(end_expr);
1747                }
1748
1749                // Trailing comment handling.
1750                let is_trailing = if let Some(style) = self.print_comments(
1751                    span.hi(),
1752                    CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(),
1753                ) {
1754                    skip_break = true;
1755                    style.is_trailing()
1756                } else {
1757                    false
1758                };
1759
1760                // Adjust indentation and line breaks.
1761                match (skip_break, end.is_some()) {
1762                    (true, true) => {
1763                        self.break_offset_if_not_bol(0, -2 * self.ind, false);
1764                        self.end();
1765                        if !is_trailing {
1766                            self.break_offset_if_not_bol(0, -self.ind, false);
1767                        }
1768                    }
1769                    (true, false) => {
1770                        self.break_offset_if_not_bol(0, -self.ind, false);
1771                    }
1772                    (false, true) => {
1773                        self.end();
1774                    }
1775                    _ => {}
1776                }
1777            }
1778        }
1779
1780        if !skip_break {
1781            self.zerobreak();
1782            self.s.offset(-self.ind);
1783        }
1784
1785        self.end();
1786        self.word("]");
1787    }
1788
1789    /// Prints a ternary expression of the form `cond ? then : else`.
1790    fn print_ternary_expr(
1791        &mut self,
1792        cond: &'ast ast::Expr<'ast>,
1793        then: &'ast ast::Expr<'ast>,
1794        els: &'ast ast::Expr<'ast>,
1795    ) {
1796        self.s.cbox(self.ind);
1797        self.s.ibox(0);
1798
1799        let print_sub_expr = |this: &mut Self, span_lo, prefix, expr: &'ast ast::Expr<'ast>| {
1800            match prefix {
1801                Some(prefix) => {
1802                    if this.peek_comment_before(span_lo).is_some() {
1803                        this.space();
1804                    }
1805                    this.print_comments(span_lo, CommentConfig::skip_ws());
1806                    this.end();
1807                    if !this.is_bol_or_only_ind() {
1808                        this.space();
1809                    }
1810                    this.s.ibox(0);
1811                    this.word(prefix);
1812                }
1813                None => {
1814                    this.print_comments(expr.span.lo(), CommentConfig::skip_ws());
1815                }
1816            };
1817            this.print_expr(expr);
1818        };
1819
1820        // conditional expression
1821        self.s.ibox(-self.ind);
1822        print_sub_expr(self, then.span.lo(), None, cond);
1823        self.end();
1824        // then expression
1825        print_sub_expr(self, then.span.lo(), Some("? "), then);
1826        // else expression
1827        print_sub_expr(self, els.span.lo(), Some(": "), els);
1828
1829        self.end();
1830        self.neverbreak();
1831        self.s.offset(-self.ind);
1832        self.end();
1833    }
1834
1835    // If `add_parens_if_empty` is true, then add parentheses `()` even if there are no arguments.
1836    fn print_modifier_call(
1837        &mut self,
1838        modifier: &'ast ast::Modifier<'ast>,
1839        add_parens_if_empty: bool,
1840    ) {
1841        let ast::Modifier { name, arguments } = modifier;
1842        self.print_path(name, false);
1843        if !arguments.is_empty() || add_parens_if_empty {
1844            self.print_call_args(
1845                arguments,
1846                ListFormat::compact().break_cmnts(),
1847                name.to_string().len(),
1848                false,
1849            );
1850        }
1851    }
1852
1853    fn member_suffix_emits_break(&self, expr: &ast::Expr<'_>, member_expr: &ast::Expr<'_>) -> bool {
1854        match member_expr.kind {
1855            ast::ExprKind::Ident(_) | ast::ExprKind::Type(_) => false,
1856            ast::ExprKind::Index(..) if self.skip_index_break => false,
1857            _ if self
1858                .chained_named_call
1859                .is_some_and(|call| call.keep_inline && call.callee.contains(expr.span)) =>
1860            {
1861                false
1862            }
1863            // Don't add a break when accessing a field after a call with named args.
1864            // e.g., `_lzSend({_dstEid: x, ...}).guid` should keep `.guid`
1865            // on the same line as the closing `})`.
1866            // See: https://github.com/foundry-rs/foundry/issues/12399
1867            _ if is_call_with_named_args(&member_expr.kind) => false,
1868            _ => true,
1869        }
1870    }
1871
1872    fn print_member_or_call_chain<F>(
1873        &mut self,
1874        child_expr: &'ast ast::Expr<'ast>,
1875        member_or_args: MemberOrCallArgs,
1876        print_suffix: F,
1877    ) where
1878        F: FnOnce(&mut Self),
1879    {
1880        fn member_depth(depth: usize, expr: &ast::Expr<'_>) -> usize {
1881            if let ast::ExprKind::Member(child, ..) = &expr.kind {
1882                member_depth(depth + 1, child)
1883            } else {
1884                depth
1885            }
1886        }
1887
1888        let (mut extra_box, skip_cache) = (false, self.skip_index_break);
1889        let parent_is_chain = self.call_stack.last().copied().is_some_and(|call| call.is_chained());
1890        if !parent_is_chain {
1891            // Estimate sizes of callee and optional member
1892            let callee_size = get_callee_head_size(child_expr) + member_or_args.member_size();
1893            let expr_size = self.estimate_size(child_expr.span);
1894
1895            let callee_fits_line = self.space_left() > callee_size + 1;
1896            let total_fits_line = self.space_left() > expr_size + member_or_args.size() + 2;
1897            let no_cmnt_or_mixed =
1898                self.peek_comment_before(child_expr.span.hi()).is_none_or(|c| c.style.is_mixed());
1899
1900            // If call with options, add an extra box to prioritize breaking the call args.
1901            if self.call_with_opts_and_args {
1902                self.cbox(0);
1903                extra_box = true;
1904            }
1905
1906            // Determine if this chain will add its own indentation
1907            let keep_chain_inline = self
1908                .chained_named_call
1909                .is_some_and(|call| call.keep_inline && call.callee.contains(child_expr.span));
1910            let chain_has_indent = !keep_chain_inline
1911                && (is_call_chain(&child_expr.kind, true)
1912                    || !(no_cmnt_or_mixed
1913                        || matches!(&child_expr.kind, ast::ExprKind::CallOptions(..)))
1914                    || !callee_fits_line
1915                    || (member_depth(0, child_expr) >= 2
1916                        && (!total_fits_line || member_or_args.has_comments())));
1917
1918            // Start a new chain if needed
1919            if is_call_chain(&child_expr.kind, false) {
1920                self.call_stack.push(CallContext::chained(callee_size, chain_has_indent));
1921            }
1922
1923            if chain_has_indent {
1924                self.s.cbox(self.ind);
1925            } else {
1926                self.skip_index_break = true;
1927                self.cbox(0);
1928            }
1929        }
1930
1931        // Recursively print the child/prefix expression.
1932        self.print_expr(child_expr);
1933
1934        // If an extra box was opened, close it
1935        if extra_box {
1936            self.end();
1937        }
1938
1939        // Call the closure to print the suffix for the current link, with the calculated position.
1940        print_suffix(self);
1941
1942        // If a chain was started, clean up the state and end the box.
1943        if !parent_is_chain {
1944            if is_call_chain(&child_expr.kind, false) {
1945                self.call_stack.pop();
1946            }
1947            self.end();
1948        }
1949
1950        // Restore cache
1951        if self.skip_index_break {
1952            self.skip_index_break = skip_cache;
1953        }
1954    }
1955
1956    fn print_call_args(
1957        &mut self,
1958        args: &'ast ast::CallArgs<'ast>,
1959        format: ListFormat,
1960        callee_size: usize,
1961        callee_suffix_can_break: bool,
1962    ) {
1963        let ast::CallArgs { span, ref kind } = *args;
1964        if self.handle_span(span, true) {
1965            return;
1966        }
1967
1968        self.call_stack.push(CallContext::nested(callee_size));
1969
1970        // Clear the binary expression cache before the call.
1971        let cache = self.binary_expr.take();
1972
1973        match kind {
1974            ast::CallArgsKind::Unnamed(exprs) => {
1975                self.print_tuple(
1976                    exprs,
1977                    span.lo(),
1978                    span.hi(),
1979                    |this, e| this.print_expr(e),
1980                    get_span!(),
1981                    format,
1982                );
1983            }
1984            ast::CallArgsKind::Named(named_args) => {
1985                let without_ind =
1986                    self.call_stack.has_indented_parent_chain() && !callee_suffix_can_break;
1987                self.print_inside_parens(|state| {
1988                    state.print_named_args(named_args, span.hi(), without_ind)
1989                });
1990            }
1991        }
1992
1993        // Restore the cache to continue with the current chain.
1994        self.binary_expr = cache;
1995        self.call_stack.pop();
1996    }
1997
1998    fn print_named_args(
1999        &mut self,
2000        args: &'ast [ast::NamedArg<'ast>],
2001        pos_hi: BytePos,
2002        without_ind: bool,
2003    ) {
2004        let list_format = match (self.config.bracket_spacing, self.config.prefer_compact.calls()) {
2005            (false, true) => ListFormat::compact(),
2006            (false, false) => ListFormat::consistent(),
2007            (true, true) => ListFormat::compact().with_space(),
2008            (true, false) => ListFormat::consistent().with_space(),
2009        };
2010
2011        self.word("{");
2012        // Use the start position of the first argument's name for comment processing.
2013        if let Some(first_arg) = args.first() {
2014            let list_lo = first_arg.name.span.lo();
2015            self.commasep(
2016                args,
2017                list_lo,
2018                pos_hi,
2019                // Closure to print a single named argument (`name: value`)
2020                |s, arg| {
2021                    s.cbox(0);
2022                    s.print_ident(&arg.name);
2023                    s.word(":");
2024                    if s.same_source_line(arg.name.span.hi(), arg.value.span.hi())
2025                        || !s.print_trailing_comment(arg.name.span.hi(), None)
2026                    {
2027                        s.nbsp();
2028                    }
2029                    s.print_comments(
2030                        arg.value.span.lo(),
2031                        CommentConfig::skip_ws().mixed_no_break().mixed_post_nbsp(),
2032                    );
2033                    s.print_expr(arg.value);
2034                    s.end();
2035                },
2036                |arg| arg.name.span.until(arg.value.span),
2037                list_format
2038                    .break_cmnts()
2039                    .break_single(true)
2040                    .without_ind(without_ind)
2041                    .with_delimiters(!self.call_with_opts_and_args),
2042            );
2043        } else if self.config.bracket_spacing {
2044            self.nbsp();
2045        }
2046        self.word("}");
2047    }
2048
2049    /* --- Statements --- */
2050    /// Prints the given statement in the source code, handling formatting, inline documentation,
2051    /// trailing comments and layout logic for various statement kinds.
2052    fn print_stmt(&mut self, stmt: &'ast ast::Stmt<'ast>) {
2053        self.print_stmt_bound(stmt, None);
2054    }
2055
2056    /// Prints a statement with a bounded trailing-comment scan.
2057    fn print_stmt_bound(&mut self, stmt: &'ast ast::Stmt<'ast>, next_pos: Option<BytePos>) {
2058        let ast::Stmt { ref docs, span, ref kind } = *stmt;
2059        self.print_docs(docs);
2060
2061        // Handle disabled statements.
2062        if self.handle_span(span, false) {
2063            self.print_trailing_comment_no_break(stmt.span.hi(), next_pos);
2064            return;
2065        }
2066
2067        // return statements can't have a preceding comment in the same line.
2068        let force_break = matches!(kind, ast::StmtKind::Return(..))
2069            && self.peek_comment_before(span.lo()).is_some_and(|cmnt| cmnt.style.is_mixed());
2070
2071        match kind {
2072            ast::StmtKind::Assembly(ast::StmtAssembly { dialect, flags, block }) => {
2073                self.print_assembly_stmt(span, dialect, flags, block)
2074            }
2075            ast::StmtKind::DeclSingle(var) => self.print_var(var, true),
2076            ast::StmtKind::DeclMulti(vars, init_expr) => {
2077                self.print_multi_decl_stmt(span, vars, init_expr)
2078            }
2079            ast::StmtKind::Block(stmts) => self.print_block(stmts, span),
2080            ast::StmtKind::Break => self.word("break"),
2081            ast::StmtKind::Continue => self.word("continue"),
2082            ast::StmtKind::DoWhile(stmt, cond) => {
2083                self.word("do ");
2084                self.print_stmt_as_block(stmt, cond.span.lo(), false);
2085                self.nbsp();
2086                self.print_if_cond("while", cond, cond.span.hi());
2087            }
2088            ast::StmtKind::Emit(path, args) => self.print_emit_or_revert("emit", path, args),
2089            ast::StmtKind::Expr(expr) => self.print_expr(expr),
2090            ast::StmtKind::For { init, cond, next, body } => {
2091                self.print_for_stmt(span, init, cond, next, body)
2092            }
2093            ast::StmtKind::If(cond, then, els_opt) => self.print_if_stmt(span, cond, then, els_opt),
2094            ast::StmtKind::Return(expr) => self.print_return_stmt(force_break, expr),
2095            ast::StmtKind::Revert(path, args) => self.print_emit_or_revert("revert", path, args),
2096            ast::StmtKind::Try(ast::StmtTry { expr, clauses }) => {
2097                self.print_try_stmt(expr, clauses)
2098            }
2099            ast::StmtKind::UncheckedBlock(block) => {
2100                self.word("unchecked ");
2101                self.print_block(block, stmt.span);
2102            }
2103            ast::StmtKind::While(cond, stmt) => {
2104                // Check if blocks should be inlined and update cache if necessary
2105                let inline = self.is_single_line_block(span.lo(), cond, stmt, None);
2106                if !inline.is_cached && self.single_line_stmt.is_none() {
2107                    self.single_line_stmt = Some(inline.outcome);
2108                }
2109
2110                // Print while cond and its statement
2111                self.print_if_cond("while", cond, stmt.span.lo());
2112                self.nbsp();
2113                self.print_stmt_as_block(stmt, stmt.span.hi(), inline.outcome);
2114
2115                // Clear cache if necessary
2116                if !inline.is_cached && self.single_line_stmt.is_some() {
2117                    self.single_line_stmt = None;
2118                }
2119            }
2120            ast::StmtKind::Placeholder => self.word("_"),
2121        }
2122        if stmt_needs_semi(kind) {
2123            self.neverbreak(); // semicolon shouldn't account for linebreaks
2124            self.word(";");
2125            self.cursor.advance_to(span.hi(), true);
2126        }
2127        // print comments without breaks, as those are handled by the caller.
2128        let ends_with_line_comment = self
2129            .comments
2130            .iter()
2131            .take_while(|cmnt| cmnt.pos() < stmt.span.hi())
2132            .filter(|cmnt| !cmnt.style.is_blank())
2133            .last()
2134            .is_some_and(|cmnt| {
2135                cmnt.style.is_trailing() && matches!(cmnt.kind, ast::CommentKind::Line)
2136            });
2137        self.print_comments(
2138            stmt.span.hi(),
2139            CommentConfig::skip_trailing_ws()
2140                .trailing_no_break()
2141                .mixed_no_break()
2142                .mixed_prev_space(),
2143        );
2144        if ends_with_line_comment && self.peek_comment().is_some() {
2145            self.hardbreak_if_not_bol();
2146        }
2147        self.print_trailing_comment_no_break(stmt.span.hi(), next_pos);
2148    }
2149
2150    /// Prints an `assembly` statement, including optional dialect and flags,
2151    /// followed by its Yul block.
2152    fn print_assembly_stmt(
2153        &mut self,
2154        span: Span,
2155        dialect: &'ast Option<ast::StrLit>,
2156        flags: &'ast [ast::StrLit],
2157        block: &'ast ast::yul::Block<'ast>,
2158    ) {
2159        _ = self.handle_span(self.cursor.span(span.lo()), false);
2160        if !self.handle_span(span.until(block.span), false) {
2161            self.cursor.advance_to(span.lo(), true);
2162            self.print_word("assembly "); // 9 chars
2163            if let Some(dialect) = dialect {
2164                self.print_ast_str_lit(dialect);
2165                self.print_sep(Separator::Nbsp);
2166            }
2167            if !flags.is_empty() {
2168                self.print_tuple(
2169                    flags,
2170                    span.lo(),
2171                    block.span.lo(),
2172                    Self::print_ast_str_lit,
2173                    get_span!(),
2174                    ListFormat::consistent(),
2175                );
2176                self.print_sep(Separator::Nbsp);
2177            }
2178        }
2179        self.print_yul_block(block, block.span, false, 9);
2180    }
2181
2182    /// Prints a multiple-variable declaration with a single initializer expression,
2183    /// formatted as a tuple-style assignment (e.g., `(a, b) = foo();`).
2184    fn print_multi_decl_stmt(
2185        &mut self,
2186        span: Span,
2187        vars: &'ast BoxSlice<'ast, SpannedOption<ast::VariableDefinition<'ast>>>,
2188        init_expr: &'ast ast::Expr<'ast>,
2189    ) {
2190        let space_left = self.space_left();
2191
2192        self.s.ibox(self.ind);
2193        self.s.ibox(-self.ind);
2194        self.print_tuple(
2195            vars,
2196            span.lo(),
2197            init_expr.span.lo(),
2198            |this, var| match var {
2199                SpannedOption::Some(var) => this.print_var(var, true),
2200                SpannedOption::None(span) => {
2201                    this.print_comments(span.hi(), CommentConfig::skip_ws().mixed_no_break_post());
2202                }
2203            },
2204            |var| match var {
2205                SpannedOption::Some(var) => var.span,
2206                // Manually handled by printing the comment when `None`
2207                SpannedOption::None(..) => Span::DUMMY,
2208            },
2209            ListFormat::consistent(),
2210        );
2211        self.end();
2212        self.word(" =");
2213
2214        if self.estimate_size(init_expr.span) + self.config.tab_width
2215            <= std::cmp::max(space_left, self.space_left())
2216        {
2217            self.print_sep(Separator::Space);
2218            self.ibox(0);
2219        } else {
2220            self.print_sep(Separator::Nbsp);
2221            self.neverbreak();
2222            self.s.ibox(-self.ind);
2223        }
2224        self.print_expr(init_expr);
2225        self.end();
2226        self.end();
2227    }
2228
2229    /// Prints a `for` loop statement, including its initializer, condition,
2230    /// increment expression, and loop body, with formatting and spacing.
2231    fn print_for_stmt(
2232        &mut self,
2233        span: Span,
2234        init: &'ast Option<&mut ast::Stmt<'ast>>,
2235        cond: &'ast Option<&mut ast::Expr<'ast>>,
2236        next: &'ast Option<&mut ast::Expr<'ast>>,
2237        body: &'ast ast::Stmt<'ast>,
2238    ) {
2239        self.cbox(0);
2240        self.s.ibox(self.ind);
2241        let open_paren = self.find_uncommented_char(span, '(').unwrap();
2242        self.print_word("for");
2243        if self
2244            .print_comments(
2245                open_paren,
2246                CommentConfig::skip_ws().mixed_prev_space().mixed_post_nbsp(),
2247            )
2248            .is_none()
2249        {
2250            self.nbsp();
2251        }
2252        self.cursor.advance_to(open_paren, true);
2253        self.print_word("(");
2254        let init_has_leading_comment =
2255            init.as_ref().is_some_and(|stmt| self.peek_comment_before(stmt.span.lo()).is_some());
2256        if !init_has_leading_comment {
2257            self.zerobreak();
2258        }
2259
2260        // Print init.
2261        self.s.cbox(0);
2262        let init_trailing_comment = match init {
2263            Some(init_stmt) => {
2264                let has_trailing_comment = cond.as_ref().is_some_and(|cond| {
2265                    self.comments
2266                        .iter()
2267                        .skip_while(|cmnt| cmnt.pos() < init_stmt.span.hi())
2268                        .take_while(|cmnt| cmnt.pos() < cond.span.lo())
2269                        .any(|cmnt| cmnt.style.is_trailing())
2270                });
2271                self.print_stmt_bound(init_stmt, Some(init_stmt.span.hi()));
2272                has_trailing_comment
2273            }
2274            None => {
2275                self.print_word(";");
2276                false
2277            }
2278        };
2279
2280        // Print condition.
2281        match cond {
2282            Some(cond_expr) => {
2283                if init_trailing_comment {
2284                    self.hardbreak_if_not_bol();
2285                }
2286                self.print_sep(Separator::Space);
2287                self.print_expr(cond_expr);
2288            }
2289            None => self.zerobreak(),
2290        }
2291        self.print_word(";");
2292
2293        // Print next clause.
2294        match next {
2295            Some(next_expr) => {
2296                self.space();
2297                self.print_expr(next_expr);
2298            }
2299            None => self.zerobreak(),
2300        }
2301
2302        // Close head.
2303        self.break_offset_if_not_bol(0, -self.ind, false);
2304        self.end();
2305        self.print_word(") ");
2306        self.neverbreak();
2307        self.end();
2308
2309        // Print comments and body.
2310        self.print_comments(body.span.lo(), CommentConfig::skip_ws());
2311        self.print_stmt_as_block(body, span.hi(), false);
2312        self.end();
2313    }
2314
2315    /// Prints an `if` statement, including its condition, `then` block, and any chained
2316    /// `else` or `else if` branches, handling inline formatting decisions and comments.
2317    fn print_if_stmt(
2318        &mut self,
2319        span: Span,
2320        cond: &'ast ast::Expr<'ast>,
2321        then: &'ast ast::Stmt<'ast>,
2322        els_opt: &'ast Option<&mut ast::Stmt<'ast>>,
2323    ) {
2324        // Check if blocks should be inlined and update cache if necessary
2325        let inline = self.is_single_line_block(span.lo(), cond, then, els_opt.as_ref());
2326        let set_inline_cache = !inline.is_cached && self.single_line_stmt.is_none();
2327        if set_inline_cache {
2328            self.single_line_stmt = Some(inline.outcome);
2329        }
2330
2331        self.cbox(0);
2332        self.ibox(0);
2333        // Print if stmt
2334        self.print_if_no_else(cond, then, inline.outcome);
2335
2336        // Print else (if) stmts, if any
2337        let mut current_else = els_opt.as_deref();
2338        while let Some(els) = current_else {
2339            if self.ends_with('}') {
2340                // If there are comments with line breaks, don't add spaces to mixed comments
2341                if self.has_comment_before_with(els.span.lo(), |cmnt| !cmnt.style.is_mixed()) {
2342                    // If last comment is miced, ensure line break
2343                    if self
2344                        .print_comments(els.span.lo(), CommentConfig::skip_ws().mixed_no_break())
2345                        .is_some_and(|cmnt| cmnt.is_mixed())
2346                    {
2347                        self.hardbreak();
2348                    }
2349                }
2350                // Otherwise, ensure a non-breaking space is added
2351                else if self
2352                    .print_comments(
2353                        els.span.lo(),
2354                        CommentConfig::skip_ws()
2355                            .mixed_no_break()
2356                            .mixed_prev_space()
2357                            .mixed_post_nbsp(),
2358                    )
2359                    .is_none()
2360                {
2361                    self.nbsp();
2362                }
2363            } else {
2364                self.hardbreak_if_not_bol();
2365                if self
2366                    .print_comments(els.span.lo(), CommentConfig::skip_ws())
2367                    .is_some_and(|cmnt| cmnt.is_mixed())
2368                {
2369                    self.hardbreak();
2370                };
2371            }
2372
2373            self.ibox(0);
2374            self.print_word("else ");
2375            match &els.kind {
2376                ast::StmtKind::If(cond, then, next_else) => {
2377                    self.print_if_no_else(cond, then, inline.outcome);
2378                    current_else = next_else.as_deref();
2379                }
2380                _ => {
2381                    self.print_stmt_as_block(els, span.hi(), inline.outcome);
2382                    self.end(); // end ibox for final else
2383                    break;
2384                }
2385            }
2386        }
2387        self.end();
2388
2389        // Clear inline cache if we set it earlier.
2390        if set_inline_cache {
2391            self.single_line_stmt = None;
2392        }
2393    }
2394
2395    /// Prints a `return` statement, optionally including a return expression.
2396    /// Handles spacing, line breaking, and formatting.
2397    fn print_return_stmt(&mut self, force_break: bool, expr: &'ast Option<&mut ast::Expr<'ast>>) {
2398        if force_break {
2399            self.hardbreak_if_not_bol();
2400        }
2401
2402        let space_left = self.space_left();
2403        let expr_size = expr.as_ref().map_or(0, |expr| self.estimate_size(expr.span));
2404
2405        // `return ' + expr + ';'
2406        let overflows = space_left < 8 + expr_size;
2407        let fits_alone = space_left > expr_size;
2408
2409        if let Some(expr) = expr {
2410            let is_simple = matches!(expr.kind, ast::ExprKind::Lit(..) | ast::ExprKind::Ident(..));
2411            let allow_break = overflows && fits_alone;
2412
2413            self.return_bin_expr = is_binary_expr(&expr.kind);
2414            self.s.ibox(if is_simple || allow_break { self.ind } else { 0 });
2415
2416            self.print_word("return");
2417
2418            match self.print_comments(
2419                expr.span.lo(),
2420                CommentConfig::skip_ws().mixed_no_break().mixed_prev_space().mixed_post_nbsp(),
2421            ) {
2422                Some(_) if !is_simple => self.s.offset(self.ind),
2423                None => self.print_sep(Separator::SpaceOrNbsp(allow_break)),
2424                _ => {}
2425            }
2426
2427            self.print_expr(expr);
2428            self.end();
2429            self.return_bin_expr = false;
2430        } else {
2431            self.print_word("return");
2432        }
2433    }
2434
2435    /// Prints a `try` statement along with its associated `catch` clauses,
2436    /// following Solidity's `try ... returns (...) { ... } catch (...) { ... }` syntax.
2437    fn print_try_stmt(
2438        &mut self,
2439        expr: &'ast ast::Expr<'ast>,
2440        clauses: &'ast [ast::TryCatchClause<'ast>],
2441    ) {
2442        self.cbox(0);
2443        if let Some((first, other)) = clauses.split_first() {
2444            // Print the 'try' clause
2445            let ast::TryCatchClause { args, block, span: try_span, .. } = first;
2446            self.cbox(0);
2447            self.ibox(0);
2448            self.print_word("try ");
2449            self.print_comments(expr.span.lo(), CommentConfig::skip_ws());
2450            self.print_expr(expr);
2451
2452            // Print comments.
2453            self.print_comments(
2454                args.first().map(|p| p.span.lo()).unwrap_or_else(|| expr.span.lo()),
2455                CommentConfig::skip_ws(),
2456            );
2457            if !self.is_beginning_of_line() {
2458                self.nbsp();
2459            }
2460
2461            if args.is_empty() {
2462                self.end();
2463            } else {
2464                self.print_word("returns ");
2465                self.print_word("(");
2466                self.zerobreak();
2467                self.end();
2468                let span = args.span.with_hi(block.span.lo());
2469                self.commasep(
2470                    args,
2471                    span.lo(),
2472                    span.hi(),
2473                    |fmt, var| fmt.print_var(var, false),
2474                    get_span!(),
2475                    ListFormat::compact().with_delimiters(false),
2476                );
2477                self.print_word(")");
2478                self.nbsp();
2479            }
2480            if block.is_empty() {
2481                self.print_block(block, *try_span);
2482                self.end();
2483            } else {
2484                self.print_word("{");
2485                self.end();
2486                self.neverbreak();
2487                self.print_trailing_comment_no_break(try_span.lo(), None);
2488                self.print_block_without_braces(block, try_span.hi(), Some(self.ind));
2489                if self.cursor.enabled || self.cursor.pos < try_span.hi() {
2490                    self.print_word("}");
2491                    self.cursor.advance_to(try_span.hi(), true);
2492                }
2493            }
2494
2495            let mut skip_ind = false;
2496            if self.print_trailing_comment(try_span.hi(), other.first().map(|c| c.span.lo())) {
2497                // if a trailing comment is printed at the very end, we have to manually
2498                // adjust the offset to avoid having a double break.
2499                self.break_offset_if_not_bol(0, self.ind, false);
2500                skip_ind = true;
2501            };
2502
2503            let mut prev_block_multiline = self.is_multiline_block(block, false, true);
2504
2505            // Handle 'catch' clauses
2506            for (pos, ast::TryCatchClause { name, args, block, span: catch_span }) in
2507                other.iter().delimited()
2508            {
2509                let current_block_multiline = self.is_multiline_block(block, false, true);
2510                if !pos.is_first || !skip_ind {
2511                    if (pos.is_first && block.is_empty() && is_call_with_named_args(&expr.kind))
2512                        || (prev_block_multiline && (current_block_multiline || pos.is_last))
2513                    {
2514                        self.nbsp();
2515                    } else {
2516                        self.space();
2517                        if !current_block_multiline {
2518                            self.s.offset(self.ind);
2519                        }
2520                    }
2521                }
2522                self.s.ibox(self.ind);
2523                self.print_comments(
2524                    catch_span.lo(),
2525                    CommentConfig::skip_ws().mixed_no_break().mixed_post_nbsp(),
2526                );
2527
2528                self.print_word("catch ");
2529                if !args.is_empty() {
2530                    self.print_comments(
2531                        args[0].span.lo(),
2532                        CommentConfig::skip_ws().mixed_no_break().mixed_post_nbsp(),
2533                    );
2534                    if let Some(name) = name {
2535                        self.print_ident(name);
2536                    }
2537                    self.print_parameter_list(
2538                        args,
2539                        args.span.with_hi(block.span.lo()),
2540                        ListFormat::inline(),
2541                    );
2542                    self.nbsp();
2543                }
2544                self.print_word("{");
2545                self.end();
2546                if !block.is_empty() {
2547                    self.print_trailing_comment_no_break(catch_span.lo(), None);
2548                }
2549                self.print_block_without_braces(block, catch_span.hi(), Some(self.ind));
2550                if self.cursor.enabled || self.cursor.pos < try_span.hi() {
2551                    self.print_word("}");
2552                    self.cursor.advance_to(catch_span.hi(), true);
2553                }
2554
2555                prev_block_multiline = current_block_multiline;
2556            }
2557        }
2558        self.end();
2559    }
2560
2561    fn print_if_no_else(
2562        &mut self,
2563        cond: &'ast ast::Expr<'ast>,
2564        then: &'ast ast::Stmt<'ast>,
2565        inline: bool,
2566    ) {
2567        if !self.handle_span(cond.span.until(then.span), true) {
2568            self.print_if_cond("if", cond, then.span.lo());
2569            // if empty block without comments, ensure braces are inlined
2570            if let ast::StmtKind::Block(block) = &then.kind
2571                && block.is_empty()
2572                && self.peek_comment_before(then.span.hi()).is_none()
2573            {
2574                self.neverbreak();
2575                self.print_sep(Separator::Nbsp);
2576            } else if inline
2577                && is_call(&cond.kind)
2578                && matches!(
2579                    self.config.single_line_statement_blocks,
2580                    config::SingleLineBlockStyle::Preserve
2581                )
2582            {
2583                // Keep the body beside a wrapped call condition so Preserve sees the same
2584                // layout on the next pass.
2585                self.print_sep(Separator::Nbsp);
2586            } else {
2587                self.print_sep(Separator::Space);
2588            }
2589        }
2590        self.end();
2591        self.print_stmt_as_block(then, then.span.hi(), inline);
2592        self.cursor.advance_to(then.span.hi(), true);
2593    }
2594
2595    fn print_if_cond(&mut self, kw: &'static str, cond: &'ast ast::Expr<'ast>, pos_hi: BytePos) {
2596        self.print_word(kw);
2597        self.print_sep_unhandled(Separator::Nbsp);
2598        self.print_tuple(
2599            std::slice::from_ref(cond),
2600            cond.span.lo(),
2601            pos_hi,
2602            Self::print_expr,
2603            get_span!(),
2604            ListFormat::compact().break_cmnts().break_single(is_binary_expr(&cond.kind)),
2605        );
2606    }
2607
2608    fn print_emit_or_revert(
2609        &mut self,
2610        kw: &'static str,
2611        path: &'ast ast::PathSlice,
2612        args: &'ast ast::CallArgs<'ast>,
2613    ) {
2614        self.word(kw);
2615        if self
2616            .print_comments(
2617                path.span().lo(),
2618                CommentConfig::skip_ws().mixed_no_break().mixed_prev_space().mixed_post_nbsp(),
2619            )
2620            .is_none()
2621        {
2622            self.nbsp();
2623        };
2624        self.s.cbox(0);
2625        self.emit_or_revert = path.segments().len() > 1;
2626        self.print_path(path, false);
2627        let format = if self.config.prefer_compact.calls() {
2628            ListFormat::compact()
2629        } else {
2630            ListFormat::consistent()
2631        };
2632        self.print_call_args(args, format.break_cmnts(), path.to_string().len(), false);
2633        self.emit_or_revert = false;
2634        self.end();
2635    }
2636
2637    fn print_block(&mut self, block: &'ast [ast::Stmt<'ast>], span: Span) {
2638        self.print_block_inner(
2639            block,
2640            BlockFormat::Regular,
2641            Self::print_stmt,
2642            |b| b.span,
2643            span.hi(),
2644        );
2645    }
2646
2647    fn print_block_without_braces(
2648        &mut self,
2649        block: &'ast [ast::Stmt<'ast>],
2650        pos_hi: BytePos,
2651        offset: Option<isize>,
2652    ) {
2653        self.print_block_inner(
2654            block,
2655            BlockFormat::NoBraces(offset),
2656            Self::print_stmt,
2657            |b| b.span,
2658            pos_hi,
2659        );
2660    }
2661
2662    // Body of a if/loop.
2663    fn print_stmt_as_block(&mut self, stmt: &'ast ast::Stmt<'ast>, pos_hi: BytePos, inline: bool) {
2664        if self.handle_span(stmt.span, false) {
2665            return;
2666        }
2667
2668        let stmts = if let ast::StmtKind::Block(stmts) = &stmt.kind {
2669            stmts
2670        } else {
2671            std::slice::from_ref(stmt)
2672        };
2673
2674        if inline && stmts.len() == 1 {
2675            self.neverbreak();
2676            self.print_block_without_braces(stmts, pos_hi, None);
2677        } else {
2678            // Reset cache for nested (child) stmts within this (parent) block.
2679            let inline_parent = self.single_line_stmt.take();
2680
2681            self.print_word("{");
2682            self.print_block_without_braces(stmts, pos_hi, Some(self.ind));
2683            self.print_word("}");
2684
2685            // Restore cache for the rest of stmts within the same height.
2686            self.single_line_stmt = inline_parent;
2687        }
2688    }
2689
2690    /// Determines if an `if/else` block should be inlined.
2691    /// Also returns if the value was cached, so that it can be cleaned afterwards.
2692    ///
2693    /// # Returns
2694    ///
2695    /// A tuple `(should_inline, was_cached)`. The second boolean is `true` if the
2696    /// decision was retrieved from the cache or is a final decision based on config,
2697    /// preventing the caller from clearing a cache value that was never set.
2698    fn is_single_line_block(
2699        &mut self,
2700        stmt_span_lo: BytePos,
2701        cond: &'ast ast::Expr<'ast>,
2702        then: &'ast ast::Stmt<'ast>,
2703        els_opt: Option<&'ast &'ast mut ast::Stmt<'ast>>,
2704    ) -> Decision {
2705        // Dangling-else guard runs before the cache check so an inlined parent can't
2706        // coerce this `if` into dropping braces and rebinding its `else`.
2707        if Self::then_block_can_capture_trailing_else(then, els_opt.is_some()) {
2708            return Decision { outcome: false, is_cached: false };
2709        }
2710
2711        // If a decision is already cached from a parent, use it directly.
2712        if let Some(cached_decision) = self.single_line_stmt {
2713            return Decision { outcome: cached_decision, is_cached: true };
2714        }
2715
2716        // Empty statements are always printed as blocks.
2717        if std::slice::from_ref(then).is_empty() {
2718            return Decision { outcome: false, is_cached: false };
2719        }
2720
2721        // Comments near cond can break single-line layouts. Print as blocks in this case
2722        if self.peek_comment_between(stmt_span_lo, then.span.lo()).is_some() {
2723            return Decision { outcome: false, is_cached: false };
2724        }
2725
2726        // If possible, take an early decision based on the block style configuration.
2727        match self.config.single_line_statement_blocks {
2728            config::SingleLineBlockStyle::Preserve => {
2729                if self.is_stmt_in_new_line(cond, then) || self.is_multiline_block_stmt(then, true)
2730                {
2731                    return Decision { outcome: false, is_cached: false };
2732                }
2733            }
2734            config::SingleLineBlockStyle::Single => {
2735                if self.is_multiline_block_stmt(then, true) {
2736                    return Decision { outcome: false, is_cached: false };
2737                }
2738            }
2739            config::SingleLineBlockStyle::Multi => {
2740                return Decision { outcome: false, is_cached: false };
2741            }
2742        };
2743
2744        // If no decision was made, estimate the length to be formatted.
2745        // NOTE: conservative check -> worst-case scenario is formatting as multi-line block.
2746        if !self.can_stmts_be_inlined(cond, then, els_opt) {
2747            return Decision { outcome: false, is_cached: false };
2748        }
2749
2750        // If the parent would fit, check all of its children.
2751        if let ast::StmtKind::If(child_cond, child_then, child_els_opt) = &then.kind {
2752            let child_decision = self.is_single_line_block(
2753                then.span.lo(),
2754                child_cond,
2755                child_then,
2756                child_els_opt.as_ref(),
2757            );
2758            if !child_decision.outcome {
2759                return child_decision;
2760            }
2761        }
2762        if let Some(stmt) = els_opt {
2763            if let ast::StmtKind::If(child_cond, child_then, child_els_opt) = &stmt.kind {
2764                return self.is_single_line_block(
2765                    stmt.span.lo(),
2766                    child_cond,
2767                    child_then,
2768                    child_els_opt.as_ref(),
2769                );
2770            } else if self.is_multiline_block_stmt(stmt, true) {
2771                return Decision { outcome: false, is_cached: false };
2772            }
2773        }
2774
2775        // If all children can also fit, allow single-line block.
2776        Decision { outcome: true, is_cached: false }
2777    }
2778
2779    fn is_inline_stmt(&self, stmt: &'ast ast::Stmt<'ast>, cond_len: usize) -> bool {
2780        if let ast::StmtKind::If(cond, then, els_opt) = &stmt.kind {
2781            let if_span = cond.span.to(then.span);
2782            if !self.same_source_line(if_span.lo(), if_span.hi())
2783                && matches!(
2784                    self.config.single_line_statement_blocks,
2785                    config::SingleLineBlockStyle::Preserve
2786                )
2787            {
2788                return false;
2789            }
2790            if cond_len + self.estimate_size(if_span) >= self.space_left() {
2791                return false;
2792            }
2793            if let Some(els) = els_opt
2794                && !self.is_inline_stmt(els, 6)
2795            {
2796                return false;
2797            }
2798        } else {
2799            if matches!(
2800                self.config.single_line_statement_blocks,
2801                config::SingleLineBlockStyle::Preserve
2802            ) && !self.same_source_line(stmt.span.lo(), stmt.span.hi())
2803            {
2804                return false;
2805            }
2806            if cond_len + self.estimate_size(stmt.span) >= self.space_left() {
2807                return false;
2808            }
2809        }
2810        true
2811    }
2812
2813    /// Checks if a statement was explicitly written in a new line.
2814    fn is_stmt_in_new_line(
2815        &self,
2816        cond: &'ast ast::Expr<'ast>,
2817        then: &'ast ast::Stmt<'ast>,
2818    ) -> bool {
2819        let span_between = cond.span.between(then.span);
2820        if let Some(snip) = self.snippet(span_between) {
2821            // Check for newlines after the closing parenthesis of the `if (...)`.
2822            if let Some((_, after_paren)) = snip.split_once(')') {
2823                return after_paren.lines().count() > 1;
2824            }
2825        }
2826        false
2827    }
2828
2829    /// Returns true if eliding the braces of `then` would expose an inner `if` to a
2830    /// trailing `else` or change the AST shape on round-trip.
2831    fn then_block_can_capture_trailing_else(
2832        then: &'ast ast::Stmt<'ast>,
2833        has_outer_else: bool,
2834    ) -> bool {
2835        let ast::StmtKind::Block(block) = &then.kind else { return false };
2836        if block.stmts.len() != 1 {
2837            return false;
2838        }
2839        match &block.stmts[0].kind {
2840            ast::StmtKind::If(_, _, inner_else) => has_outer_else || inner_else.is_some(),
2841            ast::StmtKind::While(..) | ast::StmtKind::For { .. } => has_outer_else,
2842            _ => false,
2843        }
2844    }
2845
2846    /// Checks if a block statement `{ ... }` contains more than one line of actual code.
2847    fn is_multiline_block_stmt(
2848        &mut self,
2849        stmt: &'ast ast::Stmt<'ast>,
2850        empty_as_multiline: bool,
2851    ) -> bool {
2852        match &stmt.kind {
2853            ast::StmtKind::Block(block) => {
2854                self.is_multiline_block(block, empty_as_multiline, false)
2855            }
2856            ast::StmtKind::While(cond, body) => {
2857                !self.is_single_line_block(stmt.span.lo(), cond, body, None).outcome
2858            }
2859            ast::StmtKind::For { body, .. } => {
2860                // In `print_for_stmt`, `print_stmt_as_block(body, span.hi(), false)` is called with
2861                // `inline = false`. So only empty can be single-line.
2862                if let ast::StmtKind::Block(block) = &body.kind {
2863                    self.is_multiline_block(block, empty_as_multiline, true)
2864                } else {
2865                    true
2866                }
2867            }
2868
2869            ast::StmtKind::If(_, _, Some(_)) => true,
2870            ast::StmtKind::If(_, then, None) => {
2871                self.is_multiline_block_stmt(then, empty_as_multiline)
2872            }
2873
2874            // these ones always has an inner block, so we mark them as multiline
2875            ast::StmtKind::Assembly(_)
2876            | ast::StmtKind::DoWhile(_, _)
2877            | ast::StmtKind::Try(_)
2878            | ast::StmtKind::UncheckedBlock(_) => true,
2879
2880            ast::StmtKind::Break
2881            | ast::StmtKind::Continue
2882            | ast::StmtKind::DeclMulti(_, _)
2883            | ast::StmtKind::DeclSingle(_)
2884            | ast::StmtKind::Emit(_, _)
2885            | ast::StmtKind::Expr(_)
2886            | ast::StmtKind::Return(_)
2887            | ast::StmtKind::Revert(_, _)
2888            | ast::StmtKind::Placeholder => false,
2889        }
2890    }
2891
2892    /// Checks if a block statement `{ ... }` should be treated as multiline,
2893    /// either because it spans multiple lines or contains multiple statements.
2894    fn is_multiline_block(
2895        &mut self,
2896        block: &'ast ast::Block<'ast>,
2897        empty_as_multiline: bool,
2898        force_single_as_multiline: bool,
2899    ) -> bool {
2900        if block.stmts.is_empty() {
2901            return empty_as_multiline;
2902        }
2903        // A block with multiple statements should never be inlined, regardless of
2904        // whether it was written on a single line in the source.
2905        if block.stmts.len() > 1 {
2906            return true;
2907        }
2908
2909        if force_single_as_multiline {
2910            return true;
2911        }
2912
2913        // Check for multiline block.span first.
2914        // Block can spans multipline because of comments.
2915        if !self.same_source_line(block.span.lo(), block.span.hi())
2916            && let Some(snip) = self.snippet(block.span)
2917        {
2918            let code_lines = snip.lines().filter(|line| {
2919                let trimmed = line.trim();
2920                // Ignore empty lines and lines with only '{' or '}'
2921                if empty_as_multiline {
2922                    !trimmed.is_empty() && trimmed != "{" && trimmed != "}"
2923                } else {
2924                    !trimmed.is_empty()
2925                }
2926            });
2927            if code_lines.count() > 1 {
2928                return true;
2929            }
2930        }
2931
2932        let stmt = &block.stmts[0];
2933
2934        // Comments can break single-line layout. Mark block as multiline if there is a comment at
2935        // the beginning.
2936        if self.peek_comment_between(block.span.lo(), stmt.span.lo()).is_some() {
2937            return true;
2938        }
2939
2940        self.is_multiline_block_stmt(stmt, empty_as_multiline)
2941    }
2942
2943    /// Performs a size estimation to see if the if/else can fit on one line.
2944    fn can_stmts_be_inlined(
2945        &mut self,
2946        cond: &'ast ast::Expr<'ast>,
2947        then: &'ast ast::Stmt<'ast>,
2948        els_opt: Option<&'ast &'ast mut ast::Stmt<'ast>>,
2949    ) -> bool {
2950        let cond_len = self.estimate_size(cond.span);
2951
2952        // If the condition fits in one line, 6 chars: 'if (' + {cond} + ') ' + {then}
2953        // Otherwise chars: ') ' + {then}
2954        let then_margin = if 6 + cond_len < self.space_left() { 6 + cond_len } else { 2 };
2955
2956        if !self.is_inline_stmt(then, then_margin) {
2957            return false;
2958        }
2959
2960        // Always 6 chars for the else: 'else '
2961        els_opt.is_none_or(|els| self.is_inline_stmt(els, 6))
2962    }
2963
2964    fn can_header_be_inlined(&mut self, func: &ast::ItemFunction<'_>) -> bool {
2965        self.estimate_header_size(func) <= self.space_left()
2966    }
2967
2968    fn can_header_params_be_inlined(&mut self, func: &ast::ItemFunction<'_>) -> bool {
2969        self.estimate_header_params_size(func) <= self.space_left()
2970    }
2971
2972    fn estimate_header_size(&mut self, func: &ast::ItemFunction<'_>) -> usize {
2973        let ast::ItemFunction { kind: _, ref header, ref body, body_span: _ } = *func;
2974
2975        // ' ' + visibility
2976        let visibility = header.visibility.map_or(0, |v| self.estimate_size(v.span) + 1);
2977        // ' ' + state mutability
2978        let mutability = header.state_mutability.map_or(0, |sm| self.estimate_size(sm.span) + 1);
2979        // ' ' + modifier + (' ' + modifier)
2980        let m = header.modifiers.iter().fold(0, |len, m| len + self.estimate_size(m.span()));
2981        let modifiers = if m != 0 { m + 1 } else { 0 };
2982        // ' ' + override
2983        let override_ = header.override_.as_ref().map_or(0, |o| self.estimate_size(o.span) + 1);
2984        // ' ' + virtual
2985        let virtual_ = if header.virtual_.is_none() { 0 } else { 8 };
2986        // ' returns(' + var + (', ' + var) + ')'
2987        let returns = header.returns.as_ref().map_or(0, |ret| {
2988            ret.vars
2989                .iter()
2990                .fold(0, |len, p| if len != 0 { len + 2 } else { 10 } + self.estimate_size(p.span))
2991        });
2992        // ' {' or ';'
2993        let end = if body.is_some() { 2 } else { 1 };
2994
2995        self.estimate_header_params_size(func)
2996            + visibility
2997            + mutability
2998            + modifiers
2999            + override_
3000            + virtual_
3001            + returns
3002            + end
3003    }
3004
3005    fn estimate_header_params_size(&mut self, func: &ast::ItemFunction<'_>) -> usize {
3006        let ast::ItemFunction { kind, ref header, body: _, body_span: _ } = *func;
3007
3008        let kw = match kind {
3009            ast::FunctionKind::Constructor => 11, // 'constructor'
3010            ast::FunctionKind::Function => 9,     // 'function '
3011            ast::FunctionKind::Modifier => 9,     // 'modifier '
3012            ast::FunctionKind::Fallback => 8,     // 'fallback'
3013            ast::FunctionKind::Receive => 7,      // 'receive'
3014        };
3015
3016        // '(' + param + (', ' + param) + ')'
3017        let params = header
3018            .parameters
3019            .vars
3020            .iter()
3021            .fold(0, |len, p| if len != 0 { len + 2 } else { 2 } + self.estimate_size(p.span));
3022
3023        kw + header.name.map_or(0, |name| self.estimate_size(name.span)) + std::cmp::max(2, params)
3024    }
3025
3026    /// Estimates a comment-free binary expression using the printed operator spacing.
3027    fn estimate_binary_size(&self, expr: &ast::Expr<'_>) -> usize {
3028        match &expr.kind {
3029            ast::ExprKind::Binary(lhs, op, rhs) => {
3030                let spaces = if self.config.pow_no_space && matches!(op.kind, ast::BinOpKind::Pow) {
3031                    0
3032                } else {
3033                    2
3034                };
3035                self.estimate_binary_size(lhs)
3036                    + op.kind.to_str().len()
3037                    + spaces
3038                    + self.estimate_binary_size(rhs)
3039            }
3040            ast::ExprKind::Tuple(exprs) if let [SpannedOption::Some(inner)] = exprs.as_ref() => {
3041                self.estimate_binary_size(inner) + 2
3042            }
3043            _ => self.estimate_size(expr.span),
3044        }
3045    }
3046
3047    fn estimate_lhs_size(&self, expr: &ast::Expr<'_>, parent_op: &ast::BinOp) -> usize {
3048        match &expr.kind {
3049            ast::ExprKind::Binary(lhs, op, _) if op.kind.group() == parent_op.kind.group() => {
3050                self.estimate_lhs_size(lhs, op)
3051            }
3052            _ => self.estimate_size(expr.span),
3053        }
3054    }
3055
3056    fn estimate_call_chain_size(&self, expr: &ast::Expr<'_>) -> Option<usize> {
3057        match &expr.kind {
3058            ast::ExprKind::Call(callee, args) => {
3059                let ast::CallArgsKind::Unnamed(args) = &args.kind else { return None };
3060                let mut size = self.estimate_call_chain_size(callee)? + 2;
3061                for arg in args.iter() {
3062                    size += self.estimate_call_chain_size(arg)?;
3063                }
3064                Some(size + args.len().saturating_sub(1) * 2)
3065            }
3066            ast::ExprKind::Ident(ident) => Some(ident.to_string().len()),
3067            ast::ExprKind::Index(expr, kind) => {
3068                let index_size = match kind {
3069                    ast::IndexKind::Index(Some(index)) => self.estimate_call_chain_size(index)?,
3070                    ast::IndexKind::Index(None) => 0,
3071                    ast::IndexKind::Range(start, end) => {
3072                        let start = match start {
3073                            Some(start) => self.estimate_call_chain_size(start)?,
3074                            None => 0,
3075                        };
3076                        let end = match end {
3077                            Some(end) => self.estimate_call_chain_size(end)?,
3078                            None => 0,
3079                        };
3080                        start + end + 1
3081                    }
3082                };
3083                Some(self.estimate_call_chain_size(expr)? + index_size + 2)
3084            }
3085            // Zero is invariant under all number underscore configurations.
3086            ast::ExprKind::Lit(lit, None)
3087                if matches!(lit.kind, ast::LitKind::Number(_)) && lit.symbol.as_str() == "0" =>
3088            {
3089                Some(1)
3090            }
3091            ast::ExprKind::Member(expr, ident) => {
3092                Some(self.estimate_call_chain_size(expr)? + ident.to_string().len() + 1)
3093            }
3094            ast::ExprKind::Tuple(exprs) if let [SpannedOption::Some(expr)] = exprs.as_ref() => {
3095                Some(self.estimate_call_chain_size(expr)? + 2)
3096            }
3097            _ => None,
3098        }
3099    }
3100
3101    fn has_comments_between_elements<I>(&self, limits: Span, elements: I) -> bool
3102    where
3103        I: IntoIterator<Item = &'ast ast::Expr<'ast>>,
3104    {
3105        let mut last_span_end = limits.lo();
3106        for expr in elements {
3107            if self.has_comment_between(last_span_end, expr.span.lo()) {
3108                return true;
3109            }
3110            last_span_end = expr.span.hi();
3111        }
3112
3113        self.has_comment_between(last_span_end, limits.hi())
3114    }
3115}
3116
3117// -- HELPERS (language-specific) ----------------------------------------------
3118
3119#[derive(Debug)]
3120enum MemberOrCallArgs {
3121    Member(usize),
3122    CallArgs(usize, bool),
3123}
3124
3125impl MemberOrCallArgs {
3126    const fn size(&self) -> usize {
3127        match self {
3128            Self::CallArgs(size, ..) | Self::Member(size) => *size,
3129        }
3130    }
3131
3132    const fn member_size(&self) -> usize {
3133        match self {
3134            Self::CallArgs(..) => 0,
3135            Self::Member(size) => *size,
3136        }
3137    }
3138
3139    const fn has_comments(&self) -> bool {
3140        matches!(self, Self::CallArgs(.., true))
3141    }
3142}
3143
3144#[derive(Debug, Clone)]
3145#[expect(dead_code)]
3146enum AttributeKind<'ast> {
3147    Visibility(ast::Visibility),
3148    StateMutability(ast::StateMutability),
3149    Virtual,
3150    Override(&'ast ast::Override<'ast>),
3151    Modifier(&'ast ast::Modifier<'ast>),
3152}
3153
3154type AttributeCommentMap = HashMap<BytePos, (Vec<Comment>, Vec<Comment>, Vec<Comment>)>;
3155
3156#[derive(Debug, Clone)]
3157struct AttributeInfo<'ast> {
3158    kind: AttributeKind<'ast>,
3159    span: Span,
3160}
3161
3162/// Helper struct to map attributes to their associated comments in function headers.
3163struct AttributeCommentMapper<'ast> {
3164    limit_pos: BytePos,
3165    comments: Vec<Comment>,
3166    attributes: Vec<AttributeInfo<'ast>>,
3167}
3168
3169impl<'ast> AttributeCommentMapper<'ast> {
3170    fn new(returns: Option<&'ast ast::ParameterList<'ast>>, body_pos: BytePos) -> Self {
3171        Self {
3172            comments: Vec::new(),
3173            attributes: Vec::new(),
3174            limit_pos: returns.as_ref().map_or(body_pos, |ret| ret.span.lo()),
3175        }
3176    }
3177
3178    #[allow(clippy::type_complexity)]
3179    fn build(
3180        mut self,
3181        state: &mut State<'_, 'ast>,
3182        header: &'ast ast::FunctionHeader<'ast>,
3183    ) -> (AttributeCommentMap, Vec<AttributeInfo<'ast>>, BytePos) {
3184        let first_attr = self.collect_attributes(header);
3185        if !self.attributes.is_empty() {
3186            self.cache_comments(state);
3187        }
3188        (self.map(), self.attributes, first_attr)
3189    }
3190
3191    fn map(&mut self) -> AttributeCommentMap {
3192        let mut map = HashMap::new();
3193        for a in 0..self.attributes.len() {
3194            let is_last = a == self.attributes.len() - 1;
3195            let (mut before, mut inner, mut after) = (Vec::new(), Vec::new(), Vec::new());
3196
3197            let before_limit = self.attributes[a].span.lo();
3198            let inner_limit = self.attributes[a].span.hi();
3199            let after_limit =
3200                if is_last { self.limit_pos } else { self.attributes[a + 1].span.lo() };
3201
3202            let mut c = 0;
3203            while c < self.comments.len() {
3204                if self.comments[c].pos() <= before_limit {
3205                    before.push(self.comments.remove(c));
3206                } else if self.comments[c].pos() <= inner_limit {
3207                    inner.push(self.comments.remove(c));
3208                } else if (after.is_empty() || is_last) && self.comments[c].pos() <= after_limit {
3209                    after.push(self.comments.remove(c));
3210                } else {
3211                    c += 1;
3212                }
3213            }
3214            map.insert(before_limit, (before, inner, after));
3215        }
3216        map
3217    }
3218
3219    fn collect_attributes(&mut self, header: &'ast ast::FunctionHeader<'ast>) -> BytePos {
3220        let mut first_pos = BytePos(u32::MAX);
3221        if let Some(v) = header.visibility {
3222            if v.span.lo() < first_pos {
3223                first_pos = v.span.lo()
3224            }
3225            self.attributes
3226                .push(AttributeInfo { kind: AttributeKind::Visibility(*v), span: v.span });
3227        }
3228        if let Some(sm) = header.state_mutability {
3229            if sm.span.lo() < first_pos {
3230                first_pos = sm.span.lo()
3231            }
3232            self.attributes
3233                .push(AttributeInfo { kind: AttributeKind::StateMutability(*sm), span: sm.span });
3234        }
3235        if let Some(span) = header.virtual_ {
3236            if span.lo() < first_pos {
3237                first_pos = span.lo()
3238            }
3239            self.attributes.push(AttributeInfo { kind: AttributeKind::Virtual, span });
3240        }
3241        if let Some(ref o) = header.override_ {
3242            if o.span.lo() < first_pos {
3243                first_pos = o.span.lo()
3244            }
3245            self.attributes.push(AttributeInfo { kind: AttributeKind::Override(o), span: o.span });
3246        }
3247        for m in header.modifiers.iter() {
3248            if m.span().lo() < first_pos {
3249                first_pos = m.span().lo()
3250            }
3251            self.attributes
3252                .push(AttributeInfo { kind: AttributeKind::Modifier(m), span: m.span() });
3253        }
3254        self.attributes.sort_by_key(|attr| attr.span.lo());
3255        first_pos
3256    }
3257
3258    fn cache_comments(&mut self, state: &mut State<'_, 'ast>) {
3259        let mut pending = None;
3260        for cmnt in state.comments.iter() {
3261            if cmnt.pos() >= self.limit_pos {
3262                break;
3263            }
3264            match pending {
3265                Some(ref p) => pending = Some(p + 1),
3266                None => pending = Some(0),
3267            }
3268        }
3269        while let Some(p) = pending {
3270            if p == 0 {
3271                pending = None;
3272            } else {
3273                pending = Some(p - 1);
3274            }
3275            let cmnt = state.next_comment().unwrap();
3276            if cmnt.style.is_blank() {
3277                continue;
3278            }
3279            self.comments.push(cmnt);
3280        }
3281    }
3282}
3283
3284const fn stmt_needs_semi(stmt: &ast::StmtKind<'_>) -> bool {
3285    match stmt {
3286        ast::StmtKind::Assembly { .. }
3287        | ast::StmtKind::Block { .. }
3288        | ast::StmtKind::For { .. }
3289        | ast::StmtKind::If { .. }
3290        | ast::StmtKind::Try { .. }
3291        | ast::StmtKind::UncheckedBlock { .. }
3292        | ast::StmtKind::While { .. } => false,
3293
3294        ast::StmtKind::DeclSingle { .. }
3295        | ast::StmtKind::DeclMulti { .. }
3296        | ast::StmtKind::Break { .. }
3297        | ast::StmtKind::Continue { .. }
3298        | ast::StmtKind::DoWhile { .. }
3299        | ast::StmtKind::Emit { .. }
3300        | ast::StmtKind::Expr { .. }
3301        | ast::StmtKind::Return { .. }
3302        | ast::StmtKind::Revert { .. }
3303        | ast::StmtKind::Placeholder { .. } => true,
3304    }
3305}
3306
3307/// Returns `true` if the item needs an isolated line break.
3308fn item_needs_iso(item: &ast::ItemKind<'_>) -> bool {
3309    match item {
3310        ast::ItemKind::Pragma(..)
3311        | ast::ItemKind::Import(..)
3312        | ast::ItemKind::Using(..)
3313        | ast::ItemKind::Variable(..)
3314        | ast::ItemKind::Udvt(..)
3315        | ast::ItemKind::Enum(..)
3316        | ast::ItemKind::Error(..)
3317        | ast::ItemKind::Event(..) => false,
3318
3319        ast::ItemKind::Contract(..) => true,
3320
3321        ast::ItemKind::Struct(strukt) => !strukt.fields.is_empty(),
3322        ast::ItemKind::Function(func) => {
3323            func.body.as_ref().is_some_and(|b| !b.is_empty())
3324                && !matches!(func.kind, ast::FunctionKind::Modifier)
3325        }
3326    }
3327}
3328
3329const fn is_binary_expr(expr_kind: &ast::ExprKind<'_>) -> bool {
3330    matches!(expr_kind, ast::ExprKind::Binary(..))
3331}
3332
3333fn has_complex_successor(expr_kind: &ast::ExprKind<'_>, left: bool) -> bool {
3334    match expr_kind {
3335        ast::ExprKind::Binary(lhs, _, rhs) => {
3336            if left {
3337                has_complex_successor(&lhs.kind, left)
3338            } else {
3339                has_complex_successor(&rhs.kind, left)
3340            }
3341        }
3342        ast::ExprKind::Unary(_, expr) => has_complex_successor(&expr.kind, left),
3343        ast::ExprKind::Lit(..) | ast::ExprKind::Ident(_) => false,
3344        ast::ExprKind::Tuple(..) => false,
3345        _ => true,
3346    }
3347}
3348
3349const fn is_call(expr_kind: &ast::ExprKind<'_>) -> bool {
3350    matches!(expr_kind, ast::ExprKind::Call(..))
3351}
3352
3353/// Returns true if this is a call with named arguments (struct-style syntax).
3354/// Used to determine if `.field` after such a call should avoid breaking.
3355/// E.g., `_lzSend({_dstEid: x, ...}).guid` → true (named args call)
3356/// E.g., `someFunc(a, b).field` → false (positional args)
3357const fn is_call_with_named_args(expr_kind: &ast::ExprKind<'_>) -> bool {
3358    if let ast::ExprKind::Call(_, args) = expr_kind {
3359        matches!(args.kind, ast::CallArgsKind::Named(_))
3360    } else {
3361        false
3362    }
3363}
3364
3365fn is_call_chain(expr_kind: &ast::ExprKind<'_>, must_have_child: bool) -> bool {
3366    match expr_kind {
3367        ast::ExprKind::Index(child, ..) | ast::ExprKind::Member(child, ..) => {
3368            is_call_chain(&child.kind, false)
3369        }
3370        ast::ExprKind::Tuple(exprs) if let [SpannedOption::Some(child)] = exprs.as_ref() => {
3371            is_call_chain(&child.kind, must_have_child)
3372        }
3373        _ => !must_have_child && is_call(expr_kind),
3374    }
3375}
3376
3377fn call_chain_contains_options(expr: &ast::Expr<'_>) -> bool {
3378    match &expr.peel_parens().kind {
3379        ast::ExprKind::CallOptions(..) => true,
3380        ast::ExprKind::Call(expr, ..)
3381        | ast::ExprKind::Index(expr, ..)
3382        | ast::ExprKind::Member(expr, ..) => call_chain_contains_options(expr),
3383        _ => false,
3384    }
3385}
3386
3387fn is_call_with_opts_and_args(expr_kind: &ast::ExprKind<'_>) -> bool {
3388    if let ast::ExprKind::Call(call_expr, call_args) = expr_kind {
3389        matches!(call_expr.kind, ast::ExprKind::CallOptions(..)) && !call_args.is_empty()
3390    } else {
3391        false
3392    }
3393}
3394
3395#[derive(Debug)]
3396struct Decision {
3397    outcome: bool,
3398    is_cached: bool,
3399}
3400
3401#[derive(Clone, Copy, PartialEq, Eq)]
3402pub(crate) enum BinOpGroup {
3403    Arithmetic,
3404    Bitwise,
3405    Comparison,
3406    Logical,
3407}
3408
3409trait BinOpExt {
3410    fn group(&self) -> BinOpGroup;
3411}
3412
3413impl BinOpExt for ast::BinOpKind {
3414    fn group(&self) -> BinOpGroup {
3415        match self {
3416            Self::Or | Self::And => BinOpGroup::Logical,
3417            Self::Eq | Self::Ne | Self::Lt | Self::Le | Self::Gt | Self::Ge => {
3418                BinOpGroup::Comparison
3419            }
3420            Self::BitOr | Self::BitXor | Self::BitAnd | Self::Shl | Self::Shr | Self::Sar => {
3421                BinOpGroup::Bitwise
3422            }
3423            Self::Add | Self::Sub | Self::Mul | Self::Div | Self::Rem | Self::Pow => {
3424                BinOpGroup::Arithmetic
3425            }
3426        }
3427    }
3428}
3429
3430/// Calculates the size the callee's "head," excluding its arguments.
3431///
3432/// # Examples
3433///
3434/// - `myFunction(..)`: 8 (length of `myFunction`)
3435/// - `uint256(..)`: 7 (length of `uint256`)
3436/// - `abi.encode(..)`: 10 (length of `abi.encode`)
3437/// - `foo(..).bar(..)`: 3 (length of `foo`)
3438pub(super) fn get_callee_head_size(callee: &ast::Expr<'_>) -> usize {
3439    match &callee.kind {
3440        ast::ExprKind::Ident(id) => id.as_str().len(),
3441        ast::ExprKind::Type(ast::Type { kind: ast::TypeKind::Elementary(ty), .. }) => {
3442            ty.to_abi_str().len()
3443        }
3444        ast::ExprKind::Index(base, idx) => {
3445            let idx_len = match idx {
3446                ast::IndexKind::Index(expr) => expr.as_ref().map_or(0, |e| get_callee_head_size(e)),
3447                ast::IndexKind::Range(e1, e2) => {
3448                    1 + e1.as_ref().map_or(0, |e| get_callee_head_size(e))
3449                        + e2.as_ref().map_or(0, |e| get_callee_head_size(e))
3450                }
3451            };
3452            get_callee_head_size(base) + 2 + idx_len
3453        }
3454        ast::ExprKind::Member(base, member_ident) => {
3455            match &base.kind {
3456                ast::ExprKind::Ident(..) | ast::ExprKind::Type(..) => {
3457                    get_callee_head_size(base) + 1 + member_ident.as_str().len()
3458                }
3459
3460                // Chainned calls are not traversed, and instead just the member identifier is used
3461                ast::ExprKind::Member(child, ..) if !is_call(&child.kind) => {
3462                    get_callee_head_size(base) + 1 + member_ident.as_str().len()
3463                }
3464                _ => member_ident.as_str().len(),
3465            }
3466        }
3467        ast::ExprKind::Binary(lhs, _, _) => get_callee_head_size(lhs),
3468
3469        // If the callee is not an identifier or member access, it has no "head"
3470        _ => 0,
3471    }
3472}
3473
3474/// Counts member links in an assignment RHS, through calls and indexes.
3475fn assignment_member_depth(expr: &ast::Expr<'_>) -> usize {
3476    match &expr.kind {
3477        ast::ExprKind::Member(child, _) => 1 + assignment_member_depth(child),
3478        ast::ExprKind::Call(child, _) | ast::ExprKind::Index(child, _) => {
3479            assignment_member_depth(child)
3480        }
3481        _ => 0,
3482    }
3483}
3484
3485#[cfg(test)]
3486mod tests {
3487    use super::*;
3488    use crate::{FormatterConfig, InlineConfig};
3489    use foundry_common::comments::Comments;
3490    use solar::{
3491        interface::{Session, source_map::FileName},
3492        sema::Compiler,
3493    };
3494    use std::sync::Arc;
3495
3496    /// This helper extracts function headers from the AST and passes them to the test function.
3497    fn parse_and_test<F>(source: &str, test_fn: F)
3498    where
3499        F: FnOnce(&mut State<'_, '_>, &ast::ItemFunction<'_>) + Send,
3500    {
3501        let session = Session::builder().with_buffer_emitter(Default::default()).build();
3502        let mut compiler = Compiler::new(session);
3503
3504        compiler
3505            .enter_mut(|c| -> solar::interface::Result<()> {
3506                let mut pcx = c.parse();
3507                pcx.set_resolve_imports(false);
3508
3509                // Create a source file using stdin as the filename
3510                let file = c
3511                    .sess()
3512                    .source_map()
3513                    .new_source_file(FileName::Stdin, source)
3514                    .map_err(|e| c.sess().dcx.err(e.to_string()).emit())?;
3515
3516                pcx.add_file(file.clone());
3517                pcx.parse();
3518                c.dcx().has_errors()?;
3519
3520                // Get AST from parsed source and setup the formatter
3521                let gcx = c.gcx();
3522                let (_, source_obj) = gcx.get_ast_source(&file.name).expect("Failed to get AST");
3523                let ast = source_obj.ast.as_ref().expect("No AST found");
3524                let comments =
3525                    Comments::new(&source_obj.file, gcx.sess.source_map(), true, false, None);
3526                let config = Arc::new(FormatterConfig::default());
3527                let inline_config = InlineConfig::default();
3528                let mut state = State::new(&source_obj.file, config, inline_config, comments);
3529
3530                // Extract the first function header (either top-level or inside a contract)
3531                let func = ast
3532                    .items
3533                    .iter()
3534                    .find_map(|item| match &item.kind {
3535                        ast::ItemKind::Function(func) => Some(func),
3536                        ast::ItemKind::Contract(contract) => {
3537                            contract.body.iter().find_map(|contract_item| {
3538                                match &contract_item.kind {
3539                                    ast::ItemKind::Function(func) => Some(func),
3540                                    _ => None,
3541                                }
3542                            })
3543                        }
3544                        _ => None,
3545                    })
3546                    .expect("No function found in source");
3547
3548                // Run the closure
3549                test_fn(&mut state, func);
3550
3551                Ok(())
3552            })
3553            .expect("Test failed");
3554    }
3555
3556    #[test]
3557    fn test_estimate_header_sizes() {
3558        let test_cases = [
3559            ("function foo();", 14, 15),
3560            ("function foo() {}", 14, 16),
3561            ("function foo() public {}", 14, 23),
3562            ("function foo(uint256 a) public {}", 23, 32),
3563            ("function foo(uint256 a, address b, bool c) public {}", 42, 51),
3564            ("function foo() public pure {}", 14, 28),
3565            ("function foo() public virtual {}", 14, 31),
3566            ("function foo() public override {}", 14, 32),
3567            ("function foo() public onlyOwner {}", 14, 33),
3568            ("function foo() public returns(uint256) {}", 14, 40),
3569            ("function foo() public returns(uint256, address) {}", 14, 49),
3570            ("function foo(uint256 a) public virtual override returns(uint256) {}", 23, 66),
3571            ("function foo() external payable {}", 14, 33),
3572            // other function types
3573            ("contract C { constructor() {} }", 13, 15),
3574            ("contract C { constructor(uint256 a) {} }", 22, 24),
3575            ("contract C { modifier onlyOwner() {} }", 20, 22),
3576            ("contract C { modifier onlyRole(bytes32 role) {} }", 31, 33),
3577            ("contract C { fallback() external payable {} }", 10, 29),
3578            ("contract C { receive() external payable {} }", 9, 28),
3579        ];
3580
3581        for (source, expected_params, expected_header) in &test_cases {
3582            parse_and_test(source, |state, func| {
3583                let params_size = state.estimate_header_params_size(func);
3584                assert_eq!(
3585                    params_size, *expected_params,
3586                    "Failed params size: expected {expected_params}, got {params_size} for source: {source}",
3587                );
3588
3589                let header_size = state.estimate_header_size(func);
3590                assert_eq!(
3591                    header_size, *expected_header,
3592                    "Failed header size: expected {expected_header}, got {header_size} for source: {source}",
3593                );
3594            });
3595        }
3596    }
3597}