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