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