1use std::mem;
2use std::ops::Range;
3
4use itertools::Itertools;
5use pulldown_cmark::{
6 BrokenLink, BrokenLinkCallback, CowStr, Event, LinkType, Options, Parser, Tag,
7};
8use rustc_ast as ast;
9use rustc_ast::attr::AttributeExt;
10use rustc_ast::util::comments::beautify_doc_string;
11use rustc_data_structures::fx::FxIndexMap;
12use rustc_data_structures::unord::UnordSet;
13use rustc_middle::ty::TyCtxt;
14use rustc_span::def_id::DefId;
15use rustc_span::source_map::SourceMap;
16use rustc_span::{DUMMY_SP, InnerSpan, Span, Symbol, sym};
17use thin_vec::ThinVec;
18use tracing::{debug, trace};
19
20#[cfg(test)]
21mod tests;
22
23#[derive(Clone, Copy, PartialEq, Eq, Debug)]
24pub enum DocFragmentKind {
25 SugaredDoc,
27 RawDoc,
29}
30
31#[derive(Clone, PartialEq, Eq, Debug)]
40pub struct DocFragment {
41 pub span: Span,
42 pub item_id: Option<DefId>,
49 pub doc: Symbol,
50 pub kind: DocFragmentKind,
51 pub indent: usize,
52 pub from_expansion: bool,
55}
56
57#[derive(Clone, Copy, Debug)]
58pub enum MalformedGenerics {
59 UnbalancedAngleBrackets,
63 MissingType,
69 HasFullyQualifiedSyntax,
76 InvalidPathSeparator,
88 TooManyAngleBrackets,
92 EmptyAngleBrackets,
96}
97
98pub fn unindent_doc_fragments(docs: &mut [DocFragment]) {
112 let add = if docs.windows(2).any(|arr| arr[0].kind != arr[1].kind)
125 && docs.iter().any(|d| d.kind == DocFragmentKind::SugaredDoc)
126 {
127 1
130 } else {
131 0
132 };
133
134 let Some(min_indent) = docs
144 .iter()
145 .map(|fragment| {
146 fragment
147 .doc
148 .as_str()
149 .lines()
150 .filter(|line| line.chars().any(|c| !c.is_whitespace()))
151 .map(|line| {
152 let whitespace = line.chars().take_while(|c| *c == ' ' || *c == '\t').count();
155 whitespace
156 + (if fragment.kind == DocFragmentKind::SugaredDoc { 0 } else { add })
157 })
158 .min()
159 .unwrap_or(usize::MAX)
160 })
161 .min()
162 else {
163 return;
164 };
165
166 for fragment in docs {
167 if fragment.doc == sym::empty {
168 continue;
169 }
170
171 let indent = if fragment.kind != DocFragmentKind::SugaredDoc && min_indent > 0 {
172 min_indent - add
173 } else {
174 min_indent
175 };
176
177 fragment.indent = indent;
178 }
179}
180
181pub fn add_doc_fragment(out: &mut String, frag: &DocFragment) {
187 if frag.doc == sym::empty {
188 out.push('\n');
189 return;
190 }
191 let s = frag.doc.as_str();
192 let mut iter = s.lines();
193
194 while let Some(line) = iter.next() {
195 if line.chars().any(|c| !c.is_whitespace()) {
196 assert!(line.len() >= frag.indent);
197 out.push_str(&line[frag.indent..]);
198 } else {
199 out.push_str(line);
200 }
201 out.push('\n');
202 }
203}
204
205pub fn attrs_to_doc_fragments<'a, A: AttributeExt + Clone + 'a>(
206 attrs: impl Iterator<Item = (&'a A, Option<DefId>)>,
207 doc_only: bool,
208) -> (Vec<DocFragment>, ThinVec<A>) {
209 let mut doc_fragments = Vec::new();
210 let mut other_attrs = ThinVec::<A>::new();
211 for (attr, item_id) in attrs {
212 if let Some((doc_str, comment_kind)) = attr.doc_str_and_comment_kind() {
213 let doc = beautify_doc_string(doc_str, comment_kind);
214 let (span, kind, from_expansion) = if attr.is_doc_comment() {
215 let span = attr.span();
216 (span, DocFragmentKind::SugaredDoc, span.from_expansion())
217 } else {
218 let attr_span = attr.span();
219 let (span, from_expansion) = match attr.value_span() {
220 Some(sp) => (sp.with_ctxt(attr_span.ctxt()), sp.from_expansion()),
221 None => (attr_span, attr_span.from_expansion()),
222 };
223 (span, DocFragmentKind::RawDoc, from_expansion)
224 };
225 let fragment = DocFragment { span, doc, kind, item_id, indent: 0, from_expansion };
226 doc_fragments.push(fragment);
227 } else if !doc_only {
228 other_attrs.push(attr.clone());
229 }
230 }
231
232 unindent_doc_fragments(&mut doc_fragments);
233
234 (doc_fragments, other_attrs)
235}
236
237pub fn prepare_to_doc_link_resolution(
243 doc_fragments: &[DocFragment],
244) -> FxIndexMap<Option<DefId>, String> {
245 let mut res = FxIndexMap::default();
246 for fragment in doc_fragments {
247 let out_str = res.entry(fragment.item_id).or_default();
248 add_doc_fragment(out_str, fragment);
249 }
250 res
251}
252
253pub fn main_body_opts() -> Options {
255 Options::ENABLE_TABLES
256 | Options::ENABLE_FOOTNOTES
257 | Options::ENABLE_STRIKETHROUGH
258 | Options::ENABLE_TASKLISTS
259 | Options::ENABLE_SMART_PUNCTUATION
260}
261
262fn strip_generics_from_path_segment(segment: Vec<char>) -> Result<String, MalformedGenerics> {
263 let mut stripped_segment = String::new();
264 let mut param_depth = 0;
265
266 let mut latest_generics_chunk = String::new();
267
268 for c in segment {
269 if c == '<' {
270 param_depth += 1;
271 latest_generics_chunk.clear();
272 } else if c == '>' {
273 param_depth -= 1;
274 if latest_generics_chunk.contains(" as ") {
275 return Err(MalformedGenerics::HasFullyQualifiedSyntax);
278 }
279 } else if param_depth == 0 {
280 stripped_segment.push(c);
281 } else {
282 latest_generics_chunk.push(c);
283 }
284 }
285
286 if param_depth == 0 {
287 Ok(stripped_segment)
288 } else {
289 Err(MalformedGenerics::UnbalancedAngleBrackets)
291 }
292}
293
294pub fn strip_generics_from_path(path_str: &str) -> Result<Box<str>, MalformedGenerics> {
295 if !path_str.contains(['<', '>']) {
296 return Ok(path_str.into());
297 }
298 let mut stripped_segments = vec![];
299 let mut path = path_str.chars().peekable();
300 let mut segment = Vec::new();
301
302 while let Some(chr) = path.next() {
303 match chr {
304 ':' => {
305 if path.next_if_eq(&':').is_some() {
306 let stripped_segment =
307 strip_generics_from_path_segment(mem::take(&mut segment))?;
308 if !stripped_segment.is_empty() {
309 stripped_segments.push(stripped_segment);
310 }
311 } else {
312 return Err(MalformedGenerics::InvalidPathSeparator);
313 }
314 }
315 '<' => {
316 segment.push(chr);
317
318 match path.next() {
319 Some('<') => {
320 return Err(MalformedGenerics::TooManyAngleBrackets);
321 }
322 Some('>') => {
323 return Err(MalformedGenerics::EmptyAngleBrackets);
324 }
325 Some(chr) => {
326 segment.push(chr);
327
328 while let Some(chr) = path.next_if(|c| *c != '>') {
329 segment.push(chr);
330 }
331 }
332 None => break,
333 }
334 }
335 _ => segment.push(chr),
336 }
337 trace!("raw segment: {:?}", segment);
338 }
339
340 if !segment.is_empty() {
341 let stripped_segment = strip_generics_from_path_segment(segment)?;
342 if !stripped_segment.is_empty() {
343 stripped_segments.push(stripped_segment);
344 }
345 }
346
347 debug!("path_str: {path_str:?}\nstripped segments: {stripped_segments:?}");
348
349 let stripped_path = stripped_segments.join("::");
350
351 if !stripped_path.is_empty() {
352 Ok(stripped_path.into())
353 } else {
354 Err(MalformedGenerics::MissingType)
355 }
356}
357
358pub fn inner_docs(attrs: &[impl AttributeExt]) -> bool {
363 for attr in attrs {
364 if let Some(attr_style) = attr.doc_resolution_scope() {
365 return attr_style == ast::AttrStyle::Inner;
366 }
367 }
368 true
369}
370
371pub fn has_primitive_or_keyword_docs(attrs: &[impl AttributeExt]) -> bool {
373 for attr in attrs {
374 if attr.has_name(sym::rustc_doc_primitive) {
375 return true;
376 } else if attr.has_name(sym::doc)
377 && let Some(items) = attr.meta_item_list()
378 {
379 for item in items {
380 if item.has_name(sym::keyword) {
381 return true;
382 }
383 }
384 }
385 }
386 false
387}
388
389fn preprocess_link(link: &str) -> Box<str> {
393 let link = link.replace('`', "");
394 let link = link.split('#').next().unwrap();
395 let link = link.trim();
396 let link = link.rsplit('@').next().unwrap();
397 let link = link.strip_suffix("()").unwrap_or(link);
398 let link = link.strip_suffix("{}").unwrap_or(link);
399 let link = link.strip_suffix("[]").unwrap_or(link);
400 let link = if link != "!" { link.strip_suffix('!').unwrap_or(link) } else { link };
401 let link = link.trim();
402 strip_generics_from_path(link).unwrap_or_else(|_| link.into())
403}
404
405pub fn may_be_doc_link(link_type: LinkType) -> bool {
408 match link_type {
409 LinkType::Inline
410 | LinkType::Reference
411 | LinkType::ReferenceUnknown
412 | LinkType::Collapsed
413 | LinkType::CollapsedUnknown
414 | LinkType::Shortcut
415 | LinkType::ShortcutUnknown => true,
416 LinkType::Autolink | LinkType::Email => false,
417 }
418}
419
420pub(crate) fn attrs_to_preprocessed_links<A: AttributeExt + Clone>(attrs: &[A]) -> Vec<Box<str>> {
423 let (doc_fragments, _) = attrs_to_doc_fragments(attrs.iter().map(|attr| (attr, None)), true);
424 let doc = prepare_to_doc_link_resolution(&doc_fragments).into_values().next().unwrap();
425
426 parse_links(&doc)
427}
428
429fn parse_links<'md>(doc: &'md str) -> Vec<Box<str>> {
432 let mut broken_link_callback = |link: BrokenLink<'md>| Some((link.reference, "".into()));
433 let mut event_iter = Parser::new_with_broken_link_callback(
434 doc,
435 main_body_opts(),
436 Some(&mut broken_link_callback),
437 );
438 let mut links = Vec::new();
439
440 let mut refids = UnordSet::default();
441
442 while let Some(event) = event_iter.next() {
443 match event {
444 Event::Start(Tag::Link { link_type, dest_url, title: _, id })
445 if may_be_doc_link(link_type) =>
446 {
447 if matches!(
448 link_type,
449 LinkType::Inline
450 | LinkType::ReferenceUnknown
451 | LinkType::Reference
452 | LinkType::Shortcut
453 | LinkType::ShortcutUnknown
454 ) {
455 if let Some(display_text) = collect_link_data(&mut event_iter) {
456 links.push(display_text);
457 }
458 }
459 if matches!(
460 link_type,
461 LinkType::Reference | LinkType::Shortcut | LinkType::Collapsed
462 ) {
463 refids.insert(id);
464 }
465
466 links.push(preprocess_link(&dest_url));
467 }
468 _ => {}
469 }
470 }
471
472 for (label, refdef) in event_iter.reference_definitions().iter().sorted_by_key(|x| x.0) {
473 if !refids.contains(label) {
474 links.push(preprocess_link(&refdef.dest));
475 }
476 }
477
478 links
479}
480
481fn collect_link_data<'input, F: BrokenLinkCallback<'input>>(
483 event_iter: &mut Parser<'input, F>,
484) -> Option<Box<str>> {
485 let mut display_text: Option<String> = None;
486 let mut append_text = |text: CowStr<'_>| {
487 if let Some(display_text) = &mut display_text {
488 display_text.push_str(&text);
489 } else {
490 display_text = Some(text.to_string());
491 }
492 };
493
494 while let Some(event) = event_iter.next() {
495 match event {
496 Event::Text(text) => {
497 append_text(text);
498 }
499 Event::Code(code) => {
500 append_text(code);
501 }
502 Event::End(_) => {
503 break;
504 }
505 _ => {}
506 }
507 }
508
509 display_text.map(String::into_boxed_str)
510}
511
512pub fn span_of_fragments_with_expansion(fragments: &[DocFragment]) -> Option<(Span, bool)> {
515 let (first_fragment, last_fragment) = match fragments {
516 [] => return None,
517 [first, .., last] => (first, last),
518 [first] => (first, first),
519 };
520 if first_fragment.span == DUMMY_SP {
521 return None;
522 }
523 Some((
524 first_fragment.span.to(last_fragment.span),
525 fragments.iter().any(|frag| frag.from_expansion),
526 ))
527}
528
529pub fn span_of_fragments(fragments: &[DocFragment]) -> Option<Span> {
531 span_of_fragments_with_expansion(fragments).map(|(sp, _)| sp)
532}
533
534pub fn source_span_for_markdown_range(
556 tcx: TyCtxt<'_>,
557 markdown: &str,
558 md_range: &Range<usize>,
559 fragments: &[DocFragment],
560) -> Option<(Span, bool)> {
561 let map = tcx.sess.source_map();
562 source_span_for_markdown_range_inner(map, markdown, md_range, fragments)
563}
564
565pub fn source_span_for_markdown_range_inner(
567 map: &SourceMap,
568 markdown: &str,
569 md_range: &Range<usize>,
570 fragments: &[DocFragment],
571) -> Option<(Span, bool)> {
572 use rustc_span::BytePos;
573
574 if let &[fragment] = &fragments
575 && fragment.kind == DocFragmentKind::RawDoc
576 && let Ok(snippet) = map.span_to_snippet(fragment.span)
577 && snippet.trim_end() == markdown.trim_end()
578 && let Ok(md_range_lo) = u32::try_from(md_range.start)
579 && let Ok(md_range_hi) = u32::try_from(md_range.end)
580 {
581 return Some((
583 Span::new(
584 fragment.span.lo() + rustc_span::BytePos(md_range_lo),
585 fragment.span.lo() + rustc_span::BytePos(md_range_hi),
586 fragment.span.ctxt(),
587 fragment.span.parent(),
588 ),
589 fragment.from_expansion,
590 ));
591 }
592
593 let is_all_sugared_doc = fragments.iter().all(|frag| frag.kind == DocFragmentKind::SugaredDoc);
594
595 if !is_all_sugared_doc {
596 let mut match_data = None;
601 let pat = &markdown[md_range.clone()];
602 if pat.is_empty() {
604 return None;
605 }
606 for (i, fragment) in fragments.iter().enumerate() {
607 if let Ok(snippet) = map.span_to_snippet(fragment.span)
608 && let Some(match_start) = snippet.find(pat)
609 {
610 if match_data.is_none()
615 && !snippet.as_bytes()[match_start + 1..]
616 .windows(pat.len())
617 .any(|s| s == pat.as_bytes())
618 {
619 match_data = Some((i, match_start));
620 } else {
621 return None;
623 }
624 }
625 }
626 if let Some((i, match_start)) = match_data {
627 let fragment = &fragments[i];
628 let sp = fragment.span;
629 let lo = sp.lo() + BytePos(match_start as u32);
632 return Some((
633 sp.with_lo(lo).with_hi(lo + BytePos((md_range.end - md_range.start) as u32)),
634 fragment.from_expansion,
635 ));
636 }
637 return None;
638 }
639
640 let snippet = map.span_to_snippet(span_of_fragments(fragments)?).ok()?;
641
642 let starting_line = markdown[..md_range.start].matches('\n').count();
643 let ending_line = starting_line + markdown[md_range.start..md_range.end].matches('\n').count();
644
645 let mut src_lines = snippet.split_terminator('\n');
648 let md_lines = markdown.split_terminator('\n');
649
650 let mut start_bytes = 0;
653 let mut end_bytes = 0;
654
655 'outer: for (line_no, md_line) in md_lines.enumerate() {
656 loop {
657 let source_line = src_lines.next()?;
658 match source_line.find(md_line) {
659 Some(offset) => {
660 if line_no == starting_line {
661 start_bytes += offset;
662
663 if starting_line == ending_line {
664 break 'outer;
665 }
666 } else if line_no == ending_line {
667 end_bytes += offset;
668 break 'outer;
669 } else if line_no < starting_line {
670 start_bytes += source_line.len() - md_line.len();
671 } else {
672 end_bytes += source_line.len() - md_line.len();
673 }
674 break;
675 }
676 None => {
677 if line_no <= starting_line {
680 start_bytes += source_line.len() + 1;
681 } else {
682 end_bytes += source_line.len() + 1;
683 }
684 }
685 }
686 }
687 }
688
689 let (span, _) = span_of_fragments_with_expansion(fragments)?;
690 let src_span = span.from_inner(InnerSpan::new(
691 md_range.start + start_bytes,
692 md_range.end + start_bytes + end_bytes,
693 ));
694 Some((
695 src_span,
696 fragments.iter().any(|frag| frag.span.overlaps(src_span) && frag.from_expansion),
697 ))
698}