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 pub(crate) fn next_comment(&mut self) -> Option<Comment> {
976 self.comments.next()
977 }
978
979 fn peek_trailing_comment<'b>(
980 &'b self,
981 span_pos: BytePos,
982 next_pos: Option<BytePos>,
983 ) -> Option<&'b Comment>
984 where
985 'sess: 'b,
986 {
987 self.comments.peek_trailing(self.sm, span_pos, next_pos).map(|(cmnt, _)| cmnt)
988 }
989
990 fn print_trailing_comment_inner(
991 &mut self,
992 span_pos: BytePos,
993 next_pos: Option<BytePos>,
994 config: Option<CommentConfig>,
995 ) -> bool {
996 let mut printed = 0;
997 if let Some((_, n)) = self.comments.peek_trailing(self.sm, span_pos, next_pos) {
998 let config =
999 config.unwrap_or(CommentConfig::skip_ws().mixed_no_break().mixed_prev_space());
1000 while printed <= n {
1001 let cmnt = self.comments.next().unwrap();
1002 if let Some(cmnt) = self.handle_comment(cmnt, config.trailing_no_break) {
1003 self.print_comment(cmnt, config);
1004 };
1005 printed += 1;
1006 }
1007 }
1008 printed != 0
1009 }
1010
1011 fn print_trailing_comment(&mut self, span_pos: BytePos, next_pos: Option<BytePos>) -> bool {
1012 self.print_trailing_comment_inner(span_pos, next_pos, None)
1013 }
1014
1015 fn print_trailing_comment_no_break(&mut self, span_pos: BytePos, next_pos: Option<BytePos>) {
1016 self.print_trailing_comment_inner(
1017 span_pos,
1018 next_pos,
1019 Some(CommentConfig::skip_ws().trailing_no_break().mixed_no_break().mixed_prev_space()),
1020 );
1021 }
1022
1023 fn print_remaining_comments(&mut self, skip_leading_ws: bool) {
1024 if self.peek_comment().is_none() && !self.is_bol_or_only_ind() {
1027 self.hardbreak();
1028 return;
1029 }
1030
1031 let mut is_leading = true;
1032 while let Some(cmnt) = self.next_comment() {
1033 if cmnt.style.is_blank() && skip_leading_ws && is_leading {
1034 continue;
1035 }
1036
1037 is_leading = false;
1038 if let Some(cmnt) = self.handle_comment(cmnt, false) {
1039 self.print_comment(cmnt, CommentConfig::default());
1040 } else if self.peek_comment().is_none() && !self.is_bol_or_only_ind() {
1041 self.hardbreak();
1042 }
1043 }
1044 }
1045}
1046
1047#[derive(Clone, Copy)]
1048enum Skip {
1049 All,
1050 Leading { resettable: bool },
1051 Trailing,
1052}
1053
1054#[derive(Default, Clone, Copy)]
1055pub(crate) struct CommentConfig {
1056 skip_blanks: Option<Skip>,
1058 offset: isize,
1059
1060 iso_no_break: bool,
1062 trailing_no_break: bool,
1064 mixed_prev_space: bool,
1066 mixed_post_nbsp: bool,
1067 mixed_no_break_prev: bool,
1068 mixed_no_break_post: bool,
1069}
1070
1071impl CommentConfig {
1072 pub(crate) fn skip_ws() -> Self {
1073 Self { skip_blanks: Some(Skip::All), ..Default::default() }
1074 }
1075
1076 pub(crate) fn skip_leading_ws(resettable: bool) -> Self {
1077 Self { skip_blanks: Some(Skip::Leading { resettable }), ..Default::default() }
1078 }
1079
1080 pub(crate) fn skip_trailing_ws() -> Self {
1081 Self { skip_blanks: Some(Skip::Trailing), ..Default::default() }
1082 }
1083
1084 pub(crate) const fn offset(mut self, off: isize) -> Self {
1085 self.offset = off;
1086 self
1087 }
1088
1089 pub(crate) const fn no_breaks(mut self) -> Self {
1090 self.iso_no_break = true;
1091 self.trailing_no_break = true;
1092 self.mixed_no_break_prev = true;
1093 self.mixed_no_break_post = true;
1094 self
1095 }
1096
1097 pub(crate) const fn trailing_no_break(mut self) -> Self {
1098 self.trailing_no_break = true;
1099 self
1100 }
1101
1102 pub(crate) const fn mixed_no_break(mut self) -> Self {
1103 self.mixed_no_break_prev = true;
1104 self.mixed_no_break_post = true;
1105 self
1106 }
1107
1108 pub(crate) const fn mixed_no_break_post(mut self) -> Self {
1109 self.mixed_no_break_post = true;
1110 self
1111 }
1112
1113 pub(crate) const fn mixed_prev_space(mut self) -> Self {
1114 self.mixed_prev_space = true;
1115 self
1116 }
1117
1118 pub(crate) const fn mixed_post_nbsp(mut self) -> Self {
1119 self.mixed_post_nbsp = true;
1120 self
1121 }
1122
1123 pub(crate) fn hardbreak_if_not_bol(&self, is_bol: bool, p: &mut pp::Printer) {
1124 if self.offset != 0 && !is_bol {
1125 self.hardbreak(p);
1126 } else {
1127 p.hardbreak_if_not_bol();
1128 }
1129 }
1130
1131 pub(crate) fn hardbreak(&self, p: &mut pp::Printer) {
1132 p.break_offset(SIZE_INFINITY as usize, self.offset);
1133 }
1134
1135 pub(crate) fn space(&self, p: &mut pp::Printer) {
1136 p.break_offset(1, self.offset);
1137 }
1138
1139 pub(crate) fn nbsp_or_space(&self, breaks: bool, p: &mut pp::Printer) {
1140 if breaks {
1141 self.space(p);
1142 } else {
1143 p.nbsp();
1144 }
1145 }
1146
1147 pub(crate) fn zerobreak(&self, p: &mut pp::Printer) {
1148 p.break_offset(0, self.offset);
1149 }
1150}
1151
1152fn snippet_with_tabs(s: String, tab_width: usize) -> String {
1153 let trimmed = s.trim_start_matches('\n');
1155 let num_breaks = s.len() - trimmed.len();
1156 let mut formatted = std::iter::repeat_n('\n', num_breaks).collect::<String>();
1157
1158 for (pos, line) in trimmed.lines().delimited() {
1160 line_with_tabs(&mut formatted, line, tab_width, None);
1161 if !pos.is_last {
1162 formatted.push('\n');
1163 }
1164 }
1165
1166 formatted
1167}
1168
1169fn style_doc_comment(style: DocCommentStyle, mut cmnt: Comment) -> Comment {
1173 match style {
1174 DocCommentStyle::Line if cmnt.kind == ast::CommentKind::Block => {
1175 let mut new_lines = Vec::new();
1176 for (pos, line) in cmnt.lines.iter().delimited() {
1177 if pos.is_first || pos.is_last {
1178 continue;
1180 }
1181
1182 let trimmed = line.trim_start();
1184 if let Some(content) = trimmed.strip_prefix('*') {
1185 new_lines.push(format!("///{content}"));
1186 } else if !trimmed.is_empty() {
1187 new_lines.push(format!("/// {trimmed}"));
1188 }
1189 }
1190
1191 cmnt.lines = new_lines;
1192 cmnt.kind = ast::CommentKind::Line;
1193 cmnt
1194 }
1195 DocCommentStyle::Block if cmnt.kind == ast::CommentKind::Line => {
1196 let mut new_lines = vec!["/**".to_string()];
1197
1198 for line in &cmnt.lines {
1199 new_lines.push(format!(" *{content}", content = &line[3..]))
1201 }
1202
1203 new_lines.push(" */".to_string());
1204 cmnt.lines = new_lines;
1205 cmnt.kind = ast::CommentKind::Block;
1206 cmnt
1207 }
1208 _ => cmnt,
1210 }
1211}