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};
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 estimate_size(&self, span: Span) -> usize {
459 if let Some(snip) = self.snippet(span) {
460 let (mut size, mut first, mut prev_needs_space) = (0, true, false);
461
462 for line in snip.lines() {
463 let line = line.trim();
464
465 if prev_needs_space {
466 size += 1;
467 } else if !first && let Some(char) = line.chars().next() {
468 match char {
473 '&' | '|' | '=' | '>' | '<' | '+' | '-' | '*' | '/' | '%' | '^' | '?'
474 | ':' => size += 1,
475 '}' | ')' | ']' if self.config.bracket_spacing => size += 1,
476 _ => (),
477 }
478 }
479 first = false;
480
481 let mut search = line;
483 loop {
484 if let Some((lhs, comment)) = search.split_once(r#"/*"#) {
485 size += lhs.trim_end().len() + 2;
486 search = comment;
487 } else if let Some((comment, rhs)) = search.split_once(r#"*/"#) {
488 size += comment.len() + 2;
489 search = rhs;
490 } else {
491 size += search.trim().len();
492 break;
493 }
494 }
495
496 prev_needs_space = match line.chars().next_back() {
501 Some('[' | '(' | '{') => self.config.bracket_spacing,
502 Some(',' | ';') => true,
503 _ => false,
504 };
505 }
506 return size;
507 }
508
509 span.to_range().len()
510 }
511
512 fn same_source_line(&self, a: BytePos, b: BytePos) -> bool {
513 self.source_line(a) == self.source_line(b)
514 }
515}
516
517impl<'sess> State<'sess, '_> {
519 #[must_use]
521 fn handle_comment(&mut self, cmnt: Comment, skip_break: bool) -> Option<Comment> {
522 if self.cursor.enabled {
523 if self.inline_config.is_disabled(cmnt.span) {
524 if cmnt.style.is_trailing() && !self.last_token_is_space() {
525 self.nbsp();
526 }
527 self.print_span_cold(cmnt.span);
528 if !skip_break && (cmnt.style.is_isolated() || cmnt.style.is_trailing()) {
529 self.print_sep(Separator::Hardbreak);
530 }
531 return None;
532 }
533 } else if self.print_span_if_disabled(cmnt.span) {
534 if !skip_break && (cmnt.style.is_isolated() || cmnt.style.is_trailing()) {
535 self.print_sep(Separator::Hardbreak);
536 }
537 return None;
538 }
539 Some(cmnt)
540 }
541
542 fn cmnt_config(&self) -> CommentConfig {
543 Default::default()
544 }
545
546 const fn print_docs(&mut self, docs: &'_ ast::DocComments<'_>) {
547 let _ = docs;
549 }
550
551 fn print_comments(&mut self, pos: BytePos, mut config: CommentConfig) -> Option<CommentStyle> {
556 let mut last_style: Option<CommentStyle> = None;
557 let mut is_leading = true;
558 let config_cache = config;
559 let mut buffered_blank = None;
560 let mut previous_mixed_at_bol = false;
561 while self.peek_comment().is_some_and(|c| c.pos() < pos) {
562 let mut cmnt = self.next_comment().unwrap();
563 let style_cache = cmnt.style;
564
565 if self.config.docs_style == foundry_config::fmt::DocCommentStyle::Block
567 && cmnt.is_doc
568 && cmnt.kind == ast::CommentKind::Line
569 {
570 let mut ref_line = self.source_line(cmnt.span.hi());
571 while let Some(next_cmnt) = self.peek_comment() {
572 if !next_cmnt.is_doc
573 || next_cmnt.kind != ast::CommentKind::Line
574 || ref_line + 1 != self.source_line(next_cmnt.span.lo())
575 {
576 break;
577 }
578
579 let next_to_merge = self.next_comment().unwrap();
580 cmnt.lines.extend(next_to_merge.lines);
581 cmnt.span = cmnt.span.to(next_to_merge.span);
582 ref_line += 1;
583 }
584 }
585
586 if self.peek_comment_before(pos).is_some() {
588 config.iso_no_break = false;
589 config.trailing_no_break = false;
590 }
591
592 let Some(mut cmnt) = self.handle_comment(
594 cmnt,
595 if style_cache.is_isolated() {
596 config.iso_no_break
597 } else {
598 config.trailing_no_break
599 },
600 ) else {
601 last_style = Some(style_cache);
602 continue;
603 };
604
605 if cmnt.style.is_blank() {
606 match config.skip_blanks {
607 Some(Skip::All) => continue,
608 Some(Skip::Leading { resettable: true }) if is_leading => continue,
609 Some(Skip::Leading { resettable: false }) if last_style.is_none() => continue,
610 Some(Skip::Trailing) => {
611 buffered_blank = Some(cmnt);
612 continue;
613 }
614 _ => (),
615 }
616 } else if !cmnt.is_doc {
618 is_leading = false;
619 }
620
621 if let Some(blank) = buffered_blank.take() {
622 self.print_comment(blank, config);
623 }
624
625 if previous_mixed_at_bol
626 && cmnt.style.is_trailing()
627 && matches!(cmnt.kind, ast::CommentKind::Line)
628 {
629 self.hardbreak_if_not_bol();
630 }
631
632 if cmnt.style.is_trailing() && self.is_bol_or_only_ind() {
635 cmnt.style = CommentStyle::Isolated;
636 }
637
638 if cmnt.style.is_mixed() {
640 if let Some(cmnt) = self.peek_comment_before(pos) {
641 config.mixed_no_break_prev = true;
642 config.mixed_no_break_post = true;
643 config.mixed_post_nbsp = false;
644 if cmnt.style.is_mixed() {
648 config = config.mixed_post_glued();
649 }
650 }
651
652 if last_style.is_some_and(|s| s.is_mixed()) {
654 config.mixed_no_break_prev = true;
655 config.mixed_no_break_post = true;
656 config.mixed_prev_space = false;
657 }
658 } else if config.offset != 0
659 && cmnt.style.is_isolated()
660 && last_style.is_some_and(|s| s.is_isolated())
661 {
662 self.offset(config.offset);
663 }
664
665 last_style = Some(cmnt.style);
666 previous_mixed_at_bol = cmnt.style.is_mixed() && self.is_bol_or_only_ind();
667 self.print_comment(cmnt, config);
668 config = config_cache;
669 }
670 last_style
671 }
672
673 fn print_wrapped_line(
675 &mut self,
676 line: &str,
677 prefix: &'static str,
678 break_offset: isize,
679 is_doc: bool,
680 ) {
681 if !line.starts_with(prefix) {
682 self.word(line.to_owned());
683 return;
684 }
685
686 fn post_break_prefix(prefix: &'static str, has_content: bool) -> &'static str {
687 if !has_content {
688 return prefix;
689 }
690 match prefix {
691 "///" => "/// ",
692 "//" => "// ",
693 "/*" => "/* ",
694 " *" => " * ",
695 _ => prefix,
696 }
697 }
698
699 self.ibox(0);
700 self.word(prefix);
701
702 let content = &line[prefix.len()..];
703 let content = if is_doc {
704 let ws_len = content
706 .char_indices()
707 .take_while(|(_, c)| c.is_whitespace())
708 .last()
709 .map_or(0, |(idx, c)| idx + c.len_utf8());
710 let (leading_ws, rest) = content.split_at(ws_len);
711 if !leading_ws.is_empty() {
712 self.word(leading_ws.to_owned());
713 }
714 rest
715 } else {
716 if let Some(first_char) = content.chars().next() {
718 if first_char.is_whitespace() {
719 self.nbsp();
720 &content[first_char.len_utf8()..]
721 } else {
722 content
723 }
724 } else {
725 ""
726 }
727 };
728
729 let post_break = post_break_prefix(prefix, !content.is_empty());
730
731 let (mut chars, mut current_word) = (content.chars().peekable(), String::new());
733 while let Some(ch) = chars.next() {
734 if ch.is_whitespace() {
735 if !current_word.is_empty() {
737 self.word(std::mem::take(&mut current_word));
738 }
739
740 let mut ws_count = 1;
742 while chars.peek().is_some_and(|c| c.is_whitespace()) {
743 ws_count += 1;
744 chars.next();
745 }
746 self.s.scan_break(BreakToken {
747 offset: break_offset,
748 blank_space: ws_count,
749 post_break: if post_break.starts_with("/*") { None } else { Some(post_break) },
750 ..Default::default()
751 });
752 continue;
753 }
754
755 current_word.push(ch);
756 }
757
758 if !current_word.is_empty() {
760 self.word(current_word);
761 }
762
763 self.end();
764 }
765
766 fn merge_comment_lines(&self, lines: &[String], prefix: &str) -> Vec<String> {
768 if lines.is_empty() || lines.len() < 2 || !prefix.starts_with("//") {
770 return lines.to_vec();
771 }
772
773 let mut result = Vec::new();
774 let mut i = 0;
775
776 while i < lines.len() {
777 let current_line = &lines[i];
778
779 if current_line.trim().is_empty() || !current_line.starts_with(prefix) {
781 result.push(current_line.clone());
782 i += 1;
783 continue;
784 }
785
786 if i + 1 < lines.len() {
787 let next_line = &lines[i + 1];
788
789 if next_line.starts_with(prefix) && !next_line.trim().is_empty() {
791 let next_content = next_line[prefix.len()..].trim_start();
792
793 if next_content.starts_with('@') {
796 result.push(current_line.clone());
797 i += 1;
798 continue;
799 }
800
801 if estimate_line_width(current_line, self.config.tab_width) > self.space_left()
803 {
804 let merged_line = format!("{current_line} {next_content}");
806 result.push(merged_line);
807
808 i += 2;
810 continue;
811 }
812 }
813 }
814
815 result.push(current_line.clone());
817 i += 1;
818 }
819
820 result
821 }
822
823 fn print_comment(&mut self, mut cmnt: Comment, mut config: CommentConfig) {
824 self.cursor.advance_to(cmnt.span.hi(), true);
825
826 if cmnt.is_doc {
827 cmnt = style_doc_comment(self.config.docs_style, cmnt);
828 }
829
830 match cmnt.style {
831 CommentStyle::Mixed => {
832 let Some(prefix) = cmnt.prefix() else { return };
833 let never_break = self.last_token_is_neverbreak();
834 let starts_line = self.is_bol_or_only_ind() || self.last_token_is_break();
835 if !self.is_bol_or_only_ind() {
836 match (never_break || config.mixed_no_break_prev, config.mixed_prev_space) {
837 (false, true) => config.space(&mut self.s),
838 (false, false) => config.zerobreak(&mut self.s),
839 (true, true) => self.nbsp(),
840 (true, false) => (),
841 };
842 }
843 if self.config.wrap_comments {
844 if starts_line {
846 self.ibox(config.offset);
847 }
848 let merged_lines = self.merge_comment_lines(&cmnt.lines, prefix);
849 for (pos, line) in merged_lines.into_iter().delimited() {
850 self.print_wrapped_line(&line, prefix, 0, cmnt.is_doc);
851 if !pos.is_last {
852 self.hardbreak();
853 }
854 }
855 if starts_line {
856 self.end();
857 }
858 } else {
859 self.visual_align();
861 for (pos, line) in cmnt.lines.into_iter().delimited() {
862 if !line.is_empty() {
863 self.word(line);
864 }
865 if !pos.is_last {
866 self.hardbreak();
867 }
868 }
869 self.end();
870 }
871 if config.mixed_post_nbsp {
872 if config.mixed_post_glued {
873 self.nbsp();
874 } else {
875 config.nbsp_or_space(self.config.wrap_comments, &mut self.s);
876 }
877 self.cursor.advance(1);
878 } else if !config.mixed_no_break_post {
879 config.space(&mut self.s);
880 self.cursor.advance(1);
881 }
882 }
883 CommentStyle::Isolated => {
884 let Some(mut prefix) = cmnt.prefix() else { return };
885 if !config.iso_no_break {
886 config.hardbreak_if_not_bol(self.is_bol_or_only_ind(), &mut self.s);
887 }
888
889 if self.config.wrap_comments {
890 let merged_lines = self.merge_comment_lines(&cmnt.lines, prefix);
892 for (pos, line) in merged_lines.into_iter().delimited() {
893 let hb = |this: &mut Self| {
894 this.hardbreak();
895 if pos.is_last {
896 this.cursor.next_line(this.is_at_crlf());
897 }
898 };
899 if line.is_empty() {
900 hb(self);
901 continue;
902 }
903 if pos.is_first {
904 self.ibox(config.offset);
905 if cmnt.is_doc && matches!(prefix, "/**") {
906 self.word(prefix);
907 hb(self);
908 prefix = " * ";
909 continue;
910 }
911 }
912
913 self.print_wrapped_line(&line, prefix, 0, cmnt.is_doc);
914
915 if pos.is_last {
916 self.end();
917 if !config.iso_no_break {
918 hb(self);
919 }
920 } else {
921 hb(self);
922 }
923 }
924 } else {
925 for (pos, line) in cmnt.lines.into_iter().delimited() {
927 let hb = |this: &mut Self| {
928 this.hardbreak();
929 if pos.is_last {
930 this.cursor.next_line(this.is_at_crlf());
931 }
932 };
933 if line.is_empty() {
934 hb(self);
935 continue;
936 }
937 if pos.is_first {
938 self.ibox(config.offset);
939 if cmnt.is_doc && matches!(prefix, "/**") {
940 self.word(prefix);
941 hb(self);
942 prefix = " * ";
943 continue;
944 }
945 }
946
947 self.word(line);
948
949 if pos.is_last {
950 self.end();
951 if !config.iso_no_break {
952 hb(self);
953 }
954 } else {
955 hb(self);
956 }
957 }
958 }
959 }
960 CommentStyle::Trailing => {
961 let Some(prefix) = cmnt.prefix() else { return };
962 self.neverbreak();
963 if !self.is_bol_or_only_ind() {
964 self.nbsp();
965 }
966
967 if !self.config.wrap_comments && cmnt.lines.len() == 1 {
968 self.word(cmnt.lines.pop().unwrap());
969 } else if self.config.wrap_comments {
970 if cmnt.is_doc || matches!(cmnt.kind, ast::CommentKind::Line) {
971 config.offset = 0;
972 } else {
973 config.offset = self.ind;
974 }
975 for (lpos, line) in cmnt.lines.into_iter().delimited() {
976 if !line.is_empty() {
977 self.print_wrapped_line(&line, prefix, config.offset, cmnt.is_doc);
978 }
979 if !lpos.is_last {
980 config.hardbreak(&mut self.s);
981 }
982 }
983 } else {
984 self.visual_align();
985 for (pos, line) in cmnt.lines.into_iter().delimited() {
986 if !line.is_empty() {
987 self.word(line);
988 if !pos.is_last {
989 self.hardbreak();
990 }
991 }
992 }
993 self.end();
994 }
995
996 if !config.trailing_no_break {
997 self.print_sep(Separator::Hardbreak);
998 }
999 }
1000
1001 CommentStyle::BlankLine => {
1002 if !self.last_token_is_break() && !self.is_bol_or_only_ind() {
1004 config.hardbreak(&mut self.s);
1005 self.cursor.next_line(self.is_at_crlf());
1006 }
1007
1008 let twice = match self.last_token() {
1010 Some(Token::String(s)) => ";" == s,
1011 Some(Token::Begin(_)) => true,
1012 Some(Token::End) => true,
1013 _ => false,
1014 };
1015 if twice {
1016 config.hardbreak(&mut self.s);
1017 self.cursor.next_line(self.is_at_crlf());
1018 }
1019 config.hardbreak(&mut self.s);
1020 self.cursor.next_line(self.is_at_crlf());
1021 }
1022 }
1023 }
1024
1025 fn peek_comment<'b>(&'b self) -> Option<&'b Comment>
1026 where
1027 'sess: 'b,
1028 {
1029 self.comments.peek()
1030 }
1031
1032 fn peek_comment_before<'b>(&'b self, pos: BytePos) -> Option<&'b Comment>
1033 where
1034 'sess: 'b,
1035 {
1036 self.comments.iter().take_while(|c| c.pos() < pos).find(|c| !c.style.is_blank())
1037 }
1038
1039 fn peek_mixed_comment_before(&self, pos: Option<BytePos>) -> bool {
1042 pos.is_some_and(|pos| {
1043 self.peek_comment().is_some_and(|cmnt| cmnt.pos() < pos && cmnt.style.is_mixed())
1044 })
1045 }
1046
1047 fn has_comment_before_with<F>(&self, pos: BytePos, f: F) -> bool
1048 where
1049 F: FnMut(&Comment) -> bool,
1050 {
1051 self.comments.iter().take_while(|c| c.pos() < pos).any(f)
1052 }
1053
1054 fn peek_comment_between<'b>(&'b self, pos_lo: BytePos, pos_hi: BytePos) -> Option<&'b Comment>
1055 where
1056 'sess: 'b,
1057 {
1058 self.comments
1059 .iter()
1060 .skip_while(|c| c.pos() < pos_lo)
1061 .take_while(|c| c.pos() < pos_hi)
1062 .find(|c| !c.style.is_blank())
1063 }
1064
1065 fn has_comment_between(&self, start_pos: BytePos, end_pos: BytePos) -> bool {
1066 self.comments.iter().filter(|c| c.pos() > start_pos && c.pos() < end_pos).any(|_| true)
1067 }
1068
1069 fn has_breakable_comment_between(&self, start_pos: BytePos, end_pos: BytePos) -> bool {
1070 self.comments
1071 .iter()
1072 .filter(|comment| comment.pos() >= start_pos && comment.pos() < end_pos)
1073 .any(|comment| !comment.style.is_blank())
1074 }
1075
1076 pub(crate) fn next_comment(&mut self) -> Option<Comment> {
1077 self.comments.next()
1078 }
1079
1080 fn peek_trailing_comment<'b>(
1081 &'b self,
1082 span_pos: BytePos,
1083 next_pos: Option<BytePos>,
1084 ) -> Option<&'b Comment>
1085 where
1086 'sess: 'b,
1087 {
1088 self.comments.peek_trailing_in_file(self.file, span_pos, next_pos).map(|(cmnt, _)| cmnt)
1089 }
1090
1091 fn print_trailing_comment_inner(
1092 &mut self,
1093 span_pos: BytePos,
1094 next_pos: Option<BytePos>,
1095 config: Option<CommentConfig>,
1096 ) -> bool {
1097 let mut printed = 0;
1098 if let Some((_, n)) = self.comments.peek_trailing_in_file(self.file, span_pos, next_pos) {
1099 let config =
1100 config.unwrap_or(CommentConfig::skip_ws().mixed_no_break().mixed_prev_space());
1101 while printed <= n {
1102 let cmnt = self.comments.next().unwrap();
1103 if let Some(cmnt) = self.handle_comment(cmnt, config.trailing_no_break) {
1104 self.print_comment(cmnt, config);
1105 };
1106 printed += 1;
1107 }
1108 }
1109 printed != 0
1110 }
1111
1112 fn print_trailing_comment(&mut self, span_pos: BytePos, next_pos: Option<BytePos>) -> bool {
1113 self.print_trailing_comment_inner(span_pos, next_pos, None)
1114 }
1115
1116 fn print_trailing_comment_no_break(&mut self, span_pos: BytePos, next_pos: Option<BytePos>) {
1117 self.print_trailing_comment_inner(
1118 span_pos,
1119 next_pos,
1120 Some(CommentConfig::skip_ws().trailing_no_break().mixed_no_break().mixed_prev_space()),
1121 );
1122 }
1123
1124 fn print_remaining_comments(&mut self, skip_leading_ws: bool) {
1125 if self.peek_comment().is_none() && !self.is_bol_or_only_ind() {
1128 self.hardbreak();
1129 return;
1130 }
1131
1132 let mut is_leading = true;
1133 while let Some(cmnt) = self.next_comment() {
1134 if cmnt.style.is_blank() && skip_leading_ws && is_leading {
1135 continue;
1136 }
1137
1138 is_leading = false;
1139 if let Some(cmnt) = self.handle_comment(cmnt, false) {
1140 self.print_comment(cmnt, CommentConfig::default());
1141 } else if self.peek_comment().is_none() && !self.is_bol_or_only_ind() {
1142 self.hardbreak();
1143 }
1144 }
1145 }
1146}
1147
1148#[derive(Clone, Copy)]
1149enum Skip {
1150 All,
1151 Leading { resettable: bool },
1152 Trailing,
1153}
1154
1155#[derive(Default, Clone, Copy)]
1156pub(crate) struct CommentConfig {
1157 skip_blanks: Option<Skip>,
1159 offset: isize,
1160
1161 iso_no_break: bool,
1163 trailing_no_break: bool,
1165 mixed_prev_space: bool,
1167 mixed_post_nbsp: bool,
1168 mixed_post_glued: bool,
1172 mixed_no_break_prev: bool,
1173 mixed_no_break_post: bool,
1174}
1175
1176impl CommentConfig {
1177 pub(crate) fn skip_ws() -> Self {
1178 Self { skip_blanks: Some(Skip::All), ..Default::default() }
1179 }
1180
1181 pub(crate) fn empty_block() -> Self {
1184 Self::skip_ws().mixed_no_break().mixed_prev_space().mixed_post_glued()
1185 }
1186
1187 pub(crate) fn skip_leading_ws(resettable: bool) -> Self {
1188 Self { skip_blanks: Some(Skip::Leading { resettable }), ..Default::default() }
1189 }
1190
1191 pub(crate) fn skip_trailing_ws() -> Self {
1192 Self { skip_blanks: Some(Skip::Trailing), ..Default::default() }
1193 }
1194
1195 pub(crate) const fn offset(mut self, off: isize) -> Self {
1196 self.offset = off;
1197 self
1198 }
1199
1200 pub(crate) const fn no_breaks(mut self) -> Self {
1201 self.iso_no_break = true;
1202 self.trailing_no_break = true;
1203 self.mixed_no_break_prev = true;
1204 self.mixed_no_break_post = true;
1205 self
1206 }
1207
1208 pub(crate) const fn trailing_no_break(mut self) -> Self {
1209 self.trailing_no_break = true;
1210 self
1211 }
1212
1213 pub(crate) const fn mixed_no_break(mut self) -> Self {
1214 self.mixed_no_break_prev = true;
1215 self.mixed_no_break_post = true;
1216 self
1217 }
1218
1219 pub(crate) const fn mixed_no_break_post(mut self) -> Self {
1220 self.mixed_no_break_post = true;
1221 self
1222 }
1223
1224 pub(crate) const fn mixed_prev_space(mut self) -> Self {
1225 self.mixed_prev_space = true;
1226 self
1227 }
1228
1229 pub(crate) const fn mixed_post_nbsp(mut self) -> Self {
1230 self.mixed_post_nbsp = true;
1231 self
1232 }
1233
1234 pub(crate) const fn mixed_post_glued(mut self) -> Self {
1235 self.mixed_post_nbsp = true;
1236 self.mixed_post_glued = true;
1237 self
1238 }
1239
1240 pub(crate) fn hardbreak_if_not_bol(&self, is_bol: bool, p: &mut pp::Printer) {
1241 if self.offset != 0 && !is_bol {
1242 self.hardbreak(p);
1243 } else {
1244 p.hardbreak_if_not_bol();
1245 }
1246 }
1247
1248 pub(crate) fn hardbreak(&self, p: &mut pp::Printer) {
1249 p.break_offset(SIZE_INFINITY as usize, self.offset);
1250 }
1251
1252 pub(crate) fn space(&self, p: &mut pp::Printer) {
1253 p.break_offset(1, self.offset);
1254 }
1255
1256 pub(crate) fn nbsp_or_space(&self, breaks: bool, p: &mut pp::Printer) {
1257 if breaks {
1258 self.space(p);
1259 } else {
1260 p.nbsp();
1261 }
1262 }
1263
1264 pub(crate) fn zerobreak(&self, p: &mut pp::Printer) {
1265 p.break_offset(0, self.offset);
1266 }
1267}
1268
1269fn snippet_with_tabs(s: String, tab_width: usize) -> String {
1270 let mut formatted = String::with_capacity(s.len());
1271 for line in s.split_inclusive('\n') {
1272 let (line, has_newline) =
1273 line.strip_suffix('\n').map_or((line, false), |line| (line, true));
1274 let line = line.strip_suffix('\r').unwrap_or(line);
1275 line_with_tabs(&mut formatted, line, tab_width, None);
1276 if has_newline {
1277 formatted.push('\n');
1278 }
1279 }
1280
1281 formatted
1282}
1283
1284fn style_doc_comment(style: DocCommentStyle, mut cmnt: Comment) -> Comment {
1288 match style {
1289 DocCommentStyle::Line if cmnt.kind == ast::CommentKind::Block => {
1290 let mut new_lines = Vec::new();
1291 for (pos, line) in cmnt.lines.iter().delimited() {
1292 if pos.is_first || pos.is_last {
1293 continue;
1295 }
1296
1297 let trimmed = line.trim_start();
1299 if let Some(content) = trimmed.strip_prefix('*') {
1300 new_lines.push(format!("///{content}"));
1301 } else if !trimmed.is_empty() {
1302 new_lines.push(format!("/// {trimmed}"));
1303 }
1304 }
1305
1306 cmnt.lines = new_lines;
1307 cmnt.kind = ast::CommentKind::Line;
1308 cmnt
1309 }
1310 DocCommentStyle::Block if cmnt.kind == ast::CommentKind::Line => {
1311 let mut new_lines = vec!["/**".to_string()];
1312
1313 for line in &cmnt.lines {
1314 new_lines.push(format!(" *{content}", content = &line[3..]))
1316 }
1317
1318 new_lines.push(" */".to_string());
1319 cmnt.lines = new_lines;
1320 cmnt.kind = ast::CommentKind::Block;
1321 cmnt
1322 }
1323 _ => cmnt,
1325 }
1326}