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