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