Skip to content

Commit

Permalink
Auto merge of rust-lang#74489 - jyn514:assoc-items, r=manishearth,pet…
Browse files Browse the repository at this point in the history
…rochenkov

Fix intra-doc links for associated items

@Manishearth and I found that links of the following sort are broken:
```rust
$ cat str_from.rs
/// [`String::from`]
pub fn foo() {}
$ rustdoc str_from.rs
warning: `[String::from]` cannot be resolved, ignoring it.
 --> str_from.rs:4:6
  |
4 | /// [`String::from`]
  |      ^^^^^^^^^^^^^^ cannot be resolved, ignoring
```
It turns out this is because the current implementation only looks at inherent impls (`impl Bar {}`) and traits _for the item being documented_. Note that this is not the same as the item being _linked_ to. So this code would work:

```rust
pub trait T1 {
    fn method();
}

pub struct S;
impl T1 for S {
    /// [S::method] on method
    fn method() {}
}
```

but putting the documentation on `trait T1` would not.

~~I realized that writing it up that my fix is only partially correct: It removes the inherent impls code when it should instead remove the `trait_item` code.~~ Fixed.

Additionally, I discovered while writing this there is some ambiguity: you could have multiple methods with the same name, but for different traits:

```rust
pub trait T1 {
    fn method();
}

pub trait T2 {
    fn method();
}

/// See [S::method]
pub struct S;
```

Rustdoc should give an ambiguity error here, but since there is currently no way to disambiguate the traits (rust-lang#74563) it does not (rust-lang#74489 (comment)).

There is a _third_ ambiguity that pops up: What if the trait is generic and is implemented multiple times with different generic parameters? In this case, my fix does not do very well: it thinks there is only one trait instantiated and links to that trait:

```
/// [`String::from`] -- this resolves to https://doc.rust-lang.org/nightly/alloc/string/struct.String.html#method.from
pub fn foo() {}
```

However, every `From` implementation has a method called `from`! So the browser picks a random one. This is not the desired behavior, but it's not clear how to avoid it.

To be consistent with the rest of intra-doc links, this only resolves associated items from traits that are in scope. This required changes to rustc_resolve to work cross-crate; the relevant commits are prefixed with `resolve: `. As a bonus, considering only traits in scope is slightly faster. To avoid re-calculating the traits over and over, rustdoc uses a cache to store the traits in scope for a given module.
  • Loading branch information
bors committed Aug 23, 2020
2 parents 5180f3d + d77eff2 commit 8fdce9b
Show file tree
Hide file tree
Showing 10 changed files with 480 additions and 174 deletions.
95 changes: 15 additions & 80 deletions src/librustc_resolve/late.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use RibKind::*;

use crate::{path_names_to_string, BindingError, CrateLint, LexicalScopeBinding};
use crate::{Module, ModuleOrUniformRoot, NameBindingKind, ParentScope, PathResult};
use crate::{Module, ModuleOrUniformRoot, ParentScope, PathResult};
use crate::{ResolutionError, Resolver, Segment, UseError};

use rustc_ast::ptr::P;
Expand All @@ -24,7 +24,6 @@ use rustc_hir::def_id::{DefId, CRATE_DEF_INDEX};
use rustc_hir::TraitCandidate;
use rustc_middle::{bug, span_bug};
use rustc_session::lint;
use rustc_span::def_id::LocalDefId;
use rustc_span::symbol::{kw, sym, Ident, Symbol};
use rustc_span::Span;
use smallvec::{smallvec, SmallVec};
Expand Down Expand Up @@ -2342,95 +2341,31 @@ impl<'a, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
ident.span = ident.span.normalize_to_macros_2_0();
let mut search_module = self.parent_scope.module;
loop {
self.get_traits_in_module_containing_item(ident, ns, search_module, &mut found_traits);
self.r.get_traits_in_module_containing_item(
ident,
ns,
search_module,
&mut found_traits,
&self.parent_scope,
);
search_module =
unwrap_or!(self.r.hygienic_lexical_parent(search_module, &mut ident.span), break);
}

if let Some(prelude) = self.r.prelude {
if !search_module.no_implicit_prelude {
self.get_traits_in_module_containing_item(ident, ns, prelude, &mut found_traits);
self.r.get_traits_in_module_containing_item(
ident,
ns,
prelude,
&mut found_traits,
&self.parent_scope,
);
}
}

found_traits
}

fn get_traits_in_module_containing_item(
&mut self,
ident: Ident,
ns: Namespace,
module: Module<'a>,
found_traits: &mut Vec<TraitCandidate>,
) {
assert!(ns == TypeNS || ns == ValueNS);
let mut traits = module.traits.borrow_mut();
if traits.is_none() {
let mut collected_traits = Vec::new();
module.for_each_child(self.r, |_, name, ns, binding| {
if ns != TypeNS {
return;
}
match binding.res() {
Res::Def(DefKind::Trait | DefKind::TraitAlias, _) => {
collected_traits.push((name, binding))
}
_ => (),
}
});
*traits = Some(collected_traits.into_boxed_slice());
}

for &(trait_name, binding) in traits.as_ref().unwrap().iter() {
// Traits have pseudo-modules that can be used to search for the given ident.
if let Some(module) = binding.module() {
let mut ident = ident;
if ident.span.glob_adjust(module.expansion, binding.span).is_none() {
continue;
}
if self
.r
.resolve_ident_in_module_unadjusted(
ModuleOrUniformRoot::Module(module),
ident,
ns,
&self.parent_scope,
false,
module.span,
)
.is_ok()
{
let import_ids = self.find_transitive_imports(&binding.kind, trait_name);
let trait_def_id = module.def_id().unwrap();
found_traits.push(TraitCandidate { def_id: trait_def_id, import_ids });
}
} else if let Res::Def(DefKind::TraitAlias, _) = binding.res() {
// For now, just treat all trait aliases as possible candidates, since we don't
// know if the ident is somewhere in the transitive bounds.
let import_ids = self.find_transitive_imports(&binding.kind, trait_name);
let trait_def_id = binding.res().def_id();
found_traits.push(TraitCandidate { def_id: trait_def_id, import_ids });
} else {
bug!("candidate is not trait or trait alias?")
}
}
}

fn find_transitive_imports(
&mut self,
mut kind: &NameBindingKind<'_>,
trait_name: Ident,
) -> SmallVec<[LocalDefId; 1]> {
let mut import_ids = smallvec![];
while let NameBindingKind::Import { import, binding, .. } = kind {
let id = self.r.local_def_id(import.id);
self.r.maybe_unused_trait_imports.insert(id);
self.r.add_to_glob_map(&import, trait_name);
import_ids.push(id);
kind = &binding.kind;
}
import_ids
}
}

impl<'a> Resolver<'a> {
Expand Down
116 changes: 115 additions & 1 deletion src/librustc_resolve/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ use rustc_index::vec::IndexVec;
use rustc_metadata::creader::{CStore, CrateLoader};
use rustc_middle::hir::exports::ExportMap;
use rustc_middle::middle::cstore::{CrateStore, MetadataLoaderDyn};
use rustc_middle::span_bug;
use rustc_middle::ty::query::Providers;
use rustc_middle::ty::{self, DefIdTree, ResolverOutputs};
use rustc_middle::{bug, span_bug};
use rustc_session::lint;
use rustc_session::lint::{BuiltinLintDiagnostics, LintBuffer};
use rustc_session::Session;
Expand All @@ -54,6 +54,7 @@ use rustc_span::source_map::Spanned;
use rustc_span::symbol::{kw, sym, Ident, Symbol};
use rustc_span::{Span, DUMMY_SP};

use smallvec::{smallvec, SmallVec};
use std::cell::{Cell, RefCell};
use std::collections::BTreeSet;
use std::{cmp, fmt, iter, ptr};
Expand Down Expand Up @@ -521,6 +522,29 @@ impl<'a> ModuleData<'a> {
}
}

/// This modifies `self` in place. The traits will be stored in `self.traits`.
fn ensure_traits<R>(&'a self, resolver: &mut R)
where
R: AsMut<Resolver<'a>>,
{
let mut traits = self.traits.borrow_mut();
if traits.is_none() {
let mut collected_traits = Vec::new();
self.for_each_child(resolver, |_, name, ns, binding| {
if ns != TypeNS {
return;
}
match binding.res() {
Res::Def(DefKind::Trait | DefKind::TraitAlias, _) => {
collected_traits.push((name, binding))
}
_ => (),
}
});
*traits = Some(collected_traits.into_boxed_slice());
}
}

fn res(&self) -> Option<Res> {
match self.kind {
ModuleKind::Def(kind, def_id, _) => Some(Res::Def(kind, def_id)),
Expand Down Expand Up @@ -1430,6 +1454,68 @@ impl<'a> Resolver<'a> {
self.crate_loader.postprocess(krate);
}

fn get_traits_in_module_containing_item(
&mut self,
ident: Ident,
ns: Namespace,
module: Module<'a>,
found_traits: &mut Vec<TraitCandidate>,
parent_scope: &ParentScope<'a>,
) {
assert!(ns == TypeNS || ns == ValueNS);
module.ensure_traits(self);
let traits = module.traits.borrow();

for &(trait_name, binding) in traits.as_ref().unwrap().iter() {
// Traits have pseudo-modules that can be used to search for the given ident.
if let Some(module) = binding.module() {
let mut ident = ident;
if ident.span.glob_adjust(module.expansion, binding.span).is_none() {
continue;
}
if self
.resolve_ident_in_module_unadjusted(
ModuleOrUniformRoot::Module(module),
ident,
ns,
parent_scope,
false,
module.span,
)
.is_ok()
{
let import_ids = self.find_transitive_imports(&binding.kind, trait_name);
let trait_def_id = module.def_id().unwrap();
found_traits.push(TraitCandidate { def_id: trait_def_id, import_ids });
}
} else if let Res::Def(DefKind::TraitAlias, _) = binding.res() {
// For now, just treat all trait aliases as possible candidates, since we don't
// know if the ident is somewhere in the transitive bounds.
let import_ids = self.find_transitive_imports(&binding.kind, trait_name);
let trait_def_id = binding.res().def_id();
found_traits.push(TraitCandidate { def_id: trait_def_id, import_ids });
} else {
bug!("candidate is not trait or trait alias?")
}
}
}

fn find_transitive_imports(
&mut self,
mut kind: &NameBindingKind<'_>,
trait_name: Ident,
) -> SmallVec<[LocalDefId; 1]> {
let mut import_ids = smallvec![];
while let NameBindingKind::Import { import, binding, .. } = kind {
let id = self.local_def_id(import.id);
self.maybe_unused_trait_imports.insert(id);
self.add_to_glob_map(&import, trait_name);
import_ids.push(id);
kind = &binding.kind;
}
import_ids
}

fn new_module(
&self,
parent: Module<'a>,
Expand Down Expand Up @@ -3039,6 +3125,34 @@ impl<'a> Resolver<'a> {
})
}

/// This is equivalent to `get_traits_in_module_containing_item`, but without filtering by the associated item.
///
/// This is used by rustdoc for intra-doc links.
pub fn traits_in_scope(&mut self, module_id: DefId) -> Vec<TraitCandidate> {
let module = self.get_module(module_id);
module.ensure_traits(self);
let traits = module.traits.borrow();
let to_candidate =
|this: &mut Self, &(trait_name, binding): &(Ident, &NameBinding<'_>)| TraitCandidate {
def_id: binding.res().def_id(),
import_ids: this.find_transitive_imports(&binding.kind, trait_name),
};

let mut candidates: Vec<_> =
traits.as_ref().unwrap().iter().map(|x| to_candidate(self, x)).collect();

if let Some(prelude) = self.prelude {
if !module.no_implicit_prelude {
prelude.ensure_traits(self);
candidates.extend(
prelude.traits.borrow().as_ref().unwrap().iter().map(|x| to_candidate(self, x)),
);
}
}

candidates
}

/// Rustdoc uses this to resolve things in a recoverable way. `ResolutionError<'a>`
/// isn't something that can be returned because it can't be made to live that long,
/// and also it's a private type. Fortunately rustdoc doesn't need to know the error,
Expand Down
13 changes: 11 additions & 2 deletions src/librustdoc/clean/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use rustc_hir::lang_items;
use rustc_hir::Mutability;
use rustc_index::vec::IndexVec;
use rustc_middle::middle::stability;
use rustc_middle::ty::TyCtxt;
use rustc_middle::ty::{AssocKind, TyCtxt};
use rustc_span::hygiene::MacroKind;
use rustc_span::source_map::DUMMY_SP;
use rustc_span::symbol::{kw, sym, Ident, Symbol};
Expand Down Expand Up @@ -282,12 +282,21 @@ pub enum ItemEnum {
}

impl ItemEnum {
pub fn is_associated(&self) -> bool {
pub fn is_type_alias(&self) -> bool {
match *self {
ItemEnum::TypedefItem(_, _) | ItemEnum::AssocTypeItem(_, _) => true,
_ => false,
}
}

pub fn as_assoc_kind(&self) -> Option<AssocKind> {
match *self {
ItemEnum::AssocConstItem(..) => Some(AssocKind::Const),
ItemEnum::AssocTypeItem(..) => Some(AssocKind::Type),
ItemEnum::TyMethodItem(..) | ItemEnum::MethodItem(..) => Some(AssocKind::Fn),
_ => None,
}
}
}

#[derive(Clone, Debug)]
Expand Down
6 changes: 6 additions & 0 deletions src/librustdoc/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ pub struct DocContext<'tcx> {
pub auto_traits: Vec<DefId>,
/// The options given to rustdoc that could be relevant to a pass.
pub render_options: RenderOptions,
/// The traits in scope for a given module.
///
/// See `collect_intra_doc_links::traits_implemented_by` for more details.
/// `map<module, set<trait>>`
pub module_trait_cache: RefCell<FxHashMap<DefId, FxHashSet<DefId>>>,
}

impl<'tcx> DocContext<'tcx> {
Expand Down Expand Up @@ -510,6 +515,7 @@ pub fn run_core(options: RustdocOptions) -> (clean::Crate, RenderInfo, RenderOpt
.filter(|trait_def_id| tcx.trait_is_auto(*trait_def_id))
.collect(),
render_options,
module_trait_cache: RefCell::new(FxHashMap::default()),
};
debug!("crate: {:?}", tcx.hir().krate());

Expand Down
2 changes: 1 addition & 1 deletion src/librustdoc/html/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3612,7 +3612,7 @@ fn render_impl(
};

let (is_hidden, extra_class) =
if (trait_.is_none() || item.doc_value().is_some() || item.inner.is_associated())
if (trait_.is_none() || item.doc_value().is_some() || item.inner.is_type_alias())
&& !is_default_item
{
(false, "")
Expand Down
Loading

0 comments on commit 8fdce9b

Please sign in to comment.