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