]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/passes/collect_intra_doc_links.rs
turn intra-doc-link collection into an early pass
[rust.git] / src / librustdoc / passes / collect_intra_doc_links.rs
1 // Copyright 2018 The Rust Project Developers. See the COPYRIGHT
2 // file at the top-level directory of this distribution and at
3 // http://rust-lang.org/COPYRIGHT.
4 //
5 // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8 // option. This file may not be copied, modified, or distributed
9 // except according to those terms.
10
11 use clean::*;
12
13 use rustc::lint as lint;
14 use rustc::hir;
15 use rustc::hir::def::Def;
16 use rustc::ty;
17 use syntax;
18 use syntax::ast::{self, Ident, NodeId};
19 use syntax::feature_gate::UnstableFeatures;
20 use syntax::symbol::Symbol;
21 use syntax_pos::{self, DUMMY_SP};
22
23 use std::ops::Range;
24
25 use core::DocContext;
26 use fold::DocFolder;
27 use html::markdown::markdown_links;
28 use passes::Pass;
29
30 pub const COLLECT_INTRA_DOC_LINKS: Pass =
31     Pass::early("collect-intra-doc-links", collect_intra_doc_links,
32                 "reads a crate's documentation to resolve intra-doc-links");
33
34 pub fn collect_intra_doc_links(krate: Crate, cx: &DocContext) -> Crate {
35     if !UnstableFeatures::from_environment().is_nightly_build() {
36         krate
37     } else {
38         let mut coll = LinkCollector::new(cx);
39
40         coll.fold_crate(krate)
41     }
42 }
43
44 #[derive(Debug)]
45 enum PathKind {
46     /// can be either value or type, not a macro
47     Unknown,
48     /// macro
49     Macro,
50     /// values, functions, consts, statics, everything in the value namespace
51     Value,
52     /// types, traits, everything in the type namespace
53     Type,
54 }
55
56 struct LinkCollector<'a, 'tcx: 'a, 'rcx: 'a, 'cstore: 'rcx> {
57     cx: &'a DocContext<'a, 'tcx, 'rcx, 'cstore>,
58     mod_ids: Vec<NodeId>,
59 }
60
61 impl<'a, 'tcx, 'rcx, 'cstore> LinkCollector<'a, 'tcx, 'rcx, 'cstore> {
62     fn new(cx: &'a DocContext<'a, 'tcx, 'rcx, 'cstore>) -> Self {
63         LinkCollector {
64             cx,
65             mod_ids: Vec::new(),
66         }
67     }
68
69     /// Resolve a given string as a path, along with whether or not it is
70     /// in the value namespace. Also returns an optional URL fragment in the case
71     /// of variants and methods
72     fn resolve(&self, path_str: &str, is_val: bool, current_item: &Option<String>)
73         -> Result<(Def, Option<String>), ()>
74     {
75         let cx = self.cx;
76
77         // In case we're in a module, try to resolve the relative
78         // path
79         if let Some(id) = self.mod_ids.last() {
80             let result = cx.resolver.borrow_mut()
81                                     .with_scope(*id,
82                 |resolver| {
83                     resolver.resolve_str_path_error(DUMMY_SP,
84                                                     &path_str, is_val)
85             });
86
87             if let Ok(result) = result {
88                 // In case this is a trait item, skip the
89                 // early return and try looking for the trait
90                 let value = match result.def {
91                     Def::Method(_) | Def::AssociatedConst(_) => true,
92                     Def::AssociatedTy(_) => false,
93                     Def::Variant(_) => return handle_variant(cx, result.def),
94                     // not a trait item, just return what we found
95                     _ => return Ok((result.def, None))
96                 };
97
98                 if value != is_val {
99                     return Err(())
100                 }
101             } else if let Some(prim) = is_primitive(path_str, is_val) {
102                 return Ok((prim, Some(path_str.to_owned())))
103             } else {
104                 // If resolution failed, it may still be a method
105                 // because methods are not handled by the resolver
106                 // If so, bail when we're not looking for a value
107                 if !is_val {
108                     return Err(())
109                 }
110             }
111
112             // Try looking for methods and associated items
113             let mut split = path_str.rsplitn(2, "::");
114             let item_name = if let Some(first) = split.next() {
115                 first
116             } else {
117                 return Err(())
118             };
119
120             let mut path = if let Some(second) = split.next() {
121                 second.to_owned()
122             } else {
123                 return Err(())
124             };
125
126             if path == "self" || path == "Self" {
127                 if let Some(name) = current_item.as_ref() {
128                     path = name.clone();
129                 }
130             }
131
132             let ty = cx.resolver.borrow_mut()
133                                 .with_scope(*id,
134                 |resolver| {
135                     resolver.resolve_str_path_error(DUMMY_SP, &path, false)
136             })?;
137             match ty.def {
138                 Def::Struct(did) | Def::Union(did) | Def::Enum(did) | Def::TyAlias(did) => {
139                     let item = cx.tcx.inherent_impls(did)
140                                      .iter()
141                                      .flat_map(|imp| cx.tcx.associated_items(*imp))
142                                      .find(|item| item.ident.name == item_name);
143                     if let Some(item) = item {
144                         let out = match item.kind {
145                             ty::AssociatedKind::Method if is_val => "method",
146                             ty::AssociatedKind::Const if is_val => "associatedconstant",
147                             _ => return Err(())
148                         };
149                         Ok((ty.def, Some(format!("{}.{}", out, item_name))))
150                     } else {
151                         match cx.tcx.type_of(did).sty {
152                             ty::TyAdt(def, _) => {
153                                 if let Some(item) = if def.is_enum() {
154                                     def.all_fields().find(|item| item.ident.name == item_name)
155                                 } else {
156                                     def.non_enum_variant()
157                                        .fields
158                                        .iter()
159                                        .find(|item| item.ident.name == item_name)
160                                 } {
161                                     Ok((ty.def,
162                                         Some(format!("{}.{}",
163                                                      if def.is_enum() {
164                                                          "variant"
165                                                      } else {
166                                                          "structfield"
167                                                      },
168                                                      item.ident))))
169                                 } else {
170                                     Err(())
171                                 }
172                             }
173                             _ => Err(()),
174                         }
175                     }
176                 }
177                 Def::Trait(did) => {
178                     let item = cx.tcx.associated_item_def_ids(did).iter()
179                                  .map(|item| cx.tcx.associated_item(*item))
180                                  .find(|item| item.ident.name == item_name);
181                     if let Some(item) = item {
182                         let kind = match item.kind {
183                             ty::AssociatedKind::Const if is_val => "associatedconstant",
184                             ty::AssociatedKind::Type if !is_val => "associatedtype",
185                             ty::AssociatedKind::Method if is_val => {
186                                 if item.defaultness.has_value() {
187                                     "method"
188                                 } else {
189                                     "tymethod"
190                                 }
191                             }
192                             _ => return Err(())
193                         };
194
195                         Ok((ty.def, Some(format!("{}.{}", kind, item_name))))
196                     } else {
197                         Err(())
198                     }
199                 }
200                 _ => Err(())
201             }
202         } else {
203             Err(())
204         }
205     }
206 }
207
208 impl<'a, 'tcx, 'rcx, 'cstore> DocFolder for LinkCollector<'a, 'tcx, 'rcx, 'cstore> {
209     fn fold_item(&mut self, mut item: Item) -> Option<Item> {
210         let item_node_id = if item.is_mod() {
211             if let Some(id) = self.cx.tcx.hir.as_local_node_id(item.def_id) {
212                 Some(id)
213             } else {
214                 debug!("attempting to fold on a non-local item: {:?}", item);
215                 return self.fold_item_recur(item);
216             }
217         } else {
218             None
219         };
220
221         let current_item = match item.inner {
222             ModuleItem(..) => {
223                 if item.attrs.inner_docs {
224                     if item_node_id.unwrap() != NodeId::new(0) {
225                         item.name.clone()
226                     } else {
227                         None
228                     }
229                 } else {
230                     match self.mod_ids.last() {
231                         Some(parent) if *parent != NodeId::new(0) => {
232                             //FIXME: can we pull the parent module's name from elsewhere?
233                             Some(self.cx.tcx.hir.name(*parent).to_string())
234                         }
235                         _ => None,
236                     }
237                 }
238             }
239             ImplItem(Impl { ref for_, .. }) => {
240                 for_.def_id().map(|did| self.cx.tcx.item_name(did).to_string())
241             }
242             ExternCrateItem(ref name, ..) => Some(name.clone()),
243             ImportItem(Import::Simple(ref name, ..)) => Some(name.clone()),
244             MacroItem(..) => None,
245             _ => item.name.clone(),
246         };
247
248         if item.is_mod() && item.attrs.inner_docs {
249             self.mod_ids.push(item_node_id.unwrap());
250         }
251
252         let cx = self.cx;
253         let dox = item.attrs.collapsed_doc_value().unwrap_or_else(String::new);
254
255         for (ori_link, link_range) in markdown_links(&dox) {
256             // bail early for real links
257             if ori_link.contains('/') {
258                 continue;
259             }
260             let link = ori_link.replace("`", "");
261             let (def, fragment) = {
262                 let mut kind = PathKind::Unknown;
263                 let path_str = if let Some(prefix) =
264                     ["struct@", "enum@", "type@",
265                      "trait@", "union@"].iter()
266                                       .find(|p| link.starts_with(**p)) {
267                     kind = PathKind::Type;
268                     link.trim_left_matches(prefix)
269                 } else if let Some(prefix) =
270                     ["const@", "static@",
271                      "value@", "function@", "mod@",
272                      "fn@", "module@", "method@"]
273                         .iter().find(|p| link.starts_with(**p)) {
274                     kind = PathKind::Value;
275                     link.trim_left_matches(prefix)
276                 } else if link.ends_with("()") {
277                     kind = PathKind::Value;
278                     link.trim_right_matches("()")
279                 } else if link.starts_with("macro@") {
280                     kind = PathKind::Macro;
281                     link.trim_left_matches("macro@")
282                 } else if link.ends_with('!') {
283                     kind = PathKind::Macro;
284                     link.trim_right_matches('!')
285                 } else {
286                     &link[..]
287                 }.trim();
288
289                 if path_str.contains(|ch: char| !(ch.is_alphanumeric() ||
290                                                   ch == ':' || ch == '_')) {
291                     continue;
292                 }
293
294                 match kind {
295                     PathKind::Value => {
296                         if let Ok(def) = self.resolve(path_str, true, &current_item) {
297                             def
298                         } else {
299                             resolution_failure(cx, &item.attrs, path_str, &dox, link_range);
300                             // this could just be a normal link or a broken link
301                             // we could potentially check if something is
302                             // "intra-doc-link-like" and warn in that case
303                             continue;
304                         }
305                     }
306                     PathKind::Type => {
307                         if let Ok(def) = self.resolve(path_str, false, &current_item) {
308                             def
309                         } else {
310                             resolution_failure(cx, &item.attrs, path_str, &dox, link_range);
311                             // this could just be a normal link
312                             continue;
313                         }
314                     }
315                     PathKind::Unknown => {
316                         // try everything!
317                         if let Some(macro_def) = macro_resolve(cx, path_str) {
318                             if let Ok(type_def) = self.resolve(path_str, false, &current_item) {
319                                 let (type_kind, article, type_disambig)
320                                     = type_ns_kind(type_def.0, path_str);
321                                 ambiguity_error(cx, &item.attrs, path_str,
322                                                 article, type_kind, &type_disambig,
323                                                 "a", "macro", &format!("macro@{}", path_str));
324                                 continue;
325                             } else if let Ok(value_def) = self.resolve(path_str,
326                                                                        true,
327                                                                        &current_item) {
328                                 let (value_kind, value_disambig)
329                                     = value_ns_kind(value_def.0, path_str)
330                                         .expect("struct and mod cases should have been \
331                                                  caught in previous branch");
332                                 ambiguity_error(cx, &item.attrs, path_str,
333                                                 "a", value_kind, &value_disambig,
334                                                 "a", "macro", &format!("macro@{}", path_str));
335                             }
336                             (macro_def, None)
337                         } else if let Ok(type_def) = self.resolve(path_str, false, &current_item) {
338                             // It is imperative we search for not-a-value first
339                             // Otherwise we will find struct ctors for when we are looking
340                             // for structs, and the link won't work.
341                             // if there is something in both namespaces
342                             if let Ok(value_def) = self.resolve(path_str, true, &current_item) {
343                                 let kind = value_ns_kind(value_def.0, path_str);
344                                 if let Some((value_kind, value_disambig)) = kind {
345                                     let (type_kind, article, type_disambig)
346                                         = type_ns_kind(type_def.0, path_str);
347                                     ambiguity_error(cx, &item.attrs, path_str,
348                                                     article, type_kind, &type_disambig,
349                                                     "a", value_kind, &value_disambig);
350                                     continue;
351                                 }
352                             }
353                             type_def
354                         } else if let Ok(value_def) = self.resolve(path_str, true, &current_item) {
355                             value_def
356                         } else {
357                             resolution_failure(cx, &item.attrs, path_str, &dox, link_range);
358                             // this could just be a normal link
359                             continue;
360                         }
361                     }
362                     PathKind::Macro => {
363                         if let Some(def) = macro_resolve(cx, path_str) {
364                             (def, None)
365                         } else {
366                             resolution_failure(cx, &item.attrs, path_str, &dox, link_range);
367                             continue
368                         }
369                     }
370                 }
371             };
372
373             if let Def::PrimTy(_) = def {
374                 item.attrs.links.push((ori_link, None, fragment));
375             } else {
376                 let id = register_def(cx, def);
377                 item.attrs.links.push((ori_link, Some(id), fragment));
378             }
379         }
380
381         cx.sess().abort_if_errors();
382
383         if item.is_mod() && !item.attrs.inner_docs {
384             self.mod_ids.push(item_node_id.unwrap());
385         }
386
387         if item.is_mod() {
388             let ret = self.fold_item_recur(item);
389
390             self.mod_ids.pop();
391
392             ret
393         } else {
394             self.fold_item_recur(item)
395         }
396     }
397 }
398
399 /// Resolve a string as a macro
400 fn macro_resolve(cx: &DocContext, path_str: &str) -> Option<Def> {
401     use syntax::ext::base::{MacroKind, SyntaxExtension};
402     use syntax::ext::hygiene::Mark;
403     let segment = ast::PathSegment::from_ident(Ident::from_str(path_str));
404     let path = ast::Path { segments: vec![segment], span: DUMMY_SP };
405     let mut resolver = cx.resolver.borrow_mut();
406     let mark = Mark::root();
407     let res = resolver
408         .resolve_macro_to_def_inner(mark, &path, MacroKind::Bang, false);
409     if let Ok(def) = res {
410         if let SyntaxExtension::DeclMacro { .. } = *resolver.get_macro(def) {
411             return Some(def);
412         }
413     }
414     if let Some(def) = resolver.all_macros.get(&Symbol::intern(path_str)) {
415         return Some(*def);
416     }
417     None
418 }
419
420 fn span_of_attrs(attrs: &Attributes) -> syntax_pos::Span {
421     if attrs.doc_strings.is_empty() {
422         return DUMMY_SP;
423     }
424     let start = attrs.doc_strings[0].span();
425     let end = attrs.doc_strings.last().expect("No doc strings provided").span();
426     start.to(end)
427 }
428
429 fn resolution_failure(
430     cx: &DocContext,
431     attrs: &Attributes,
432     path_str: &str,
433     dox: &str,
434     link_range: Option<Range<usize>>,
435 ) {
436     let sp = span_of_attrs(attrs);
437     let msg = format!("`[{}]` cannot be resolved, ignoring it...", path_str);
438
439     let code_dox = sp.to_src(cx);
440
441     let doc_comment_padding = 3;
442     let mut diag = if let Some(link_range) = link_range {
443         // blah blah blah\nblah\nblah [blah] blah blah\nblah blah
444         //                       ^    ~~~~~~
445         //                       |    link_range
446         //                       last_new_line_offset
447
448         let mut diag;
449         if dox.lines().count() == code_dox.lines().count() {
450             let line_offset = dox[..link_range.start].lines().count();
451             // The span starts in the `///`, so we don't have to account for the leading whitespace
452             let code_dox_len = if line_offset <= 1 {
453                 doc_comment_padding
454             } else {
455                 // The first `///`
456                 doc_comment_padding +
457                     // Each subsequent leading whitespace and `///`
458                     code_dox.lines().skip(1).take(line_offset - 1).fold(0, |sum, line| {
459                         sum + doc_comment_padding + line.len() - line.trim().len()
460                     })
461             };
462
463             // Extract the specific span
464             let sp = sp.from_inner_byte_pos(
465                 link_range.start + code_dox_len,
466                 link_range.end + code_dox_len,
467             );
468
469             diag = cx.tcx.struct_span_lint_node(lint::builtin::INTRA_DOC_LINK_RESOLUTION_FAILURE,
470                                                 NodeId::new(0),
471                                                 sp,
472                                                 &msg);
473             diag.span_label(sp, "cannot be resolved, ignoring");
474         } else {
475             diag = cx.tcx.struct_span_lint_node(lint::builtin::INTRA_DOC_LINK_RESOLUTION_FAILURE,
476                                                 NodeId::new(0),
477                                                 sp,
478                                                 &msg);
479
480             let last_new_line_offset = dox[..link_range.start].rfind('\n').map_or(0, |n| n + 1);
481             let line = dox[last_new_line_offset..].lines().next().unwrap_or("");
482
483             // Print the line containing the `link_range` and manually mark it with '^'s
484             diag.note(&format!(
485                 "the link appears in this line:\n\n{line}\n\
486                  {indicator: <before$}{indicator:^<found$}",
487                 line=line,
488                 indicator="",
489                 before=link_range.start - last_new_line_offset,
490                 found=link_range.len(),
491             ));
492         }
493         diag
494     } else {
495         cx.tcx.struct_span_lint_node(lint::builtin::INTRA_DOC_LINK_RESOLUTION_FAILURE,
496                                      NodeId::new(0),
497                                      sp,
498                                      &msg)
499     };
500     diag.help("to escape `[` and `]` characters, just add '\\' before them like \
501                `\\[` or `\\]`");
502     diag.emit();
503 }
504
505 fn ambiguity_error(cx: &DocContext, attrs: &Attributes,
506                    path_str: &str,
507                    article1: &str, kind1: &str, disambig1: &str,
508                    article2: &str, kind2: &str, disambig2: &str) {
509     let sp = span_of_attrs(attrs);
510     cx.sess()
511       .struct_span_warn(sp,
512                         &format!("`{}` is both {} {} and {} {}",
513                                  path_str, article1, kind1,
514                                  article2, kind2))
515       .help(&format!("try `{}` if you want to select the {}, \
516                       or `{}` if you want to \
517                       select the {}",
518                       disambig1, kind1, disambig2,
519                       kind2))
520       .emit();
521 }
522
523 /// Given a def, returns its name and disambiguator
524 /// for a value namespace
525 ///
526 /// Returns None for things which cannot be ambiguous since
527 /// they exist in both namespaces (structs and modules)
528 fn value_ns_kind(def: Def, path_str: &str) -> Option<(&'static str, String)> {
529     match def {
530         // structs, variants, and mods exist in both namespaces. skip them
531         Def::StructCtor(..) | Def::Mod(..) | Def::Variant(..) | Def::VariantCtor(..) => None,
532         Def::Fn(..)
533             => Some(("function", format!("{}()", path_str))),
534         Def::Method(..)
535             => Some(("method", format!("{}()", path_str))),
536         Def::Const(..)
537             => Some(("const", format!("const@{}", path_str))),
538         Def::Static(..)
539             => Some(("static", format!("static@{}", path_str))),
540         _ => Some(("value", format!("value@{}", path_str))),
541     }
542 }
543
544 /// Given a def, returns its name, the article to be used, and a disambiguator
545 /// for the type namespace
546 fn type_ns_kind(def: Def, path_str: &str) -> (&'static str, &'static str, String) {
547     let (kind, article) = match def {
548         // we can still have non-tuple structs
549         Def::Struct(..) => ("struct", "a"),
550         Def::Enum(..) => ("enum", "an"),
551         Def::Trait(..) => ("trait", "a"),
552         Def::Union(..) => ("union", "a"),
553         _ => ("type", "a"),
554     };
555     (kind, article, format!("{}@{}", kind, path_str))
556 }
557
558 /// Given an enum variant's def, return the def of its enum and the associated fragment
559 fn handle_variant(cx: &DocContext, def: Def) -> Result<(Def, Option<String>), ()> {
560     use rustc::ty::DefIdTree;
561
562     let parent = if let Some(parent) = cx.tcx.parent(def.def_id()) {
563         parent
564     } else {
565         return Err(())
566     };
567     let parent_def = Def::Enum(parent);
568     let variant = cx.tcx.expect_variant_def(def);
569     Ok((parent_def, Some(format!("{}.v", variant.name))))
570 }
571
572 const PRIMITIVES: &[(&str, Def)] = &[
573     ("u8",    Def::PrimTy(hir::PrimTy::TyUint(syntax::ast::UintTy::U8))),
574     ("u16",   Def::PrimTy(hir::PrimTy::TyUint(syntax::ast::UintTy::U16))),
575     ("u32",   Def::PrimTy(hir::PrimTy::TyUint(syntax::ast::UintTy::U32))),
576     ("u64",   Def::PrimTy(hir::PrimTy::TyUint(syntax::ast::UintTy::U64))),
577     ("u128",  Def::PrimTy(hir::PrimTy::TyUint(syntax::ast::UintTy::U128))),
578     ("usize", Def::PrimTy(hir::PrimTy::TyUint(syntax::ast::UintTy::Usize))),
579     ("i8",    Def::PrimTy(hir::PrimTy::TyInt(syntax::ast::IntTy::I8))),
580     ("i16",   Def::PrimTy(hir::PrimTy::TyInt(syntax::ast::IntTy::I16))),
581     ("i32",   Def::PrimTy(hir::PrimTy::TyInt(syntax::ast::IntTy::I32))),
582     ("i64",   Def::PrimTy(hir::PrimTy::TyInt(syntax::ast::IntTy::I64))),
583     ("i128",  Def::PrimTy(hir::PrimTy::TyInt(syntax::ast::IntTy::I128))),
584     ("isize", Def::PrimTy(hir::PrimTy::TyInt(syntax::ast::IntTy::Isize))),
585     ("f32",   Def::PrimTy(hir::PrimTy::TyFloat(syntax::ast::FloatTy::F32))),
586     ("f64",   Def::PrimTy(hir::PrimTy::TyFloat(syntax::ast::FloatTy::F64))),
587     ("str",   Def::PrimTy(hir::PrimTy::TyStr)),
588     ("bool",  Def::PrimTy(hir::PrimTy::TyBool)),
589     ("char",  Def::PrimTy(hir::PrimTy::TyChar)),
590 ];
591
592 fn is_primitive(path_str: &str, is_val: bool) -> Option<Def> {
593     if is_val {
594         None
595     } else {
596         PRIMITIVES.iter().find(|x| x.0 == path_str).map(|x| x.1)
597     }
598 }