Skip to main content

forge_fmt/state/
sol.rs

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