forge_fmt/state/
sol.rs

1#![allow(clippy::too_many_arguments)]
2
3use super::{
4    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| if c.pos() < span.lo() { Some(c.style) } else { None })
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        match items {
209            ast::ImportItems::Plain(_) | ast::ImportItems::Glob(_) => {
210                self.print_ast_str_lit(path);
211                if let Some(ident) = items.source_alias() {
212                    self.word(" as ");
213                    self.print_ident(&ident);
214                }
215            }
216
217            ast::ImportItems::Aliases(aliases) => {
218                self.s.cbox(self.ind);
219                self.word("{");
220                self.braces_break();
221
222                if self.config.sort_imports {
223                    let mut sorted: Vec<_> = aliases.iter().collect();
224                    sorted.sort_by_key(|(ident, _alias)| ident.name.as_str());
225                    self.print_commasep_aliases(sorted.into_iter());
226                } else {
227                    self.print_commasep_aliases(aliases.iter());
228                };
229
230                self.braces_break();
231                self.s.offset(-self.ind);
232                self.word("}");
233                self.end();
234                self.word(" from ");
235                self.print_ast_str_lit(path);
236            }
237        }
238        self.word(";");
239    }
240
241    fn print_using(&mut self, using: &'ast ast::UsingDirective<'ast>) {
242        let ast::UsingDirective { list, ty, global } = using;
243        self.word("using ");
244        match list {
245            ast::UsingList::Single(path) => self.print_path(path, true),
246            ast::UsingList::Multiple(items) => {
247                self.s.cbox(self.ind);
248                self.word("{");
249                self.braces_break();
250                for (pos, (path, op)) in items.iter().delimited() {
251                    self.print_path(path, true);
252                    if let Some(op) = op {
253                        self.word(" as ");
254                        self.word(op.to_str());
255                    }
256                    if !pos.is_last {
257                        self.word(",");
258                        self.space();
259                    }
260                }
261                self.braces_break();
262                self.s.offset(-self.ind);
263                self.word("}");
264                self.end();
265            }
266        }
267        self.word(" for ");
268        if let Some(ty) = ty {
269            self.print_ty(ty);
270        } else {
271            self.word("*");
272        }
273        if *global {
274            self.word(" global");
275        }
276        self.word(";");
277    }
278
279    fn print_contract(&mut self, c: &'ast ast::ItemContract<'ast>, span: Span) {
280        let ast::ItemContract { kind, name, layout, bases, body } = c;
281        self.contract = Some(c);
282        self.cursor.advance_to(span.lo(), true);
283
284        self.s.cbox(self.ind);
285        self.ibox(0);
286        self.cbox(0);
287        self.word_nbsp(kind.to_str());
288        self.print_ident(name);
289        self.nbsp();
290
291        if let Some(first) = bases.first().map(|base| base.span())
292            && let Some(last) = bases.last().map(|base| base.span())
293            && self.inline_config.is_disabled(first.to(last))
294        {
295            _ = self.handle_span(first.until(last), false);
296        } else if !bases.is_empty() {
297            self.word("is");
298            self.space();
299            for (pos, base) in bases.iter().delimited() {
300                if !self.handle_span(base.span(), false) {
301                    self.print_modifier_call(base, false);
302                    if !pos.is_last {
303                        self.word(",");
304                        self.space();
305                    }
306                }
307            }
308            self.space();
309            self.s.offset(-self.ind);
310        }
311        self.end();
312        if let Some(layout) = layout
313            && !self.handle_span(layout.span, false)
314        {
315            self.word("layout at ");
316            self.print_expr(layout.slot);
317            self.print_sep(Separator::Space);
318        }
319
320        self.print_word("{");
321        self.end();
322        if !body.is_empty() {
323            // update block depth
324            self.block_depth += 1;
325
326            self.print_sep(Separator::Hardbreak);
327            if self.config.contract_new_lines {
328                self.hardbreak();
329            }
330            let body_lo = body[0].span.lo();
331            if self.peek_comment_before(body_lo).is_some() {
332                self.print_comments(body_lo, CommentConfig::skip_leading_ws(true));
333            }
334
335            let mut is_first = true;
336            let mut items = body.iter().peekable();
337            while let Some(item) = items.next() {
338                self.print_item(item, is_first);
339                is_first = false;
340                if let Some(next_item) = items.peek() {
341                    if self.inline_config.is_disabled(next_item.span) {
342                        _ = self.handle_span(next_item.span, false);
343                    } else {
344                        self.separate_items(next_item, true);
345                    }
346                }
347            }
348
349            if let Some(cmnt) = self.print_comments(span.hi(), CommentConfig::skip_trailing_ws())
350                && self.config.contract_new_lines
351                && !cmnt.is_blank()
352            {
353                self.print_sep(Separator::Hardbreak);
354            }
355            self.s.offset(-self.ind);
356            self.end();
357            if self.config.contract_new_lines {
358                self.hardbreak_if_nonempty();
359            }
360
361            // restore block depth
362            self.block_depth -= 1;
363        } else {
364            if self.print_comments(span.hi(), CommentConfig::skip_ws()).is_some() {
365                self.zerobreak();
366            } else if self.config.bracket_spacing {
367                self.nbsp();
368            };
369            self.end();
370        }
371        self.print_word("}");
372
373        self.cursor.advance_to(span.hi(), true);
374        self.contract = None;
375    }
376
377    fn print_struct(&mut self, strukt: &'ast ast::ItemStruct<'ast>, span: Span) {
378        let ast::ItemStruct { name, fields } = strukt;
379        let ind = if self.estimate_size(name.span) + 8 >= self.space_left() { self.ind } else { 0 };
380        self.s.ibox(self.ind);
381        self.word("struct");
382        self.space();
383        self.print_ident(name);
384        self.word(" {");
385        if !fields.is_empty() {
386            self.break_offset(SIZE_INFINITY as usize, ind);
387        }
388        self.s.ibox(0);
389        for var in fields.iter() {
390            self.print_var_def(var);
391            if !self.print_trailing_comment(var.span.hi(), None) {
392                self.hardbreak();
393            }
394        }
395        self.print_comments(span.hi(), CommentConfig::skip_ws());
396        if ind == 0 {
397            self.s.offset(-self.ind);
398        }
399        self.end();
400        self.end();
401        self.word("}");
402    }
403
404    fn print_enum(&mut self, enm: &'ast ast::ItemEnum<'ast>, span: Span) {
405        let ast::ItemEnum { name, variants } = enm;
406        self.s.cbox(self.ind);
407        self.word("enum ");
408        self.print_ident(name);
409        self.word(" {");
410        self.hardbreak_if_nonempty();
411        for (pos, ident) in variants.iter().delimited() {
412            self.print_comments(ident.span.lo(), CommentConfig::default());
413            self.print_ident(ident);
414            if !pos.is_last {
415                self.word(",");
416            }
417            if !self.print_trailing_comment(ident.span.hi(), None) {
418                self.hardbreak();
419            }
420        }
421        self.print_comments(span.hi(), CommentConfig::skip_ws());
422        self.s.offset(-self.ind);
423        self.end();
424        self.word("}");
425    }
426
427    fn print_udvt(&mut self, udvt: &'ast ast::ItemUdvt<'ast>) {
428        let ast::ItemUdvt { name, ty } = udvt;
429        self.word("type ");
430        self.print_ident(name);
431        self.word(" is ");
432        self.print_ty(ty);
433        self.word(";");
434    }
435
436    // NOTE(rusowsky): Functions are the only source unit item that handle inline (disabled) format
437    fn print_function(&mut self, func: &'ast ast::ItemFunction<'ast>) {
438        let ast::ItemFunction { kind, ref header, ref body, body_span } = *func;
439        let ast::FunctionHeader {
440            name,
441            ref parameters,
442            visibility,
443            state_mutability: sm,
444            virtual_,
445            ref override_,
446            ref returns,
447            ..
448        } = *header;
449
450        self.s.cbox(self.ind);
451
452        // Print fn name and params
453        _ = self.handle_span(self.cursor.span(header.span.lo()), false);
454        self.print_word(kind.to_str());
455        if let Some(name) = name {
456            self.print_sep(Separator::Nbsp);
457            self.print_ident(&name);
458            self.cursor.advance_to(name.span.hi(), true);
459        }
460        self.s.cbox(-self.ind);
461        let header_style = self.config.multiline_func_header;
462        let params_format = match header_style {
463            MultilineFuncHeaderStyle::ParamsAlways => ListFormat::always_break(),
464            MultilineFuncHeaderStyle::AllParams
465                if !header.parameters.is_empty() && !self.can_header_be_inlined(header) =>
466            {
467                ListFormat::always_break()
468            }
469            _ => ListFormat::consistent().break_cmnts().break_single(
470                // ensure fn params are always breakable when there is a single `Contract.Struct`
471                parameters.len() == 1
472                    && matches!(
473                        &parameters[0].ty,
474                        ast::Type { kind: ast::TypeKind::Custom(ty), .. } if ty.segments().len() > 1
475                    ),
476            ),
477        };
478        self.print_parameter_list(parameters, parameters.span, params_format);
479        self.end();
480
481        // Map attributes to their corresponding comments
482        let (mut map, attributes, first_attrib_pos) =
483            AttributeCommentMapper::new(returns.as_ref(), body_span.lo()).build(self, header);
484
485        let mut handle_pre_cmnts = |this: &mut Self, span: Span| -> bool {
486            if this.inline_config.is_disabled(span)
487                // Note: `map` is still captured from the outer scope, which is fine.
488                && let Some((pre_cmnts, ..)) = map.remove(&span.lo())
489            {
490                for (pos, cmnt) in pre_cmnts.into_iter().delimited() {
491                    if pos.is_first && cmnt.style.is_isolated() && !this.is_bol_or_only_ind() {
492                        this.print_sep(Separator::Hardbreak);
493                    }
494                    if let Some(cmnt) = this.handle_comment(cmnt, false) {
495                        this.print_comment(cmnt, CommentConfig::skip_ws().mixed_post_nbsp());
496                    }
497                    if pos.is_last {
498                        return true;
499                    }
500                }
501            }
502            false
503        };
504
505        let skip_attribs = returns.as_ref().is_some_and(|ret| {
506            let attrib_span = Span::new(first_attrib_pos, ret.span.lo());
507            handle_pre_cmnts(self, attrib_span);
508            self.handle_span(attrib_span, false)
509        });
510        let skip_returns = {
511            let pos = if skip_attribs { self.cursor.pos } else { first_attrib_pos };
512            let ret_span = Span::new(pos, body_span.lo());
513            handle_pre_cmnts(self, ret_span);
514            self.handle_span(ret_span, false)
515        };
516
517        let attrib_box = self.config.multiline_func_header.params_first()
518            || (self.config.multiline_func_header.attrib_first()
519                && !self.can_header_params_be_inlined(header));
520        if attrib_box {
521            self.s.cbox(0);
522        }
523        if !(skip_attribs || skip_returns) {
524            // Print fn attributes in correct order
525            if let Some(v) = visibility {
526                self.print_fn_attribute(v.span, &mut map, &mut |s| s.word(v.to_str()));
527            }
528            if let Some(sm) = sm
529                && !matches!(*sm, ast::StateMutability::NonPayable)
530            {
531                self.print_fn_attribute(sm.span, &mut map, &mut |s| s.word(sm.to_str()));
532            }
533            if let Some(v) = virtual_ {
534                self.print_fn_attribute(v, &mut map, &mut |s| s.word("virtual"));
535            }
536            if let Some(o) = override_ {
537                self.print_fn_attribute(o.span, &mut map, &mut |s| s.print_override(o));
538            }
539            for m in attributes.iter().filter(|a| matches!(a.kind, AttributeKind::Modifier(_))) {
540                if let AttributeKind::Modifier(modifier) = m.kind {
541                    let is_base = self.is_modifier_a_base_contract(kind, modifier);
542                    self.print_fn_attribute(m.span, &mut map, &mut |s| {
543                        s.print_modifier_call(modifier, is_base)
544                    });
545                }
546            }
547        }
548        if !skip_returns
549            && let Some(ret) = returns
550            && !ret.is_empty()
551            && let Some(ret) = returns
552        {
553            if !self.handle_span(self.cursor.span(ret.span.lo()), false) {
554                if !self.is_bol_or_only_ind() && !self.last_token_is_space() {
555                    self.print_sep(Separator::Space);
556                }
557                self.cursor.advance_to(ret.span.lo(), true);
558                self.print_word("returns ");
559            }
560            self.print_parameter_list(
561                ret,
562                ret.span,
563                ListFormat::consistent(), // .with_cmnts_break(false),
564            );
565        }
566
567        // Print fn body
568        if let Some(body) = body {
569            if self.handle_span(self.cursor.span(body_span.lo()), false) {
570                // Print spacing if necessary. Updates cursor.
571            } else {
572                if let Some(cmnt) = self.peek_comment_before(body_span.lo()) {
573                    if cmnt.style.is_mixed() {
574                        // These shouldn't update the cursor, as we've already dealt with it above
575                        self.space();
576                        self.s.offset(-self.ind);
577                        self.print_comments(body_span.lo(), CommentConfig::skip_ws());
578                    } else {
579                        self.zerobreak();
580                        self.s.offset(-self.ind);
581                        self.print_comments(body_span.lo(), CommentConfig::skip_ws());
582                        self.s.offset(-self.ind);
583                    }
584                } else {
585                    // If there are no modifiers, overrides, nor returns never break
586                    if header.modifiers.is_empty()
587                        && header.override_.is_none()
588                        && returns.as_ref().is_none_or(|r| r.is_empty())
589                        && (header.visibility().is_none() || body.is_empty())
590                    {
591                        self.nbsp();
592                    } else {
593                        self.space();
594                        self.s.offset(-self.ind);
595                    }
596                }
597                self.cursor.advance_to(body_span.lo(), true);
598            }
599            self.print_word("{");
600            self.end();
601            if attrib_box {
602                self.end();
603            }
604
605            self.print_block_without_braces(body, body_span.hi(), Some(self.ind));
606            if self.cursor.enabled || self.cursor.pos < body_span.hi() {
607                self.print_word("}");
608                self.cursor.advance_to(body_span.hi(), true);
609            }
610        } else {
611            self.print_comments(body_span.lo(), CommentConfig::skip_ws().mixed_prev_space());
612            self.end();
613            if attrib_box {
614                self.end();
615            }
616            self.neverbreak();
617            self.print_word(";");
618        }
619
620        if let Some(cmnt) = self.peek_trailing_comment(body_span.hi(), None) {
621            if cmnt.is_doc {
622                // trailing doc comments after the fn body are isolated
623                // these shouldn't update the cursor, as this is our own formatting
624                self.hardbreak();
625                self.hardbreak();
626            }
627            self.print_trailing_comment(body_span.hi(), None);
628        }
629    }
630
631    fn print_fn_attribute(
632        &mut self,
633        span: Span,
634        map: &mut AttributeCommentMap,
635        print_fn: &mut dyn FnMut(&mut Self),
636    ) {
637        match map.remove(&span.lo()) {
638            Some((pre_cmnts, inner_cmnts, post_cmnts)) => {
639                // Print preceding comments.
640                for cmnt in pre_cmnts {
641                    let Some(cmnt) = self.handle_comment(cmnt, false) else {
642                        continue;
643                    };
644                    self.print_comment(cmnt, CommentConfig::default());
645                }
646                // Push the inner comments back to the queue, so that they are printed in their
647                // intended place.
648                for cmnt in inner_cmnts.into_iter().rev() {
649                    self.comments.push_front(cmnt);
650                }
651                let mut enabled = false;
652                if !self.handle_span(span, false) {
653                    if !self.is_bol_or_only_ind() {
654                        self.space();
655                    }
656                    self.ibox(0);
657                    print_fn(self);
658                    self.cursor.advance_to(span.hi(), true);
659                    enabled = true;
660                }
661                // Print subsequent comments.
662                for cmnt in post_cmnts {
663                    let Some(cmnt) = self.handle_comment(cmnt, false) else {
664                        continue;
665                    };
666                    self.print_comment(cmnt, CommentConfig::default().mixed_prev_space());
667                }
668                if enabled {
669                    self.end();
670                }
671            }
672            // Fallback for attributes not in the map (should never happen)
673            None => {
674                if !self.is_bol_or_only_ind() {
675                    self.space();
676                }
677                print_fn(self);
678                self.cursor.advance_to(span.hi(), true);
679            }
680        }
681    }
682
683    fn is_modifier_a_base_contract(
684        &self,
685        kind: ast::FunctionKind,
686        modifier: &'ast ast::Modifier<'ast>,
687    ) -> bool {
688        // Add `()` in functions when the modifier is a base contract.
689        // HACK: heuristics:
690        // 1. exactly matches the name of a base contract as declared in the `contract is`;
691        // this does not account for inheritance;
692        let is_contract_base = self.contract.is_some_and(|contract| {
693            contract
694                .bases
695                .iter()
696                .any(|contract_base| contract_base.name.to_string() == modifier.name.to_string())
697        });
698        // 2. assume that title case names in constructors are bases.
699        // LEGACY: constructors used to also be `function NameOfContract...`; not checked.
700        let is_constructor = matches!(kind, ast::FunctionKind::Constructor);
701        // LEGACY: we are checking the beginning of the path, not the last segment.
702        is_contract_base
703            || (is_constructor
704                && modifier.name.first().name.as_str().starts_with(char::is_uppercase))
705    }
706
707    fn print_error(&mut self, err: &'ast ast::ItemError<'ast>) {
708        let ast::ItemError { name, parameters } = err;
709        self.word("error ");
710        self.print_ident(name);
711        self.print_parameter_list(parameters, parameters.span, ListFormat::compact());
712        self.word(";");
713    }
714
715    fn print_event(&mut self, event: &'ast ast::ItemEvent<'ast>) {
716        let ast::ItemEvent { name, parameters, anonymous } = event;
717        self.word("event ");
718        self.print_ident(name);
719        self.print_parameter_list(parameters, parameters.span, ListFormat::compact().break_cmnts());
720        if *anonymous {
721            self.word(" anonymous");
722        }
723        self.word(";");
724    }
725
726    fn print_var_def(&mut self, var: &'ast ast::VariableDefinition<'ast>) {
727        self.print_var(var, true);
728        self.word(";");
729    }
730
731    fn print_var(&mut self, var: &'ast ast::VariableDefinition<'ast>, is_var_def: bool) {
732        let ast::VariableDefinition {
733            span,
734            ty,
735            visibility,
736            mutability,
737            data_location,
738            override_,
739            indexed,
740            name,
741            initializer,
742        } = var;
743
744        if self.handle_span(*span, false) {
745            return;
746        }
747
748        // NOTE(rusowsky): this is hacky but necessary to properly estimate if we figure out if we
749        // have double breaks (which should have double indentation) or not.
750        // Alternatively, we could achieve the same behavior with a new box group that supports
751        // "continuation" which would only increase indentation if its parent box broke.
752        let init_space_left = self.space_left();
753        let mut pre_init_size = self.estimate_size(ty.span);
754
755        // Non-elementary types use commasep which has its own padding.
756        self.s.ibox(0);
757        if override_.is_some() {
758            self.s.cbox(self.ind);
759        } else {
760            self.s.ibox(self.ind);
761        }
762        self.print_ty(ty);
763
764        self.print_attribute(visibility.map(|v| v.to_str()), is_var_def, &mut pre_init_size);
765        self.print_attribute(mutability.map(|m| m.to_str()), is_var_def, &mut pre_init_size);
766        self.print_attribute(data_location.map(|d| d.to_str()), is_var_def, &mut pre_init_size);
767
768        if let Some(override_) = override_ {
769            if self
770                .print_comments(override_.span.lo(), CommentConfig::skip_ws().mixed_prev_space())
771                .is_none()
772            {
773                self.print_sep(Separator::SpaceOrNbsp(is_var_def));
774            }
775            self.ibox(0);
776            self.print_override(override_);
777            pre_init_size += self.estimate_size(override_.span) + 1;
778        }
779
780        if *indexed {
781            self.print_attribute(indexed.then_some("indexed"), is_var_def, &mut pre_init_size);
782        }
783
784        if let Some(ident) = name {
785            self.print_sep(Separator::SpaceOrNbsp(is_var_def && override_.is_none()));
786            self.print_comments(
787                ident.span.lo(),
788                CommentConfig::skip_ws().mixed_no_break().mixed_post_nbsp(),
789            );
790            self.print_ident(ident);
791            pre_init_size += self.estimate_size(ident.span) + 1;
792        }
793        if let Some(init) = initializer {
794            let cache = self.var_init;
795            self.var_init = true;
796
797            pre_init_size += 2;
798            self.print_word(" =");
799            if override_.is_some() {
800                self.end();
801            }
802            self.end();
803            if pre_init_size <= init_space_left {
804                self.neverbreak();
805                pre_init_size += 1;
806                self.call_stack.add_precall(pre_init_size);
807            } else {
808                self.call_stack.add_precall(init_space_left + self.config.tab_width);
809            }
810
811            if let Some(cmnt) = self.peek_comment_before(init.span.lo())
812                && self.inline_config.is_disabled(cmnt.span)
813            {
814                self.print_sep(Separator::Nbsp);
815            }
816            if self
817                .print_comments(
818                    init.span.lo(),
819                    CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(),
820                )
821                .is_some_and(|cmnt| cmnt.is_trailing())
822            {
823                self.break_offset_if_not_bol(SIZE_INFINITY as usize, self.ind, false);
824            }
825
826            if let ast::ExprKind::Lit(lit, ..) = &init.kind
827                && lit.is_str_concatenation()
828            {
829                self.print_sep(Separator::Nbsp);
830                self.neverbreak();
831                self.s.ibox(self.ind);
832                self.print_expr(init);
833                self.end();
834            } else if is_binary_expr(&init.kind) {
835                if !self.is_bol_or_only_ind() {
836                    self.print_sep_unhandled(Separator::Space);
837                }
838                if matches!(ty.kind, ast::TypeKind::Elementary(..) | ast::TypeKind::Mapping(..)) {
839                    self.s.offset(self.ind);
840                }
841                self.print_expr(init);
842            } else {
843                let callee_doesnt_fit = if let ast::ExprKind::Call(call_expr, ..) = &init.kind {
844                    let callee_size = get_callee_head_size(call_expr);
845                    callee_size + pre_init_size > init_space_left
846                        && callee_size + self.config.tab_width < init_space_left
847                } else {
848                    false
849                };
850
851                if (pre_init_size + 1 >= init_space_left && !is_call_chain(&init.kind, false))
852                    || callee_doesnt_fit
853                {
854                    self.s.ibox(self.ind);
855                } else {
856                    self.s.ibox(0);
857                };
858
859                if has_complex_successor(&init.kind, true)
860                    && !matches!(&init.kind, ast::ExprKind::Member(..))
861                {
862                    // delegate breakpoints to `self.commasep(..)`
863                    if !self.is_bol_or_only_ind() {
864                        let init_size = self.estimate_size(init.span);
865                        if callee_doesnt_fit {
866                            self.print_sep(Separator::Space);
867                        } else if init_size + pre_init_size + 1 >= init_space_left
868                            && init_size + self.config.tab_width < init_space_left
869                            && !self.has_comment_between(init.span.lo(), init.span.hi())
870                        {
871                            self.print_sep(Separator::Space);
872                            self.s.offset(self.ind);
873                        } else {
874                            self.print_sep(Separator::Nbsp);
875                        }
876                    }
877                } else {
878                    if !self.is_bol_or_only_ind() {
879                        self.print_sep_unhandled(Separator::Space);
880                    }
881                    if matches!(ty.kind, ast::TypeKind::Elementary(..) | ast::TypeKind::Mapping(..))
882                    {
883                        self.s.offset(self.ind);
884                    }
885                }
886                self.print_expr(init);
887                self.end();
888            }
889            self.var_init = cache;
890            self.call_stack.reset_precall();
891        } else {
892            self.end();
893        }
894        self.end();
895    }
896
897    fn print_attribute(
898        &mut self,
899        attribute: Option<&'static str>,
900        is_var_def: bool,
901        size: &mut usize,
902    ) {
903        if let Some(s) = attribute {
904            self.print_sep(Separator::SpaceOrNbsp(is_var_def));
905            self.print_word(s);
906            *size += s.len() + 1;
907        }
908    }
909
910    fn print_parameter_list(
911        &mut self,
912        parameters: &'ast [ast::VariableDefinition<'ast>],
913        span: Span,
914        format: ListFormat,
915    ) {
916        if self.handle_span(span, false) {
917            return;
918        }
919
920        self.print_tuple(
921            parameters,
922            span.lo(),
923            span.hi(),
924            |fmt, var| fmt.print_var(var, false),
925            get_span!(),
926            format,
927        );
928    }
929
930    fn print_ident_or_strlit(&mut self, value: &'ast ast::IdentOrStrLit) {
931        match value {
932            ast::IdentOrStrLit::Ident(ident) => self.print_ident(ident),
933            ast::IdentOrStrLit::StrLit(strlit) => self.print_ast_str_lit(strlit),
934        }
935    }
936
937    /// Prints a raw AST string literal, which is unescaped.
938    fn print_ast_str_lit(&mut self, strlit: &'ast ast::StrLit) {
939        self.print_str_lit(ast::StrKind::Str, strlit.span.lo(), strlit.value.as_str());
940    }
941
942    fn print_ty(&mut self, ty: &'ast ast::Type<'ast>) {
943        if self.handle_span(ty.span, false) {
944            return;
945        }
946
947        match &ty.kind {
948            &ast::TypeKind::Elementary(ty) => 'b: {
949                match ty {
950                    // `address payable` is normalized to `address`.
951                    ast::ElementaryType::Address(true) => {
952                        self.word("address payable");
953                        break 'b;
954                    }
955                    // Integers are normalized to long form.
956                    ast::ElementaryType::Int(size) | ast::ElementaryType::UInt(size) => {
957                        match (self.config.int_types, size.bits_raw()) {
958                            (config::IntTypes::Short, 0 | 256)
959                            | (config::IntTypes::Preserve, 0) => {
960                                let short = match ty {
961                                    ast::ElementaryType::Int(_) => "int",
962                                    ast::ElementaryType::UInt(_) => "uint",
963                                    _ => unreachable!(),
964                                };
965                                self.word(short);
966                                break 'b;
967                            }
968                            _ => {}
969                        }
970                    }
971                    _ => {}
972                }
973                self.word(ty.to_abi_str());
974            }
975            ast::TypeKind::Array(ast::TypeArray { element, size }) => {
976                self.print_ty(element);
977                if let Some(size) = size {
978                    self.word("[");
979                    self.print_expr(size);
980                    self.word("]");
981                } else {
982                    self.word("[]");
983                }
984            }
985            ast::TypeKind::Function(ast::TypeFunction {
986                parameters,
987                visibility,
988                state_mutability,
989                returns,
990            }) => {
991                self.cbox(0);
992                self.word("function");
993                self.print_parameter_list(parameters, parameters.span, ListFormat::inline());
994
995                if let Some(v) = visibility {
996                    self.space();
997                    self.word(v.to_str());
998                }
999                if let Some(sm) = state_mutability
1000                    && !matches!(**sm, ast::StateMutability::NonPayable)
1001                {
1002                    self.space();
1003                    self.word(sm.to_str());
1004                }
1005                if let Some(ret) = returns
1006                    && !ret.is_empty()
1007                {
1008                    self.nbsp();
1009                    self.word("returns");
1010                    self.nbsp();
1011                    self.print_parameter_list(
1012                        ret,
1013                        ret.span,
1014                        ListFormat::consistent(), // .with_cmnts_break(false),
1015                    );
1016                }
1017                self.end();
1018            }
1019            ast::TypeKind::Mapping(ast::TypeMapping { key, key_name, value, value_name }) => {
1020                self.word("mapping(");
1021                self.s.cbox(0);
1022                if let Some(cmnt) = self.peek_comment_before(key.span.lo()) {
1023                    if cmnt.style.is_mixed() {
1024                        self.print_comments(
1025                            key.span.lo(),
1026                            CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(),
1027                        );
1028                        self.break_offset_if_not_bol(SIZE_INFINITY as usize, 0, false);
1029                    } else {
1030                        self.print_comments(key.span.lo(), CommentConfig::skip_ws());
1031                    }
1032                }
1033                // Fitting a mapping in one line takes, at least, 16 chars (one-char var name):
1034                // 'mapping(' + {key} + ' => ' {value} ') ' + {name} + ';'
1035                // To be more conservative, we use 18 to decide whether to force a break or not.
1036                else if 18
1037                    + self.estimate_size(key.span)
1038                    + key_name.map(|k| self.estimate_size(k.span)).unwrap_or(0)
1039                    + self.estimate_size(value.span)
1040                    + value_name.map(|v| self.estimate_size(v.span)).unwrap_or(0)
1041                    >= self.space_left()
1042                {
1043                    self.hardbreak();
1044                } else {
1045                    self.zerobreak();
1046                }
1047                self.s.cbox(0);
1048                self.print_ty(key);
1049                if let Some(ident) = key_name {
1050                    if self
1051                        .print_comments(
1052                            ident.span.lo(),
1053                            CommentConfig::skip_ws()
1054                                .mixed_no_break()
1055                                .mixed_prev_space()
1056                                .mixed_post_nbsp(),
1057                        )
1058                        .is_none()
1059                    {
1060                        self.nbsp();
1061                    }
1062                    self.print_ident(ident);
1063                }
1064                // NOTE(rusowsky): unless we add more spans to solar, using `value.span.lo()`
1065                // consumes "comment6" of which should be printed after the `=>`
1066                self.print_comments(
1067                    value.span.lo(),
1068                    CommentConfig::skip_ws()
1069                        .trailing_no_break()
1070                        .mixed_no_break()
1071                        .mixed_prev_space(),
1072                );
1073                self.space();
1074                self.s.offset(self.ind);
1075                self.word("=> ");
1076                self.s.ibox(self.ind);
1077                self.print_ty(value);
1078                if let Some(ident) = value_name {
1079                    self.neverbreak();
1080                    if self
1081                        .print_comments(
1082                            ident.span.lo(),
1083                            CommentConfig::skip_ws()
1084                                .mixed_no_break()
1085                                .mixed_prev_space()
1086                                .mixed_post_nbsp(),
1087                        )
1088                        .is_none()
1089                    {
1090                        self.nbsp();
1091                    }
1092                    self.print_ident(ident);
1093                    if self
1094                        .peek_comment_before(ty.span.hi())
1095                        .is_some_and(|cmnt| cmnt.style.is_mixed())
1096                    {
1097                        self.neverbreak();
1098                        self.print_comments(
1099                            value.span.lo(),
1100                            CommentConfig::skip_ws().mixed_no_break(),
1101                        );
1102                    }
1103                }
1104                self.end();
1105                self.end();
1106                if self
1107                    .print_comments(
1108                        ty.span.hi(),
1109                        CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(),
1110                    )
1111                    .is_some_and(|cmnt| !cmnt.is_mixed())
1112                {
1113                    self.break_offset_if_not_bol(0, -self.ind, false);
1114                } else {
1115                    self.zerobreak();
1116                    self.s.offset(-self.ind);
1117                }
1118                self.end();
1119                self.word(")");
1120            }
1121            ast::TypeKind::Custom(path) => self.print_path(path, false),
1122        }
1123    }
1124
1125    fn print_override(&mut self, override_: &'ast ast::Override<'ast>) {
1126        let ast::Override { span, paths } = override_;
1127        if self.handle_span(*span, false) {
1128            return;
1129        }
1130        self.word("override");
1131        if !paths.is_empty() {
1132            if self.config.override_spacing {
1133                self.nbsp();
1134            }
1135            self.print_tuple(
1136                paths,
1137                span.lo(),
1138                span.hi(),
1139                |this, path| this.print_path(path, false),
1140                get_span!(()),
1141                ListFormat::consistent(), // .with_cmnts_break(false),
1142            );
1143        }
1144    }
1145
1146    /* --- Expressions --- */
1147    /// Prints an expression by matching on its variant and delegating to the appropriate
1148    /// printer method, handling all Solidity expression kinds.
1149    fn print_expr(&mut self, expr: &'ast ast::Expr<'ast>) {
1150        let ast::Expr { span, ref kind } = *expr;
1151        if self.handle_span(span, false) {
1152            return;
1153        }
1154
1155        match kind {
1156            ast::ExprKind::Array(exprs) => {
1157                self.print_array(exprs, expr.span, |this, e| this.print_expr(e), get_span!())
1158            }
1159            ast::ExprKind::Assign(lhs, None, rhs) => self.print_assign_expr(lhs, rhs),
1160            ast::ExprKind::Assign(lhs, Some(op), rhs) => self.print_bin_expr(lhs, op, rhs, true),
1161            ast::ExprKind::Binary(lhs, op, rhs) => self.print_bin_expr(lhs, op, rhs, false),
1162            ast::ExprKind::Call(call_expr, call_args) => self.print_member_or_call_chain(
1163                call_expr,
1164                MemberOrCallArgs::CallArgs(
1165                    self.estimate_size(call_args.span),
1166                    self.has_comments_between_elements(call_args.span, call_args.exprs()),
1167                ),
1168                |s| {
1169                    s.print_call_args(
1170                        call_args,
1171                        ListFormat::compact()
1172                            .break_cmnts()
1173                            .break_single(true)
1174                            .without_ind(s.return_bin_expr),
1175                        get_callee_head_size(call_expr),
1176                    );
1177                },
1178            ),
1179            ast::ExprKind::CallOptions(expr, named_args) => {
1180                self.print_expr(expr);
1181                self.print_named_args(named_args, span.hi());
1182            }
1183            ast::ExprKind::Delete(expr) => {
1184                self.word("delete ");
1185                self.print_expr(expr);
1186            }
1187            ast::ExprKind::Ident(ident) => self.print_ident(ident),
1188            ast::ExprKind::Index(expr, kind) => self.print_index_expr(span, expr, kind),
1189            ast::ExprKind::Lit(lit, unit) => {
1190                self.print_lit(lit);
1191                if let Some(unit) = unit {
1192                    self.nbsp();
1193                    self.word(unit.to_str());
1194                }
1195            }
1196            ast::ExprKind::Member(member_expr, ident) => {
1197                self.print_member_or_call_chain(
1198                    member_expr,
1199                    MemberOrCallArgs::Member(self.estimate_size(ident.span)),
1200                    |s| {
1201                        s.print_trailing_comment(member_expr.span.hi(), Some(ident.span.lo()));
1202                        if !matches!(
1203                            member_expr.kind,
1204                            ast::ExprKind::Ident(_) | ast::ExprKind::Type(_)
1205                        ) {
1206                            s.zerobreak();
1207                        }
1208                        s.word(".");
1209                        s.print_ident(ident);
1210                    },
1211                );
1212            }
1213            ast::ExprKind::New(ty) => {
1214                self.word("new ");
1215                self.print_ty(ty);
1216            }
1217            ast::ExprKind::Payable(args) => {
1218                self.word("payable");
1219                self.print_call_args(args, ListFormat::compact().break_cmnts(), 7);
1220            }
1221            ast::ExprKind::Ternary(cond, then, els) => self.print_ternary_expr(cond, then, els),
1222            ast::ExprKind::Tuple(exprs) => self.print_tuple(
1223                exprs,
1224                span.lo(),
1225                span.hi(),
1226                |this, expr| match expr.as_ref() {
1227                    SpannedOption::Some(expr) => this.print_expr(expr),
1228                    SpannedOption::None(span) => {
1229                        this.print_comments(span.hi(), CommentConfig::skip_ws().no_breaks());
1230                    }
1231                },
1232                |expr| match expr.as_ref() {
1233                    SpannedOption::Some(expr) => expr.span,
1234                    // Manually handled by printing the comment when `None`
1235                    SpannedOption::None(..) => Span::DUMMY,
1236                },
1237                ListFormat::compact().break_single(is_binary_expr(&expr.kind)),
1238            ),
1239            ast::ExprKind::TypeCall(ty) => {
1240                self.word("type");
1241                self.print_tuple(
1242                    std::slice::from_ref(ty),
1243                    span.lo(),
1244                    span.hi(),
1245                    Self::print_ty,
1246                    get_span!(),
1247                    ListFormat::consistent(),
1248                );
1249            }
1250            ast::ExprKind::Type(ty) => self.print_ty(ty),
1251            ast::ExprKind::Unary(un_op, expr) => {
1252                let prefix = un_op.kind.is_prefix();
1253                let op = un_op.kind.to_str();
1254                if prefix {
1255                    self.word(op);
1256                }
1257                self.print_expr(expr);
1258                if !prefix {
1259                    debug_assert!(un_op.kind.is_postfix());
1260                    self.word(op);
1261                }
1262            }
1263        }
1264        self.cursor.advance_to(span.hi(), true);
1265    }
1266
1267    /// Prints a simple assignment expression of the form `lhs = rhs`.
1268    fn print_assign_expr(&mut self, lhs: &'ast ast::Expr<'ast>, rhs: &'ast ast::Expr<'ast>) {
1269        let prev_var_init = self.var_init;
1270        self.var_init = true;
1271
1272        // Estimate layout constraints
1273        let space_left = self.space_left();
1274        let lhs_size = self.estimate_size(lhs.span);
1275        let rhs_size = self.estimate_size(rhs.span);
1276
1277        let total_size = lhs_size + rhs_size + 4; // 'lhs' + ' = ' + 'rhs' + ';'
1278        let overflows = total_size >= space_left;
1279        let fits_alone = rhs_size + self.config.tab_width < space_left;
1280
1281        self.call_stack.add_precall(lhs_size);
1282
1283        let is_simple_rhs = matches!(rhs.kind, ast::ExprKind::Lit(..) | ast::ExprKind::Ident(..));
1284
1285        if (overflows && fits_alone) || is_simple_rhs {
1286            self.s.ibox(self.ind)
1287        } else {
1288            self.s.ibox(0)
1289        }
1290
1291        // Print LHS and '='
1292        self.print_expr(lhs);
1293        self.word(" =");
1294
1295        // Handle RHS printing strategy
1296        match &rhs.kind {
1297            ast::ExprKind::Lit(lit, ..) if lit.is_str_concatenation() => {
1298                self.print_sep(Separator::Nbsp);
1299                self.neverbreak();
1300                self.s.ibox(self.ind);
1301                self.print_expr(rhs);
1302                self.end();
1303            }
1304            _ if overflows && (fits_alone || is_simple_rhs) => {
1305                self.print_sep(Separator::Space);
1306                self.print_expr(rhs);
1307            }
1308            _ => {
1309                self.print_sep(Separator::Nbsp);
1310                self.neverbreak();
1311                self.print_expr(rhs);
1312            }
1313        }
1314
1315        self.end();
1316        self.var_init = prev_var_init;
1317        self.call_stack.reset_precall();
1318    }
1319
1320    /// Prints a binary operator expression. Handles operator chains and formatting.
1321    fn print_bin_expr(
1322        &mut self,
1323        lhs: &'ast ast::Expr<'ast>,
1324        bin_op: &ast::BinOp,
1325        rhs: &'ast ast::Expr<'ast>,
1326        is_assign: bool,
1327    ) {
1328        let prev_chain = self.binary_expr;
1329        let is_chain = prev_chain.is_some_and(|prev| prev == bin_op.kind.group());
1330
1331        // Opening box if starting a new operator chain.
1332        if !is_chain {
1333            self.binary_expr = Some(bin_op.kind.group());
1334
1335            let indent = if (is_assign && has_complex_successor(&rhs.kind, true))
1336                || self.call_stack.is_nested()
1337                    && is_call_chain(&lhs.kind, false)
1338                    && self.estimate_size(lhs.span) >= self.space_left()
1339            {
1340                0
1341            } else {
1342                self.ind
1343            };
1344            self.s.ibox(indent);
1345        }
1346
1347        // Print LHS.
1348        self.print_expr(lhs);
1349
1350        // Handle assignment (`+=`, etc.) vs binary ops (`+`, `*`, etc.).
1351        let no_trailing_comment = !self.print_trailing_comment(lhs.span.hi(), Some(rhs.span.lo()));
1352        if is_assign {
1353            if no_trailing_comment {
1354                self.nbsp();
1355            }
1356            self.word(bin_op.kind.to_str());
1357            self.word("= ");
1358        } else {
1359            if no_trailing_comment
1360                && self
1361                    .print_comments(
1362                        bin_op.span.lo(),
1363                        CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(),
1364                    )
1365                    .is_none_or(|cmnt| cmnt.is_mixed())
1366            {
1367                if !self.config.pow_no_space || !matches!(bin_op.kind, ast::BinOpKind::Pow) {
1368                    self.space_if_not_bol();
1369                } else if !self.is_bol_or_only_ind() && !self.last_token_is_break() {
1370                    self.zerobreak();
1371                }
1372            }
1373
1374            self.word(bin_op.kind.to_str());
1375
1376            if !self.config.pow_no_space || !matches!(bin_op.kind, ast::BinOpKind::Pow) {
1377                self.nbsp();
1378            }
1379        }
1380
1381        // Print RHS with optional ibox if mixed comment precedes.
1382        let rhs_has_mixed_comment =
1383            self.peek_comment_before(rhs.span.lo()).is_some_and(|cmnt| cmnt.style.is_mixed());
1384        if rhs_has_mixed_comment {
1385            self.ibox(0);
1386            self.print_expr(rhs);
1387            self.end();
1388        } else {
1389            self.print_expr(rhs);
1390        }
1391
1392        // End current box if this was top-level in the chain.
1393        if !is_chain {
1394            self.binary_expr = prev_chain;
1395            self.end();
1396        }
1397    }
1398
1399    /// Prints an indexing expression.
1400    fn print_index_expr(
1401        &mut self,
1402        span: Span,
1403        expr: &'ast ast::Expr<'ast>,
1404        kind: &'ast ast::IndexKind<'ast>,
1405    ) {
1406        self.print_expr(expr);
1407        self.word("[");
1408        self.s.cbox(self.ind);
1409
1410        let mut skip_break = false;
1411
1412        match kind {
1413            ast::IndexKind::Index(Some(inner_expr)) => {
1414                self.zerobreak();
1415                self.print_expr(inner_expr);
1416            }
1417            ast::IndexKind::Index(None) => {}
1418            ast::IndexKind::Range(start, end) => {
1419                if let Some(start_expr) = start {
1420                    if self
1421                        .print_comments(start_expr.span.lo(), CommentConfig::skip_ws())
1422                        .is_none_or(|s| s.is_mixed())
1423                    {
1424                        self.zerobreak();
1425                    }
1426                    self.print_expr(start_expr);
1427                } else {
1428                    self.zerobreak();
1429                }
1430
1431                self.word(":");
1432
1433                if let Some(end_expr) = end {
1434                    self.s.ibox(self.ind);
1435                    if start.is_some() {
1436                        self.zerobreak();
1437                    }
1438                    self.print_comments(
1439                        end_expr.span.lo(),
1440                        CommentConfig::skip_ws()
1441                            .mixed_prev_space()
1442                            .mixed_no_break()
1443                            .mixed_post_nbsp(),
1444                    );
1445                    self.print_expr(end_expr);
1446                }
1447
1448                // Trailing comment handling.
1449                let mut is_trailing = false;
1450                if let Some(style) = self.print_comments(
1451                    span.hi(),
1452                    CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(),
1453                ) {
1454                    skip_break = true;
1455                    is_trailing = style.is_trailing();
1456                }
1457
1458                // Adjust indentation and line breaks.
1459                match (skip_break, end.is_some()) {
1460                    (true, true) => {
1461                        self.break_offset_if_not_bol(0, -2 * self.ind, false);
1462                        self.end();
1463                        if !is_trailing {
1464                            self.break_offset_if_not_bol(0, -self.ind, false);
1465                        }
1466                    }
1467                    (true, false) => {
1468                        self.break_offset_if_not_bol(0, -self.ind, false);
1469                    }
1470                    (false, true) => {
1471                        self.end();
1472                    }
1473                    _ => {}
1474                }
1475            }
1476        }
1477
1478        if !skip_break {
1479            self.zerobreak();
1480            self.s.offset(-self.ind);
1481        }
1482
1483        self.end();
1484        self.word("]");
1485    }
1486
1487    /// Prints a ternary expression of the form `cond ? then : else`.
1488    fn print_ternary_expr(
1489        &mut self,
1490        cond: &'ast ast::Expr<'ast>,
1491        then: &'ast ast::Expr<'ast>,
1492        els: &'ast ast::Expr<'ast>,
1493    ) {
1494        self.s.cbox(self.ind);
1495        self.s.ibox(0);
1496
1497        let mut print_ternary_expr =
1498            |span_lo, prefix: Option<&'static str>, expr: &'ast ast::Expr<'ast>| {
1499                match prefix {
1500                    Some(prefix) => {
1501                        if self.peek_comment_before(span_lo).is_some() {
1502                            self.space();
1503                        }
1504                        self.print_comments(span_lo, CommentConfig::skip_ws());
1505                        self.end();
1506                        if !self.is_bol_or_only_ind() {
1507                            self.space();
1508                        }
1509                        self.s.ibox(0);
1510                        self.word(prefix);
1511                    }
1512                    None => {
1513                        self.print_comments(expr.span.lo(), CommentConfig::skip_ws());
1514                    }
1515                };
1516                self.print_expr(expr);
1517            };
1518
1519        // conditional expression
1520        print_ternary_expr(then.span.lo(), None, cond);
1521        // then expression
1522        print_ternary_expr(then.span.lo(), Some("? "), then);
1523        // else expression
1524        print_ternary_expr(els.span.lo(), Some(": "), els);
1525
1526        self.end();
1527        self.neverbreak();
1528        self.s.offset(-self.ind);
1529        self.end();
1530    }
1531
1532    // If `add_parens_if_empty` is true, then add parentheses `()` even if there are no arguments.
1533    fn print_modifier_call(
1534        &mut self,
1535        modifier: &'ast ast::Modifier<'ast>,
1536        add_parens_if_empty: bool,
1537    ) {
1538        let ast::Modifier { name, arguments } = modifier;
1539        self.print_path(name, false);
1540        if !arguments.is_empty() || add_parens_if_empty {
1541            self.print_call_args(
1542                arguments,
1543                ListFormat::compact().break_cmnts(),
1544                name.to_string().len(),
1545            );
1546        }
1547    }
1548
1549    fn print_member_or_call_chain<F>(
1550        &mut self,
1551        child_expr: &'ast ast::Expr<'ast>,
1552        member_or_args: MemberOrCallArgs,
1553        print_suffix: F,
1554    ) where
1555        F: FnOnce(&mut Self),
1556    {
1557        fn member_depth(depth: usize, expr: &ast::Expr<'_>) -> usize {
1558            if let ast::ExprKind::Member(child, ..) = &expr.kind {
1559                member_depth(depth + 1, child)
1560            } else {
1561                depth
1562            }
1563        }
1564
1565        let parent_is_chain = self.call_stack.last().copied().is_some_and(|call| call.is_chained());
1566        if !parent_is_chain {
1567            // Estimate sizes of callee and optional member
1568            let callee_size = get_callee_head_size(child_expr) + member_or_args.member_size();
1569            let expr_size = self.estimate_size(child_expr.span);
1570
1571            // Start a new chain if needed
1572            if is_call_chain(&child_expr.kind, false) {
1573                self.call_stack.push(CallContext::chained(callee_size));
1574            }
1575
1576            let callee_fits_line = self.space_left() > callee_size + 1;
1577            let total_fits_line = self.space_left() > expr_size + member_or_args.size() + 2;
1578            let no_mixed_comment =
1579                self.peek_comment_before(child_expr.span.hi()).is_none_or(|c| c.style.is_mixed());
1580
1581            if !is_call_chain(&child_expr.kind, true)
1582                && no_mixed_comment
1583                && callee_fits_line
1584                && (member_depth(0, child_expr) < 2
1585                    // calls with cmnts between the args always break
1586                    || (total_fits_line && !member_or_args.has_comments()))
1587            {
1588                self.s.ibox(0);
1589            } else {
1590                self.s.ibox(self.ind);
1591            }
1592        }
1593
1594        // Recursively print the child/prefix expression.
1595        self.print_expr(child_expr);
1596
1597        // Call the closure to print the suffix for the current link, with the calculated position.
1598        print_suffix(self);
1599
1600        // If a chain was started, clean up the state and end the box.
1601        if !parent_is_chain {
1602            if is_call_chain(&child_expr.kind, false) {
1603                self.call_stack.pop();
1604            }
1605            self.end();
1606        }
1607    }
1608
1609    fn print_call_args(
1610        &mut self,
1611        args: &'ast ast::CallArgs<'ast>,
1612        format: ListFormat,
1613        callee_size: usize,
1614    ) {
1615        let ast::CallArgs { span, ref kind } = *args;
1616        if self.handle_span(span, true) {
1617            return;
1618        }
1619
1620        self.call_stack.push(CallContext::nested(callee_size));
1621
1622        // Clear the binary expression cache before the call.
1623        let cache = self.binary_expr.take();
1624
1625        match kind {
1626            ast::CallArgsKind::Unnamed(exprs) => {
1627                self.print_tuple(
1628                    exprs,
1629                    span.lo(),
1630                    span.hi(),
1631                    |this, e| this.print_expr(e),
1632                    get_span!(),
1633                    format,
1634                );
1635            }
1636            ast::CallArgsKind::Named(named_args) => {
1637                self.print_inside_parens(|state| state.print_named_args(named_args, span.hi()));
1638            }
1639        }
1640
1641        // Restore the cache to continue with the current chain.
1642        self.binary_expr = cache;
1643        self.call_stack.pop();
1644    }
1645
1646    fn print_named_args(&mut self, args: &'ast [ast::NamedArg<'ast>], pos_hi: BytePos) {
1647        let cache = self.named_call_expr;
1648        if !cache {
1649            self.named_call_expr = true;
1650        };
1651
1652        let list_format = match (self.config.bracket_spacing, self.config.call_compact_args) {
1653            (false, true) => ListFormat::compact(),
1654            (false, false) => ListFormat::consistent(),
1655            (true, true) => ListFormat::compact().with_space(),
1656            (true, false) => ListFormat::consistent().with_space(),
1657        };
1658
1659        self.word("{");
1660        // Use the start position of the first argument's name for comment processing.
1661        let list_lo = args.first().map_or(pos_hi, |arg| arg.name.span.lo());
1662
1663        self.commasep(
1664            args,
1665            list_lo,
1666            pos_hi,
1667            // Closure to print a single named argument (`name: value`)
1668            |s, arg| {
1669                s.cbox(0);
1670                s.print_ident(&arg.name);
1671                s.word(":");
1672                if s.same_source_line(arg.name.span.hi(), arg.value.span.hi())
1673                    || !s.print_trailing_comment(arg.name.span.hi(), None)
1674                {
1675                    s.nbsp();
1676                }
1677                s.print_comments(
1678                    arg.value.span.lo(),
1679                    CommentConfig::skip_ws().mixed_no_break().mixed_post_nbsp(),
1680                );
1681                s.print_expr(arg.value);
1682                s.end();
1683            },
1684            |arg| arg.name.span.until(arg.value.span),
1685            list_format.break_cmnts().break_single(true).without_ind(self.call_stack.is_chain()),
1686        );
1687        self.word("}");
1688
1689        if !cache {
1690            self.named_call_expr = false;
1691        }
1692    }
1693
1694    /* --- Statements --- */
1695    /// Prints the given statement in the source code, handling formatting, inline documentation,
1696    /// trailing comments and layout logic for various statement kinds.
1697    fn print_stmt(&mut self, stmt: &'ast ast::Stmt<'ast>) {
1698        let ast::Stmt { ref docs, span, ref kind } = *stmt;
1699        self.print_docs(docs);
1700
1701        // Handle disabled statements.
1702        if self.handle_span(span, false) {
1703            self.print_trailing_comment_no_break(stmt.span.hi(), None);
1704            return;
1705        }
1706
1707        // return statements can't have a preceding comment in the same line.
1708        let force_break = matches!(kind, ast::StmtKind::Return(..))
1709            && self.peek_comment_before(span.lo()).is_some_and(|cmnt| cmnt.style.is_mixed());
1710
1711        match kind {
1712            ast::StmtKind::Assembly(ast::StmtAssembly { dialect, flags, block }) => {
1713                self.print_assembly_stmt(span, dialect, flags, block)
1714            }
1715            ast::StmtKind::DeclSingle(var) => self.print_var(var, true),
1716            ast::StmtKind::DeclMulti(vars, init_expr) => {
1717                self.print_multi_decl_stmt(span, vars, init_expr)
1718            }
1719            ast::StmtKind::Block(stmts) => self.print_block(stmts, span),
1720            ast::StmtKind::Break => self.word("break"),
1721            ast::StmtKind::Continue => self.word("continue"),
1722            ast::StmtKind::DoWhile(stmt, cond) => {
1723                self.word("do ");
1724                self.print_stmt_as_block(stmt, cond.span.lo(), false);
1725                self.nbsp();
1726                self.print_if_cond("while", cond, cond.span.hi());
1727            }
1728            ast::StmtKind::Emit(path, args) => self.print_emit_or_revert("emit", path, args),
1729            ast::StmtKind::Expr(expr) => self.print_expr(expr),
1730            ast::StmtKind::For { init, cond, next, body } => {
1731                self.print_for_stmt(span, init, cond, next, body)
1732            }
1733            ast::StmtKind::If(cond, then, els_opt) => self.print_if_stmt(span, cond, then, els_opt),
1734            ast::StmtKind::Return(expr) => self.print_return_stmt(force_break, expr),
1735            ast::StmtKind::Revert(path, args) => self.print_emit_or_revert("revert", path, args),
1736            ast::StmtKind::Try(ast::StmtTry { expr, clauses }) => {
1737                self.print_try_stmt(expr, clauses)
1738            }
1739            ast::StmtKind::UncheckedBlock(block) => {
1740                self.word("unchecked ");
1741                self.print_block(block, stmt.span);
1742            }
1743            ast::StmtKind::While(cond, stmt) => {
1744                // Check if blocks should be inlined and update cache if necessary
1745                let inline = self.is_single_line_block(cond, stmt, None);
1746                if !inline.is_cached && self.single_line_stmt.is_none() {
1747                    self.single_line_stmt = Some(inline.outcome);
1748                }
1749
1750                // Print while cond and its statement
1751                self.print_if_cond("while", cond, stmt.span.lo());
1752                self.nbsp();
1753                self.print_stmt_as_block(stmt, stmt.span.hi(), inline.outcome);
1754
1755                // Clear cache if necessary
1756                if !inline.is_cached && self.single_line_stmt.is_some() {
1757                    self.single_line_stmt = None;
1758                }
1759            }
1760            ast::StmtKind::Placeholder => self.word("_"),
1761        }
1762        if stmt_needs_semi(kind) {
1763            self.neverbreak(); // semicolon shouldn't account for linebreaks
1764            self.word(";");
1765            self.cursor.advance_to(span.hi(), true);
1766        }
1767        // print comments without breaks, as those are handled by the caller.
1768        self.print_comments(
1769            stmt.span.hi(),
1770            CommentConfig::default().trailing_no_break().mixed_no_break().mixed_prev_space(),
1771        );
1772        self.print_trailing_comment_no_break(stmt.span.hi(), None);
1773    }
1774
1775    /// Prints an `assembly` statement, including optional dialect and flags,
1776    /// followed by its Yul block.
1777    fn print_assembly_stmt(
1778        &mut self,
1779        span: Span,
1780        dialect: &'ast Option<ast::StrLit>,
1781        flags: &'ast [ast::StrLit],
1782        block: &'ast ast::yul::Block<'ast>,
1783    ) {
1784        _ = self.handle_span(self.cursor.span(span.lo()), false);
1785        if !self.handle_span(span.until(block.span), false) {
1786            self.cursor.advance_to(span.lo(), true);
1787            self.print_word("assembly ");
1788            if let Some(dialect) = dialect {
1789                self.print_ast_str_lit(dialect);
1790                self.print_sep(Separator::Nbsp);
1791            }
1792            if !flags.is_empty() {
1793                self.print_tuple(
1794                    flags,
1795                    span.lo(),
1796                    block.span.lo(),
1797                    Self::print_ast_str_lit,
1798                    get_span!(),
1799                    ListFormat::consistent(),
1800                );
1801                self.print_sep(Separator::Nbsp);
1802            }
1803        }
1804        self.print_yul_block(block, block.span, false);
1805    }
1806
1807    /// Prints a multiple-variable declaration with a single initializer expression,
1808    /// formatted as a tuple-style assignment (e.g., `(a, b) = foo();`).
1809    fn print_multi_decl_stmt(
1810        &mut self,
1811        span: Span,
1812        vars: &'ast BoxSlice<'ast, SpannedOption<ast::VariableDefinition<'ast>>>,
1813        init_expr: &'ast ast::Expr<'ast>,
1814    ) {
1815        let space_left = self.space_left();
1816
1817        self.s.ibox(self.ind);
1818        self.s.ibox(-self.ind);
1819        self.print_tuple(
1820            vars,
1821            span.lo(),
1822            init_expr.span.lo(),
1823            |this, var| match var {
1824                SpannedOption::Some(var) => this.print_var(var, true),
1825                SpannedOption::None(span) => {
1826                    this.print_comments(span.hi(), CommentConfig::skip_ws().no_breaks());
1827                }
1828            },
1829            |var| match var {
1830                SpannedOption::Some(var) => var.span,
1831                // Manually handled by printing the comment when `None`
1832                SpannedOption::None(..) => Span::DUMMY,
1833            },
1834            ListFormat::consistent(),
1835        );
1836        self.end();
1837        self.word(" =");
1838
1839        // '(' + var + ', ' + var + ')' + ' ='
1840        let vars_size = vars.iter().fold(0, |acc, var| {
1841            acc + var.as_ref().unspan().map_or(0, |v| self.estimate_size(v.span)) + 2
1842        }) + 2;
1843        self.call_stack.add_precall(vars_size);
1844
1845        if self.estimate_size(init_expr.span) + self.config.tab_width
1846            <= std::cmp::max(space_left, self.space_left())
1847        {
1848            self.print_sep(Separator::Space);
1849            self.ibox(0);
1850        } else {
1851            self.print_sep(Separator::Nbsp);
1852            self.neverbreak();
1853            self.s.ibox(-self.ind);
1854        }
1855        self.print_expr(init_expr);
1856        self.end();
1857        self.end();
1858    }
1859
1860    /// Prints a `for` loop statement, including its initializer, condition,
1861    /// increment expression, and loop body, with formatting and spacing.
1862    fn print_for_stmt(
1863        &mut self,
1864        span: Span,
1865        init: &'ast Option<&mut ast::Stmt<'ast>>,
1866        cond: &'ast Option<&mut ast::Expr<'ast>>,
1867        next: &'ast Option<&mut ast::Expr<'ast>>,
1868        body: &'ast ast::Stmt<'ast>,
1869    ) {
1870        self.cbox(0);
1871        self.s.ibox(self.ind);
1872        self.print_word("for (");
1873        self.zerobreak();
1874
1875        // Print init.
1876        self.s.cbox(0);
1877        match init {
1878            Some(init_stmt) => self.print_stmt(init_stmt),
1879            None => self.print_word(";"),
1880        }
1881
1882        // Print condition.
1883        match cond {
1884            Some(cond_expr) => {
1885                self.print_sep(Separator::Space);
1886                self.print_expr(cond_expr);
1887            }
1888            None => self.zerobreak(),
1889        }
1890        self.print_word(";");
1891
1892        // Print next clause.
1893        match next {
1894            Some(next_expr) => {
1895                self.space();
1896                self.print_expr(next_expr);
1897            }
1898            None => self.zerobreak(),
1899        }
1900
1901        // Close head.
1902        self.break_offset_if_not_bol(0, -self.ind, false);
1903        self.end();
1904        self.print_word(") ");
1905        self.neverbreak();
1906        self.end();
1907
1908        // Print comments and body.
1909        self.print_comments(body.span.lo(), CommentConfig::skip_ws());
1910        self.print_stmt_as_block(body, span.hi(), false);
1911        self.end();
1912    }
1913
1914    /// Prints an `if` statement, including its condition, `then` block, and any chained
1915    /// `else` or `else if` branches, handling inline formatting decisions and comments.
1916    fn print_if_stmt(
1917        &mut self,
1918        span: Span,
1919        cond: &'ast ast::Expr<'ast>,
1920        then: &'ast ast::Stmt<'ast>,
1921        els_opt: &'ast Option<&mut ast::Stmt<'ast>>,
1922    ) {
1923        // Check if blocks should be inlined and update cache if necessary
1924        let inline = self.is_single_line_block(cond, then, els_opt.as_ref());
1925        let set_inline_cache = !inline.is_cached && self.single_line_stmt.is_none();
1926        if set_inline_cache {
1927            self.single_line_stmt = Some(inline.outcome);
1928        }
1929
1930        self.cbox(0);
1931        self.ibox(0);
1932        // Print if stmt
1933        self.print_if_no_else(cond, then, inline.outcome);
1934
1935        // Print else (if) stmts, if any
1936        let mut current_else = els_opt.as_deref();
1937        while let Some(els) = current_else {
1938            if self.ends_with('}') {
1939                // If there are comments with line breaks, don't add spaces to mixed comments
1940                if self.has_comment_before_with(els.span.lo(), |cmnt| !cmnt.style.is_mixed()) {
1941                    // If last comment is miced, ensure line break
1942                    if self
1943                        .print_comments(els.span.lo(), CommentConfig::skip_ws().mixed_no_break())
1944                        .is_some_and(|cmnt| cmnt.is_mixed())
1945                    {
1946                        self.hardbreak();
1947                    }
1948                }
1949                // Otherwise, ensure a non-breaking space is added
1950                else if self
1951                    .print_comments(
1952                        els.span.lo(),
1953                        CommentConfig::skip_ws()
1954                            .mixed_no_break()
1955                            .mixed_prev_space()
1956                            .mixed_post_nbsp(),
1957                    )
1958                    .is_none()
1959                {
1960                    self.nbsp();
1961                }
1962            } else {
1963                self.hardbreak_if_not_bol();
1964                if self
1965                    .print_comments(els.span.lo(), CommentConfig::skip_ws())
1966                    .is_some_and(|cmnt| cmnt.is_mixed())
1967                {
1968                    self.hardbreak();
1969                };
1970            }
1971
1972            self.ibox(0);
1973            self.print_word("else ");
1974            match &els.kind {
1975                ast::StmtKind::If(cond, then, next_else) => {
1976                    self.print_if_no_else(cond, then, inline.outcome);
1977                    current_else = next_else.as_deref();
1978                }
1979                _ => {
1980                    self.print_stmt_as_block(els, span.hi(), inline.outcome);
1981                    self.end(); // end ibox for final else
1982                    break;
1983                }
1984            }
1985        }
1986        self.end();
1987
1988        // Clear inline cache if we set it earlier.
1989        if set_inline_cache {
1990            self.single_line_stmt = None;
1991        }
1992    }
1993
1994    /// Prints a `return` statement, optionally including a return expression.
1995    /// Handles spacing, line breaking, and formatting.
1996    fn print_return_stmt(&mut self, force_break: bool, expr: &'ast Option<&mut ast::Expr<'ast>>) {
1997        if force_break {
1998            self.hardbreak_if_not_bol();
1999        }
2000
2001        let space_left = self.space_left();
2002        let expr_size = expr.as_ref().map_or(0, |expr| self.estimate_size(expr.span));
2003
2004        // `return ' + expr + ';'
2005        let overflows = space_left < 8 + expr_size;
2006        let fits_alone = space_left > expr_size;
2007
2008        if let Some(expr) = expr {
2009            let is_simple = matches!(expr.kind, ast::ExprKind::Lit(..) | ast::ExprKind::Ident(..));
2010            let allow_break = overflows && fits_alone;
2011
2012            self.return_bin_expr = matches!(expr.kind, ast::ExprKind::Binary(..));
2013            self.s.ibox(if is_simple || allow_break { self.ind } else { 0 });
2014
2015            self.print_word("return");
2016
2017            match self.print_comments(
2018                expr.span.lo(),
2019                CommentConfig::skip_ws().mixed_no_break().mixed_prev_space().mixed_post_nbsp(),
2020            ) {
2021                Some(cmnt) if cmnt.is_trailing() && !is_simple => self.s.offset(self.ind),
2022                None => self.print_sep(Separator::SpaceOrNbsp(allow_break)),
2023                _ => {}
2024            }
2025
2026            self.print_expr(expr);
2027            self.end();
2028            self.return_bin_expr = false;
2029        } else {
2030            self.print_word("return");
2031        }
2032    }
2033
2034    /// Prints a `try` statement along with its associated `catch` clauses,
2035    /// following Solidity's `try ... returns (...) { ... } catch (...) { ... }` syntax.
2036    fn print_try_stmt(
2037        &mut self,
2038        expr: &'ast ast::Expr<'ast>,
2039        clauses: &'ast [ast::TryCatchClause<'ast>],
2040    ) {
2041        self.cbox(0);
2042        if let Some((first, other)) = clauses.split_first() {
2043            // Print the 'try' clause
2044            let ast::TryCatchClause { args, block, span: try_span, .. } = first;
2045            self.ibox(0);
2046            self.print_word("try ");
2047            self.print_comments(expr.span.lo(), CommentConfig::skip_ws());
2048            self.print_expr(expr);
2049
2050            // Print comments.
2051            self.print_comments(
2052                args.first().map(|p| p.span.lo()).unwrap_or_else(|| expr.span.lo()),
2053                CommentConfig::skip_ws(),
2054            );
2055            if !self.is_beginning_of_line() {
2056                self.nbsp();
2057            }
2058
2059            if !args.is_empty() {
2060                self.print_word("returns ");
2061                self.print_parameter_list(
2062                    args,
2063                    args.span.with_hi(block.span.lo()),
2064                    ListFormat::compact(),
2065                );
2066                self.nbsp();
2067            }
2068            if block.is_empty() {
2069                self.print_block(block, *try_span);
2070                self.end();
2071            } else {
2072                self.print_word("{");
2073                self.end();
2074                self.neverbreak();
2075                self.print_trailing_comment_no_break(try_span.lo(), None);
2076                self.print_block_without_braces(block, try_span.hi(), Some(self.ind));
2077                if self.cursor.enabled || self.cursor.pos < try_span.hi() {
2078                    self.print_word("}");
2079                    self.cursor.advance_to(try_span.hi(), true);
2080                }
2081            }
2082
2083            let mut skip_ind = false;
2084            if self.print_trailing_comment(try_span.hi(), other.first().map(|c| c.span.lo())) {
2085                // if a trailing comment is printed at the very end, we have to manually
2086                // adjust the offset to avoid having a double break.
2087                self.break_offset_if_not_bol(0, self.ind, false);
2088                skip_ind = true;
2089            };
2090
2091            let mut prev_block_multiline = self.is_multiline_block(block, false);
2092
2093            // Handle 'catch' clauses
2094            for (pos, ast::TryCatchClause { name, args, block, span: catch_span }) in
2095                other.iter().delimited()
2096            {
2097                let current_block_multiline = self.is_multiline_block(block, false);
2098                if !pos.is_first || !skip_ind {
2099                    if prev_block_multiline && (current_block_multiline || pos.is_last) {
2100                        self.nbsp();
2101                    } else {
2102                        self.space();
2103                        if !current_block_multiline {
2104                            self.s.offset(self.ind);
2105                        }
2106                    }
2107                }
2108                self.s.ibox(self.ind);
2109                self.print_comments(
2110                    catch_span.lo(),
2111                    CommentConfig::skip_ws().mixed_no_break().mixed_post_nbsp(),
2112                );
2113
2114                self.print_word("catch ");
2115                if !args.is_empty() {
2116                    self.print_comments(
2117                        args[0].span.lo(),
2118                        CommentConfig::skip_ws().mixed_no_break().mixed_post_nbsp(),
2119                    );
2120                    if let Some(name) = name {
2121                        self.print_ident(name);
2122                    }
2123                    self.print_parameter_list(
2124                        args,
2125                        args.span.with_hi(block.span.lo()),
2126                        ListFormat::inline(),
2127                    );
2128                    self.nbsp();
2129                }
2130                self.print_word("{");
2131                self.end();
2132                self.print_trailing_comment_no_break(catch_span.lo(), None);
2133                self.print_block_without_braces(block, catch_span.hi(), Some(self.ind));
2134                if self.cursor.enabled || self.cursor.pos < try_span.hi() {
2135                    self.print_word("}");
2136                    self.cursor.advance_to(catch_span.hi(), true);
2137                }
2138
2139                prev_block_multiline = current_block_multiline;
2140            }
2141        }
2142        self.end();
2143    }
2144
2145    fn print_if_no_else(
2146        &mut self,
2147        cond: &'ast ast::Expr<'ast>,
2148        then: &'ast ast::Stmt<'ast>,
2149        inline: bool,
2150    ) {
2151        if !self.handle_span(cond.span.until(then.span), true) {
2152            self.print_if_cond("if", cond, then.span.lo());
2153            // if empty block without comments, ensure braces are inlined
2154            if let ast::StmtKind::Block(block) = &then.kind
2155                && block.is_empty()
2156                && self.peek_comment_before(then.span.hi()).is_none()
2157            {
2158                self.neverbreak();
2159                self.print_sep(Separator::Nbsp);
2160            } else {
2161                self.print_sep(Separator::Space);
2162            }
2163        }
2164        self.end();
2165        self.print_stmt_as_block(then, then.span.hi(), inline);
2166        self.cursor.advance_to(then.span.hi(), true);
2167    }
2168
2169    fn print_if_cond(&mut self, kw: &'static str, cond: &'ast ast::Expr<'ast>, pos_hi: BytePos) {
2170        self.print_word(kw);
2171        self.print_sep_unhandled(Separator::Nbsp);
2172        self.print_tuple(
2173            std::slice::from_ref(cond),
2174            cond.span.lo(),
2175            pos_hi,
2176            Self::print_expr,
2177            get_span!(),
2178            ListFormat::compact().break_cmnts().break_single(is_binary_expr(&cond.kind)),
2179        );
2180    }
2181
2182    fn print_emit_or_revert(
2183        &mut self,
2184        kw: &'static str,
2185        path: &'ast ast::PathSlice,
2186        args: &'ast ast::CallArgs<'ast>,
2187    ) {
2188        self.word(kw);
2189        if self
2190            .print_comments(
2191                path.span().lo(),
2192                CommentConfig::skip_ws().mixed_no_break().mixed_prev_space().mixed_post_nbsp(),
2193            )
2194            .is_none()
2195        {
2196            self.nbsp();
2197        };
2198        self.s.cbox(0);
2199        self.print_path(path, false);
2200        self.print_call_args(
2201            args,
2202            if args.len() == 1 {
2203                ListFormat::compact().break_cmnts()
2204            } else {
2205                ListFormat::compact().break_cmnts().no_delimiters()
2206            },
2207            path.to_string().len(),
2208        );
2209        self.end();
2210    }
2211
2212    fn print_block(&mut self, block: &'ast [ast::Stmt<'ast>], span: Span) {
2213        self.print_block_inner(
2214            block,
2215            BlockFormat::Regular,
2216            Self::print_stmt,
2217            |b| b.span,
2218            span.hi(),
2219        );
2220    }
2221
2222    fn print_block_without_braces(
2223        &mut self,
2224        block: &'ast [ast::Stmt<'ast>],
2225        pos_hi: BytePos,
2226        offset: Option<isize>,
2227    ) {
2228        self.print_block_inner(
2229            block,
2230            BlockFormat::NoBraces(offset),
2231            Self::print_stmt,
2232            |b| b.span,
2233            pos_hi,
2234        );
2235    }
2236
2237    // Body of a if/loop.
2238    fn print_stmt_as_block(&mut self, stmt: &'ast ast::Stmt<'ast>, pos_hi: BytePos, inline: bool) {
2239        if self.handle_span(stmt.span, false) {
2240            return;
2241        }
2242
2243        let stmts = if let ast::StmtKind::Block(stmts) = &stmt.kind {
2244            stmts
2245        } else {
2246            std::slice::from_ref(stmt)
2247        };
2248
2249        if inline && !stmts.is_empty() {
2250            self.neverbreak();
2251            self.print_block_without_braces(stmts, pos_hi, None);
2252        } else {
2253            // Reset cache for nested (child) stmts within this (parent) block.
2254            let inline_parent = self.single_line_stmt.take();
2255
2256            self.print_word("{");
2257            self.print_block_without_braces(stmts, pos_hi, Some(self.ind));
2258            self.print_word("}");
2259
2260            // Restore cache for the rest of stmts within the same height.
2261            self.single_line_stmt = inline_parent;
2262        }
2263    }
2264
2265    /// Determines if an `if/else` block should be inlined.
2266    /// Also returns if the value was cached, so that it can be cleaned afterwards.
2267    ///
2268    /// # Returns
2269    ///
2270    /// A tuple `(should_inline, was_cached)`. The second boolean is `true` if the
2271    /// decision was retrieved from the cache or is a final decision based on config,
2272    /// preventing the caller from clearing a cache value that was never set.
2273    fn is_single_line_block(
2274        &mut self,
2275        cond: &'ast ast::Expr<'ast>,
2276        then: &'ast ast::Stmt<'ast>,
2277        els_opt: Option<&'ast &'ast mut ast::Stmt<'ast>>,
2278    ) -> Decision {
2279        // If a decision is already cached from a parent, use it directly.
2280        if let Some(cached_decision) = self.single_line_stmt {
2281            return Decision { outcome: cached_decision, is_cached: true };
2282        }
2283
2284        // Empty statements are always printed as blocks.
2285        if std::slice::from_ref(then).is_empty() {
2286            return Decision { outcome: false, is_cached: false };
2287        }
2288
2289        // If possible, take an early decision based on the block style configuration.
2290        match self.config.single_line_statement_blocks {
2291            config::SingleLineBlockStyle::Preserve => {
2292                if self.is_stmt_in_new_line(cond, then) || self.is_multiline_block_stmt(then, true)
2293                {
2294                    return Decision { outcome: false, is_cached: false };
2295                }
2296            }
2297            config::SingleLineBlockStyle::Single => {
2298                if self.is_multiline_block_stmt(then, true) {
2299                    return Decision { outcome: false, is_cached: false };
2300                }
2301            }
2302            config::SingleLineBlockStyle::Multi => {
2303                return Decision { outcome: false, is_cached: false };
2304            }
2305        };
2306
2307        // If no decision was made, estimate the length to be formatted.
2308        // NOTE: conservative check -> worst-case scenario is formatting as multi-line block.
2309        if !self.can_stmts_be_inlined(cond, then, els_opt) {
2310            return Decision { outcome: false, is_cached: false };
2311        }
2312
2313        // If the parent would fit, check all of its children.
2314        if let Some(stmt) = els_opt {
2315            if let ast::StmtKind::If(child_cond, child_then, child_els_opt) = &stmt.kind {
2316                return self.is_single_line_block(child_cond, child_then, child_els_opt.as_ref());
2317            } else if self.is_multiline_block_stmt(stmt, true) {
2318                return Decision { outcome: false, is_cached: false };
2319            }
2320        }
2321
2322        // If all children can also fit, allow single-line block.
2323        Decision { outcome: true, is_cached: false }
2324    }
2325
2326    fn is_inline_stmt(&self, stmt: &'ast ast::Stmt<'ast>, cond_len: usize) -> bool {
2327        if let ast::StmtKind::If(cond, then, els_opt) = &stmt.kind {
2328            let if_span = cond.span.to(then.span);
2329            if self.sm.is_multiline(if_span)
2330                && matches!(
2331                    self.config.single_line_statement_blocks,
2332                    config::SingleLineBlockStyle::Preserve
2333                )
2334            {
2335                return false;
2336            }
2337            if cond_len + self.estimate_size(if_span) >= self.space_left() {
2338                return false;
2339            }
2340            if let Some(els) = els_opt
2341                && !self.is_inline_stmt(els, 6)
2342            {
2343                return false;
2344            }
2345        } else {
2346            if matches!(
2347                self.config.single_line_statement_blocks,
2348                config::SingleLineBlockStyle::Preserve
2349            ) && self.sm.is_multiline(stmt.span)
2350            {
2351                return false;
2352            }
2353            if cond_len + self.estimate_size(stmt.span) >= self.space_left() {
2354                return false;
2355            }
2356        }
2357        true
2358    }
2359
2360    /// Checks if a statement was explicitly written in a new line.
2361    fn is_stmt_in_new_line(
2362        &self,
2363        cond: &'ast ast::Expr<'ast>,
2364        then: &'ast ast::Stmt<'ast>,
2365    ) -> bool {
2366        let span_between = cond.span.between(then.span);
2367        if let Ok(snip) = self.sm.span_to_snippet(span_between) {
2368            // Check for newlines after the closing parenthesis of the `if (...)`.
2369            if let Some((_, after_paren)) = snip.split_once(')') {
2370                return after_paren.lines().count() > 1;
2371            }
2372        }
2373        false
2374    }
2375
2376    /// Checks if a block statement `{ ... }` contains more than one line of actual code.
2377    fn is_multiline_block_stmt(
2378        &self,
2379        stmt: &'ast ast::Stmt<'ast>,
2380        empty_as_multiline: bool,
2381    ) -> bool {
2382        if let ast::StmtKind::Block(block) = &stmt.kind {
2383            return self.is_multiline_block(block, empty_as_multiline);
2384        }
2385        false
2386    }
2387
2388    /// Checks if a block statement `{ ... }` contains more than one line of actual code.
2389    fn is_multiline_block(&self, block: &'ast ast::Block<'ast>, empty_as_multiline: bool) -> bool {
2390        if block.stmts.is_empty() {
2391            return empty_as_multiline;
2392        }
2393        if self.sm.is_multiline(block.span)
2394            && let Ok(snip) = self.sm.span_to_snippet(block.span)
2395        {
2396            let code_lines = snip.lines().filter(|line| {
2397                let trimmed = line.trim();
2398                // Ignore empty lines and lines with only '{' or '}'
2399                if empty_as_multiline {
2400                    !trimmed.is_empty() && trimmed != "{" && trimmed != "}"
2401                } else {
2402                    !trimmed.is_empty()
2403                }
2404            });
2405            return code_lines.count() > 1;
2406        }
2407        false
2408    }
2409
2410    /// Performs a size estimation to see if the if/else can fit on one line.
2411    fn can_stmts_be_inlined(
2412        &mut self,
2413        cond: &'ast ast::Expr<'ast>,
2414        then: &'ast ast::Stmt<'ast>,
2415        els_opt: Option<&'ast &'ast mut ast::Stmt<'ast>>,
2416    ) -> bool {
2417        let cond_len = self.estimate_size(cond.span);
2418
2419        // If the condition fits in one line, 6 chars: 'if (' + {cond} + ') ' + {then}
2420        // Otherwise chars: ') ' + {then}
2421        let then_margin = if 6 + cond_len < self.space_left() { 6 + cond_len } else { 2 };
2422
2423        if !self.is_inline_stmt(then, then_margin) {
2424            return false;
2425        }
2426
2427        // Always 6 chars for the else: 'else '
2428        els_opt.is_none_or(|els| self.is_inline_stmt(els, 6))
2429    }
2430
2431    fn can_header_be_inlined(&mut self, header: &ast::FunctionHeader<'_>) -> bool {
2432        // ' ' + visibility
2433        let visibility = header.visibility.map_or(0, |v| self.estimate_size(v.span) + 1);
2434        // ' ' + state mutability
2435        let mutability = header.state_mutability.map_or(0, |sm| self.estimate_size(sm.span) + 1);
2436        // ' ' + modifier + (' ' + modifier)
2437        let modifiers =
2438            header.modifiers.iter().fold(0, |len, m| len + self.estimate_size(m.span())) + 1;
2439        // ' ' + override
2440        let override_ = header.override_.as_ref().map_or(0, |o| self.estimate_size(o.span) + 1);
2441        // ' returns(' + var + (', ' + var) + ')'
2442        let returns = header.returns.as_ref().map_or(0, |ret| {
2443            ret.vars
2444                .iter()
2445                .fold(0, |len, p| if len != 0 { len + 2 } else { 8 } + self.estimate_size(p.span))
2446        });
2447
2448        self.estimate_header_params_size(header)
2449            + visibility
2450            + mutability
2451            + modifiers
2452            + override_
2453            + returns
2454            <= self.space_left()
2455    }
2456
2457    fn estimate_header_params_size(&mut self, header: &ast::FunctionHeader<'_>) -> usize {
2458        // '(' + param + (', ' + param) + ')'
2459        let params = header
2460            .parameters
2461            .vars
2462            .iter()
2463            .fold(0, |len, p| if len != 0 { len + 2 } else { 2 } + self.estimate_size(p.span));
2464
2465        // 'function ' + name + ' ' + params
2466        9 + header.name.map_or(0, |name| self.estimate_size(name.span) + 1) + params
2467    }
2468
2469    fn can_header_params_be_inlined(&mut self, header: &ast::FunctionHeader<'_>) -> bool {
2470        self.estimate_header_params_size(header) <= self.space_left()
2471    }
2472
2473    fn has_comments_between_elements<I>(&self, limits: Span, elements: I) -> bool
2474    where
2475        I: IntoIterator<Item = &'ast ast::Expr<'ast>>,
2476    {
2477        let mut last_span_end = limits.lo();
2478        for expr in elements {
2479            if self.has_comment_between(last_span_end, expr.span.lo()) {
2480                return true;
2481            }
2482            last_span_end = expr.span.hi();
2483        }
2484
2485        if self.has_comment_between(last_span_end, limits.hi()) {
2486            return true;
2487        }
2488
2489        false
2490    }
2491}
2492
2493// -- HELPERS (language-specific) ----------------------------------------------
2494
2495#[derive(Debug)]
2496enum MemberOrCallArgs {
2497    Member(usize),
2498    CallArgs(usize, bool),
2499}
2500
2501impl MemberOrCallArgs {
2502    fn size(&self) -> usize {
2503        match self {
2504            Self::CallArgs(size, ..) | Self::Member(size) => *size,
2505        }
2506    }
2507
2508    fn member_size(&self) -> usize {
2509        match self {
2510            Self::CallArgs(..) => 0,
2511            Self::Member(size) => *size,
2512        }
2513    }
2514
2515    fn has_comments(&self) -> bool {
2516        matches!(self, Self::CallArgs(.., true))
2517    }
2518}
2519
2520#[derive(Debug, Clone)]
2521#[expect(dead_code)]
2522enum AttributeKind<'ast> {
2523    Visibility(ast::Visibility),
2524    StateMutability(ast::StateMutability),
2525    Virtual,
2526    Override(&'ast ast::Override<'ast>),
2527    Modifier(&'ast ast::Modifier<'ast>),
2528}
2529
2530type AttributeCommentMap = HashMap<BytePos, (Vec<Comment>, Vec<Comment>, Vec<Comment>)>;
2531
2532#[derive(Debug, Clone)]
2533struct AttributeInfo<'ast> {
2534    kind: AttributeKind<'ast>,
2535    span: Span,
2536}
2537
2538/// Helper struct to map attributes to their associated comments in function headers.
2539struct AttributeCommentMapper<'ast> {
2540    limit_pos: BytePos,
2541    comments: Vec<Comment>,
2542    attributes: Vec<AttributeInfo<'ast>>,
2543}
2544
2545impl<'ast> AttributeCommentMapper<'ast> {
2546    fn new(returns: Option<&'ast ast::ParameterList<'ast>>, body_pos: BytePos) -> Self {
2547        Self {
2548            comments: Vec::new(),
2549            attributes: Vec::new(),
2550            limit_pos: returns.as_ref().map_or(body_pos, |ret| ret.span.lo()),
2551        }
2552    }
2553
2554    #[allow(clippy::type_complexity)]
2555    fn build(
2556        mut self,
2557        state: &mut State<'_, 'ast>,
2558        header: &'ast ast::FunctionHeader<'ast>,
2559    ) -> (AttributeCommentMap, Vec<AttributeInfo<'ast>>, BytePos) {
2560        let first_attr = self.collect_attributes(header);
2561        self.cache_comments(state);
2562        (self.map(), self.attributes, first_attr)
2563    }
2564
2565    fn map(&mut self) -> AttributeCommentMap {
2566        let mut map = HashMap::new();
2567        for a in 0..self.attributes.len() {
2568            let is_last = a == self.attributes.len() - 1;
2569            let (mut before, mut inner, mut after) = (Vec::new(), Vec::new(), Vec::new());
2570
2571            let before_limit = self.attributes[a].span.lo();
2572            let inner_limit = self.attributes[a].span.hi();
2573            let after_limit =
2574                if !is_last { self.attributes[a + 1].span.lo() } else { self.limit_pos };
2575
2576            let mut c = 0;
2577            while c < self.comments.len() {
2578                if self.comments[c].pos() <= before_limit {
2579                    before.push(self.comments.remove(c));
2580                } else if self.comments[c].pos() <= inner_limit {
2581                    inner.push(self.comments.remove(c));
2582                } else if (after.is_empty() || is_last) && self.comments[c].pos() <= after_limit {
2583                    after.push(self.comments.remove(c));
2584                } else {
2585                    c += 1;
2586                }
2587            }
2588            map.insert(before_limit, (before, inner, after));
2589        }
2590        map
2591    }
2592
2593    fn collect_attributes(&mut self, header: &'ast ast::FunctionHeader<'ast>) -> BytePos {
2594        let mut first_pos = BytePos(u32::MAX);
2595        if let Some(v) = header.visibility {
2596            if v.span.lo() < first_pos {
2597                first_pos = v.span.lo()
2598            }
2599            self.attributes
2600                .push(AttributeInfo { kind: AttributeKind::Visibility(*v), span: v.span });
2601        }
2602        if let Some(sm) = header.state_mutability {
2603            if sm.span.lo() < first_pos {
2604                first_pos = sm.span.lo()
2605            }
2606            self.attributes
2607                .push(AttributeInfo { kind: AttributeKind::StateMutability(*sm), span: sm.span });
2608        }
2609        if let Some(span) = header.virtual_ {
2610            if span.lo() < first_pos {
2611                first_pos = span.lo()
2612            }
2613            self.attributes.push(AttributeInfo { kind: AttributeKind::Virtual, span });
2614        }
2615        if let Some(ref o) = header.override_ {
2616            if o.span.lo() < first_pos {
2617                first_pos = o.span.lo()
2618            }
2619            self.attributes.push(AttributeInfo { kind: AttributeKind::Override(o), span: o.span });
2620        }
2621        for m in header.modifiers.iter() {
2622            if m.span().lo() < first_pos {
2623                first_pos = m.span().lo()
2624            }
2625            self.attributes
2626                .push(AttributeInfo { kind: AttributeKind::Modifier(m), span: m.span() });
2627        }
2628        self.attributes.sort_by_key(|attr| attr.span.lo());
2629        first_pos
2630    }
2631
2632    fn cache_comments(&mut self, state: &mut State<'_, 'ast>) {
2633        let mut pending = None;
2634        for cmnt in state.comments.iter() {
2635            if cmnt.pos() >= self.limit_pos {
2636                break;
2637            }
2638            match pending {
2639                Some(ref p) => pending = Some(p + 1),
2640                None => pending = Some(0),
2641            }
2642        }
2643        while let Some(p) = pending {
2644            if p == 0 {
2645                pending = None;
2646            } else {
2647                pending = Some(p - 1);
2648            }
2649            let cmnt = state.next_comment().unwrap();
2650            if cmnt.style.is_blank() {
2651                continue;
2652            }
2653            self.comments.push(cmnt);
2654        }
2655    }
2656}
2657
2658fn stmt_needs_semi(stmt: &ast::StmtKind<'_>) -> bool {
2659    match stmt {
2660        ast::StmtKind::Assembly { .. }
2661        | ast::StmtKind::Block { .. }
2662        | ast::StmtKind::For { .. }
2663        | ast::StmtKind::If { .. }
2664        | ast::StmtKind::Try { .. }
2665        | ast::StmtKind::UncheckedBlock { .. }
2666        | ast::StmtKind::While { .. } => false,
2667
2668        ast::StmtKind::DeclSingle { .. }
2669        | ast::StmtKind::DeclMulti { .. }
2670        | ast::StmtKind::Break { .. }
2671        | ast::StmtKind::Continue { .. }
2672        | ast::StmtKind::DoWhile { .. }
2673        | ast::StmtKind::Emit { .. }
2674        | ast::StmtKind::Expr { .. }
2675        | ast::StmtKind::Return { .. }
2676        | ast::StmtKind::Revert { .. }
2677        | ast::StmtKind::Placeholder { .. } => true,
2678    }
2679}
2680
2681/// Returns `true` if the item needs an isolated line break.
2682fn item_needs_iso(item: &ast::ItemKind<'_>) -> bool {
2683    match item {
2684        ast::ItemKind::Pragma(..)
2685        | ast::ItemKind::Import(..)
2686        | ast::ItemKind::Using(..)
2687        | ast::ItemKind::Variable(..)
2688        | ast::ItemKind::Udvt(..)
2689        | ast::ItemKind::Enum(..)
2690        | ast::ItemKind::Error(..)
2691        | ast::ItemKind::Event(..) => false,
2692
2693        ast::ItemKind::Contract(..) => true,
2694
2695        ast::ItemKind::Struct(strukt) => !strukt.fields.is_empty(),
2696        ast::ItemKind::Function(func) => {
2697            func.body.as_ref().is_some_and(|b| !b.is_empty())
2698                && !matches!(func.kind, ast::FunctionKind::Modifier)
2699        }
2700    }
2701}
2702
2703fn is_binary_expr(expr_kind: &ast::ExprKind<'_>) -> bool {
2704    matches!(expr_kind, ast::ExprKind::Binary(..))
2705}
2706
2707fn has_complex_successor(expr_kind: &ast::ExprKind<'_>, left: bool) -> bool {
2708    match expr_kind {
2709        ast::ExprKind::Binary(lhs, _, rhs) => {
2710            if left {
2711                has_complex_successor(&lhs.kind, left)
2712            } else {
2713                has_complex_successor(&rhs.kind, left)
2714            }
2715        }
2716        ast::ExprKind::Unary(_, expr) => has_complex_successor(&expr.kind, left),
2717        ast::ExprKind::Lit(..) | ast::ExprKind::Ident(_) => false,
2718        _ => true,
2719    }
2720}
2721
2722fn is_call(expr_kind: &ast::ExprKind<'_>) -> bool {
2723    matches!(expr_kind, ast::ExprKind::Call(..))
2724}
2725
2726fn is_call_chain(expr_kind: &ast::ExprKind<'_>, must_have_child: bool) -> bool {
2727    if let ast::ExprKind::Member(child, ..) = expr_kind {
2728        is_call_chain(&child.kind, false)
2729    } else {
2730        !must_have_child && is_call(expr_kind)
2731    }
2732}
2733
2734#[derive(Debug)]
2735struct Decision {
2736    outcome: bool,
2737    is_cached: bool,
2738}
2739
2740#[derive(Clone, Copy, PartialEq, Eq)]
2741pub(crate) enum BinOpGroup {
2742    Arithmetic,
2743    Bitwise,
2744    Comparison,
2745    Logical,
2746}
2747
2748trait BinOpExt {
2749    fn group(&self) -> BinOpGroup;
2750}
2751
2752impl BinOpExt for ast::BinOpKind {
2753    fn group(&self) -> BinOpGroup {
2754        match self {
2755            Self::Or | Self::And => BinOpGroup::Logical,
2756            Self::Eq | Self::Ne | Self::Lt | Self::Le | Self::Gt | Self::Ge => {
2757                BinOpGroup::Comparison
2758            }
2759            Self::BitOr | Self::BitXor | Self::BitAnd | Self::Shl | Self::Shr | Self::Sar => {
2760                BinOpGroup::Bitwise
2761            }
2762            Self::Add | Self::Sub | Self::Mul | Self::Div | Self::Rem | Self::Pow => {
2763                BinOpGroup::Arithmetic
2764            }
2765        }
2766    }
2767}
2768
2769/// Calculates the size the callee's "head," excluding its arguments.
2770///
2771/// # Examples
2772///
2773/// - `myFunction(..)`: 8 (length of `myFunction`)
2774/// - `uint256(..)`: 7 (length of `uint256`)
2775/// - `abi.encode(..)`: 10 (length of `abi.encode`)
2776/// - `foo(..).bar(..)`: 3 (length of `foo`)
2777pub(super) fn get_callee_head_size(callee: &ast::Expr<'_>) -> usize {
2778    match &callee.kind {
2779        ast::ExprKind::Ident(id) => id.as_str().len(),
2780        ast::ExprKind::Type(ast::Type { kind: ast::TypeKind::Elementary(ty), .. }) => {
2781            ty.to_abi_str().len()
2782        }
2783        ast::ExprKind::Member(base, member_ident) => {
2784            match &base.kind {
2785                ast::ExprKind::Ident(..) | ast::ExprKind::Type(..) => {
2786                    get_callee_head_size(base) + 1 + member_ident.as_str().len()
2787                }
2788
2789                // Chainned calls are not traversed, and instead just the member identifier is used
2790                ast::ExprKind::Member(child, ..)
2791                    if !matches!(&child.kind, ast::ExprKind::Call(..)) =>
2792                {
2793                    get_callee_head_size(base) + 1 + member_ident.as_str().len()
2794                }
2795                _ => member_ident.as_str().len(),
2796            }
2797        }
2798
2799        // If the callee is not an identifier or member access, it has no "head"
2800        _ => 0,
2801    }
2802}