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, SourceMap},
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 sm: &'sess SourceMap,
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 sm: &'sess SourceMap,
212 start_pos: BytePos,
213 config: Arc<FormatterConfig>,
214 inline_config: InlineConfig<()>,
215 comments: Comments,
216 ) -> Self {
217 Self {
218 s: pp::Printer::new(
219 config.line_length,
220 matches!(config.style, IndentStyle::Tab).then(|| config.tab_width),
221 ),
222 ind: config.tab_width as isize,
223 sm,
224 comments,
225 config,
226 inline_config,
227 cursor: SourcePos { pos: start_pos, enabled: true },
228 has_crlf: false,
229 contract: None,
230 single_line_stmt: None,
231 call_with_opts_and_args: false,
232 chained_named_call: None,
233 skip_index_break: false,
234 binary_expr: None,
235 return_bin_expr: false,
236 emit_or_revert: false,
237 var_init: false,
238 block_depth: 0,
239 call_stack: CallStack::default(),
240 }
241 }
242
243 fn check_crlf(&mut self, span: Span) {
249 if let Ok(snip) = self.sm.span_to_snippet(span)
250 && snip.contains('\r')
251 {
252 self.has_crlf = true;
253 }
254 }
255
256 fn is_at_crlf(&self) -> bool {
259 self.has_crlf && self.char_at(self.cursor.pos) == Some('\r')
260 }
261
262 fn cursor_next_line(&mut self) {
268 if self.cursor.enabled || matches!(self.char_at(self.cursor.pos), Some('\n' | '\r')) {
269 self.cursor.next_line(self.is_at_crlf());
270 }
271 }
272
273 fn space_left(&self) -> usize {
275 std::cmp::min(self.s.space_left(), self.max_space_left(0))
276 }
277
278 fn max_space_left(&self, prefix_len: usize) -> usize {
281 self.config
282 .line_length
283 .saturating_sub(self.block_depth * self.config.tab_width + prefix_len)
284 }
285
286 fn break_offset_if_not_bol(&mut self, n: usize, off: isize, search: bool) {
287 if search {
290 self.find_and_replace_last_token_still_buffered(
293 pp::Printer::hardbreak_tok_offset(off),
294 |token| token.is_hardbreak(),
295 );
296 return;
297 }
298
299 if !self.is_beginning_of_line() {
301 self.break_offset(n, off)
302 } else if off != 0
303 && let Some(last_token) = self.last_token_still_buffered()
304 && last_token.is_hardbreak()
305 {
306 self.replace_last_token_still_buffered(pp::Printer::hardbreak_tok_offset(off));
309 }
310 }
311
312 fn braces_break(&mut self) {
313 if self.config.bracket_spacing {
314 self.space();
315 } else {
316 self.zerobreak();
317 }
318 }
319}
320
321impl State<'_, '_> {
323 fn char_at(&self, pos: BytePos) -> Option<char> {
324 let res = self.sm.lookup_byte_offset(pos);
325 res.sf.src.get(res.pos.to_usize()..)?.chars().next()
326 }
327
328 fn find_opening_brace(&self, span: Span) -> Option<BytePos> {
330 let snip = self.sm.span_to_snippet(span).ok()?;
331 let mut idx = 0;
332 while idx < snip.len() {
333 let rest = &snip[idx..];
334 if rest.starts_with('{') {
335 return Some(span.lo() + idx as u32);
336 }
337 idx += if let Some(line) = rest.strip_prefix("//") {
338 2 + line.find('\n').unwrap_or(line.len())
339 } else if let Some(block) = rest.strip_prefix("/*") {
340 2 + block.find("*/").map_or(block.len(), |end| end + 2)
341 } else {
342 rest.chars().next().map_or(1, char::len_utf8)
343 };
344 }
345 None
346 }
347
348 fn print_span(&mut self, span: Span) {
349 match self.sm.span_to_snippet(span) {
350 Ok(s) => self.s.word(if matches!(self.config.style, IndentStyle::Tab) {
351 snippet_with_tabs(s, self.config.tab_width)
352 } else {
353 s
354 }),
355 Err(e) => panic!("failed to print {span:?}: {e:#?}"),
356 }
357 while let Some(cmnt) = self.peek_comment() {
359 if cmnt.pos() >= span.hi() {
360 break;
361 }
362 let _ = self.next_comment().unwrap();
363 }
364 self.cursor.advance_to(span.hi(), false);
366 }
367
368 #[must_use]
370 fn handle_span(&mut self, span: Span, skip_prev_cmnts: bool) -> bool {
371 if !skip_prev_cmnts {
372 self.print_comments(span.lo(), CommentConfig::default());
373 }
374 self.print_span_if_disabled(span)
375 }
376
377 #[inline]
379 #[must_use]
380 fn print_span_if_disabled(&mut self, span: Span) -> bool {
381 let cursor_span = self.cursor.span(span.hi());
382 if self.inline_config.is_disabled(cursor_span) {
383 self.print_span_cold(cursor_span);
384 return true;
385 }
386 if self.inline_config.is_disabled(span) {
387 self.print_span_cold(span);
388 return true;
389 }
390 false
391 }
392
393 #[cold]
394 fn print_span_cold(&mut self, span: Span) {
395 self.print_span(span);
396 }
397
398 fn print_tokens(&mut self, tokens: &[token::Token]) {
399 let span = Span::join_first_last(tokens.iter().map(|t| t.span));
401 self.print_span(span);
402 }
403
404 fn print_word(&mut self, w: impl Into<Cow<'static, str>>) {
405 let cow = w.into();
406 self.cursor.advance(cow.len() as u32);
407 self.word(cow);
408 }
409
410 fn print_sep(&mut self, sep: Separator) {
411 if self.handle_span(
412 self.cursor.span(self.cursor.pos + if self.is_at_crlf() { 2 } else { 1 }),
413 true,
414 ) {
415 return;
416 }
417
418 self.print_sep_unhandled(sep);
419 }
420
421 fn print_sep_unhandled(&mut self, sep: Separator) {
422 sep.print(&mut self.s);
423 self.cursor_next_line();
424 }
425
426 fn print_ident(&mut self, ident: &ast::Ident) {
427 if self.handle_span(ident.span, true) {
428 return;
429 }
430
431 self.print_comments(ident.span.lo(), CommentConfig::skip_ws());
432 self.word(ident.to_string());
433 }
434
435 fn print_inside_parens<F>(&mut self, f: F)
436 where
437 F: FnOnce(&mut Self),
438 {
439 self.print_word("(");
440 f(self);
441 self.print_word(")");
442 }
443
444 fn estimate_size(&self, span: Span) -> usize {
445 if let Ok(snip) = self.sm.span_to_snippet(span) {
446 let (mut size, mut first, mut prev_needs_space) = (0, true, false);
447
448 for line in snip.lines() {
449 let line = line.trim();
450
451 if prev_needs_space {
452 size += 1;
453 } else if !first && let Some(char) = line.chars().next() {
454 match char {
459 '&' | '|' | '=' | '>' | '<' | '+' | '-' | '*' | '/' | '%' | '^' | '?'
460 | ':' => size += 1,
461 '}' | ')' | ']' if self.config.bracket_spacing => size += 1,
462 _ => (),
463 }
464 }
465 first = false;
466
467 let mut search = line;
469 loop {
470 if let Some((lhs, comment)) = search.split_once(r#"/*"#) {
471 size += lhs.trim_end().len() + 2;
472 search = comment;
473 } else if let Some((comment, rhs)) = search.split_once(r#"*/"#) {
474 size += comment.len() + 2;
475 search = rhs;
476 } else {
477 size += search.trim().len();
478 break;
479 }
480 }
481
482 prev_needs_space = match line.chars().next_back() {
487 Some('[' | '(' | '{') => self.config.bracket_spacing,
488 Some(',' | ';') => true,
489 _ => false,
490 };
491 }
492 return size;
493 }
494
495 span.to_range().len()
496 }
497
498 fn same_source_line(&self, a: BytePos, b: BytePos) -> bool {
499 self.sm.lookup_char_pos(a).line == self.sm.lookup_char_pos(b).line
500 }
501}
502
503impl<'sess> State<'sess, '_> {
505 #[must_use]
507 fn handle_comment(&mut self, cmnt: Comment, skip_break: bool) -> Option<Comment> {
508 if self.cursor.enabled {
509 if self.inline_config.is_disabled(cmnt.span) {
510 if cmnt.style.is_trailing() && !self.last_token_is_space() {
511 self.nbsp();
512 }
513 self.print_span_cold(cmnt.span);
514 if !skip_break && (cmnt.style.is_isolated() || cmnt.style.is_trailing()) {
515 self.print_sep(Separator::Hardbreak);
516 }
517 return None;
518 }
519 } else if self.print_span_if_disabled(cmnt.span) {
520 if !skip_break && (cmnt.style.is_isolated() || cmnt.style.is_trailing()) {
521 self.print_sep(Separator::Hardbreak);
522 }
523 return None;
524 }
525 Some(cmnt)
526 }
527
528 fn cmnt_config(&self) -> CommentConfig {
529 Default::default()
530 }
531
532 const fn print_docs(&mut self, docs: &'_ ast::DocComments<'_>) {
533 let _ = docs;
535 }
536
537 fn print_comments(&mut self, pos: BytePos, mut config: CommentConfig) -> Option<CommentStyle> {
542 let mut last_style: Option<CommentStyle> = None;
543 let mut is_leading = true;
544 let config_cache = config;
545 let mut buffered_blank = None;
546 while self.peek_comment().is_some_and(|c| c.pos() < pos) {
547 let mut cmnt = self.next_comment().unwrap();
548 let style_cache = cmnt.style;
549
550 if self.config.docs_style == foundry_config::fmt::DocCommentStyle::Block
552 && cmnt.is_doc
553 && cmnt.kind == ast::CommentKind::Line
554 {
555 let mut ref_line = self.sm.lookup_char_pos(cmnt.span.hi()).line;
556 while let Some(next_cmnt) = self.peek_comment() {
557 if !next_cmnt.is_doc
558 || next_cmnt.kind != ast::CommentKind::Line
559 || ref_line + 1 != self.sm.lookup_char_pos(next_cmnt.span.lo()).line
560 {
561 break;
562 }
563
564 let next_to_merge = self.next_comment().unwrap();
565 cmnt.lines.extend(next_to_merge.lines);
566 cmnt.span = cmnt.span.to(next_to_merge.span);
567 ref_line += 1;
568 }
569 }
570
571 if self.peek_comment_before(pos).is_some() {
573 config.iso_no_break = false;
574 config.trailing_no_break = false;
575 }
576
577 let Some(cmnt) = self.handle_comment(
579 cmnt,
580 if style_cache.is_isolated() {
581 config.iso_no_break
582 } else {
583 config.trailing_no_break
584 },
585 ) else {
586 last_style = Some(style_cache);
587 continue;
588 };
589
590 if cmnt.style.is_blank() {
591 match config.skip_blanks {
592 Some(Skip::All) => continue,
593 Some(Skip::Leading { resettable: true }) if is_leading => continue,
594 Some(Skip::Leading { resettable: false }) if last_style.is_none() => continue,
595 Some(Skip::Trailing) => {
596 buffered_blank = Some(cmnt);
597 continue;
598 }
599 _ => (),
600 }
601 } else if !cmnt.is_doc {
603 is_leading = false;
604 }
605
606 if let Some(blank) = buffered_blank.take() {
607 self.print_comment(blank, config);
608 }
609
610 if cmnt.style.is_mixed() {
612 if let Some(cmnt) = self.peek_comment_before(pos) {
613 config.mixed_no_break_prev = true;
614 config.mixed_no_break_post = true;
615 config.mixed_post_nbsp = false;
616 if cmnt.style.is_mixed() {
620 config = config.mixed_post_glued();
621 }
622 }
623
624 if last_style.is_some_and(|s| s.is_mixed()) {
626 config.mixed_no_break_prev = true;
627 config.mixed_no_break_post = true;
628 config.mixed_prev_space = false;
629 }
630 } else if config.offset != 0
631 && cmnt.style.is_isolated()
632 && last_style.is_some_and(|s| s.is_isolated())
633 {
634 self.offset(config.offset);
635 }
636
637 last_style = Some(cmnt.style);
638 self.print_comment(cmnt, config);
639 config = config_cache;
640 }
641 last_style
642 }
643
644 fn print_wrapped_line(
646 &mut self,
647 line: &str,
648 prefix: &'static str,
649 break_offset: isize,
650 is_doc: bool,
651 ) {
652 if !line.starts_with(prefix) {
653 self.word(line.to_owned());
654 return;
655 }
656
657 fn post_break_prefix(prefix: &'static str, has_content: bool) -> &'static str {
658 if !has_content {
659 return prefix;
660 }
661 match prefix {
662 "///" => "/// ",
663 "//" => "// ",
664 "/*" => "/* ",
665 " *" => " * ",
666 _ => prefix,
667 }
668 }
669
670 self.ibox(0);
671 self.word(prefix);
672
673 let content = &line[prefix.len()..];
674 let content = if is_doc {
675 let ws_len = content
677 .char_indices()
678 .take_while(|(_, c)| c.is_whitespace())
679 .last()
680 .map_or(0, |(idx, c)| idx + c.len_utf8());
681 let (leading_ws, rest) = content.split_at(ws_len);
682 if !leading_ws.is_empty() {
683 self.word(leading_ws.to_owned());
684 }
685 rest
686 } else {
687 if let Some(first_char) = content.chars().next() {
689 if first_char.is_whitespace() {
690 self.nbsp();
691 &content[first_char.len_utf8()..]
692 } else {
693 content
694 }
695 } else {
696 ""
697 }
698 };
699
700 let post_break = post_break_prefix(prefix, !content.is_empty());
701
702 let (mut chars, mut current_word) = (content.chars().peekable(), String::new());
704 while let Some(ch) = chars.next() {
705 if ch.is_whitespace() {
706 if !current_word.is_empty() {
708 self.word(std::mem::take(&mut current_word));
709 }
710
711 let mut ws_count = 1;
713 while chars.peek().is_some_and(|c| c.is_whitespace()) {
714 ws_count += 1;
715 chars.next();
716 }
717 self.s.scan_break(BreakToken {
718 offset: break_offset,
719 blank_space: ws_count,
720 post_break: if post_break.starts_with("/*") { None } else { Some(post_break) },
721 ..Default::default()
722 });
723 continue;
724 }
725
726 current_word.push(ch);
727 }
728
729 if !current_word.is_empty() {
731 self.word(current_word);
732 }
733
734 self.end();
735 }
736
737 fn merge_comment_lines(&self, lines: &[String], prefix: &str) -> Vec<String> {
739 if lines.is_empty() || lines.len() < 2 || !prefix.starts_with("//") {
741 return lines.to_vec();
742 }
743
744 let mut result = Vec::new();
745 let mut i = 0;
746
747 while i < lines.len() {
748 let current_line = &lines[i];
749
750 if current_line.trim().is_empty() || !current_line.starts_with(prefix) {
752 result.push(current_line.clone());
753 i += 1;
754 continue;
755 }
756
757 if i + 1 < lines.len() {
758 let next_line = &lines[i + 1];
759
760 if next_line.starts_with(prefix) && !next_line.trim().is_empty() {
762 let next_content = next_line[prefix.len()..].trim_start();
763
764 if next_content.starts_with('@') {
767 result.push(current_line.clone());
768 i += 1;
769 continue;
770 }
771
772 if estimate_line_width(current_line, self.config.tab_width) > self.space_left()
774 {
775 let merged_line = format!("{current_line} {next_content}");
777 result.push(merged_line);
778
779 i += 2;
781 continue;
782 }
783 }
784 }
785
786 result.push(current_line.clone());
788 i += 1;
789 }
790
791 result
792 }
793
794 fn print_comment(&mut self, mut cmnt: Comment, mut config: CommentConfig) {
795 self.cursor.advance_to(cmnt.span.hi(), true);
796
797 if cmnt.is_doc {
798 cmnt = style_doc_comment(self.config.docs_style, cmnt);
799 }
800
801 match cmnt.style {
802 CommentStyle::Mixed => {
803 let Some(prefix) = cmnt.prefix() else { return };
804 let never_break = self.last_token_is_neverbreak();
805 if !self.is_bol_or_only_ind() {
806 match (never_break || config.mixed_no_break_prev, config.mixed_prev_space) {
807 (false, true) => config.space(&mut self.s),
808 (false, false) => config.zerobreak(&mut self.s),
809 (true, true) => self.nbsp(),
810 (true, false) => (),
811 };
812 }
813 if self.config.wrap_comments {
814 let merged_lines = self.merge_comment_lines(&cmnt.lines, prefix);
816 for (pos, line) in merged_lines.into_iter().delimited() {
817 self.print_wrapped_line(&line, prefix, 0, cmnt.is_doc);
818 if !pos.is_last {
819 self.hardbreak();
820 }
821 }
822 } else {
823 for (pos, line) in cmnt.lines.into_iter().delimited() {
825 self.word(line);
826 if !pos.is_last {
827 self.hardbreak();
828 }
829 }
830 }
831 if config.mixed_post_nbsp {
832 if config.mixed_post_glued {
833 self.nbsp();
834 } else {
835 config.nbsp_or_space(self.config.wrap_comments, &mut self.s);
836 }
837 self.cursor.advance(1);
838 } else if !config.mixed_no_break_post {
839 config.space(&mut self.s);
840 self.cursor.advance(1);
841 }
842 }
843 CommentStyle::Isolated => {
844 let Some(mut prefix) = cmnt.prefix() else { return };
845 if !config.iso_no_break {
846 config.hardbreak_if_not_bol(self.is_bol_or_only_ind(), &mut self.s);
847 }
848
849 if self.config.wrap_comments {
850 let merged_lines = self.merge_comment_lines(&cmnt.lines, prefix);
852 for (pos, line) in merged_lines.into_iter().delimited() {
853 let hb = |this: &mut Self| {
854 this.hardbreak();
855 if pos.is_last {
856 this.cursor.next_line(this.is_at_crlf());
857 }
858 };
859 if line.is_empty() {
860 hb(self);
861 continue;
862 }
863 if pos.is_first {
864 self.ibox(config.offset);
865 if cmnt.is_doc && matches!(prefix, "/**") {
866 self.word(prefix);
867 hb(self);
868 prefix = " * ";
869 continue;
870 }
871 }
872
873 self.print_wrapped_line(&line, prefix, 0, cmnt.is_doc);
874
875 if pos.is_last {
876 self.end();
877 if !config.iso_no_break {
878 hb(self);
879 }
880 } else {
881 hb(self);
882 }
883 }
884 } else {
885 for (pos, line) in cmnt.lines.into_iter().delimited() {
887 let hb = |this: &mut Self| {
888 this.hardbreak();
889 if pos.is_last {
890 this.cursor.next_line(this.is_at_crlf());
891 }
892 };
893 if line.is_empty() {
894 hb(self);
895 continue;
896 }
897 if pos.is_first {
898 self.ibox(config.offset);
899 if cmnt.is_doc && matches!(prefix, "/**") {
900 self.word(prefix);
901 hb(self);
902 prefix = " * ";
903 continue;
904 }
905 }
906
907 self.word(line);
908
909 if pos.is_last {
910 self.end();
911 if !config.iso_no_break {
912 hb(self);
913 }
914 } else {
915 hb(self);
916 }
917 }
918 }
919 }
920 CommentStyle::Trailing => {
921 let Some(prefix) = cmnt.prefix() else { return };
922 self.neverbreak();
923 if !self.is_bol_or_only_ind() {
924 self.nbsp();
925 }
926
927 if !self.config.wrap_comments && cmnt.lines.len() == 1 {
928 self.word(cmnt.lines.pop().unwrap());
929 } else if self.config.wrap_comments {
930 if cmnt.is_doc || matches!(cmnt.kind, ast::CommentKind::Line) {
931 config.offset = 0;
932 } else {
933 config.offset = self.ind;
934 }
935 for (lpos, line) in cmnt.lines.into_iter().delimited() {
936 if !line.is_empty() {
937 self.print_wrapped_line(&line, prefix, config.offset, cmnt.is_doc);
938 }
939 if !lpos.is_last {
940 config.hardbreak(&mut self.s);
941 }
942 }
943 } else {
944 self.visual_align();
945 for (pos, line) in cmnt.lines.into_iter().delimited() {
946 if !line.is_empty() {
947 self.word(line);
948 if !pos.is_last {
949 self.hardbreak();
950 }
951 }
952 }
953 self.end();
954 }
955
956 if !config.trailing_no_break {
957 self.print_sep(Separator::Hardbreak);
958 }
959 }
960
961 CommentStyle::BlankLine => {
962 if !self.last_token_is_break() && !self.is_bol_or_only_ind() {
964 config.hardbreak(&mut self.s);
965 self.cursor.next_line(self.is_at_crlf());
966 }
967
968 let twice = match self.last_token() {
970 Some(Token::String(s)) => ";" == s,
971 Some(Token::Begin(_)) => true,
972 Some(Token::End) => true,
973 _ => false,
974 };
975 if twice {
976 config.hardbreak(&mut self.s);
977 self.cursor.next_line(self.is_at_crlf());
978 }
979 config.hardbreak(&mut self.s);
980 self.cursor.next_line(self.is_at_crlf());
981 }
982 }
983 }
984
985 fn peek_comment<'b>(&'b self) -> Option<&'b Comment>
986 where
987 'sess: 'b,
988 {
989 self.comments.peek()
990 }
991
992 fn peek_comment_before<'b>(&'b self, pos: BytePos) -> Option<&'b Comment>
993 where
994 'sess: 'b,
995 {
996 self.comments.iter().take_while(|c| c.pos() < pos).find(|c| !c.style.is_blank())
997 }
998
999 fn peek_mixed_comment_before(&self, pos: Option<BytePos>) -> bool {
1002 pos.is_some_and(|pos| {
1003 self.peek_comment().is_some_and(|cmnt| cmnt.pos() < pos && cmnt.style.is_mixed())
1004 })
1005 }
1006
1007 fn has_comment_before_with<F>(&self, pos: BytePos, f: F) -> bool
1008 where
1009 F: FnMut(&Comment) -> bool,
1010 {
1011 self.comments.iter().take_while(|c| c.pos() < pos).any(f)
1012 }
1013
1014 fn peek_comment_between<'b>(&'b self, pos_lo: BytePos, pos_hi: BytePos) -> Option<&'b Comment>
1015 where
1016 'sess: 'b,
1017 {
1018 self.comments
1019 .iter()
1020 .skip_while(|c| c.pos() < pos_lo)
1021 .take_while(|c| c.pos() < pos_hi)
1022 .find(|c| !c.style.is_blank())
1023 }
1024
1025 fn has_comment_between(&self, start_pos: BytePos, end_pos: BytePos) -> bool {
1026 self.comments.iter().filter(|c| c.pos() > start_pos && c.pos() < end_pos).any(|_| true)
1027 }
1028
1029 fn has_breakable_comment_between(&self, start_pos: BytePos, end_pos: BytePos) -> bool {
1030 self.comments
1031 .iter()
1032 .filter(|comment| comment.pos() >= start_pos && comment.pos() < end_pos)
1033 .any(|comment| !comment.style.is_blank())
1034 }
1035
1036 pub(crate) fn next_comment(&mut self) -> Option<Comment> {
1037 self.comments.next()
1038 }
1039
1040 fn peek_trailing_comment<'b>(
1041 &'b self,
1042 span_pos: BytePos,
1043 next_pos: Option<BytePos>,
1044 ) -> Option<&'b Comment>
1045 where
1046 'sess: 'b,
1047 {
1048 self.comments.peek_trailing(self.sm, span_pos, next_pos).map(|(cmnt, _)| cmnt)
1049 }
1050
1051 fn print_trailing_comment_inner(
1052 &mut self,
1053 span_pos: BytePos,
1054 next_pos: Option<BytePos>,
1055 config: Option<CommentConfig>,
1056 ) -> bool {
1057 let mut printed = 0;
1058 if let Some((_, n)) = self.comments.peek_trailing(self.sm, span_pos, next_pos) {
1059 let config =
1060 config.unwrap_or(CommentConfig::skip_ws().mixed_no_break().mixed_prev_space());
1061 while printed <= n {
1062 let cmnt = self.comments.next().unwrap();
1063 if let Some(cmnt) = self.handle_comment(cmnt, config.trailing_no_break) {
1064 self.print_comment(cmnt, config);
1065 };
1066 printed += 1;
1067 }
1068 }
1069 printed != 0
1070 }
1071
1072 fn print_trailing_comment(&mut self, span_pos: BytePos, next_pos: Option<BytePos>) -> bool {
1073 self.print_trailing_comment_inner(span_pos, next_pos, None)
1074 }
1075
1076 fn print_trailing_comment_no_break(&mut self, span_pos: BytePos, next_pos: Option<BytePos>) {
1077 self.print_trailing_comment_inner(
1078 span_pos,
1079 next_pos,
1080 Some(CommentConfig::skip_ws().trailing_no_break().mixed_no_break().mixed_prev_space()),
1081 );
1082 }
1083
1084 fn print_remaining_comments(&mut self, skip_leading_ws: bool) {
1085 if self.peek_comment().is_none() && !self.is_bol_or_only_ind() {
1088 self.hardbreak();
1089 return;
1090 }
1091
1092 let mut is_leading = true;
1093 while let Some(cmnt) = self.next_comment() {
1094 if cmnt.style.is_blank() && skip_leading_ws && is_leading {
1095 continue;
1096 }
1097
1098 is_leading = false;
1099 if let Some(cmnt) = self.handle_comment(cmnt, false) {
1100 self.print_comment(cmnt, CommentConfig::default());
1101 } else if self.peek_comment().is_none() && !self.is_bol_or_only_ind() {
1102 self.hardbreak();
1103 }
1104 }
1105 }
1106}
1107
1108#[derive(Clone, Copy)]
1109enum Skip {
1110 All,
1111 Leading { resettable: bool },
1112 Trailing,
1113}
1114
1115#[derive(Default, Clone, Copy)]
1116pub(crate) struct CommentConfig {
1117 skip_blanks: Option<Skip>,
1119 offset: isize,
1120
1121 iso_no_break: bool,
1123 trailing_no_break: bool,
1125 mixed_prev_space: bool,
1127 mixed_post_nbsp: bool,
1128 mixed_post_glued: bool,
1132 mixed_no_break_prev: bool,
1133 mixed_no_break_post: bool,
1134}
1135
1136impl CommentConfig {
1137 pub(crate) fn skip_ws() -> Self {
1138 Self { skip_blanks: Some(Skip::All), ..Default::default() }
1139 }
1140
1141 pub(crate) fn empty_block() -> Self {
1144 Self::skip_ws().mixed_no_break().mixed_prev_space().mixed_post_glued()
1145 }
1146
1147 pub(crate) fn skip_leading_ws(resettable: bool) -> Self {
1148 Self { skip_blanks: Some(Skip::Leading { resettable }), ..Default::default() }
1149 }
1150
1151 pub(crate) fn skip_trailing_ws() -> Self {
1152 Self { skip_blanks: Some(Skip::Trailing), ..Default::default() }
1153 }
1154
1155 pub(crate) const fn offset(mut self, off: isize) -> Self {
1156 self.offset = off;
1157 self
1158 }
1159
1160 pub(crate) const fn no_breaks(mut self) -> Self {
1161 self.iso_no_break = true;
1162 self.trailing_no_break = true;
1163 self.mixed_no_break_prev = true;
1164 self.mixed_no_break_post = true;
1165 self
1166 }
1167
1168 pub(crate) const fn trailing_no_break(mut self) -> Self {
1169 self.trailing_no_break = true;
1170 self
1171 }
1172
1173 pub(crate) const fn mixed_no_break(mut self) -> Self {
1174 self.mixed_no_break_prev = true;
1175 self.mixed_no_break_post = true;
1176 self
1177 }
1178
1179 pub(crate) const fn mixed_no_break_post(mut self) -> Self {
1180 self.mixed_no_break_post = true;
1181 self
1182 }
1183
1184 pub(crate) const fn mixed_prev_space(mut self) -> Self {
1185 self.mixed_prev_space = true;
1186 self
1187 }
1188
1189 pub(crate) const fn mixed_post_nbsp(mut self) -> Self {
1190 self.mixed_post_nbsp = true;
1191 self
1192 }
1193
1194 pub(crate) const fn mixed_post_glued(mut self) -> Self {
1195 self.mixed_post_nbsp = true;
1196 self.mixed_post_glued = true;
1197 self
1198 }
1199
1200 pub(crate) fn hardbreak_if_not_bol(&self, is_bol: bool, p: &mut pp::Printer) {
1201 if self.offset != 0 && !is_bol {
1202 self.hardbreak(p);
1203 } else {
1204 p.hardbreak_if_not_bol();
1205 }
1206 }
1207
1208 pub(crate) fn hardbreak(&self, p: &mut pp::Printer) {
1209 p.break_offset(SIZE_INFINITY as usize, self.offset);
1210 }
1211
1212 pub(crate) fn space(&self, p: &mut pp::Printer) {
1213 p.break_offset(1, self.offset);
1214 }
1215
1216 pub(crate) fn nbsp_or_space(&self, breaks: bool, p: &mut pp::Printer) {
1217 if breaks {
1218 self.space(p);
1219 } else {
1220 p.nbsp();
1221 }
1222 }
1223
1224 pub(crate) fn zerobreak(&self, p: &mut pp::Printer) {
1225 p.break_offset(0, self.offset);
1226 }
1227}
1228
1229fn snippet_with_tabs(s: String, tab_width: usize) -> String {
1230 let trimmed = s.trim_start_matches('\n');
1232 let num_breaks = s.len() - trimmed.len();
1233 let mut formatted = std::iter::repeat_n('\n', num_breaks).collect::<String>();
1234
1235 for (pos, line) in trimmed.lines().delimited() {
1237 line_with_tabs(&mut formatted, line, tab_width, None);
1238 if !pos.is_last {
1239 formatted.push('\n');
1240 }
1241 }
1242
1243 formatted
1244}
1245
1246fn style_doc_comment(style: DocCommentStyle, mut cmnt: Comment) -> Comment {
1250 match style {
1251 DocCommentStyle::Line if cmnt.kind == ast::CommentKind::Block => {
1252 let mut new_lines = Vec::new();
1253 for (pos, line) in cmnt.lines.iter().delimited() {
1254 if pos.is_first || pos.is_last {
1255 continue;
1257 }
1258
1259 let trimmed = line.trim_start();
1261 if let Some(content) = trimmed.strip_prefix('*') {
1262 new_lines.push(format!("///{content}"));
1263 } else if !trimmed.is_empty() {
1264 new_lines.push(format!("/// {trimmed}"));
1265 }
1266 }
1267
1268 cmnt.lines = new_lines;
1269 cmnt.kind = ast::CommentKind::Line;
1270 cmnt
1271 }
1272 DocCommentStyle::Block if cmnt.kind == ast::CommentKind::Line => {
1273 let mut new_lines = vec!["/**".to_string()];
1274
1275 for line in &cmnt.lines {
1276 new_lines.push(format!(" *{content}", content = &line[3..]))
1278 }
1279
1280 new_lines.push(" */".to_string());
1281 cmnt.lines = new_lines;
1282 cmnt.kind = ast::CommentKind::Block;
1283 cmnt
1284 }
1285 _ => cmnt,
1287 }
1288}