Skip to main content

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