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