1use crate::errors::*;
2use crate::util::{self, sub_chars};
3use crate::{Range, Span};
4
5use fancy_regex::Regex;
6use lazy_static::lazy_static;
7use proc_macro2::LineColumn;
8
9use std::fmt;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum CommentVariantCategory {
14 Doc,
16 Dev,
18 CommonMark,
20 Unmergable,
22}
23
24#[derive(Debug, Clone, Hash, Eq, PartialEq)]
26#[non_exhaustive]
27pub enum CommentVariant {
28 TripleSlash,
30 DoubleSlashEM,
32 SlashAsteriskEM,
34 SlashAsteriskAsterisk,
36 SlashAsterisk,
38 MacroDocEqStr(String, usize),
41 MacroDocEqMacro,
44 CommonMark,
46 DoubleSlash,
48 SlashStar,
50 Unknown,
52 TomlEntry,
54}
55
56impl Default for CommentVariant {
57 fn default() -> Self {
58 CommentVariant::Unknown
59 }
60}
61
62impl CommentVariant {
63 pub fn category(&self) -> CommentVariantCategory {
65 match self {
66 Self::TripleSlash => CommentVariantCategory::Doc,
67 Self::DoubleSlashEM => CommentVariantCategory::Doc,
68 Self::MacroDocEqStr(_, _) => CommentVariantCategory::Doc,
69 Self::MacroDocEqMacro => CommentVariantCategory::Doc,
70 Self::SlashAsteriskEM => CommentVariantCategory::Doc,
71 Self::SlashAsteriskAsterisk => CommentVariantCategory::Doc,
72 Self::CommonMark => CommentVariantCategory::CommonMark,
73 Self::TomlEntry => CommentVariantCategory::Unmergable,
74 _ => CommentVariantCategory::Dev,
75 }
76 }
77 pub fn prefix_string(&self) -> String {
81 match self {
82 CommentVariant::TripleSlash => "///".into(),
83 CommentVariant::DoubleSlashEM => "//!".into(),
84 CommentVariant::MacroDocEqMacro => "".into(),
85 CommentVariant::MacroDocEqStr(d, p) => {
86 let raw = match p {
87 0 => "\"".to_owned(),
90 x => format!("r{}\"", "#".repeat(x.saturating_sub(1))),
91 };
92 format!(r#"{d}{raw}"#)
93 }
94 CommentVariant::CommonMark => "".to_string(),
95 CommentVariant::DoubleSlash => "//".to_string(),
96 CommentVariant::SlashStar => "/*".to_string(),
97 CommentVariant::SlashAsterisk => "/*".to_string(),
98 CommentVariant::SlashAsteriskEM => "/*!".to_string(),
99 CommentVariant::SlashAsteriskAsterisk => "/**".to_string(),
100 CommentVariant::TomlEntry => "".to_owned(),
101 unhandled => {
102 unreachable!("String representation for comment variant {unhandled:?} exists. qed")
103 }
104 }
105 }
106 pub fn prefix_len(&self) -> usize {
110 match self {
111 CommentVariant::TripleSlash | CommentVariant::DoubleSlashEM => 3,
112 CommentVariant::MacroDocEqMacro => 0,
113 CommentVariant::MacroDocEqStr(d, p) => d.len() + *p + 1,
114 CommentVariant::SlashAsterisk => 2,
115 CommentVariant::SlashAsteriskEM | CommentVariant::SlashAsteriskAsterisk => 3,
116 _ => self.prefix_string().len(),
117 }
118 }
119
120 pub fn suffix_len(&self) -> usize {
122 match self {
123 CommentVariant::MacroDocEqStr(_, 0) => 2,
124 CommentVariant::MacroDocEqStr(_, p) => p + 1,
125 CommentVariant::SlashAsteriskAsterisk
126 | CommentVariant::SlashAsteriskEM
127 | CommentVariant::SlashAsterisk => 2,
128 CommentVariant::MacroDocEqMacro => 0,
129 _ => 0,
130 }
131 }
132
133 pub fn suffix_string(&self) -> String {
135 match self {
136 CommentVariant::MacroDocEqStr(_, p) if *p == 0 || *p == 1 => r#""]"#.to_string(),
137 CommentVariant::MacroDocEqStr(_, p) => {
138 r#"""#.to_string() + &"#".repeat(p.saturating_sub(1)) + "]"
139 }
140 CommentVariant::SlashAsteriskAsterisk
141 | CommentVariant::SlashAsteriskEM
142 | CommentVariant::SlashAsterisk => "*/".to_string(),
143 _ => "".to_string(),
144 }
145 }
146}
147
148#[derive(Clone)]
150pub struct TrimmedLiteral {
151 variant: CommentVariant,
153 span: Span,
155 rendered: String,
157 pre: usize,
159 post: usize,
161 len_in_chars: usize,
163 len_in_bytes: usize,
164}
165
166impl std::cmp::PartialEq for TrimmedLiteral {
167 fn eq(&self, other: &Self) -> bool {
168 if self.rendered != other.rendered {
169 return false;
170 }
171 if self.pre != other.pre {
172 return false;
173 }
174 if self.post != other.post {
175 return false;
176 }
177 if self.len() != other.len() {
178 return false;
179 }
180 if self.span != other.span {
181 return false;
182 }
183 if self.variant != other.variant {
184 return false;
185 }
186
187 true
188 }
189}
190
191impl std::cmp::Eq for TrimmedLiteral {}
192
193impl std::hash::Hash for TrimmedLiteral {
194 fn hash<H: std::hash::Hasher>(&self, hasher: &mut H) {
195 self.variant.hash(hasher);
196 self.rendered.hash(hasher);
197 self.span.hash(hasher);
198 self.pre.hash(hasher);
199 self.post.hash(hasher);
200 self.len_in_bytes.hash(hasher);
201 self.len_in_chars.hash(hasher);
202 }
203}
204
205fn trim_span(content: &str, span: &mut Span, pre: usize, post: usize) {
207 span.start.column += pre;
208 if span.end.column >= post {
209 span.end.column -= post;
210 } else {
211 let previous_line_length = content
213 .chars()
214 .rev()
215 .skip(post + 1)
217 .take_while(|c| *c != '\n')
218 .count();
219 span.end = LineColumn {
220 line: span.end.line - 1,
221 column: previous_line_length,
222 };
223 }
224}
225
226fn detect_comment_variant(
232 content: &str,
233 rendered: &String,
234 mut span: Span,
235) -> Result<(CommentVariant, Span, usize, usize)> {
236 let prefix_span = Span {
237 start: crate::LineColumn {
238 line: span.start.line,
239 column: 0,
240 },
241 end: crate::LineColumn {
242 line: span.start.line,
243 column: span.start.column.saturating_sub(1),
244 },
245 };
246 let prefix = util::load_span_from(content.as_bytes(), prefix_span)?
247 .trim_start()
248 .to_string();
249
250 let (variant, span, pre, post) = if rendered.starts_with("///") || rendered.starts_with("//!") {
251 let pre = 3; let post = 0; span.start.column += pre;
255
256 assert_eq!(span.start.line, span.end.line);
258 let variant = if rendered.starts_with("///") {
265 CommentVariant::TripleSlash
266 } else {
267 CommentVariant::DoubleSlashEM
268 };
269
270 (variant, span, pre, post)
271 } else if rendered.starts_with("/*") && rendered.ends_with("*/") {
272 let variant = if rendered.starts_with("/*!") {
273 CommentVariant::SlashAsteriskEM
274 } else if rendered.starts_with("/**") {
275 CommentVariant::SlashAsteriskAsterisk
276 } else {
277 CommentVariant::SlashAsterisk
278 };
279
280 let pre = variant.prefix_len();
281 let post = variant.suffix_len();
282
283 #[cfg(debug_assertions)]
284 let orig = span;
285
286 trim_span(rendered, &mut span, pre, post);
287
288 #[cfg(debug_assertions)]
289 {
290 let raw = util::load_span_from(&mut content.as_bytes(), orig)?;
291 let adjusted = util::load_span_from(&mut content.as_bytes(), span)?;
292
293 assert_eq!(adjusted.len() + pre + post, raw.len());
296 }
297
298 (variant, span, pre, post)
299 } else {
300 lazy_static! {
305 static ref BOUNDED_RAW_STR: Regex =
306 Regex::new(r##"^(r(#*)")(?:.*\s*)+?(?=(?:"\2))("\2)\s*\]?\s*$"##)
307 .expect("BOUNEDED_RAW_STR regex compiles");
308 static ref BOUNDED_STR: Regex = Regex::new(r##"^"(?:.(?!"\\"))*?"*\s*\]?\s*"$"##)
309 .expect("BOUNEDED_STR regex compiles");
310 };
311
312 let (pre, post) =
313 if let Some(captures) = BOUNDED_RAW_STR.captures(rendered.as_str()).ok().flatten() {
314 log::trace!("raw str: >{}<", rendered.as_str());
315 let pre = if let Some(prefix) = captures.get(1) {
316 log::trace!("raw str pre: >{}<", prefix.as_str());
317 prefix.as_str().len()
318 } else {
319 return Err(Error::Span(
320 "Should have a raw str pre match with a capture group".to_string(),
321 ));
322 };
323 let post = if let Some(suffix) = captures.get(captures.len() - 1) {
324 log::trace!("raw str post: >{}<", suffix.as_str());
325 suffix.as_str().len()
326 } else {
327 return Err(Error::Span(
328 "Should have a raw str post match with a capture group".to_string(),
329 ));
330 };
331
332 debug_assert_eq!(pre, post + 1);
334
335 (pre, post)
336 } else if let Some(_captures) = BOUNDED_STR.captures(rendered.as_str()).ok().flatten() {
337 let pre = 1;
339 let post = 1;
340 debug_assert_eq!('"', rendered.as_bytes()[0_usize] as char);
341 debug_assert_eq!('"', rendered.as_bytes()[rendered.len() - 1_usize] as char);
342 (pre, post)
343 } else {
344 return Err(Error::Span(format!("Regex should match >{rendered}<")));
345 };
346
347 span.start.column += pre;
348 span.end.column = span.end.column.saturating_sub(post);
349
350 (
351 CommentVariant::MacroDocEqStr(prefix, pre.saturating_sub(1)),
352 span,
353 pre,
354 post,
355 )
356 };
357 Ok((variant, span, pre, post))
358}
359
360impl TrimmedLiteral {
361 pub(crate) fn new_empty(
365 _content: impl AsRef<str>,
366 span: Span,
367 variant: CommentVariant,
368 ) -> Self {
369 Self {
370 variant,
372 span,
373 rendered: String::new(),
375 pre: 0,
376 post: 0,
377 len_in_chars: 0,
378 len_in_bytes: 0,
379 }
380 }
381
382 pub(crate) fn load_from(content: &str, mut span: Span) -> Result<Self> {
383 span.end.column = span.end.column.saturating_sub(1);
396
397 if crate::util::extract_delimiter(content)
401 .unwrap_or("\n")
402 .len()
403 > 1
404 {
405 log::trace!(target: "documentation", "Found two character line ending like CRLF");
406 span.end.column += 1;
407 }
408
409 let rendered = util::load_span_from(content.as_bytes(), span)?;
410
411 let rendered_len = rendered.chars().count();
416
417 log::trace!("extracted from source: >{rendered}< @ {span:?}");
418 let (variant, span, pre, post) = detect_comment_variant(content, &rendered, span)?;
419
420 let len_in_chars = rendered_len.saturating_sub(post + pre);
421
422 if let Some(span_len) = span.one_line_len() {
423 if log::log_enabled!(log::Level::Trace) {
424 let extracted =
425 sub_chars(rendered.as_str(), pre..rendered_len.saturating_sub(post));
426 log::trace!(target: "quirks", "{span:?} {pre}||{post} for \n extracted: >{extracted}<\n rendered: >{rendered}<");
427 assert_eq!(len_in_chars, span_len);
428 }
429 }
430
431 let len_in_bytes = rendered.len().saturating_sub(post + pre);
432 let trimmed_literal = Self {
433 variant,
434 len_in_chars,
435 len_in_bytes,
436 rendered,
437 span,
438 pre,
439 post,
440 };
441 Ok(trimmed_literal)
442 }
443}
444
445impl TrimmedLiteral {
446 pub fn from(
451 variant: CommentVariant,
452 content: &str,
453 pre: usize,
454 post: usize,
455 line: usize,
456 column: usize,
457 ) -> std::result::Result<TrimmedLiteral, String> {
458 let content_chars_len = content.chars().count();
459 let mut span = Span {
460 start: LineColumn { line, column },
461 end: LineColumn {
462 line,
463 column: column + content_chars_len,
464 },
465 };
466
467 trim_span(content, &mut span, pre, post + 1);
468
469 Ok(TrimmedLiteral {
470 variant,
471 span,
472 rendered: content.to_string(),
473 pre,
474 post,
475 len_in_chars: content_chars_len - pre - post,
476 len_in_bytes: content.len() - pre - post,
477 })
478 }
479}
480
481impl TrimmedLiteral {
482 pub fn as_str(&self) -> &str {
486 &self.rendered.as_str()[self.pre..(self.pre + self.len_in_bytes)]
487 }
488
489 pub fn prefix(&self) -> &str {
491 &self.rendered.as_str()[..self.pre]
492 }
493
494 pub fn suffix(&self) -> &str {
496 &self.rendered.as_str()[(self.pre + self.len_in_bytes)..]
497 }
498
499 pub fn as_untrimmed_str(&self) -> &str {
501 self.rendered.as_str()
502 }
503
504 pub fn len_in_chars(&self) -> usize {
506 self.len_in_chars
507 }
508
509 pub fn len(&self) -> usize {
511 self.len_in_bytes
512 }
513
514 pub fn pre(&self) -> usize {
519 self.pre
520 }
521
522 pub fn post(&self) -> usize {
527 self.post
528 }
529
530 pub fn span(&self) -> Span {
534 self.span
535 }
536
537 pub fn chars(&self) -> impl Iterator<Item = char> + '_ {
539 self.as_str().chars()
540 }
541
542 pub fn variant(&self) -> CommentVariant {
545 self.variant.clone()
546 }
547
548 #[allow(unused)]
550 pub(crate) fn display(&self, highlight: Range) -> TrimmedLiteralDisplay {
551 TrimmedLiteralDisplay::from((self, highlight))
552 }
553}
554
555impl fmt::Debug for TrimmedLiteral {
556 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
557 use console::Style;
558
559 let pick = Style::new().on_black().underlined().dim().cyan();
560 let cutoff = Style::new().on_black().bold().dim().yellow();
561
562 write!(
563 formatter,
564 "{}{}{}",
565 cutoff.apply_to(&self.prefix()),
566 pick.apply_to(&self.as_str()),
567 cutoff.apply_to(&self.suffix()),
568 )
569 }
570}
571
572#[derive(Debug, Clone)]
579pub struct TrimmedLiteralDisplay<'a>(pub &'a TrimmedLiteral, pub Range);
580
581impl<'a, R> From<(R, Range)> for TrimmedLiteralDisplay<'a>
582where
583 R: Into<&'a TrimmedLiteral>,
584{
585 fn from(tuple: (R, Range)) -> Self {
586 let tuple0 = tuple.0.into();
587 Self(tuple0, tuple.1)
588 }
589}
590
591impl<'a> From<TrimmedLiteralDisplay<'a>> for (&'a TrimmedLiteral, Range) {
592 fn from(val: TrimmedLiteralDisplay<'a>) -> Self {
593 (val.0, val.1)
594 }
595}
596
597impl<'a> fmt::Display for TrimmedLiteralDisplay<'a> {
598 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
599 use console::Style;
600
601 let cutoff = Style::new().on_black().bold().underlined().yellow();
603 let context = Style::new().on_black().bold().cyan();
605 let highlight = Style::new().on_black().bold().underlined().red().italic();
607 let oob = Style::new().blink().bold().on_yellow().red();
609
610 let literal = self.0;
612 let start = self.1.start;
613 let end = self.1.end;
614
615 assert!(start <= end);
616
617 let data = literal.as_str();
619
620 let (pre, ctx1) = if start > literal.pre() {
623 (
624 cutoff.apply_to(&data[..literal.pre()]).to_string(),
626 {
627 let s = sub_chars(data, literal.pre()..start);
628 context.apply_to(s.as_str()).to_string()
629 },
630 )
631 } else if start <= literal.len_in_chars() {
632 let s = sub_chars(data, 0..start);
633 (cutoff.apply_to(s.as_str()).to_string(), String::new())
634 } else {
635 (String::new(), "!!!".to_owned())
636 };
637 let highlight = if end >= literal.len_in_chars() {
639 let s = sub_chars(data, start..literal.len_in_chars());
640 oob.apply_to(s.as_str()).to_string()
641 } else {
642 let s = sub_chars(data, start..end);
643 highlight.apply_to(s.as_str()).to_string()
644 };
645 let post_idx = literal.pre() + literal.len_in_chars();
647 let (ctx2, post) = if post_idx > end {
648 let s_ctx = sub_chars(data, end..post_idx);
649 let s_cutoff = sub_chars(data, post_idx..literal.len_in_chars());
650 (
651 context.apply_to(s_ctx.as_str()).to_string(),
652 cutoff.apply_to(s_cutoff.as_str()).to_string(),
653 )
654 } else if end < literal.len_in_chars() {
655 let s = sub_chars(
656 data,
657 end..(literal.len_in_chars() + literal.pre() + literal.post()),
658 );
659 (String::new(), cutoff.apply_to(s.as_str()).to_string())
660 } else {
661 (String::new(), oob.apply_to("!!!").to_string())
662 };
663
664 write!(formatter, "{pre}{ctx1}{highlight}{ctx2}{post}")
665 }
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671 use crate::testcase::annotated_literals_raw;
672 use assert_matches::assert_matches;
673
674 #[test]
675 fn variant_detect() {
676 let content = r###"#[doc=r"foo"]"###.to_owned();
677 let rendered = r##"r"foo""##.to_owned();
678 assert_matches!(
679 detect_comment_variant(content.as_str(), &rendered, Span{
680 start: LineColumn {
681 line: 1,
682 column: 6,
683 },
684 end: LineColumn {
685 line: 1,
686 column: 12 + 1,
687 },
688 }), Ok((CommentVariant::MacroDocEqStr(prefix, n_pounds), _, _, _)) => {
689 assert_eq!(n_pounds, 1);
690 assert_eq!(prefix, "#[doc=");
691 });
692 }
693
694 macro_rules! block_comment_test {
695 ($name:ident, $content:literal) => {
696 #[test]
697 fn $name() {
698 const CONTENT: &str = $content;
699 let mut literals = annotated_literals_raw(CONTENT);
700 let literal = literals.next().unwrap();
701 assert!(literals.next().is_none());
702
703 let tl = TrimmedLiteral::load_from(CONTENT, Span::from(literal.span())).unwrap();
704 assert!(CONTENT.starts_with(tl.prefix()));
705 assert!(CONTENT.ends_with(tl.suffix()));
706 assert_eq!(
707 CONTENT
708 .chars()
709 .skip(tl.pre())
710 .take(tl.len_in_chars())
711 .collect::<String>(),
712 tl.as_str().to_owned()
713 )
714 }
715 };
716 }
717
718 block_comment_test!(trimmed_oneline_doc, "/** dooc */");
719 block_comment_test!(trimmed_oneline_mod, "/*! dooc */");
720
721 block_comment_test!(
722 trimmed_multi_doc,
723 "/**
724mood
725*/"
726 );
727 block_comment_test!(
728 trimmed_multi_mod,
729 "/*!
730mood
731*/"
732 );
733}