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