Skip to main content

forge_fmt/state/
sol.rs

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