1#![allow(clippy::too_many_arguments)]
2use crate::{
3 FormatterConfig, InlineConfig,
4 pp::{self, BreakToken, SIZE_INFINITY, Token},
5 state::sol::BinOpGroup,
6};
7use foundry_common::{
8 comments::{Comment, CommentStyle, Comments, estimate_line_width, line_with_tabs},
9 iter::IterDelimited,
10};
11use foundry_config::fmt::{DocCommentStyle, IndentStyle, IntTypes};
12use solar::parse::{
13 ast::{self, Span},
14 interface::{BytePos, source_map::SourceFile},
15 token,
16};
17use std::{borrow::Cow, ops::Deref, sync::Arc};
18
19mod common;
20mod sol;
21mod yul;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub(super) enum CallContextKind {
26 Chained,
28
29 Nested,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub(super) struct CallContext {
36 pub(super) kind: CallContextKind,
38
39 pub(super) size: usize,
41
42 pub(super) has_indent: bool,
44}
45
46impl CallContext {
47 pub(super) const fn nested(size: usize) -> Self {
48 Self { kind: CallContextKind::Nested, size, has_indent: false }
49 }
50
51 pub(super) const fn chained(size: usize, has_indent: bool) -> Self {
52 Self { kind: CallContextKind::Chained, size, has_indent }
53 }
54
55 pub(super) const fn is_nested(&self) -> bool {
56 matches!(self.kind, CallContextKind::Nested)
57 }
58
59 pub(super) const fn is_chained(&self) -> bool {
60 matches!(self.kind, CallContextKind::Chained)
61 }
62}
63
64#[derive(Debug, Default)]
65pub(super) struct CallStack {
66 stack: Vec<CallContext>,
67}
68
69impl Deref for CallStack {
70 type Target = [CallContext];
71 fn deref(&self) -> &Self::Target {
72 &self.stack
73 }
74}
75
76impl CallStack {
77 pub(crate) fn push(&mut self, call: CallContext) {
78 self.stack.push(call);
79 }
80
81 pub(crate) fn pop(&mut self) -> Option<CallContext> {
82 self.stack.pop()
83 }
84
85 pub(crate) fn is_nested(&self) -> bool {
86 self.last().is_some_and(|call| call.is_nested())
87 }
88
89 pub(crate) const fn has_indented_parent_chain(&self) -> bool {
92 matches!(
93 self.stack.as_slice(),
94 [.., parent, last] if last.is_nested() && parent.is_chained() && parent.has_indent
95 )
96 }
97}
98
99#[derive(Clone, Copy)]
100struct ChainedNamedCall {
101 callee: Span,
102 keep_inline: bool,
103}
104
105pub(super) struct State<'sess, 'ast> {
106 pub(super) s: pp::Printer,
108 ind: isize,
109
110 file: &'sess SourceFile,
111 pub(super) comments: Comments,
112 config: Arc<FormatterConfig>,
113 inline_config: InlineConfig<()>,
114 cursor: SourcePos,
115
116 has_crlf: bool,
119 contract: Option<&'ast ast::ItemContract<'ast>>,
121 block_depth: usize,
123 call_stack: CallStack,
125
126 single_line_stmt: Option<bool>,
128 binary_expr: Option<BinOpGroup>,
130 return_bin_expr: bool,
132 call_with_opts_and_args: bool,
134 chained_named_call: Option<ChainedNamedCall>,
136 skip_index_break: bool,
138 emit_or_revert: bool,
140 var_init: bool,
142}
143
144impl std::ops::Deref for State<'_, '_> {
145 type Target = pp::Printer;
146
147 #[inline(always)]
148 fn deref(&self) -> &Self::Target {
149 &self.s
150 }
151}
152
153impl std::ops::DerefMut for State<'_, '_> {
154 #[inline(always)]
155 fn deref_mut(&mut self) -> &mut Self::Target {
156 &mut self.s
157 }
158}
159
160struct SourcePos {
161 pos: BytePos,
162 enabled: bool,
163}
164
165impl SourcePos {
166 pub(super) fn advance(&mut self, bytes: u32) {
169 self.pos += BytePos(bytes);
170 }
171
172 pub(super) fn advance_to(&mut self, pos: BytePos, enabled: bool) {
173 if self.enabled || pos >= self.pos {
176 self.pos = std::cmp::max(pos, self.pos);
177 self.enabled = enabled;
178 }
179 }
180
181 pub(super) fn next_line(&mut self, is_at_crlf: bool) {
182 self.pos += if is_at_crlf { 2 } else { 1 };
183 }
184
185 pub(super) fn span(&self, to: BytePos) -> Span {
186 Span::new(self.pos, to)
187 }
188}
189
190pub(super) enum Separator {
191 Nbsp,
192 Space,
193 Hardbreak,
194 SpaceOrNbsp(bool),
195}
196
197impl Separator {
198 fn print(&self, p: &mut pp::Printer) {
199 match self {
200 Self::Nbsp => p.nbsp(),
201 Self::Space => p.space(),
202 Self::Hardbreak => p.hardbreak(),
203 Self::SpaceOrNbsp(breaks) => p.space_or_nbsp(*breaks),
204 }
205 }
206}
207
208impl<'sess> State<'sess, '_> {
210 pub(super) fn new(
211 file: &'sess SourceFile,
212 config: Arc<FormatterConfig>,
213 inline_config: InlineConfig<()>,
214 comments: Comments,
215 ) -> Self {
216 Self {
217 s: pp::Printer::new(
218 config.line_length,
219 matches!(config.style, IndentStyle::Tab).then(|| config.tab_width),
220 ),
221 ind: config.tab_width as isize,
222 file,
223 comments,
224 config,
225 inline_config,
226 cursor: SourcePos { pos: file.start_pos, enabled: true },
227 has_crlf: false,
228 contract: None,
229 single_line_stmt: None,
230 call_with_opts_and_args: false,
231 chained_named_call: None,
232 skip_index_break: false,
233 binary_expr: None,
234 return_bin_expr: false,
235 emit_or_revert: false,
236 var_init: false,
237 block_depth: 0,
238 call_stack: CallStack::default(),
239 }
240 }
241
242 fn check_crlf(&mut self, span: Span) {
248 if let Some(snip) = self.snippet(span)
249 && snip.contains('\r')
250 {
251 self.has_crlf = true;
252 }
253 }
254
255 fn is_at_crlf(&self) -> bool {
258 self.has_crlf && self.char_at(self.cursor.pos) == Some('\r')
259 }
260
261 fn cursor_next_line(&mut self) {
267 if self.cursor.enabled || matches!(self.char_at(self.cursor.pos), Some('\n' | '\r')) {
268 self.cursor.next_line(self.is_at_crlf());
269 }
270 }
271
272 fn space_left(&self) -> usize {
274 std::cmp::min(self.s.space_left(), self.max_space_left(0))
275 }
276
277 fn max_space_left(&self, prefix_len: usize) -> usize {
280 self.config
281 .line_length
282 .saturating_sub(self.block_depth * self.config.tab_width + prefix_len)
283 }
284
285 fn break_offset_if_not_bol(&mut self, n: usize, off: isize, search: bool) {
286 if search {
289 self.find_and_replace_last_token_still_buffered(
292 pp::Printer::hardbreak_tok_offset(off),
293 |token| token.is_hardbreak(),
294 );
295 return;
296 }
297
298 if !self.is_beginning_of_line() {
300 self.break_offset(n, off)
301 } else if off != 0
302 && let Some(last_token) = self.last_token_still_buffered()
303 && last_token.is_hardbreak()
304 {
305 self.replace_last_token_still_buffered(pp::Printer::hardbreak_tok_offset(off));
308 }
309 }
310
311 fn braces_break(&mut self) {
312 if self.config.bracket_spacing {
313 self.space();
314 } else {
315 self.zerobreak();
316 }
317 }
318}
319
320impl<'sess> State<'sess, '_> {
322 fn snippet(&self, span: Span) -> Option<&'sess str> {
323 let start = span.lo().0.checked_sub(self.file.start_pos.0)? as usize;
324 let end = span.hi().0.checked_sub(self.file.start_pos.0)? as usize;
325 self.file.src.get(start..end)
326 }
327
328 fn source_line(&self, pos: BytePos) -> usize {
329 self.file.lookup_line(self.file.relative_position(pos)).map_or(0, |line| line + 1)
330 }
331
332 fn char_at(&self, pos: BytePos) -> Option<char> {
333 let offset = pos.0.checked_sub(self.file.start_pos.0)? as usize;
334 self.file.src.get(offset..)?.chars().next()
335 }
336
337 fn find_opening_brace(&self, span: Span) -> Option<BytePos> {
339 self.find_uncommented_char(span, '{')
340 }
341
342 fn find_uncommented_char(&self, span: Span, needle: char) -> Option<BytePos> {
344 let snip = self.snippet(span)?;
345 let mut idx = 0;
346 while idx < snip.len() {
347 let rest = &snip[idx..];
348 if rest.starts_with(needle) {
349 return Some(span.lo() + idx as u32);
350 }
351 idx += if let Some(line) = rest.strip_prefix("//") {
352 2 + line.find('\n').unwrap_or(line.len())
353 } else if let Some(block) = rest.strip_prefix("/*") {
354 2 + block.find("*/").map_or(block.len(), |end| end + 2)
355 } else {
356 rest.chars().next().map_or(1, char::len_utf8)
357 };
358 }
359 None
360 }
361
362 fn print_span(&mut self, span: Span) {
363 match self.snippet(span) {
364 Some(s) => self.s.word(if matches!(self.config.style, IndentStyle::Tab) {
365 snippet_with_tabs(s.to_owned(), self.config.tab_width)
366 } else {
367 s.to_owned()
368 }),
369 None => panic!("failed to print {span:?}: invalid source span"),
370 }
371 while let Some(cmnt) = self.peek_comment() {
373 if cmnt.pos() >= span.hi() {
374 break;
375 }
376 let _ = self.next_comment().unwrap();
377 }
378 self.cursor.advance_to(span.hi(), false);
380 }
381
382 #[must_use]
384 fn handle_span(&mut self, span: Span, skip_prev_cmnts: bool) -> bool {
385 if !skip_prev_cmnts {
386 self.print_comments(span.lo(), CommentConfig::default());
387 }
388 self.print_span_if_disabled(span)
389 }
390
391 #[inline]
393 #[must_use]
394 fn print_span_if_disabled(&mut self, span: Span) -> bool {
395 let cursor_span = self.cursor.span(span.hi());
396 if self.inline_config.is_disabled(cursor_span) {
397 self.print_span_cold(cursor_span);
398 return true;
399 }
400 if self.inline_config.is_disabled(span) {
401 self.print_span_cold(span);
402 return true;
403 }
404 false
405 }
406
407 #[cold]
408 fn print_span_cold(&mut self, span: Span) {
409 self.print_span(span);
410 }
411
412 fn print_tokens(&mut self, tokens: &[token::Token]) {
413 let span = Span::join_first_last(tokens.iter().map(|t| t.span));
415 self.print_span(span);
416 }
417
418 fn print_word(&mut self, w: impl Into<Cow<'static, str>>) {
419 let cow = w.into();
420 self.cursor.advance(cow.len() as u32);
421 self.word(cow);
422 }
423
424 fn print_sep(&mut self, sep: Separator) {
425 if self.handle_span(
426 self.cursor.span(self.cursor.pos + if self.is_at_crlf() { 2 } else { 1 }),
427 true,
428 ) {
429 return;
430 }
431
432 self.print_sep_unhandled(sep);
433 }
434
435 fn print_sep_unhandled(&mut self, sep: Separator) {
436 sep.print(&mut self.s);
437 self.cursor_next_line();
438 }
439
440 fn print_ident(&mut self, ident: &ast::Ident) {
441 if self.handle_span(ident.span, true) {
442 return;
443 }
444
445 self.print_comments(ident.span.lo(), CommentConfig::skip_ws());
446 self.word(ident.to_string());
447 }
448
449 fn print_inside_parens<F>(&mut self, f: F)
450 where
451 F: FnOnce(&mut Self),
452 {
453 self.print_word("(");
454 f(self);
455 self.print_word(")");
456 }
457
458 fn brace_spacing_delta(
460 &self,
461 line: &str,
462 in_block_comment: &mut bool,
463 quote: &mut Option<u8>,
464 ) -> isize {
465 let bytes = line.as_bytes();
466 let mut delta = 0;
467 let mut i = 0;
468 while i < bytes.len() {
469 if let Some(delimiter) = *quote {
470 match bytes[i] {
471 b'\\' => i = (i + 2).min(bytes.len()),
472 c if c == delimiter => {
473 *quote = None;
474 i += 1;
475 }
476 _ => i += 1,
477 }
478 continue;
479 }
480 if *in_block_comment {
481 if bytes.get(i..i + 2) == Some(b"*/") {
482 *in_block_comment = false;
483 i += 2;
484 } else {
485 i += 1;
486 }
487 continue;
488 }
489 match bytes[i] {
490 b'/' if bytes.get(i + 1) == Some(&b'/') => break,
491 b'/' if bytes.get(i + 1) == Some(&b'*') => {
492 *in_block_comment = true;
493 i += 2;
494 }
495 delimiter @ (b'\'' | b'"') => {
496 *quote = Some(delimiter);
497 i += 1;
498 }
499 b'{' | b'}' => {
500 let inner = if bytes[i] == b'{' {
501 bytes.get(i + 1)
502 } else {
503 i.checked_sub(1).and_then(|j| bytes.get(j))
504 };
505 if let Some(&inner) = inner
506 && !matches!(inner, b'{' | b'}')
507 {
508 delta += match (matches!(inner, b' ' | b'\t'), self.config.bracket_spacing)
509 {
510 (true, false) => -1,
511 (false, true) => 1,
512 _ => 0,
513 };
514 }
515 i += 1;
516 }
517 _ => i += 1,
518 }
519 }
520 delta
521 }
522
523 fn int_type_delta(&self, ty: &ast::Type<'_>) -> isize {
526 match &ty.kind {
527 ast::TypeKind::Elementary(
528 ast::ElementaryType::Int(size) | ast::ElementaryType::UInt(size),
529 ) => match (self.config.int_types, size.bits_raw()) {
530 (IntTypes::Long, 0) => 3,
531 (IntTypes::Short, 256) => -3,
532 _ => 0,
533 },
534 ast::TypeKind::Array(array) => self.int_type_delta(&array.element),
535 ast::TypeKind::Mapping(mapping) => {
536 self.int_type_delta(&mapping.key) + self.int_type_delta(&mapping.value)
537 }
538 ast::TypeKind::Function(function) => {
539 let params: isize = function
540 .parameters
541 .vars
542 .iter()
543 .map(|param| self.int_type_delta(¶m.ty))
544 .sum();
545 let returns: isize = function.returns.as_ref().map_or(0, |returns| {
546 returns.vars.iter().map(|ret| self.int_type_delta(&ret.ty)).sum()
547 });
548 params + returns
549 }
550 _ => 0,
551 }
552 }
553
554 fn estimate_type_size(&self, ty: &ast::Type<'_>) -> usize {
555 self.estimate_size(ty.span).saturating_add_signed(self.int_type_delta(ty))
556 }
557
558 fn estimate_size(&self, span: Span) -> usize {
559 self.estimate_size_inner(span, false)
560 }
561
562 fn estimate_call_args_size(&self, span: Span) -> usize {
565 self.estimate_size_inner(span, true)
566 }
567
568 fn estimate_assignment_size(&self, span: Span) -> usize {
571 self.estimate_size_inner(span, !self.has_comment_between(span.lo(), span.hi()))
572 }
573
574 fn estimate_size_inner(&self, span: Span, normalize_delimiters: bool) -> usize {
575 if let Some(snip) = self.snippet(span) {
576 let (mut size, mut first, mut prev_needs_space) = (0, true, false);
577 let (mut in_block_comment, mut quote) = (false, None);
578
579 for line in snip.lines() {
580 let line = line.trim();
581
582 if prev_needs_space {
583 size += 1;
584 } else if !first && let Some(char) = line.chars().next() {
585 match char {
590 '&' | '|' | '=' | '>' | '<' | '+' | '-' | '*' | '/' | '%' | '^' | '?'
591 | ':' => size += 1,
592 ')' | ']'
593 if self.config.bracket_spacing
594 && (!normalize_delimiters || quote.is_some()) =>
595 {
596 size += 1
597 }
598 '}' if self.config.bracket_spacing => size += 1,
599 _ => (),
600 }
601 }
602 first = false;
603
604 let mut search = line;
606 loop {
607 if let Some((lhs, comment)) = search.split_once(r#"/*"#) {
608 size += lhs.trim_end().len() + 2;
609 search = comment;
610 } else if let Some((comment, rhs)) = search.split_once(r#"*/"#) {
611 size += comment.len() + 2;
612 search = rhs;
613 } else {
614 size += search.trim().len();
615 break;
616 }
617 }
618 let brace_delta = self.brace_spacing_delta(line, &mut in_block_comment, &mut quote);
619 size = size.saturating_add_signed(brace_delta);
620
621 prev_needs_space = match line.chars().next_back() {
627 Some('{') => self.config.bracket_spacing,
628 Some('[' | '(') => {
629 self.config.bracket_spacing && (!normalize_delimiters || quote.is_some())
630 }
631 Some(',' | ';') => true,
632 Some(
633 '&' | '|' | '=' | '>' | '<' | '+' | '-' | '*' | '/' | '%' | '^' | '?' | ':',
634 ) => !line.ends_with("*/"),
635 _ => false,
636 };
637 }
638 return size;
639 }
640
641 span.to_range().len()
642 }
643
644 fn same_source_line(&self, a: BytePos, b: BytePos) -> bool {
645 self.source_line(a) == self.source_line(b)
646 }
647}
648
649impl<'sess> State<'sess, '_> {
651 #[must_use]
653 fn handle_comment(&mut self, cmnt: Comment, skip_break: bool) -> Option<Comment> {
654 if self.cursor.enabled {
655 if self.inline_config.is_disabled(cmnt.span) {
656 if cmnt.style.is_isolated() {
660 self.hardbreak_if_not_bol();
661 } else if cmnt.style.is_trailing() && !self.last_token_is_space() {
662 self.nbsp();
663 }
664 self.print_span_cold(cmnt.span);
665 if !skip_break && (cmnt.style.is_isolated() || cmnt.style.is_trailing()) {
666 self.print_sep(Separator::Hardbreak);
667 }
668 return None;
669 }
670 } else if self.print_span_if_disabled(cmnt.span) {
671 if !skip_break && (cmnt.style.is_isolated() || cmnt.style.is_trailing()) {
672 self.print_sep(Separator::Hardbreak);
673 }
674 return None;
675 }
676 Some(cmnt)
677 }
678
679 fn cmnt_config(&self) -> CommentConfig {
680 Default::default()
681 }
682
683 const fn print_docs(&mut self, docs: &'_ ast::DocComments<'_>) {
684 let _ = docs;
686 }
687
688 fn print_comments(&mut self, pos: BytePos, mut config: CommentConfig) -> Option<CommentStyle> {
693 let mut last_style: Option<CommentStyle> = None;
694 let mut is_leading = true;
695 let config_cache = config;
696 let mut buffered_blank = None;
697 let mut previous_mixed_at_bol = false;
698 while self.peek_comment().is_some_and(|c| c.pos() < pos) {
699 let mut cmnt = self.next_comment().unwrap();
700 let style_cache = cmnt.style;
701
702 if self.config.docs_style == foundry_config::fmt::DocCommentStyle::Block
704 && cmnt.is_doc
705 && cmnt.kind == ast::CommentKind::Line
706 {
707 let mut ref_line = self.source_line(cmnt.span.hi());
708 while let Some(next_cmnt) = self.peek_comment() {
709 if !next_cmnt.is_doc
710 || next_cmnt.kind != ast::CommentKind::Line
711 || ref_line + 1 != self.source_line(next_cmnt.span.lo())
712 {
713 break;
714 }
715
716 let next_to_merge = self.next_comment().unwrap();
717 cmnt.lines.extend(next_to_merge.lines);
718 cmnt.span = cmnt.span.to(next_to_merge.span);
719 ref_line += 1;
720 }
721 }
722
723 if self.peek_comment_before(pos).is_some() {
725 config.iso_no_break = false;
726 config.trailing_no_break = false;
727 }
728
729 let Some(mut cmnt) = self.handle_comment(
731 cmnt,
732 if style_cache.is_isolated() {
733 config.iso_no_break
734 } else {
735 config.trailing_no_break
736 },
737 ) else {
738 last_style = Some(style_cache);
739 continue;
740 };
741
742 if cmnt.style.is_blank() {
743 match config.skip_blanks {
744 Some(Skip::All) => continue,
745 Some(Skip::Leading { resettable: true }) if is_leading => continue,
746 Some(Skip::Leading { resettable: false }) if last_style.is_none() => continue,
747 Some(Skip::Trailing) => {
748 buffered_blank = Some(cmnt);
749 continue;
750 }
751 _ => (),
752 }
753 } else if !cmnt.is_doc {
755 is_leading = false;
756 }
757
758 if let Some(blank) = buffered_blank.take() {
759 self.print_comment(blank, config);
760 }
761
762 if previous_mixed_at_bol
763 && cmnt.style.is_trailing()
764 && matches!(cmnt.kind, ast::CommentKind::Line)
765 {
766 self.hardbreak_if_not_bol();
767 }
768
769 if cmnt.style.is_trailing() && self.is_bol_or_only_ind() {
772 cmnt.style = CommentStyle::Isolated;
773 }
774
775 if cmnt.style.is_mixed() {
777 if let Some(cmnt) = self.peek_comment_before(pos) {
778 config.mixed_no_break_prev = true;
779 config.mixed_no_break_post = true;
780 config.mixed_post_nbsp = false;
781 if cmnt.style.is_mixed() {
785 config = config.mixed_post_glued();
786 }
787 }
788
789 if last_style.is_some_and(|s| s.is_mixed()) {
791 config.mixed_no_break_prev = true;
792 config.mixed_no_break_post = true;
793 config.mixed_prev_space = false;
794 }
795 } else if config.offset != 0
796 && cmnt.style.is_isolated()
797 && last_style.is_some_and(|s| s.is_isolated())
798 {
799 self.offset(config.offset);
800 }
801
802 last_style = Some(cmnt.style);
803 previous_mixed_at_bol = cmnt.style.is_mixed() && self.is_bol_or_only_ind();
804 self.print_comment(cmnt, config);
805 config = config_cache;
806 }
807 last_style
808 }
809
810 fn print_wrapped_line(
812 &mut self,
813 line: &str,
814 prefix: &'static str,
815 break_offset: isize,
816 is_doc: bool,
817 ) {
818 if !line.starts_with(prefix) {
819 self.word(line.to_owned());
820 return;
821 }
822
823 fn post_break_prefix(prefix: &'static str, has_content: bool) -> &'static str {
824 if !has_content {
825 return prefix;
826 }
827 match prefix {
828 "///" => "/// ",
829 "//" => "// ",
830 "/*" => "/* ",
831 " *" => " * ",
832 _ => prefix,
833 }
834 }
835
836 self.ibox(0);
837 self.word(prefix);
838
839 let content = &line[prefix.len()..];
840 let content = if is_doc {
841 let ws_len = content
843 .char_indices()
844 .take_while(|(_, c)| c.is_whitespace())
845 .last()
846 .map_or(0, |(idx, c)| idx + c.len_utf8());
847 let (leading_ws, rest) = content.split_at(ws_len);
848 if !leading_ws.is_empty() {
849 self.word(leading_ws.to_owned());
850 }
851 rest
852 } else {
853 if let Some(first_char) = content.chars().next() {
855 if first_char.is_whitespace() {
856 self.nbsp();
857 &content[first_char.len_utf8()..]
858 } else {
859 content
860 }
861 } else {
862 ""
863 }
864 };
865
866 let post_break = post_break_prefix(prefix, !content.is_empty());
867
868 let (mut chars, mut current_word) = (content.chars().peekable(), String::new());
870 while let Some(ch) = chars.next() {
871 if ch.is_whitespace() {
872 if !current_word.is_empty() {
874 self.word(std::mem::take(&mut current_word));
875 }
876
877 let mut ws_count = 1;
879 while chars.peek().is_some_and(|c| c.is_whitespace()) {
880 ws_count += 1;
881 chars.next();
882 }
883 self.s.scan_break(BreakToken {
884 offset: break_offset,
885 blank_space: ws_count,
886 post_break: if post_break.starts_with("/*") { None } else { Some(post_break) },
887 ..Default::default()
888 });
889 continue;
890 }
891
892 current_word.push(ch);
893 }
894
895 if !current_word.is_empty() {
897 self.word(current_word);
898 }
899
900 self.end();
901 }
902
903 fn merge_comment_lines(&self, lines: &[String], prefix: &str) -> Vec<String> {
905 if lines.is_empty() || lines.len() < 2 || !prefix.starts_with("//") {
907 return lines.to_vec();
908 }
909
910 let mut result = Vec::new();
911 let mut i = 0;
912
913 while i < lines.len() {
914 let current_line = &lines[i];
915
916 if current_line.trim().is_empty() || !current_line.starts_with(prefix) {
918 result.push(current_line.clone());
919 i += 1;
920 continue;
921 }
922
923 if i + 1 < lines.len() {
924 let next_line = &lines[i + 1];
925
926 if next_line.starts_with(prefix) && !next_line.trim().is_empty() {
928 let next_content = next_line[prefix.len()..].trim_start();
929
930 if next_content.starts_with('@') {
933 result.push(current_line.clone());
934 i += 1;
935 continue;
936 }
937
938 if estimate_line_width(current_line, self.config.tab_width) > self.space_left()
940 {
941 let merged_line = format!("{current_line} {next_content}");
943 result.push(merged_line);
944
945 i += 2;
947 continue;
948 }
949 }
950 }
951
952 result.push(current_line.clone());
954 i += 1;
955 }
956
957 result
958 }
959
960 fn print_comment(&mut self, mut cmnt: Comment, mut config: CommentConfig) {
961 self.cursor.advance_to(cmnt.span.hi(), true);
962
963 if cmnt.is_doc {
964 cmnt = style_doc_comment(self.config.docs_style, cmnt);
965 }
966
967 match cmnt.style {
968 CommentStyle::Mixed => {
969 let Some(prefix) = cmnt.prefix() else { return };
970 let never_break = self.last_token_is_neverbreak();
971 let starts_line = self.is_bol_or_only_ind() || self.last_token_is_break();
972 if !self.is_bol_or_only_ind() {
973 match (never_break || config.mixed_no_break_prev, config.mixed_prev_space) {
974 (false, true) => config.space(&mut self.s),
975 (false, false) => config.zerobreak(&mut self.s),
976 (true, true) => self.nbsp(),
977 (true, false) => (),
978 };
979 }
980 if self.config.wrap_comments {
981 if starts_line {
983 self.ibox(config.offset);
984 }
985 let merged_lines = self.merge_comment_lines(&cmnt.lines, prefix);
986 for (pos, line) in merged_lines.into_iter().delimited() {
987 self.print_wrapped_line(&line, prefix, 0, cmnt.is_doc);
988 if !pos.is_last {
989 self.hardbreak();
990 }
991 }
992 if starts_line {
993 self.end();
994 }
995 } else {
996 self.visual_align();
998 for (pos, line) in cmnt.lines.into_iter().delimited() {
999 if !line.is_empty() {
1000 self.word(line);
1001 }
1002 if !pos.is_last {
1003 self.hardbreak();
1004 }
1005 }
1006 self.end();
1007 }
1008 if config.mixed_post_nbsp {
1009 if config.mixed_post_glued {
1010 self.nbsp();
1011 } else {
1012 config.nbsp_or_space(self.config.wrap_comments, &mut self.s);
1013 }
1014 self.cursor.advance(1);
1015 } else if !config.mixed_no_break_post {
1016 config.space(&mut self.s);
1017 self.cursor.advance(1);
1018 }
1019 }
1020 CommentStyle::Isolated => {
1021 let Some(mut prefix) = cmnt.prefix() else { return };
1022 if !config.iso_no_break {
1023 config.hardbreak_if_not_bol(self.is_bol_or_only_ind(), &mut self.s);
1024 }
1025
1026 if self.config.wrap_comments {
1027 let merged_lines = self.merge_comment_lines(&cmnt.lines, prefix);
1029 for (pos, line) in merged_lines.into_iter().delimited() {
1030 let hb = |this: &mut Self| {
1031 this.hardbreak();
1032 if pos.is_last {
1033 this.cursor.next_line(this.is_at_crlf());
1034 }
1035 };
1036 if line.is_empty() {
1037 hb(self);
1038 continue;
1039 }
1040 if pos.is_first {
1041 self.ibox(config.offset);
1042 if cmnt.is_doc && matches!(prefix, "/**") {
1043 self.word(prefix);
1044 hb(self);
1045 prefix = " * ";
1046 continue;
1047 }
1048 }
1049
1050 self.print_wrapped_line(&line, prefix, 0, cmnt.is_doc);
1051
1052 if pos.is_last {
1053 self.end();
1054 if !config.iso_no_break {
1055 hb(self);
1056 }
1057 } else {
1058 hb(self);
1059 }
1060 }
1061 } else {
1062 for (pos, line) in cmnt.lines.into_iter().delimited() {
1064 let hb = |this: &mut Self| {
1065 this.hardbreak();
1066 if pos.is_last {
1067 this.cursor.next_line(this.is_at_crlf());
1068 }
1069 };
1070 if line.is_empty() {
1071 hb(self);
1072 continue;
1073 }
1074 if pos.is_first {
1075 self.ibox(config.offset);
1076 if cmnt.is_doc && matches!(prefix, "/**") {
1077 self.word(prefix);
1078 hb(self);
1079 prefix = " * ";
1080 continue;
1081 }
1082 }
1083
1084 self.word(line);
1085
1086 if pos.is_last {
1087 self.end();
1088 if !config.iso_no_break {
1089 hb(self);
1090 }
1091 } else {
1092 hb(self);
1093 }
1094 }
1095 }
1096 }
1097 CommentStyle::Trailing => {
1098 let Some(prefix) = cmnt.prefix() else { return };
1099 self.neverbreak();
1100 if !self.is_bol_or_only_ind() {
1101 self.nbsp();
1102 }
1103
1104 if !self.config.wrap_comments && cmnt.lines.len() == 1 {
1105 self.word(cmnt.lines.pop().unwrap());
1106 } else if self.config.wrap_comments {
1107 if cmnt.is_doc || matches!(cmnt.kind, ast::CommentKind::Line) {
1108 config.offset = 0;
1109 } else {
1110 config.offset = self.ind;
1111 }
1112 for (lpos, line) in cmnt.lines.into_iter().delimited() {
1113 if !line.is_empty() {
1114 self.print_wrapped_line(&line, prefix, config.offset, cmnt.is_doc);
1115 }
1116 if !lpos.is_last {
1117 config.hardbreak(&mut self.s);
1118 }
1119 }
1120 } else {
1121 self.visual_align();
1122 for (pos, line) in cmnt.lines.into_iter().delimited() {
1123 if !line.is_empty() {
1124 self.word(line);
1125 if !pos.is_last {
1126 self.hardbreak();
1127 }
1128 }
1129 }
1130 self.end();
1131 }
1132
1133 if !config.trailing_no_break {
1134 self.print_sep(Separator::Hardbreak);
1135 }
1136 }
1137
1138 CommentStyle::BlankLine => {
1139 if !self.last_token_is_break() && !self.is_bol_or_only_ind() {
1141 config.hardbreak(&mut self.s);
1142 self.cursor.next_line(self.is_at_crlf());
1143 }
1144
1145 let twice = match self.last_token() {
1147 Some(Token::String(s)) => ";" == s,
1148 Some(Token::Begin(_)) => true,
1149 Some(Token::End) => true,
1150 _ => false,
1151 };
1152 if twice {
1153 config.hardbreak(&mut self.s);
1154 self.cursor.next_line(self.is_at_crlf());
1155 }
1156 config.hardbreak(&mut self.s);
1157 self.cursor.next_line(self.is_at_crlf());
1158 }
1159 }
1160 }
1161
1162 fn peek_comment<'b>(&'b self) -> Option<&'b Comment>
1163 where
1164 'sess: 'b,
1165 {
1166 self.comments.peek()
1167 }
1168
1169 fn peek_comment_before<'b>(&'b self, pos: BytePos) -> Option<&'b Comment>
1170 where
1171 'sess: 'b,
1172 {
1173 self.comments.iter().take_while(|c| c.pos() < pos).find(|c| !c.style.is_blank())
1174 }
1175
1176 fn peek_mixed_comment_before(&self, pos: Option<BytePos>) -> bool {
1179 pos.is_some_and(|pos| {
1180 self.peek_comment().is_some_and(|cmnt| cmnt.pos() < pos && cmnt.style.is_mixed())
1181 })
1182 }
1183
1184 fn has_comment_before_with<F>(&self, pos: BytePos, f: F) -> bool
1185 where
1186 F: FnMut(&Comment) -> bool,
1187 {
1188 self.comments.iter().take_while(|c| c.pos() < pos).any(f)
1189 }
1190
1191 fn peek_comment_between<'b>(&'b self, pos_lo: BytePos, pos_hi: BytePos) -> Option<&'b Comment>
1192 where
1193 'sess: 'b,
1194 {
1195 self.comments
1196 .iter()
1197 .skip_while(|c| c.pos() < pos_lo)
1198 .take_while(|c| c.pos() < pos_hi)
1199 .find(|c| !c.style.is_blank())
1200 }
1201
1202 fn has_comment_between(&self, start_pos: BytePos, end_pos: BytePos) -> bool {
1203 self.comments.iter().filter(|c| c.pos() > start_pos && c.pos() < end_pos).any(|_| true)
1204 }
1205
1206 fn has_breakable_comment_between(&self, start_pos: BytePos, end_pos: BytePos) -> bool {
1207 self.comments
1208 .iter()
1209 .filter(|comment| comment.pos() >= start_pos && comment.pos() < end_pos)
1210 .any(|comment| !comment.style.is_blank())
1211 }
1212
1213 pub(crate) fn next_comment(&mut self) -> Option<Comment> {
1214 self.comments.next()
1215 }
1216
1217 fn peek_trailing_comment<'b>(
1218 &'b self,
1219 span_pos: BytePos,
1220 next_pos: Option<BytePos>,
1221 ) -> Option<&'b Comment>
1222 where
1223 'sess: 'b,
1224 {
1225 self.comments.peek_trailing_in_file(self.file, span_pos, next_pos).map(|(cmnt, _)| cmnt)
1226 }
1227
1228 fn print_trailing_comment_inner(
1229 &mut self,
1230 span_pos: BytePos,
1231 next_pos: Option<BytePos>,
1232 config: Option<CommentConfig>,
1233 ) -> bool {
1234 let mut printed = 0;
1235 if let Some((_, n)) = self.comments.peek_trailing_in_file(self.file, span_pos, next_pos) {
1236 let config =
1237 config.unwrap_or(CommentConfig::skip_ws().mixed_no_break().mixed_prev_space());
1238 while printed <= n {
1239 let cmnt = self.comments.next().unwrap();
1240 if let Some(cmnt) = self.handle_comment(cmnt, config.trailing_no_break) {
1241 self.print_comment(cmnt, config);
1242 };
1243 printed += 1;
1244 }
1245 }
1246 printed != 0
1247 }
1248
1249 fn print_trailing_comment(&mut self, span_pos: BytePos, next_pos: Option<BytePos>) -> bool {
1250 self.print_trailing_comment_inner(span_pos, next_pos, None)
1251 }
1252
1253 fn print_trailing_comment_no_break(&mut self, span_pos: BytePos, next_pos: Option<BytePos>) {
1254 self.print_trailing_comment_inner(
1255 span_pos,
1256 next_pos,
1257 Some(CommentConfig::skip_ws().trailing_no_break().mixed_no_break().mixed_prev_space()),
1258 );
1259 }
1260
1261 fn print_remaining_comments(&mut self, skip_leading_ws: bool) {
1262 if self.peek_comment().is_none() && !self.is_bol_or_only_ind() {
1265 self.hardbreak();
1266 return;
1267 }
1268
1269 let mut is_leading = true;
1270 while let Some(cmnt) = self.next_comment() {
1271 if cmnt.style.is_blank() && skip_leading_ws && is_leading {
1272 continue;
1273 }
1274
1275 is_leading = false;
1276 if let Some(cmnt) = self.handle_comment(cmnt, false) {
1277 self.print_comment(cmnt, CommentConfig::default());
1278 } else if self.peek_comment().is_none() && !self.is_bol_or_only_ind() {
1279 self.hardbreak();
1280 }
1281 }
1282 }
1283}
1284
1285#[derive(Clone, Copy)]
1286enum Skip {
1287 All,
1288 Leading { resettable: bool },
1289 Trailing,
1290}
1291
1292#[derive(Default, Clone, Copy)]
1293pub(crate) struct CommentConfig {
1294 skip_blanks: Option<Skip>,
1296 offset: isize,
1297
1298 iso_no_break: bool,
1300 trailing_no_break: bool,
1302 mixed_prev_space: bool,
1304 mixed_post_nbsp: bool,
1305 mixed_post_glued: bool,
1309 mixed_no_break_prev: bool,
1310 mixed_no_break_post: bool,
1311}
1312
1313impl CommentConfig {
1314 pub(crate) fn skip_ws() -> Self {
1315 Self { skip_blanks: Some(Skip::All), ..Default::default() }
1316 }
1317
1318 pub(crate) fn empty_block() -> Self {
1321 Self::skip_ws().mixed_no_break().mixed_prev_space().mixed_post_glued()
1322 }
1323
1324 pub(crate) fn skip_leading_ws(resettable: bool) -> Self {
1325 Self { skip_blanks: Some(Skip::Leading { resettable }), ..Default::default() }
1326 }
1327
1328 pub(crate) fn skip_trailing_ws() -> Self {
1329 Self { skip_blanks: Some(Skip::Trailing), ..Default::default() }
1330 }
1331
1332 pub(crate) const fn offset(mut self, off: isize) -> Self {
1333 self.offset = off;
1334 self
1335 }
1336
1337 pub(crate) const fn no_breaks(mut self) -> Self {
1338 self.iso_no_break = true;
1339 self.trailing_no_break = true;
1340 self.mixed_no_break_prev = true;
1341 self.mixed_no_break_post = true;
1342 self
1343 }
1344
1345 pub(crate) const fn trailing_no_break(mut self) -> Self {
1346 self.trailing_no_break = true;
1347 self
1348 }
1349
1350 pub(crate) const fn mixed_no_break(mut self) -> Self {
1351 self.mixed_no_break_prev = true;
1352 self.mixed_no_break_post = true;
1353 self
1354 }
1355
1356 pub(crate) const fn mixed_no_break_post(mut self) -> Self {
1357 self.mixed_no_break_post = true;
1358 self
1359 }
1360
1361 pub(crate) const fn mixed_prev_space(mut self) -> Self {
1362 self.mixed_prev_space = true;
1363 self
1364 }
1365
1366 pub(crate) const fn mixed_post_nbsp(mut self) -> Self {
1367 self.mixed_post_nbsp = true;
1368 self
1369 }
1370
1371 pub(crate) const fn mixed_post_glued(mut self) -> Self {
1372 self.mixed_post_nbsp = true;
1373 self.mixed_post_glued = true;
1374 self
1375 }
1376
1377 pub(crate) fn hardbreak_if_not_bol(&self, is_bol: bool, p: &mut pp::Printer) {
1378 if self.offset != 0 && !is_bol {
1379 self.hardbreak(p);
1380 } else {
1381 p.hardbreak_if_not_bol();
1382 }
1383 }
1384
1385 pub(crate) fn hardbreak(&self, p: &mut pp::Printer) {
1386 p.break_offset(SIZE_INFINITY as usize, self.offset);
1387 }
1388
1389 pub(crate) fn space(&self, p: &mut pp::Printer) {
1390 p.break_offset(1, self.offset);
1391 }
1392
1393 pub(crate) fn nbsp_or_space(&self, breaks: bool, p: &mut pp::Printer) {
1394 if breaks {
1395 self.space(p);
1396 } else {
1397 p.nbsp();
1398 }
1399 }
1400
1401 pub(crate) fn zerobreak(&self, p: &mut pp::Printer) {
1402 p.break_offset(0, self.offset);
1403 }
1404}
1405
1406fn snippet_with_tabs(s: String, tab_width: usize) -> String {
1407 let mut formatted = String::with_capacity(s.len());
1408 for line in s.split_inclusive('\n') {
1409 let (line, has_newline) =
1410 line.strip_suffix('\n').map_or((line, false), |line| (line, true));
1411 let line = line.strip_suffix('\r').unwrap_or(line);
1412 line_with_tabs(&mut formatted, line, tab_width, None);
1413 if has_newline {
1414 formatted.push('\n');
1415 }
1416 }
1417
1418 formatted
1419}
1420
1421fn style_doc_comment(style: DocCommentStyle, mut cmnt: Comment) -> Comment {
1425 match style {
1426 DocCommentStyle::Line if cmnt.kind == ast::CommentKind::Block => {
1427 let mut new_lines = Vec::new();
1428 for (pos, line) in cmnt.lines.iter().delimited() {
1429 if pos.is_first || pos.is_last {
1430 continue;
1432 }
1433
1434 let trimmed = line.trim_start();
1436 if let Some(content) = trimmed.strip_prefix('*') {
1437 new_lines.push(format!("///{content}"));
1438 } else if !trimmed.is_empty() {
1439 new_lines.push(format!("/// {trimmed}"));
1440 }
1441 }
1442
1443 cmnt.lines = new_lines;
1444 cmnt.kind = ast::CommentKind::Line;
1445 cmnt
1446 }
1447 DocCommentStyle::Block if cmnt.kind == ast::CommentKind::Line => {
1448 let mut new_lines = vec!["/**".to_string()];
1449
1450 for line in &cmnt.lines {
1451 new_lines.push(format!(" *{content}", content = &line[3..]))
1453 }
1454
1455 new_lines.push(" */".to_string());
1456 cmnt.lines = new_lines;
1457 cmnt.kind = ast::CommentKind::Block;
1458 cmnt
1459 }
1460 _ => cmnt,
1462 }
1463}