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 is_empty(&self) -> bool {
515 self.len_in_bytes == 0
516 }
517
518 pub fn pre(&self) -> usize {
523 self.pre
524 }
525
526 pub fn post(&self) -> usize {
531 self.post
532 }
533
534 pub fn span(&self) -> Span {
538 self.span
539 }
540
541 pub fn chars(&self) -> impl Iterator<Item = char> + '_ {
543 self.as_str().chars()
544 }
545
546 pub fn variant(&self) -> CommentVariant {
549 self.variant.clone()
550 }
551
552 #[allow(unused)]
554 pub(crate) fn display(&self, highlight: Range) -> TrimmedLiteralDisplay<'_> {
555 TrimmedLiteralDisplay::from((self, highlight))
556 }
557}
558
559impl fmt::Debug for TrimmedLiteral {
560 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
561 use console::Style;
562
563 let pick = Style::new().on_black().underlined().dim().cyan();
564 let cutoff = Style::new().on_black().bold().dim().yellow();
565
566 write!(
567 formatter,
568 "{}{}{}",
569 cutoff.apply_to(&self.prefix()),
570 pick.apply_to(&self.as_str()),
571 cutoff.apply_to(&self.suffix()),
572 )
573 }
574}
575
576#[derive(Debug, Clone)]
583pub struct TrimmedLiteralDisplay<'a>(pub &'a TrimmedLiteral, pub Range);
584
585impl<'a, R> From<(R, Range)> for TrimmedLiteralDisplay<'a>
586where
587 R: Into<&'a TrimmedLiteral>,
588{
589 fn from(tuple: (R, Range)) -> Self {
590 let tuple0 = tuple.0.into();
591 Self(tuple0, tuple.1)
592 }
593}
594
595impl<'a> From<TrimmedLiteralDisplay<'a>> for (&'a TrimmedLiteral, Range) {
596 fn from(val: TrimmedLiteralDisplay<'a>) -> Self {
597 (val.0, val.1)
598 }
599}
600
601impl<'a> fmt::Display for TrimmedLiteralDisplay<'a> {
602 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
603 use console::Style;
604
605 let cutoff = Style::new().on_black().bold().underlined().yellow();
607 let context = Style::new().on_black().bold().cyan();
609 let highlight = Style::new().on_black().bold().underlined().red().italic();
611 let oob = Style::new().blink().bold().on_yellow().red();
613
614 let literal = self.0;
616 let start = self.1.start;
617 let end = self.1.end;
618
619 assert!(start <= end);
620
621 let data = literal.as_str();
623
624 let (pre, ctx1) = if start > literal.pre() {
627 (
628 cutoff.apply_to(&data[..literal.pre()]).to_string(),
630 {
631 let s = sub_chars(data, literal.pre()..start);
632 context.apply_to(s.as_str()).to_string()
633 },
634 )
635 } else if start <= literal.len_in_chars() {
636 let s = sub_chars(data, 0..start);
637 (cutoff.apply_to(s.as_str()).to_string(), String::new())
638 } else {
639 (String::new(), "!!!".to_owned())
640 };
641 let highlight = if end >= literal.len_in_chars() {
643 let s = sub_chars(data, start..literal.len_in_chars());
644 oob.apply_to(s.as_str()).to_string()
645 } else {
646 let s = sub_chars(data, start..end);
647 highlight.apply_to(s.as_str()).to_string()
648 };
649 let post_idx = literal.pre() + literal.len_in_chars();
651 let (ctx2, post) = if post_idx > end {
652 let s_ctx = sub_chars(data, end..post_idx);
653 let s_cutoff = sub_chars(data, post_idx..literal.len_in_chars());
654 (
655 context.apply_to(s_ctx.as_str()).to_string(),
656 cutoff.apply_to(s_cutoff.as_str()).to_string(),
657 )
658 } else if end < literal.len_in_chars() {
659 let s = sub_chars(
660 data,
661 end..(literal.len_in_chars() + literal.pre() + literal.post()),
662 );
663 (String::new(), cutoff.apply_to(s.as_str()).to_string())
664 } else {
665 (String::new(), oob.apply_to("!!!").to_string())
666 };
667
668 write!(formatter, "{pre}{ctx1}{highlight}{ctx2}{post}")
669 }
670}
671
672#[cfg(test)]
673mod tests {
674 use super::*;
675 use crate::testcase::annotated_literals_raw;
676 use assert_matches::assert_matches;
677
678 #[test]
679 fn variant_detect() {
680 let content = r###"#[doc=r"foo"]"###.to_owned();
681 let rendered = r##"r"foo""##.to_owned();
682 assert_matches!(
683 detect_comment_variant(content.as_str(), &rendered, Span{
684 start: LineColumn {
685 line: 1,
686 column: 6,
687 },
688 end: LineColumn {
689 line: 1,
690 column: 12 + 1,
691 },
692 }), Ok((CommentVariant::MacroDocEqStr(prefix, n_pounds), _, _, _)) => {
693 assert_eq!(n_pounds, 1);
694 assert_eq!(prefix, "#[doc=");
695 });
696 }
697
698 macro_rules! block_comment_test {
699 ($name:ident, $content:literal) => {
700 #[test]
701 fn $name() {
702 const CONTENT: &str = $content;
703 let mut literals = annotated_literals_raw(CONTENT);
704 let literal = literals.next().unwrap();
705 assert!(literals.next().is_none());
706
707 let tl = TrimmedLiteral::load_from(CONTENT, Span::from(literal.span())).unwrap();
708 assert!(CONTENT.starts_with(tl.prefix()));
709 assert!(CONTENT.ends_with(tl.suffix()));
710 assert_eq!(
711 CONTENT
712 .chars()
713 .skip(tl.pre())
714 .take(tl.len_in_chars())
715 .collect::<String>(),
716 tl.as_str().to_owned()
717 )
718 }
719 };
720 }
721
722 block_comment_test!(trimmed_oneline_doc, "/** dooc */");
723 block_comment_test!(trimmed_oneline_mod, "/*! dooc */");
724
725 block_comment_test!(
726 trimmed_multi_doc,
727 "/**
728mood
729*/"
730 );
731 block_comment_test!(
732 trimmed_multi_mod,
733 "/*!
734mood
735*/"
736 );
737}