]> git.proxmox.com Git - rustc.git/blobdiff - src/librustdoc/passes/collect_intra_doc_links.rs
New upstream version 1.48.0~beta.8+dfsg1
[rustc.git] / src / librustdoc / passes / collect_intra_doc_links.rs
index bf091a0a624ddabb9be98d87e3d3066affeb4a7c..cf94ea384fd605a50cf5d7d36954374c29ae51ec 100644 (file)
@@ -2,7 +2,6 @@ use rustc_ast as ast;
 use rustc_data_structures::stable_set::FxHashSet;
 use rustc_errors::{Applicability, DiagnosticBuilder};
 use rustc_expand::base::SyntaxExtensionKind;
-use rustc_feature::UnstableFeatures;
 use rustc_hir as hir;
 use rustc_hir::def::{
     DefKind,
@@ -12,13 +11,17 @@ use rustc_hir::def::{
 use rustc_hir::def_id::DefId;
 use rustc_middle::ty;
 use rustc_resolve::ParentScope;
-use rustc_session::lint;
+use rustc_session::lint::{
+    builtin::{BROKEN_INTRA_DOC_LINKS, PRIVATE_INTRA_DOC_LINKS},
+    Lint,
+};
 use rustc_span::hygiene::MacroKind;
 use rustc_span::symbol::Ident;
 use rustc_span::symbol::Symbol;
 use rustc_span::DUMMY_SP;
-use smallvec::SmallVec;
+use smallvec::{smallvec, SmallVec};
 
+use std::borrow::Cow;
 use std::cell::Cell;
 use std::ops::Range;
 
@@ -37,28 +40,48 @@ pub const COLLECT_INTRA_DOC_LINKS: Pass = Pass {
 };
 
 pub fn collect_intra_doc_links(krate: Crate, cx: &DocContext<'_>) -> Crate {
-    if !UnstableFeatures::from_environment().is_nightly_build() {
-        krate
-    } else {
-        let mut coll = LinkCollector::new(cx);
+    let mut coll = LinkCollector::new(cx);
+    coll.fold_crate(krate)
+}
 
-        coll.fold_crate(krate)
+enum ErrorKind<'a> {
+    Resolve(Box<ResolutionFailure<'a>>),
+    AnchorFailure(AnchorFailure),
+}
+
+impl<'a> From<ResolutionFailure<'a>> for ErrorKind<'a> {
+    fn from(err: ResolutionFailure<'a>) -> Self {
+        ErrorKind::Resolve(box err)
     }
 }
 
-enum ErrorKind {
-    ResolutionFailure,
-    AnchorFailure(AnchorFailure),
+#[derive(Debug)]
+enum ResolutionFailure<'a> {
+    /// This resolved, but with the wrong namespace.
+    /// `Namespace` is the expected namespace (as opposed to the actual).
+    WrongNamespace(Res, Namespace),
+    /// The link failed to resolve. `resolution_failure` should look to see if there's
+    /// a more helpful error that can be given.
+    NotResolved { module_id: DefId, partial_res: Option<Res>, unresolved: Cow<'a, str> },
+    /// should not ever happen
+    NoParentItem,
+    /// used to communicate that this should be ignored, but shouldn't be reported to the user
+    Dummy,
+}
+
+impl ResolutionFailure<'a> {
+    // This resolved fully (not just partially) but is erroneous for some other reason
+    fn full_res(&self) -> Option<Res> {
+        match self {
+            Self::WrongNamespace(res, _) => Some(*res),
+            _ => None,
+        }
+    }
 }
 
 enum AnchorFailure {
     MultipleAnchors,
-    Primitive,
-    Variant,
-    AssocConstant,
-    AssocType,
-    Field,
-    Method,
+    RustdocAnchorConflict(Res),
 }
 
 struct LinkCollector<'a, 'tcx> {
@@ -68,7 +91,7 @@ struct LinkCollector<'a, 'tcx> {
     /// This is used to store the kind of associated items,
     /// because `clean` and the disambiguator code expect them to be different.
     /// See the code for associated items on inherent impls for details.
-    kind_side_channel: Cell<Option<DefKind>>,
+    kind_side_channel: Cell<Option<(DefKind, DefId)>>,
 }
 
 impl<'a, 'tcx> LinkCollector<'a, 'tcx> {
@@ -78,17 +101,28 @@ impl<'a, 'tcx> LinkCollector<'a, 'tcx> {
 
     fn variant_field(
         &self,
-        path_str: &str,
+        path_str: &'path str,
         current_item: &Option<String>,
         module_id: DefId,
-    ) -> Result<(Res, Option<String>), ErrorKind> {
+    ) -> Result<(Res, Option<String>), ErrorKind<'path>> {
         let cx = self.cx;
+        let no_res = || ResolutionFailure::NotResolved {
+            module_id,
+            partial_res: None,
+            unresolved: path_str.into(),
+        };
 
+        debug!("looking for enum variant {}", path_str);
         let mut split = path_str.rsplitn(3, "::");
-        let variant_field_name =
-            split.next().map(|f| Symbol::intern(f)).ok_or(ErrorKind::ResolutionFailure)?;
-        let variant_name =
-            split.next().map(|f| Symbol::intern(f)).ok_or(ErrorKind::ResolutionFailure)?;
+        let (variant_field_str, variant_field_name) = split
+            .next()
+            .map(|f| (f, Symbol::intern(f)))
+            .expect("fold_item should ensure link is non-empty");
+        let (variant_str, variant_name) =
+            // we're not sure this is a variant at all, so use the full string
+            // If there's no second component, the link looks like `[path]`.
+            // So there's no partial res and we should say the whole link failed to resolve.
+            split.next().map(|f| (f, Symbol::intern(f))).ok_or_else(no_res)?;
         let path = split
             .next()
             .map(|f| {
@@ -99,14 +133,17 @@ impl<'a, 'tcx> LinkCollector<'a, 'tcx> {
                 }
                 f.to_owned()
             })
-            .ok_or(ErrorKind::ResolutionFailure)?;
-        let (_, ty_res) = cx
+            // If there's no third component, we saw `[a::b]` before and it failed to resolve.
+            // So there's no partial res.
+            .ok_or_else(no_res)?;
+        let ty_res = cx
             .enter_resolver(|resolver| {
                 resolver.resolve_str_path_error(DUMMY_SP, &path, TypeNS, module_id)
             })
-            .map_err(|_| ErrorKind::ResolutionFailure)?;
+            .map(|(_, res)| res)
+            .unwrap_or(Res::Err);
         if let Res::Err = ty_res {
-            return Err(ErrorKind::ResolutionFailure);
+            return Err(no_res().into());
         }
         let ty_res = ty_res.map_id(|_| panic!("unexpected node_id"));
         match ty_res {
@@ -118,31 +155,47 @@ impl<'a, 'tcx> LinkCollector<'a, 'tcx> {
                     .flat_map(|imp| cx.tcx.associated_items(*imp).in_definition_order())
                     .any(|item| item.ident.name == variant_name)
                 {
-                    return Err(ErrorKind::ResolutionFailure);
+                    // This is just to let `fold_item` know that this shouldn't be considered;
+                    // it's a bug for the error to make it to the user
+                    return Err(ResolutionFailure::Dummy.into());
                 }
-                match cx.tcx.type_of(did).kind {
+                match cx.tcx.type_of(did).kind() {
                     ty::Adt(def, _) if def.is_enum() => {
                         if def.all_fields().any(|item| item.ident.name == variant_field_name) {
                             Ok((
                                 ty_res,
                                 Some(format!(
                                     "variant.{}.field.{}",
-                                    variant_name, variant_field_name
+                                    variant_str, variant_field_name
                                 )),
                             ))
                         } else {
-                            Err(ErrorKind::ResolutionFailure)
+                            Err(ResolutionFailure::NotResolved {
+                                module_id,
+                                partial_res: Some(Res::Def(DefKind::Enum, def.did)),
+                                unresolved: variant_field_str.into(),
+                            }
+                            .into())
                         }
                     }
-                    _ => Err(ErrorKind::ResolutionFailure),
+                    _ => unreachable!(),
                 }
             }
-            _ => Err(ErrorKind::ResolutionFailure),
+            _ => Err(ResolutionFailure::NotResolved {
+                module_id,
+                partial_res: Some(ty_res),
+                unresolved: variant_str.into(),
+            }
+            .into()),
         }
     }
 
     /// Resolves a string as a macro.
-    fn macro_resolve(&self, path_str: &str, parent_id: Option<DefId>) -> Option<Res> {
+    fn macro_resolve(
+        &self,
+        path_str: &'a str,
+        module_id: DefId,
+    ) -> Result<Res, ResolutionFailure<'a>> {
         let cx = self.cx;
         let path = ast::Path::from_ident(Ident::from_str(path_str));
         cx.enter_resolver(|resolver| {
@@ -154,275 +207,349 @@ impl<'a, 'tcx> LinkCollector<'a, 'tcx> {
                 false,
             ) {
                 if let SyntaxExtensionKind::LegacyBang { .. } = ext.kind {
-                    return Some(res.map_id(|_| panic!("unexpected id")));
+                    return Ok(res.map_id(|_| panic!("unexpected id")));
                 }
             }
             if let Some(res) = resolver.all_macros().get(&Symbol::intern(path_str)) {
-                return Some(res.map_id(|_| panic!("unexpected id")));
+                return Ok(res.map_id(|_| panic!("unexpected id")));
             }
-            if let Some(module_id) = parent_id {
-                if let Ok((_, res)) =
-                    resolver.resolve_str_path_error(DUMMY_SP, path_str, MacroNS, module_id)
-                {
-                    // don't resolve builtins like `#[derive]`
-                    if let Res::Def(..) = res {
-                        let res = res.map_id(|_| panic!("unexpected node_id"));
-                        return Some(res);
-                    }
+            debug!("resolving {} as a macro in the module {:?}", path_str, module_id);
+            if let Ok((_, res)) =
+                resolver.resolve_str_path_error(DUMMY_SP, path_str, MacroNS, module_id)
+            {
+                // don't resolve builtins like `#[derive]`
+                if let Res::Def(..) = res {
+                    let res = res.map_id(|_| panic!("unexpected node_id"));
+                    return Ok(res);
                 }
-            } else {
-                debug!("attempting to resolve item without parent module: {}", path_str);
             }
-            None
+            Err(ResolutionFailure::NotResolved {
+                module_id,
+                partial_res: None,
+                unresolved: path_str.into(),
+            })
         })
     }
+
     /// Resolves a string as a path within a particular namespace. Also returns an optional
     /// URL fragment in the case of variants and methods.
-    fn resolve(
+    fn resolve<'path>(
         &self,
-        path_str: &str,
+        path_str: &'path str,
         ns: Namespace,
         current_item: &Option<String>,
-        parent_id: Option<DefId>,
+        module_id: DefId,
         extra_fragment: &Option<String>,
-    ) -> Result<(Res, Option<String>), ErrorKind> {
+    ) -> Result<(Res, Option<String>), ErrorKind<'path>> {
         let cx = self.cx;
 
-        // In case we're in a module, try to resolve the relative path.
-        if let Some(module_id) = parent_id {
-            let result = cx.enter_resolver(|resolver| {
-                resolver.resolve_str_path_error(DUMMY_SP, &path_str, ns, module_id)
-            });
-            debug!("{} resolved to {:?} in namespace {:?}", path_str, result, ns);
-            let result = match result {
-                Ok((_, Res::Err)) => Err(ErrorKind::ResolutionFailure),
-                _ => result.map_err(|_| ErrorKind::ResolutionFailure),
-            };
+        let result = cx.enter_resolver(|resolver| {
+            resolver.resolve_str_path_error(DUMMY_SP, &path_str, ns, module_id)
+        });
+        debug!("{} resolved to {:?} in namespace {:?}", path_str, result, ns);
+        let result = match result {
+            Ok((_, Res::Err)) => Err(()),
+            x => x,
+        };
 
-            if let Ok((_, res)) = result {
-                let res = res.map_id(|_| panic!("unexpected node_id"));
-                // In case this is a trait item, skip the
-                // early return and try looking for the trait.
-                let value = match res {
-                    Res::Def(DefKind::AssocFn | DefKind::AssocConst, _) => true,
-                    Res::Def(DefKind::AssocTy, _) => false,
-                    Res::Def(DefKind::Variant, _) => {
-                        return handle_variant(cx, res, extra_fragment);
-                    }
-                    // Not a trait item; just return what we found.
-                    Res::PrimTy(..) => {
-                        if extra_fragment.is_some() {
-                            return Err(ErrorKind::AnchorFailure(AnchorFailure::Primitive));
-                        }
-                        return Ok((res, Some(path_str.to_owned())));
-                    }
-                    Res::Def(DefKind::Mod, _) => {
-                        return Ok((res, extra_fragment.clone()));
-                    }
-                    _ => {
-                        return Ok((res, extra_fragment.clone()));
+        if let Ok((_, res)) = result {
+            let res = res.map_id(|_| panic!("unexpected node_id"));
+            // In case this is a trait item, skip the
+            // early return and try looking for the trait.
+            let value = match res {
+                Res::Def(DefKind::AssocFn | DefKind::AssocConst, _) => true,
+                Res::Def(DefKind::AssocTy, _) => false,
+                Res::Def(DefKind::Variant, _) => {
+                    return handle_variant(cx, res, extra_fragment);
+                }
+                // Not a trait item; just return what we found.
+                Res::PrimTy(ty) => {
+                    if extra_fragment.is_some() {
+                        return Err(ErrorKind::AnchorFailure(
+                            AnchorFailure::RustdocAnchorConflict(res),
+                        ));
                     }
-                };
-
-                if value != (ns == ValueNS) {
-                    return Err(ErrorKind::ResolutionFailure);
+                    return Ok((res, Some(ty.name_str().to_owned())));
+                }
+                Res::Def(DefKind::Mod, _) => {
+                    return Ok((res, extra_fragment.clone()));
                 }
-            } else if let Some((path, prim)) = is_primitive(path_str, ns) {
-                if extra_fragment.is_some() {
-                    return Err(ErrorKind::AnchorFailure(AnchorFailure::Primitive));
+                _ => {
+                    return Ok((res, extra_fragment.clone()));
                 }
-                return Ok((prim, Some(path.to_owned())));
+            };
+
+            if value != (ns == ValueNS) {
+                return Err(ResolutionFailure::WrongNamespace(res, ns).into());
+            }
+        // FIXME: why is this necessary?
+        } else if let Some((path, prim)) = is_primitive(path_str, ns) {
+            if extra_fragment.is_some() {
+                return Err(ErrorKind::AnchorFailure(AnchorFailure::RustdocAnchorConflict(prim)));
             }
+            return Ok((prim, Some(path.to_owned())));
+        }
 
-            // Try looking for methods and associated items.
-            let mut split = path_str.rsplitn(2, "::");
-            let item_name =
-                split.next().map(|f| Symbol::intern(f)).ok_or(ErrorKind::ResolutionFailure)?;
-            let path = split
-                .next()
-                .map(|f| {
-                    if f == "self" || f == "Self" {
-                        if let Some(name) = current_item.as_ref() {
-                            return name.clone();
-                        }
-                    }
-                    f.to_owned()
-                })
-                .ok_or(ErrorKind::ResolutionFailure)?;
-
-            if let Some((path, prim)) = is_primitive(&path, TypeNS) {
-                for &impl_ in primitive_impl(cx, &path).ok_or(ErrorKind::ResolutionFailure)? {
-                    let link = cx
-                        .tcx
-                        .associated_items(impl_)
-                        .find_by_name_and_namespace(
-                            cx.tcx,
-                            Ident::with_dummy_span(item_name),
-                            ns,
-                            impl_,
-                        )
-                        .and_then(|item| match item.kind {
-                            ty::AssocKind::Fn => Some("method"),
-                            _ => None,
-                        })
-                        .map(|out| (prim, Some(format!("{}#{}.{}", path, out, item_name))));
-                    if let Some(link) = link {
-                        return Ok(link);
+        // Try looking for methods and associated items.
+        let mut split = path_str.rsplitn(2, "::");
+        // this can be an `unwrap()` because we ensure the link is never empty
+        let (item_str, item_name) = split.next().map(|i| (i, Symbol::intern(i))).unwrap();
+        let path_root = split
+            .next()
+            .map(|f| {
+                if f == "self" || f == "Self" {
+                    if let Some(name) = current_item.as_ref() {
+                        return name.clone();
                     }
                 }
-                return Err(ErrorKind::ResolutionFailure);
+                f.to_owned()
+            })
+            // If there's no `::`, it's not an associated item.
+            // So we can be sure that `rustc_resolve` was accurate when it said it wasn't resolved.
+            .ok_or_else(|| {
+                debug!("found no `::`, assumming {} was correctly not in scope", item_name);
+                ResolutionFailure::NotResolved {
+                    module_id,
+                    partial_res: None,
+                    unresolved: item_str.into(),
+                }
+            })?;
+
+        if let Some((path, prim)) = is_primitive(&path_root, TypeNS) {
+            let impls =
+                primitive_impl(cx, &path).ok_or_else(|| ResolutionFailure::NotResolved {
+                    module_id,
+                    partial_res: Some(prim),
+                    unresolved: item_str.into(),
+                })?;
+            for &impl_ in impls {
+                let link = cx
+                    .tcx
+                    .associated_items(impl_)
+                    .find_by_name_and_namespace(
+                        cx.tcx,
+                        Ident::with_dummy_span(item_name),
+                        ns,
+                        impl_,
+                    )
+                    .map(|item| match item.kind {
+                        ty::AssocKind::Fn => "method",
+                        ty::AssocKind::Const => "associatedconstant",
+                        ty::AssocKind::Type => "associatedtype",
+                    })
+                    .map(|out| (prim, Some(format!("{}#{}.{}", path, out, item_str))));
+                if let Some(link) = link {
+                    return Ok(link);
+                }
             }
+            debug!(
+                "returning primitive error for {}::{} in {} namespace",
+                path,
+                item_name,
+                ns.descr()
+            );
+            return Err(ResolutionFailure::NotResolved {
+                module_id,
+                partial_res: Some(prim),
+                unresolved: item_str.into(),
+            }
+            .into());
+        }
 
-            let (_, ty_res) = cx
-                .enter_resolver(|resolver| {
-                    resolver.resolve_str_path_error(DUMMY_SP, &path, TypeNS, module_id)
-                })
-                .map_err(|_| ErrorKind::ResolutionFailure)?;
-            if let Res::Err = ty_res {
+        let ty_res = cx
+            .enter_resolver(|resolver| {
+                // only types can have associated items
+                resolver.resolve_str_path_error(DUMMY_SP, &path_root, TypeNS, module_id)
+            })
+            .map(|(_, res)| res);
+        let ty_res = match ty_res {
+            Err(()) | Ok(Res::Err) => {
                 return if ns == Namespace::ValueNS {
                     self.variant_field(path_str, current_item, module_id)
                 } else {
-                    Err(ErrorKind::ResolutionFailure)
+                    Err(ResolutionFailure::NotResolved {
+                        module_id,
+                        partial_res: None,
+                        unresolved: path_root.into(),
+                    }
+                    .into())
                 };
             }
-            let ty_res = ty_res.map_id(|_| panic!("unexpected node_id"));
-            let res = match ty_res {
-                Res::Def(
-                    DefKind::Struct | DefKind::Union | DefKind::Enum | DefKind::TyAlias,
-                    did,
-                ) => {
-                    debug!("looking for associated item named {} for item {:?}", item_name, did);
-                    // Checks if item_name belongs to `impl SomeItem`
-                    let kind = cx
-                        .tcx
-                        .inherent_impls(did)
-                        .iter()
-                        .flat_map(|&imp| {
-                            cx.tcx.associated_items(imp).find_by_name_and_namespace(
-                                cx.tcx,
-                                Ident::with_dummy_span(item_name),
-                                ns,
-                                imp,
-                            )
-                        })
-                        .map(|item| item.kind)
-                        // There should only ever be one associated item that matches from any inherent impl
-                        .next()
-                        // Check if item_name belongs to `impl SomeTrait for SomeItem`
-                        // This gives precedence to `impl SomeItem`:
-                        // Although having both would be ambiguous, use impl version for compat. sake.
-                        // To handle that properly resolve() would have to support
-                        // something like [`ambi_fn`](<SomeStruct as SomeTrait>::ambi_fn)
-                        .or_else(|| {
-                            let kind = resolve_associated_trait_item(
-                                did, module_id, item_name, ns, &self.cx,
-                            );
-                            debug!("got associated item kind {:?}", kind);
-                            kind
-                        });
-
-                    if let Some(kind) = kind {
-                        let out = match kind {
-                            ty::AssocKind::Fn => "method",
-                            ty::AssocKind::Const => "associatedconstant",
-                            ty::AssocKind::Type => "associatedtype",
-                        };
-                        Some(if extra_fragment.is_some() {
-                            Err(ErrorKind::AnchorFailure(if kind == ty::AssocKind::Fn {
-                                AnchorFailure::Method
+            Ok(res) => res,
+        };
+        let ty_res = ty_res.map_id(|_| panic!("unexpected node_id"));
+        let res = match ty_res {
+            Res::Def(DefKind::Struct | DefKind::Union | DefKind::Enum | DefKind::TyAlias, did) => {
+                debug!("looking for associated item named {} for item {:?}", item_name, did);
+                // Checks if item_name belongs to `impl SomeItem`
+                let assoc_item = cx
+                    .tcx
+                    .inherent_impls(did)
+                    .iter()
+                    .flat_map(|&imp| {
+                        cx.tcx.associated_items(imp).find_by_name_and_namespace(
+                            cx.tcx,
+                            Ident::with_dummy_span(item_name),
+                            ns,
+                            imp,
+                        )
+                    })
+                    .map(|item| (item.kind, item.def_id))
+                    // There should only ever be one associated item that matches from any inherent impl
+                    .next()
+                    // Check if item_name belongs to `impl SomeTrait for SomeItem`
+                    // This gives precedence to `impl SomeItem`:
+                    // Although having both would be ambiguous, use impl version for compat. sake.
+                    // To handle that properly resolve() would have to support
+                    // something like [`ambi_fn`](<SomeStruct as SomeTrait>::ambi_fn)
+                    .or_else(|| {
+                        let kind =
+                            resolve_associated_trait_item(did, module_id, item_name, ns, &self.cx);
+                        debug!("got associated item kind {:?}", kind);
+                        kind
+                    });
+
+                if let Some((kind, id)) = assoc_item {
+                    let out = match kind {
+                        ty::AssocKind::Fn => "method",
+                        ty::AssocKind::Const => "associatedconstant",
+                        ty::AssocKind::Type => "associatedtype",
+                    };
+                    Some(if extra_fragment.is_some() {
+                        Err(ErrorKind::AnchorFailure(AnchorFailure::RustdocAnchorConflict(ty_res)))
+                    } else {
+                        // HACK(jynelson): `clean` expects the type, not the associated item.
+                        // but the disambiguator logic expects the associated item.
+                        // Store the kind in a side channel so that only the disambiguator logic looks at it.
+                        self.kind_side_channel.set(Some((kind.as_def_kind(), id)));
+                        Ok((ty_res, Some(format!("{}.{}", out, item_str))))
+                    })
+                } else if ns == Namespace::ValueNS {
+                    debug!("looking for variants or fields named {} for {:?}", item_name, did);
+                    match cx.tcx.type_of(did).kind() {
+                        ty::Adt(def, _) => {
+                            let field = if def.is_enum() {
+                                def.all_fields().find(|item| item.ident.name == item_name)
                             } else {
-                                AnchorFailure::AssocConstant
-                            }))
-                        } else {
-                            // HACK(jynelson): `clean` expects the type, not the associated item.
-                            // but the disambiguator logic expects the associated item.
-                            // Store the kind in a side channel so that only the disambiguator logic looks at it.
-                            self.kind_side_channel.set(Some(kind.as_def_kind()));
-                            Ok((ty_res, Some(format!("{}.{}", out, item_name))))
-                        })
-                    } else if ns == Namespace::ValueNS {
-                        match cx.tcx.type_of(did).kind {
-                            ty::Adt(def, _) => {
-                                let field = if def.is_enum() {
-                                    def.all_fields().find(|item| item.ident.name == item_name)
-                                } else {
-                                    def.non_enum_variant()
-                                        .fields
-                                        .iter()
-                                        .find(|item| item.ident.name == item_name)
-                                };
-                                field.map(|item| {
-                                    if extra_fragment.is_some() {
-                                        Err(ErrorKind::AnchorFailure(if def.is_enum() {
-                                            AnchorFailure::Variant
+                                def.non_enum_variant()
+                                    .fields
+                                    .iter()
+                                    .find(|item| item.ident.name == item_name)
+                            };
+                            field.map(|item| {
+                                if extra_fragment.is_some() {
+                                    let res = Res::Def(
+                                        if def.is_enum() {
+                                            DefKind::Variant
                                         } else {
-                                            AnchorFailure::Field
-                                        }))
-                                    } else {
-                                        Ok((
-                                            ty_res,
-                                            Some(format!(
-                                                "{}.{}",
-                                                if def.is_enum() {
-                                                    "variant"
-                                                } else {
-                                                    "structfield"
-                                                },
-                                                item.ident
-                                            )),
-                                        ))
-                                    }
-                                })
+                                            DefKind::Field
+                                        },
+                                        item.did,
+                                    );
+                                    Err(ErrorKind::AnchorFailure(
+                                        AnchorFailure::RustdocAnchorConflict(res),
+                                    ))
+                                } else {
+                                    Ok((
+                                        ty_res,
+                                        Some(format!(
+                                            "{}.{}",
+                                            if def.is_enum() { "variant" } else { "structfield" },
+                                            item.ident
+                                        )),
+                                    ))
+                                }
+                            })
+                        }
+                        _ => None,
+                    }
+                } else {
+                    // We already know this isn't in ValueNS, so no need to check variant_field
+                    return Err(ResolutionFailure::NotResolved {
+                        module_id,
+                        partial_res: Some(ty_res),
+                        unresolved: item_str.into(),
+                    }
+                    .into());
+                }
+            }
+            Res::Def(DefKind::Trait, did) => cx
+                .tcx
+                .associated_items(did)
+                .find_by_name_and_namespace(cx.tcx, Ident::with_dummy_span(item_name), ns, did)
+                .map(|item| {
+                    let kind = match item.kind {
+                        ty::AssocKind::Const => "associatedconstant",
+                        ty::AssocKind::Type => "associatedtype",
+                        ty::AssocKind::Fn => {
+                            if item.defaultness.has_value() {
+                                "method"
+                            } else {
+                                "tymethod"
                             }
-                            _ => None,
                         }
+                    };
+
+                    if extra_fragment.is_some() {
+                        Err(ErrorKind::AnchorFailure(AnchorFailure::RustdocAnchorConflict(ty_res)))
                     } else {
-                        // We already know this isn't in ValueNS, so no need to check variant_field
-                        return Err(ErrorKind::ResolutionFailure);
+                        let res = Res::Def(item.kind.as_def_kind(), item.def_id);
+                        Ok((res, Some(format!("{}.{}", kind, item_str))))
                     }
+                }),
+            _ => None,
+        };
+        res.unwrap_or_else(|| {
+            if ns == Namespace::ValueNS {
+                self.variant_field(path_str, current_item, module_id)
+            } else {
+                Err(ResolutionFailure::NotResolved {
+                    module_id,
+                    partial_res: Some(ty_res),
+                    unresolved: item_str.into(),
                 }
-                Res::Def(DefKind::Trait, did) => cx
-                    .tcx
-                    .associated_items(did)
-                    .find_by_name_and_namespace(cx.tcx, Ident::with_dummy_span(item_name), ns, did)
-                    .map(|item| {
-                        let kind = match item.kind {
-                            ty::AssocKind::Const => "associatedconstant",
-                            ty::AssocKind::Type => "associatedtype",
-                            ty::AssocKind::Fn => {
-                                if item.defaultness.has_value() {
-                                    "method"
-                                } else {
-                                    "tymethod"
-                                }
-                            }
-                        };
+                .into())
+            }
+        })
+    }
 
-                        if extra_fragment.is_some() {
-                            Err(ErrorKind::AnchorFailure(if item.kind == ty::AssocKind::Const {
-                                AnchorFailure::AssocConstant
-                            } else if item.kind == ty::AssocKind::Type {
-                                AnchorFailure::AssocType
-                            } else {
-                                AnchorFailure::Method
-                            }))
-                        } else {
-                            let res = Res::Def(item.kind.as_def_kind(), item.def_id);
-                            Ok((res, Some(format!("{}.{}", kind, item_name))))
-                        }
-                    }),
-                _ => None,
-            };
-            res.unwrap_or_else(|| {
-                if ns == Namespace::ValueNS {
-                    self.variant_field(path_str, current_item, module_id)
-                } else {
-                    Err(ErrorKind::ResolutionFailure)
+    /// Used for reporting better errors.
+    ///
+    /// Returns whether the link resolved 'fully' in another namespace.
+    /// 'fully' here means that all parts of the link resolved, not just some path segments.
+    /// This returns the `Res` even if it was erroneous for some reason
+    /// (such as having invalid URL fragments or being in the wrong namespace).
+    fn check_full_res(
+        &self,
+        ns: Namespace,
+        path_str: &str,
+        module_id: DefId,
+        current_item: &Option<String>,
+        extra_fragment: &Option<String>,
+    ) -> Option<Res> {
+        let check_full_res_inner = |this: &Self, result: Result<Res, ErrorKind<'_>>| {
+            let res = match result {
+                Ok(res) => Some(res),
+                Err(ErrorKind::Resolve(box kind)) => kind.full_res(),
+                Err(ErrorKind::AnchorFailure(AnchorFailure::RustdocAnchorConflict(res))) => {
+                    Some(res)
                 }
-            })
-        } else {
-            debug!("attempting to resolve item without parent module: {}", path_str);
-            Err(ErrorKind::ResolutionFailure)
+                Err(ErrorKind::AnchorFailure(AnchorFailure::MultipleAnchors)) => None,
+            };
+            this.kind_side_channel.take().map(|(kind, id)| Res::Def(kind, id)).or(res)
+        };
+        // cannot be used for macro namespace
+        let check_full_res = |this: &Self, ns| {
+            let result = this.resolve(path_str, ns, current_item, module_id, extra_fragment);
+            check_full_res_inner(this, result.map(|(res, _)| res))
+        };
+        let check_full_res_macro = |this: &Self| {
+            let result = this.macro_resolve(path_str, module_id);
+            check_full_res_inner(this, result.map_err(ErrorKind::from))
+        };
+        match ns {
+            Namespace::MacroNS => check_full_res_macro(self),
+            Namespace::TypeNS | Namespace::ValueNS => check_full_res(self, ns),
         }
     }
 }
@@ -433,7 +560,7 @@ fn resolve_associated_trait_item(
     item_name: Symbol,
     ns: Namespace,
     cx: &DocContext<'_>,
-) -> Option<ty::AssocKind> {
+) -> Option<(ty::AssocKind, DefId)> {
     let ty = cx.tcx.type_of(did);
     // First consider automatic impls: `impl From<T> for T`
     let implicit_impls = crate::clean::get_auto_trait_and_blanket_impls(cx, ty, did);
@@ -461,7 +588,7 @@ fn resolve_associated_trait_item(
                             // but provided methods come directly from `tcx`.
                             // Fortunately, we don't need the whole method, we just need to know
                             // what kind of associated item it is.
-                            Some((assoc.def_id, kind))
+                            Some((kind, assoc.def_id))
                         });
                         let assoc = items.next();
                         debug_assert_eq!(items.count(), 0);
@@ -483,7 +610,7 @@ fn resolve_associated_trait_item(
                                 ns,
                                 trait_,
                             )
-                            .map(|assoc| (assoc.def_id, assoc.kind))
+                            .map(|assoc| (assoc.kind, assoc.def_id))
                     }
                 }
                 _ => panic!("get_impls returned something that wasn't an impl"),
@@ -500,12 +627,12 @@ fn resolve_associated_trait_item(
             cx.tcx
                 .associated_items(trait_)
                 .find_by_name_and_namespace(cx.tcx, Ident::with_dummy_span(item_name), ns, trait_)
-                .map(|assoc| (assoc.def_id, assoc.kind))
+                .map(|assoc| (assoc.kind, assoc.def_id))
         }));
     }
     // FIXME: warn about ambiguity
     debug!("the candidates were {:?}", candidates);
-    candidates.pop().map(|(_, kind)| kind)
+    candidates.pop()
 }
 
 /// Given a type, return all traits in scope in `module` implemented by that type.
@@ -534,19 +661,21 @@ fn traits_implemented_by(cx: &DocContext<'_>, type_: DefId, module: DefId) -> Fx
             let trait_ref = cx.tcx.impl_trait_ref(impl_).expect("this is not an inherent impl");
             // Check if these are the same type.
             let impl_type = trait_ref.self_ty();
-            debug!(
+            trace!(
                 "comparing type {} with kind {:?} against type {:?}",
-                impl_type, impl_type.kind, type_
+                impl_type,
+                impl_type.kind(),
+                type_
             );
             // Fast path: if this is a primitive simple `==` will work
             saw_impl = impl_type == ty
-                || match impl_type.kind {
+                || match impl_type.kind() {
                     // Check if these are the same def_id
                     ty::Adt(def, _) => {
                         debug!("adt def_id: {:?}", def.did);
                         def.did == type_
                     }
-                    ty::Foreign(def_id) => def_id == type_,
+                    ty::Foreign(def_id) => *def_id == type_,
                     _ => false,
                 };
         });
@@ -558,10 +687,10 @@ fn traits_implemented_by(cx: &DocContext<'_>, type_: DefId, module: DefId) -> Fx
 /// Check for resolve collisions between a trait and its derive
 ///
 /// These are common and we should just resolve to the trait in that case
-fn is_derive_trait_collision<T>(ns: &PerNS<Option<(Res, T)>>) -> bool {
+fn is_derive_trait_collision<T>(ns: &PerNS<Result<(Res, T), ResolutionFailure<'_>>>) -> bool {
     if let PerNS {
-        type_ns: Some((Res::Def(DefKind::Trait, _), _)),
-        macro_ns: Some((Res::Def(DefKind::Macro(MacroKind::Derive), _), _)),
+        type_ns: Ok((Res::Def(DefKind::Trait, _), _)),
+        macro_ns: Ok((Res::Def(DefKind::Macro(MacroKind::Derive), _), _)),
         ..
     } = *ns
     {
@@ -578,6 +707,9 @@ impl<'a, 'tcx> DocFolder for LinkCollector<'a, 'tcx> {
         let parent_node = if item.is_fake() {
             // FIXME: is this correct?
             None
+        // If we're documenting the crate root itself, it has no parent. Use the root instead.
+        } else if item.def_id.is_top_level_module() {
+            Some(item.def_id)
         } else {
             let mut current = item.def_id;
             // The immediate parent might not always be a module.
@@ -589,6 +721,12 @@ impl<'a, 'tcx> DocFolder for LinkCollector<'a, 'tcx> {
                     }
                     current = parent;
                 } else {
+                    debug!(
+                        "{:?} has no parent (kind={:?}, original was {:?})",
+                        current,
+                        self.cx.tcx.def_kind(current),
+                        item.def_id
+                    );
                     break None;
                 }
             }
@@ -629,7 +767,6 @@ impl<'a, 'tcx> DocFolder for LinkCollector<'a, 'tcx> {
             self.mod_ids.push(item.def_id);
         }
 
-        let cx = self.cx;
         let dox = item.attrs.collapsed_doc_value().unwrap_or_else(String::new);
         trace!("got documentation '{}'", dox);
 
@@ -671,329 +808,452 @@ impl<'a, 'tcx> DocFolder for LinkCollector<'a, 'tcx> {
         });
 
         for (ori_link, link_range) in markdown_links(&dox) {
-            trace!("considering link '{}'", ori_link);
+            self.resolve_link(
+                &mut item,
+                &dox,
+                &current_item,
+                parent_node,
+                &parent_name,
+                ori_link,
+                link_range,
+            );
+        }
 
-            // Bail early for real links.
-            if ori_link.contains('/') {
-                continue;
+        if item.is_mod() && !item.attrs.inner_docs {
+            self.mod_ids.push(item.def_id);
+        }
+
+        if item.is_mod() {
+            let ret = self.fold_item_recur(item);
+
+            self.mod_ids.pop();
+
+            ret
+        } else {
+            self.fold_item_recur(item)
+        }
+    }
+}
+
+impl LinkCollector<'_, '_> {
+    fn resolve_link(
+        &self,
+        item: &mut Item,
+        dox: &str,
+        current_item: &Option<String>,
+        parent_node: Option<DefId>,
+        parent_name: &Option<String>,
+        ori_link: String,
+        link_range: Option<Range<usize>>,
+    ) {
+        trace!("considering link '{}'", ori_link);
+
+        // Bail early for real links.
+        if ori_link.contains('/') {
+            return;
+        }
+
+        // [] is mostly likely not supposed to be a link
+        if ori_link.is_empty() {
+            return;
+        }
+
+        let cx = self.cx;
+        let link = ori_link.replace("`", "");
+        let parts = link.split('#').collect::<Vec<_>>();
+        let (link, extra_fragment) = if parts.len() > 2 {
+            anchor_failure(cx, &item, &link, dox, link_range, AnchorFailure::MultipleAnchors);
+            return;
+        } else if parts.len() == 2 {
+            if parts[0].trim().is_empty() {
+                // This is an anchor to an element of the current page, nothing to do in here!
+                return;
             }
+            (parts[0], Some(parts[1].to_owned()))
+        } else {
+            (parts[0], None)
+        };
+        let resolved_self;
+        let link_text;
+        let mut path_str;
+        let disambiguator;
+        let (mut res, mut fragment) = {
+            path_str = if let Ok((d, path)) = Disambiguator::from_str(&link) {
+                disambiguator = Some(d);
+                path
+            } else {
+                disambiguator = None;
+                &link
+            }
+            .trim();
 
-            // [] is mostly likely not supposed to be a link
-            if ori_link.is_empty() {
-                continue;
+            if path_str.contains(|ch: char| !(ch.is_alphanumeric() || ch == ':' || ch == '_')) {
+                return;
             }
 
-            let link = ori_link.replace("`", "");
-            let parts = link.split('#').collect::<Vec<_>>();
-            let (link, extra_fragment) = if parts.len() > 2 {
-                anchor_failure(cx, &item, &link, &dox, link_range, AnchorFailure::MultipleAnchors);
-                continue;
-            } else if parts.len() == 2 {
-                if parts[0].trim().is_empty() {
-                    // This is an anchor to an element of the current page, nothing to do in here!
-                    continue;
-                }
-                (parts[0].to_owned(), Some(parts[1].to_owned()))
+            // We stripped `()` and `!` when parsing the disambiguator.
+            // Add them back to be displayed, but not prefix disambiguators.
+            link_text = disambiguator
+                .map(|d| d.display_for(path_str))
+                .unwrap_or_else(|| path_str.to_owned());
+
+            // In order to correctly resolve intra-doc-links we need to
+            // pick a base AST node to work from.  If the documentation for
+            // this module came from an inner comment (//!) then we anchor
+            // our name resolution *inside* the module.  If, on the other
+            // hand it was an outer comment (///) then we anchor the name
+            // resolution in the parent module on the basis that the names
+            // used are more likely to be intended to be parent names.  For
+            // this, we set base_node to None for inner comments since
+            // we've already pushed this node onto the resolution stack but
+            // for outer comments we explicitly try and resolve against the
+            // parent_node first.
+            let base_node = if item.is_mod() && item.attrs.inner_docs {
+                self.mod_ids.last().copied()
             } else {
-                (parts[0].to_owned(), None)
+                parent_node
             };
-            let resolved_self;
-            let mut path_str;
-            let disambiguator;
-            let (mut res, mut fragment) = {
-                path_str = if let Ok((d, path)) = Disambiguator::from_str(&link) {
-                    disambiguator = Some(d);
-                    path
-                } else {
-                    disambiguator = None;
-                    &link
-                }
-                .trim();
 
-                if path_str.contains(|ch: char| !(ch.is_alphanumeric() || ch == ':' || ch == '_')) {
-                    continue;
+            let mut module_id = if let Some(id) = base_node {
+                id
+            } else {
+                debug!("attempting to resolve item without parent module: {}", path_str);
+                let err_kind = ResolutionFailure::NoParentItem.into();
+                resolution_failure(
+                    self,
+                    &item,
+                    path_str,
+                    disambiguator,
+                    dox,
+                    link_range,
+                    smallvec![err_kind],
+                );
+                return;
+            };
+
+            // replace `Self` with suitable item's parent name
+            if path_str.starts_with("Self::") {
+                if let Some(ref name) = parent_name {
+                    resolved_self = format!("{}::{}", name, &path_str[6..]);
+                    path_str = &resolved_self;
                 }
+            } else if path_str.starts_with("crate::") {
+                use rustc_span::def_id::CRATE_DEF_INDEX;
+
+                // HACK(jynelson): rustc_resolve thinks that `crate` is the crate currently being documented.
+                // But rustdoc wants it to mean the crate this item was originally present in.
+                // To work around this, remove it and resolve relative to the crate root instead.
+                // HACK(jynelson)(2): If we just strip `crate::` then suddenly primitives become ambiguous
+                // (consider `crate::char`). Instead, change it to `self::`. This works because 'self' is now the crate root.
+                resolved_self = format!("self::{}", &path_str["crate::".len()..]);
+                path_str = &resolved_self;
+                module_id = DefId { krate: item.def_id.krate, index: CRATE_DEF_INDEX };
+            }
 
-                // In order to correctly resolve intra-doc-links we need to
-                // pick a base AST node to work from.  If the documentation for
-                // this module came from an inner comment (//!) then we anchor
-                // our name resolution *inside* the module.  If, on the other
-                // hand it was an outer comment (///) then we anchor the name
-                // resolution in the parent module on the basis that the names
-                // used are more likely to be intended to be parent names.  For
-                // this, we set base_node to None for inner comments since
-                // we've already pushed this node onto the resolution stack but
-                // for outer comments we explicitly try and resolve against the
-                // parent_node first.
-                let base_node = if item.is_mod() && item.attrs.inner_docs {
-                    self.mod_ids.last().copied()
-                } else {
-                    parent_node
-                };
+            match self.resolve_with_disambiguator(
+                disambiguator,
+                item,
+                dox,
+                path_str,
+                current_item,
+                module_id,
+                extra_fragment,
+                &ori_link,
+                link_range.clone(),
+            ) {
+                Some(x) => x,
+                None => return,
+            }
+        };
 
-                // replace `Self` with suitable item's parent name
-                if path_str.starts_with("Self::") {
-                    if let Some(ref name) = parent_name {
-                        resolved_self = format!("{}::{}", name, &path_str[6..]);
-                        path_str = &resolved_self;
+        // Check for a primitive which might conflict with a module
+        // Report the ambiguity and require that the user specify which one they meant.
+        // FIXME: could there ever be a primitive not in the type namespace?
+        if matches!(
+            disambiguator,
+            None | Some(Disambiguator::Namespace(Namespace::TypeNS) | Disambiguator::Primitive)
+        ) && !matches!(res, Res::PrimTy(_))
+        {
+            if let Some((path, prim)) = is_primitive(path_str, TypeNS) {
+                // `prim@char`
+                if matches!(disambiguator, Some(Disambiguator::Primitive)) {
+                    if fragment.is_some() {
+                        anchor_failure(
+                            cx,
+                            &item,
+                            path_str,
+                            dox,
+                            link_range,
+                            AnchorFailure::RustdocAnchorConflict(prim),
+                        );
+                        return;
                     }
+                    res = prim;
+                    fragment = Some(path.to_owned());
+                } else {
+                    // `[char]` when a `char` module is in scope
+                    let candidates = vec![res, prim];
+                    ambiguity_error(cx, &item, path_str, dox, link_range, candidates);
+                    return;
                 }
+            }
+        }
 
-                match disambiguator.map(Disambiguator::ns) {
-                    Some(ns @ (ValueNS | TypeNS)) => {
-                        match self.resolve(path_str, ns, &current_item, base_node, &extra_fragment)
-                        {
-                            Ok(res) => res,
-                            Err(ErrorKind::ResolutionFailure) => {
-                                resolution_failure(cx, &item, path_str, &dox, link_range);
-                                // This could just be a normal link or a broken link
-                                // we could potentially check if something is
-                                // "intra-doc-link-like" and warn in that case.
-                                continue;
-                            }
-                            Err(ErrorKind::AnchorFailure(msg)) => {
-                                anchor_failure(cx, &item, &ori_link, &dox, link_range, msg);
-                                continue;
-                            }
-                        }
+        let report_mismatch = |specified: Disambiguator, resolved: Disambiguator| {
+            // The resolved item did not match the disambiguator; give a better error than 'not found'
+            let msg = format!("incompatible link kind for `{}`", path_str);
+            let callback = |diag: &mut DiagnosticBuilder<'_>, sp| {
+                let note = format!(
+                    "this link resolved to {} {}, which is not {} {}",
+                    resolved.article(),
+                    resolved.descr(),
+                    specified.article(),
+                    specified.descr()
+                );
+                diag.note(&note);
+                suggest_disambiguator(resolved, diag, path_str, dox, sp, &link_range);
+            };
+            report_diagnostic(cx, BROKEN_INTRA_DOC_LINKS, &msg, &item, dox, &link_range, callback);
+        };
+        if let Res::PrimTy(..) = res {
+            match disambiguator {
+                Some(Disambiguator::Primitive | Disambiguator::Namespace(_)) | None => {
+                    item.attrs.links.push(ItemLink {
+                        link: ori_link,
+                        link_text,
+                        did: None,
+                        fragment,
+                    });
+                }
+                Some(other) => {
+                    report_mismatch(other, Disambiguator::Primitive);
+                    return;
+                }
+            }
+        } else {
+            debug!("intra-doc link to {} resolved to {:?}", path_str, res);
+
+            // Disallow e.g. linking to enums with `struct@`
+            if let Res::Def(kind, _) = res {
+                debug!("saw kind {:?} with disambiguator {:?}", kind, disambiguator);
+                match (self.kind_side_channel.take().map(|(kind, _)| kind).unwrap_or(kind), disambiguator) {
+                    | (DefKind::Const | DefKind::ConstParam | DefKind::AssocConst | DefKind::AnonConst, Some(Disambiguator::Kind(DefKind::Const)))
+                    // NOTE: this allows 'method' to mean both normal functions and associated functions
+                    // This can't cause ambiguity because both are in the same namespace.
+                    | (DefKind::Fn | DefKind::AssocFn, Some(Disambiguator::Kind(DefKind::Fn)))
+                    // These are namespaces; allow anything in the namespace to match
+                    | (_, Some(Disambiguator::Namespace(_)))
+                    // If no disambiguator given, allow anything
+                    | (_, None)
+                    // All of these are valid, so do nothing
+                    => {}
+                    (actual, Some(Disambiguator::Kind(expected))) if actual == expected => {}
+                    (_, Some(specified @ Disambiguator::Kind(_) | specified @ Disambiguator::Primitive)) => {
+                        report_mismatch(specified, Disambiguator::Kind(kind));
+                        return;
                     }
-                    None => {
-                        // Try everything!
-                        let mut candidates = PerNS {
-                            macro_ns: self
-                                .macro_resolve(path_str, base_node)
-                                .map(|res| (res, extra_fragment.clone())),
-                            type_ns: match self.resolve(
-                                path_str,
-                                TypeNS,
-                                &current_item,
-                                base_node,
-                                &extra_fragment,
-                            ) {
-                                Ok(res) => {
-                                    debug!("got res in TypeNS: {:?}", res);
-                                    Some(res)
-                                }
-                                Err(ErrorKind::AnchorFailure(msg)) => {
-                                    anchor_failure(cx, &item, &ori_link, &dox, link_range, msg);
-                                    continue;
-                                }
-                                Err(ErrorKind::ResolutionFailure) => None,
-                            },
-                            value_ns: match self.resolve(
-                                path_str,
-                                ValueNS,
-                                &current_item,
-                                base_node,
-                                &extra_fragment,
-                            ) {
-                                Ok(res) => Some(res),
-                                Err(ErrorKind::AnchorFailure(msg)) => {
-                                    anchor_failure(cx, &item, &ori_link, &dox, link_range, msg);
-                                    continue;
-                                }
-                                Err(ErrorKind::ResolutionFailure) => None,
-                            }
-                            .and_then(|(res, fragment)| {
-                                // Constructors are picked up in the type namespace.
-                                match res {
-                                    Res::Def(DefKind::Ctor(..), _) | Res::SelfCtor(..) => None,
-                                    _ => match (fragment, extra_fragment) {
-                                        (Some(fragment), Some(_)) => {
-                                            // Shouldn't happen but who knows?
-                                            Some((res, Some(fragment)))
-                                        }
-                                        (fragment, None) | (None, fragment) => {
-                                            Some((res, fragment))
-                                        }
-                                    },
-                                }
-                            }),
-                        };
+                }
+            }
 
-                        if candidates.is_empty() {
-                            resolution_failure(cx, &item, path_str, &dox, link_range);
-                            // this could just be a normal link
-                            continue;
-                        }
+            // item can be non-local e.g. when using #[doc(primitive = "pointer")]
+            if let Some((src_id, dst_id)) = res
+                .opt_def_id()
+                .and_then(|def_id| def_id.as_local())
+                .and_then(|dst_id| item.def_id.as_local().map(|src_id| (src_id, dst_id)))
+            {
+                use rustc_hir::def_id::LOCAL_CRATE;
 
-                        let len = candidates.clone().present_items().count();
+                let hir_src = self.cx.tcx.hir().local_def_id_to_hir_id(src_id);
+                let hir_dst = self.cx.tcx.hir().local_def_id_to_hir_id(dst_id);
 
-                        if len == 1 {
-                            candidates.present_items().next().unwrap()
-                        } else if len == 2 && is_derive_trait_collision(&candidates) {
-                            candidates.type_ns.unwrap()
-                        } else {
-                            if is_derive_trait_collision(&candidates) {
-                                candidates.macro_ns = None;
+                if self.cx.tcx.privacy_access_levels(LOCAL_CRATE).is_exported(hir_src)
+                    && !self.cx.tcx.privacy_access_levels(LOCAL_CRATE).is_exported(hir_dst)
+                {
+                    privacy_error(cx, &item, &path_str, dox, link_range);
+                }
+            }
+            let id = register_res(cx, res);
+            item.attrs.links.push(ItemLink { link: ori_link, link_text, did: Some(id), fragment });
+        }
+    }
+
+    fn resolve_with_disambiguator(
+        &self,
+        disambiguator: Option<Disambiguator>,
+        item: &mut Item,
+        dox: &str,
+        path_str: &str,
+        current_item: &Option<String>,
+        base_node: DefId,
+        extra_fragment: Option<String>,
+        ori_link: &str,
+        link_range: Option<Range<usize>>,
+    ) -> Option<(Res, Option<String>)> {
+        match disambiguator.map(Disambiguator::ns) {
+            Some(ns @ (ValueNS | TypeNS)) => {
+                match self.resolve(path_str, ns, &current_item, base_node, &extra_fragment) {
+                    Ok(res) => Some(res),
+                    Err(ErrorKind::Resolve(box mut kind)) => {
+                        // We only looked in one namespace. Try to give a better error if possible.
+                        if kind.full_res().is_none() {
+                            let other_ns = if ns == ValueNS { TypeNS } else { ValueNS };
+                            // FIXME: really it should be `resolution_failure` that does this, not `resolve_with_disambiguator`
+                            // See https://github.com/rust-lang/rust/pull/76955#discussion_r493953382 for a good approach
+                            for &new_ns in &[other_ns, MacroNS] {
+                                if let Some(res) = self.check_full_res(
+                                    new_ns,
+                                    path_str,
+                                    base_node,
+                                    &current_item,
+                                    &extra_fragment,
+                                ) {
+                                    kind = ResolutionFailure::WrongNamespace(res, ns);
+                                    break;
+                                }
                             }
-                            let candidates =
-                                candidates.map(|candidate| candidate.map(|(res, _)| res));
-                            let candidates = [TypeNS, ValueNS, MacroNS]
-                                .iter()
-                                .filter_map(|&ns| candidates[ns].map(|res| (res, ns)));
-                            ambiguity_error(
-                                cx,
-                                &item,
-                                path_str,
-                                &dox,
-                                link_range,
-                                candidates.collect(),
-                            );
-                            continue;
                         }
+                        resolution_failure(
+                            self,
+                            &item,
+                            path_str,
+                            disambiguator,
+                            dox,
+                            link_range,
+                            smallvec![kind],
+                        );
+                        // This could just be a normal link or a broken link
+                        // we could potentially check if something is
+                        // "intra-doc-link-like" and warn in that case.
+                        return None;
                     }
-                    Some(MacroNS) => {
-                        if let Some(res) = self.macro_resolve(path_str, base_node) {
-                            (res, extra_fragment)
-                        } else {
-                            resolution_failure(cx, &item, path_str, &dox, link_range);
-                            continue;
-                        }
+                    Err(ErrorKind::AnchorFailure(msg)) => {
+                        anchor_failure(self.cx, &item, &ori_link, dox, link_range, msg);
+                        return None;
                     }
                 }
-            };
-
-            // Check for a primitive which might conflict with a module
-            // Report the ambiguity and require that the user specify which one they meant.
-            // FIXME: could there ever be a primitive not in the type namespace?
-            if matches!(
-                disambiguator,
-                None | Some(Disambiguator::Namespace(Namespace::TypeNS) | Disambiguator::Primitive)
-            ) && !matches!(res, Res::PrimTy(_))
-            {
-                if let Some((path, prim)) = is_primitive(path_str, TypeNS) {
-                    // `prim@char`
-                    if matches!(disambiguator, Some(Disambiguator::Primitive)) {
-                        if fragment.is_some() {
-                            anchor_failure(
-                                cx,
-                                &item,
-                                path_str,
-                                &dox,
-                                link_range,
-                                AnchorFailure::Primitive,
-                            );
-                            continue;
+            }
+            None => {
+                // Try everything!
+                let mut candidates = PerNS {
+                    macro_ns: self
+                        .macro_resolve(path_str, base_node)
+                        .map(|res| (res, extra_fragment.clone())),
+                    type_ns: match self.resolve(
+                        path_str,
+                        TypeNS,
+                        &current_item,
+                        base_node,
+                        &extra_fragment,
+                    ) {
+                        Ok(res) => {
+                            debug!("got res in TypeNS: {:?}", res);
+                            Ok(res)
                         }
-                        res = prim;
-                        fragment = Some(path.to_owned());
-                    } else {
-                        // `[char]` when a `char` module is in scope
-                        let candidates = vec![(res, TypeNS), (prim, TypeNS)];
-                        ambiguity_error(cx, &item, path_str, &dox, link_range, candidates);
-                        continue;
+                        Err(ErrorKind::AnchorFailure(msg)) => {
+                            anchor_failure(self.cx, &item, ori_link, dox, link_range, msg);
+                            return None;
+                        }
+                        Err(ErrorKind::Resolve(box kind)) => Err(kind),
+                    },
+                    value_ns: match self.resolve(
+                        path_str,
+                        ValueNS,
+                        &current_item,
+                        base_node,
+                        &extra_fragment,
+                    ) {
+                        Ok(res) => Ok(res),
+                        Err(ErrorKind::AnchorFailure(msg)) => {
+                            anchor_failure(self.cx, &item, ori_link, dox, link_range, msg);
+                            return None;
+                        }
+                        Err(ErrorKind::Resolve(box kind)) => Err(kind),
                     }
-                }
-            }
+                    .and_then(|(res, fragment)| {
+                        // Constructors are picked up in the type namespace.
+                        match res {
+                            Res::Def(DefKind::Ctor(..), _) | Res::SelfCtor(..) => {
+                                Err(ResolutionFailure::WrongNamespace(res, TypeNS))
+                            }
+                            _ => match (fragment, extra_fragment) {
+                                (Some(fragment), Some(_)) => {
+                                    // Shouldn't happen but who knows?
+                                    Ok((res, Some(fragment)))
+                                }
+                                (fragment, None) | (None, fragment) => Ok((res, fragment)),
+                            },
+                        }
+                    }),
+                };
 
-            let report_mismatch = |specified: Disambiguator, resolved: Disambiguator| {
-                // The resolved item did not match the disambiguator; give a better error than 'not found'
-                let msg = format!("incompatible link kind for `{}`", path_str);
-                report_diagnostic(cx, &msg, &item, &dox, link_range.clone(), |diag, sp| {
-                    let note = format!(
-                        "this link resolved to {} {}, which is not {} {}",
-                        resolved.article(),
-                        resolved.descr(),
-                        specified.article(),
-                        specified.descr()
+                let len = candidates.iter().filter(|res| res.is_ok()).count();
+
+                if len == 0 {
+                    resolution_failure(
+                        self,
+                        &item,
+                        path_str,
+                        disambiguator,
+                        dox,
+                        link_range,
+                        candidates.into_iter().filter_map(|res| res.err()).collect(),
                     );
-                    let suggestion = resolved.display_for(path_str);
-                    let help_msg =
-                        format!("to link to the {}, use its disambiguator", resolved.descr());
-                    diag.note(&note);
-                    if let Some(sp) = sp {
-                        diag.span_suggestion(
-                            sp,
-                            &help_msg,
-                            suggestion,
-                            Applicability::MaybeIncorrect,
-                        );
-                    } else {
-                        diag.help(&format!("{}: {}", help_msg, suggestion));
-                    }
-                });
-            };
-            if let Res::PrimTy(_) = res {
-                match disambiguator {
-                    Some(Disambiguator::Primitive | Disambiguator::Namespace(_)) | None => {
-                        item.attrs.links.push((ori_link, None, fragment))
-                    }
-                    Some(other) => {
-                        report_mismatch(other, Disambiguator::Primitive);
-                        continue;
-                    }
+                    // this could just be a normal link
+                    return None;
                 }
-            } else {
-                debug!("intra-doc link to {} resolved to {:?}", path_str, res);
-
-                // Disallow e.g. linking to enums with `struct@`
-                if let Res::Def(kind, _) = res {
-                    debug!("saw kind {:?} with disambiguator {:?}", kind, disambiguator);
-                    match (self.kind_side_channel.take().unwrap_or(kind), disambiguator) {
-                        | (DefKind::Const | DefKind::ConstParam | DefKind::AssocConst | DefKind::AnonConst, Some(Disambiguator::Kind(DefKind::Const)))
-                        // NOTE: this allows 'method' to mean both normal functions and associated functions
-                        // This can't cause ambiguity because both are in the same namespace.
-                        | (DefKind::Fn | DefKind::AssocFn, Some(Disambiguator::Kind(DefKind::Fn)))
-                        // These are namespaces; allow anything in the namespace to match
-                        | (_, Some(Disambiguator::Namespace(_)))
-                        // If no disambiguator given, allow anything
-                        | (_, None)
-                        // All of these are valid, so do nothing
-                        => {}
-                        (actual, Some(Disambiguator::Kind(expected))) if actual == expected => {}
-                        (_, Some(specified @ Disambiguator::Kind(_) | specified @ Disambiguator::Primitive)) => {
-                            report_mismatch(specified, Disambiguator::Kind(kind));
-                            continue;
-                        }
+
+                if len == 1 {
+                    Some(candidates.into_iter().filter_map(|res| res.ok()).next().unwrap())
+                } else if len == 2 && is_derive_trait_collision(&candidates) {
+                    Some(candidates.type_ns.unwrap())
+                } else {
+                    if is_derive_trait_collision(&candidates) {
+                        candidates.macro_ns = Err(ResolutionFailure::Dummy);
                     }
+                    // If we're reporting an ambiguity, don't mention the namespaces that failed
+                    let candidates = candidates.map(|candidate| candidate.ok().map(|(res, _)| res));
+                    ambiguity_error(
+                        self.cx,
+                        &item,
+                        path_str,
+                        dox,
+                        link_range,
+                        candidates.present_items().collect(),
+                    );
+                    return None;
                 }
-
-                // item can be non-local e.g. when using #[doc(primitive = "pointer")]
-                if let Some((src_id, dst_id)) = res
-                    .opt_def_id()
-                    .and_then(|def_id| def_id.as_local())
-                    .and_then(|dst_id| item.def_id.as_local().map(|src_id| (src_id, dst_id)))
-                {
-                    use rustc_hir::def_id::LOCAL_CRATE;
-
-                    let hir_src = self.cx.tcx.hir().local_def_id_to_hir_id(src_id);
-                    let hir_dst = self.cx.tcx.hir().local_def_id_to_hir_id(dst_id);
-
-                    if self.cx.tcx.privacy_access_levels(LOCAL_CRATE).is_exported(hir_src)
-                        && !self.cx.tcx.privacy_access_levels(LOCAL_CRATE).is_exported(hir_dst)
-                    {
-                        privacy_error(cx, &item, &path_str, &dox, link_range);
-                        continue;
+            }
+            Some(MacroNS) => {
+                match self.macro_resolve(path_str, base_node) {
+                    Ok(res) => Some((res, extra_fragment)),
+                    Err(mut kind) => {
+                        // `macro_resolve` only looks in the macro namespace. Try to give a better error if possible.
+                        for &ns in &[TypeNS, ValueNS] {
+                            if let Some(res) = self.check_full_res(
+                                ns,
+                                path_str,
+                                base_node,
+                                &current_item,
+                                &extra_fragment,
+                            ) {
+                                kind = ResolutionFailure::WrongNamespace(res, MacroNS);
+                                break;
+                            }
+                        }
+                        resolution_failure(
+                            self,
+                            &item,
+                            path_str,
+                            disambiguator,
+                            dox,
+                            link_range,
+                            smallvec![kind],
+                        );
+                        return None;
                     }
                 }
-                let id = register_res(cx, res);
-                item.attrs.links.push((ori_link, Some(id), fragment));
             }
         }
-
-        if item.is_mod() && !item.attrs.inner_docs {
-            self.mod_ids.push(item.def_id);
-        }
-
-        if item.is_mod() {
-            let ret = self.fold_item_recur(item);
-
-            self.mod_ids.pop();
-
-            ret
-        } else {
-            self.fold_item_recur(item)
-        }
-    }
-
-    // FIXME: if we can resolve intra-doc links from other crates, we can use the stock
-    // `fold_crate`, but until then we should avoid scanning `krate.external_traits` since those
-    // will never resolve properly
-    fn fold_crate(&mut self, mut c: Crate) -> Crate {
-        c.module = c.module.take().and_then(|module| self.fold_item(module));
-
-        c
     }
 }
 
@@ -1005,6 +1265,18 @@ enum Disambiguator {
 }
 
 impl Disambiguator {
+    /// The text that should be displayed when the path is rendered as HTML.
+    ///
+    /// NOTE: `path` is not the original link given by the user, but a name suitable for passing to `resolve`.
+    fn display_for(&self, path: &str) -> String {
+        match self {
+            // FIXME: this will have different output if the user had `m!()` originally.
+            Self::Kind(DefKind::Macro(MacroKind::Bang)) => format!("{}!", path),
+            Self::Kind(DefKind::Fn) => format!("{}()", path),
+            _ => path.to_owned(),
+        }
+    }
+
     /// (disambiguator, path_str)
     fn from_str(link: &str) -> Result<(Self, &str), ()> {
         use Disambiguator::{Kind, Namespace as NS, Primitive};
@@ -1047,17 +1319,27 @@ impl Disambiguator {
         }
     }
 
-    fn display_for(self, path_str: &str) -> String {
+    /// WARNING: panics on `Res::Err`
+    fn from_res(res: Res) -> Self {
+        match res {
+            Res::Def(kind, _) => Disambiguator::Kind(kind),
+            Res::PrimTy(_) => Disambiguator::Primitive,
+            _ => Disambiguator::Namespace(res.ns().expect("can't call `from_res` on Res::err")),
+        }
+    }
+
+    fn suggestion(self) -> Suggestion {
         let kind = match self {
-            Disambiguator::Primitive => return format!("prim@{}", path_str),
+            Disambiguator::Primitive => return Suggestion::Prefix("prim"),
             Disambiguator::Kind(kind) => kind,
             Disambiguator::Namespace(_) => panic!("display_for cannot be used on namespaces"),
         };
         if kind == DefKind::Macro(MacroKind::Bang) {
-            return format!("{}!", path_str);
+            return Suggestion::Macro;
         } else if kind == DefKind::Fn || kind == DefKind::AssocFn {
-            return format!("{}()", path_str);
+            return Suggestion::Function;
         }
+
         let prefix = match kind {
             DefKind::Struct => "struct",
             DefKind::Enum => "enum",
@@ -1079,7 +1361,8 @@ impl Disambiguator {
                 Namespace::MacroNS => "macro",
             },
         };
-        format!("{}@{}", prefix, path_str)
+
+        Suggestion::Prefix(prefix)
     }
 
     fn ns(self) -> Namespace {
@@ -1111,6 +1394,31 @@ impl Disambiguator {
     }
 }
 
+enum Suggestion {
+    Prefix(&'static str),
+    Function,
+    Macro,
+}
+
+impl Suggestion {
+    fn descr(&self) -> Cow<'static, str> {
+        match self {
+            Self::Prefix(x) => format!("prefix with `{}@`", x).into(),
+            Self::Function => "add parentheses".into(),
+            Self::Macro => "add an exclamation mark".into(),
+        }
+    }
+
+    fn as_help(&self, path_str: &str) -> String {
+        // FIXME: if this is an implied shortcut link, it's bad style to suggest `@`
+        match self {
+            Self::Prefix(prefix) => format!("{}@{}", prefix, path_str),
+            Self::Function => format!("{}()", path_str),
+            Self::Macro => format!("{}!", path_str),
+        }
+    }
+}
+
 /// Reports a diagnostic for an intra-doc link.
 ///
 /// If no link range is provided, or the source span of the link cannot be determined, the span of
@@ -1123,10 +1431,11 @@ impl Disambiguator {
 /// to it.
 fn report_diagnostic(
     cx: &DocContext<'_>,
+    lint: &'static Lint,
     msg: &str,
     item: &Item,
     dox: &str,
-    link_range: Option<Range<usize>>,
+    link_range: &Option<Range<usize>>,
     decorate: impl FnOnce(&mut DiagnosticBuilder<'_>, Option<rustc_span::Span>),
 ) {
     let hir_id = match cx.as_local_hir_id(item.def_id) {
@@ -1141,7 +1450,7 @@ fn report_diagnostic(
     let attrs = &item.attrs;
     let sp = span_of_attrs(attrs).unwrap_or(item.source.span());
 
-    cx.tcx.struct_span_lint_hir(lint::builtin::BROKEN_INTRA_DOC_LINKS, hir_id, sp, |lint| {
+    cx.tcx.struct_span_lint_hir(lint, hir_id, sp, |lint| {
         let mut diag = lint.build(msg);
 
         let span = link_range
@@ -1162,7 +1471,7 @@ fn report_diagnostic(
                 // Print the line containing the `link_range` and manually mark it with '^'s.
                 diag.note(&format!(
                     "the link appears in this line:\n\n{line}\n\
-                         {indicator: <before$}{indicator:^<found$}",
+                     {indicator: <before$}{indicator:^<found$}",
                     line = line,
                     indicator = "",
                     before = link_range.start - last_new_line_offset,
@@ -1178,24 +1487,209 @@ fn report_diagnostic(
 }
 
 fn resolution_failure(
-    cx: &DocContext<'_>,
+    collector: &LinkCollector<'_, '_>,
     item: &Item,
     path_str: &str,
+    disambiguator: Option<Disambiguator>,
     dox: &str,
     link_range: Option<Range<usize>>,
+    kinds: SmallVec<[ResolutionFailure<'_>; 3]>,
 ) {
     report_diagnostic(
-        cx,
+        collector.cx,
+        BROKEN_INTRA_DOC_LINKS,
         &format!("unresolved link to `{}`", path_str),
         item,
         dox,
-        link_range,
+        &link_range,
         |diag, sp| {
-            if let Some(sp) = sp {
-                diag.span_label(sp, "unresolved link");
-            }
+            let item = |res: Res| {
+                format!(
+                    "the {} `{}`",
+                    res.descr(),
+                    collector.cx.tcx.item_name(res.def_id()).to_string()
+                )
+            };
+            let assoc_item_not_allowed = |res: Res| {
+                let def_id = res.def_id();
+                let name = collector.cx.tcx.item_name(def_id);
+                format!(
+                    "`{}` is {} {}, not a module or type, and cannot have associated items",
+                    name,
+                    res.article(),
+                    res.descr()
+                )
+            };
+            // ignore duplicates
+            let mut variants_seen = SmallVec::<[_; 3]>::new();
+            for mut failure in kinds {
+                let variant = std::mem::discriminant(&failure);
+                if variants_seen.contains(&variant) {
+                    continue;
+                }
+                variants_seen.push(variant);
+
+                if let ResolutionFailure::NotResolved { module_id, partial_res, unresolved } =
+                    &mut failure
+                {
+                    use DefKind::*;
+
+                    let module_id = *module_id;
+                    // FIXME(jynelson): this might conflict with my `Self` fix in #76467
+                    // FIXME: maybe use itertools `collect_tuple` instead?
+                    fn split(path: &str) -> Option<(&str, &str)> {
+                        let mut splitter = path.rsplitn(2, "::");
+                        splitter.next().and_then(|right| splitter.next().map(|left| (left, right)))
+                    }
 
-            diag.help(r#"to escape `[` and `]` characters, add '\' before them like `\[` or `\]`"#);
+                    // Check if _any_ parent of the path gets resolved.
+                    // If so, report it and say the first which failed; if not, say the first path segment didn't resolve.
+                    let mut name = path_str;
+                    'outer: loop {
+                        let (start, end) = if let Some(x) = split(name) {
+                            x
+                        } else {
+                            // avoid bug that marked [Quux::Z] as missing Z, not Quux
+                            if partial_res.is_none() {
+                                *unresolved = name.into();
+                            }
+                            break;
+                        };
+                        name = start;
+                        for &ns in &[TypeNS, ValueNS, MacroNS] {
+                            if let Some(res) =
+                                collector.check_full_res(ns, &start, module_id, &None, &None)
+                            {
+                                debug!("found partial_res={:?}", res);
+                                *partial_res = Some(res);
+                                *unresolved = end.into();
+                                break 'outer;
+                            }
+                        }
+                        *unresolved = end.into();
+                    }
+
+                    let last_found_module = match *partial_res {
+                        Some(Res::Def(DefKind::Mod, id)) => Some(id),
+                        None => Some(module_id),
+                        _ => None,
+                    };
+                    // See if this was a module: `[path]` or `[std::io::nope]`
+                    if let Some(module) = last_found_module {
+                        let module_name = collector.cx.tcx.item_name(module);
+                        let note = format!(
+                            "the module `{}` contains no item named `{}`",
+                            module_name, unresolved
+                        );
+                        if let Some(span) = sp {
+                            diag.span_label(span, &note);
+                        } else {
+                            diag.note(&note);
+                        }
+                        // If the link has `::` in it, assume it was meant to be an intra-doc link.
+                        // Otherwise, the `[]` might be unrelated.
+                        // FIXME: don't show this for autolinks (`<>`), `()` style links, or reference links
+                        if !path_str.contains("::") {
+                            diag.help(r#"to escape `[` and `]` characters, add '\' before them like `\[` or `\]`"#);
+                        }
+                        continue;
+                    }
+
+                    // Otherwise, it must be an associated item or variant
+                    let res = partial_res.expect("None case was handled by `last_found_module`");
+                    let diagnostic_name;
+                    let (kind, name) = match res {
+                        Res::Def(kind, def_id) => {
+                            diagnostic_name = collector.cx.tcx.item_name(def_id).as_str();
+                            (Some(kind), &*diagnostic_name)
+                        }
+                        Res::PrimTy(ty) => (None, ty.name_str()),
+                        _ => unreachable!("only ADTs and primitives are in scope at module level"),
+                    };
+                    let path_description = if let Some(kind) = kind {
+                        match kind {
+                            Mod | ForeignMod => "inner item",
+                            Struct => "field or associated item",
+                            Enum | Union => "variant or associated item",
+                            Variant
+                            | Field
+                            | Closure
+                            | Generator
+                            | AssocTy
+                            | AssocConst
+                            | AssocFn
+                            | Fn
+                            | Macro(_)
+                            | Const
+                            | ConstParam
+                            | ExternCrate
+                            | Use
+                            | LifetimeParam
+                            | Ctor(_, _)
+                            | AnonConst => {
+                                let note = assoc_item_not_allowed(res);
+                                if let Some(span) = sp {
+                                    diag.span_label(span, &note);
+                                } else {
+                                    diag.note(&note);
+                                }
+                                return;
+                            }
+                            Trait | TyAlias | ForeignTy | OpaqueTy | TraitAlias | TyParam
+                            | Static => "associated item",
+                            Impl | GlobalAsm => unreachable!("not a path"),
+                        }
+                    } else {
+                        "associated item"
+                    };
+                    let note = format!(
+                        "the {} `{}` has no {} named `{}`",
+                        res.descr(),
+                        name,
+                        disambiguator.map_or(path_description, |d| d.descr()),
+                        unresolved,
+                    );
+                    if let Some(span) = sp {
+                        diag.span_label(span, &note);
+                    } else {
+                        diag.note(&note);
+                    }
+
+                    continue;
+                }
+                let note = match failure {
+                    ResolutionFailure::NotResolved { .. } => unreachable!("handled above"),
+                    ResolutionFailure::Dummy => continue,
+                    ResolutionFailure::WrongNamespace(res, expected_ns) => {
+                        if let Res::Def(kind, _) = res {
+                            let disambiguator = Disambiguator::Kind(kind);
+                            suggest_disambiguator(
+                                disambiguator,
+                                diag,
+                                path_str,
+                                dox,
+                                sp,
+                                &link_range,
+                            )
+                        }
+
+                        format!(
+                            "this link resolves to {}, which is not in the {} namespace",
+                            item(res),
+                            expected_ns.descr()
+                        )
+                    }
+                    ResolutionFailure::NoParentItem => {
+                        diag.level = rustc_errors::Level::Bug;
+                        "all intra doc links should have a parent item".to_owned()
+                    }
+                };
+                if let Some(span) = sp {
+                    diag.span_label(span, &note);
+                } else {
+                    diag.note(&note);
+                }
+            }
         },
     );
 }
@@ -1210,31 +1704,14 @@ fn anchor_failure(
 ) {
     let msg = match failure {
         AnchorFailure::MultipleAnchors => format!("`{}` contains multiple anchors", path_str),
-        AnchorFailure::Primitive
-        | AnchorFailure::Variant
-        | AnchorFailure::AssocConstant
-        | AnchorFailure::AssocType
-        | AnchorFailure::Field
-        | AnchorFailure::Method => {
-            let kind = match failure {
-                AnchorFailure::Primitive => "primitive type",
-                AnchorFailure::Variant => "enum variant",
-                AnchorFailure::AssocConstant => "associated constant",
-                AnchorFailure::AssocType => "associated type",
-                AnchorFailure::Field => "struct field",
-                AnchorFailure::Method => "method",
-                AnchorFailure::MultipleAnchors => unreachable!("should be handled already"),
-            };
-
-            format!(
-                "`{}` contains an anchor, but links to {kind}s are already anchored",
-                path_str,
-                kind = kind
-            )
-        }
+        AnchorFailure::RustdocAnchorConflict(res) => format!(
+            "`{}` contains an anchor, but links to {kind}s are already anchored",
+            path_str,
+            kind = res.descr(),
+        ),
     };
 
-    report_diagnostic(cx, &msg, item, dox, link_range, |diag, sp| {
+    report_diagnostic(cx, BROKEN_INTRA_DOC_LINKS, &msg, item, dox, &link_range, |diag, sp| {
         if let Some(sp) = sp {
             diag.span_label(sp, "contains invalid anchor");
         }
@@ -1247,12 +1724,12 @@ fn ambiguity_error(
     path_str: &str,
     dox: &str,
     link_range: Option<Range<usize>>,
-    candidates: Vec<(Res, Namespace)>,
+    candidates: Vec<Res>,
 ) {
     let mut msg = format!("`{}` is ", path_str);
 
     match candidates.as_slice() {
-        [(first_def, _), (second_def, _)] => {
+        [first_def, second_def] => {
             msg += &format!(
                 "both {} {} and {} {}",
                 first_def.article(),
@@ -1263,7 +1740,7 @@ fn ambiguity_error(
         }
         _ => {
             let mut candidates = candidates.iter().peekable();
-            while let Some((res, _)) = candidates.next() {
+            while let Some(res) = candidates.next() {
                 if candidates.peek().is_some() {
                     msg += &format!("{} {}, ", res.article(), res.descr());
                 } else {
@@ -1273,55 +1750,43 @@ fn ambiguity_error(
         }
     }
 
-    report_diagnostic(cx, &msg, item, dox, link_range.clone(), |diag, sp| {
+    report_diagnostic(cx, BROKEN_INTRA_DOC_LINKS, &msg, item, dox, &link_range, |diag, sp| {
         if let Some(sp) = sp {
             diag.span_label(sp, "ambiguous link");
+        } else {
+            diag.note("ambiguous link");
+        }
 
-            let link_range = link_range.expect("must have a link range if we have a span");
-
-            for (res, ns) in candidates {
-                let (action, mut suggestion) = match res {
-                    Res::Def(DefKind::AssocFn | DefKind::Fn, _) => {
-                        ("add parentheses", format!("{}()", path_str))
-                    }
-                    Res::Def(DefKind::Macro(MacroKind::Bang), _) => {
-                        ("add an exclamation mark", format!("{}!", path_str))
-                    }
-                    _ => {
-                        let type_ = match (res, ns) {
-                            (Res::PrimTy(_), _) => "prim",
-                            (Res::Def(DefKind::Const, _), _) => "const",
-                            (Res::Def(DefKind::Static, _), _) => "static",
-                            (Res::Def(DefKind::Struct, _), _) => "struct",
-                            (Res::Def(DefKind::Enum, _), _) => "enum",
-                            (Res::Def(DefKind::Union, _), _) => "union",
-                            (Res::Def(DefKind::Trait, _), _) => "trait",
-                            (Res::Def(DefKind::Mod, _), _) => "module",
-                            (_, TypeNS) => "type",
-                            (_, ValueNS) => "value",
-                            (Res::Def(DefKind::Macro(MacroKind::Derive), _), MacroNS) => "derive",
-                            (_, MacroNS) => "macro",
-                        };
+        for res in candidates {
+            let disambiguator = Disambiguator::from_res(res);
+            suggest_disambiguator(disambiguator, diag, path_str, dox, sp, &link_range);
+        }
+    });
+}
 
-                        // FIXME: if this is an implied shortcut link, it's bad style to suggest `@`
-                        ("prefix with the item type", format!("{}@{}", type_, path_str))
-                    }
-                };
+fn suggest_disambiguator(
+    disambiguator: Disambiguator,
+    diag: &mut DiagnosticBuilder<'_>,
+    path_str: &str,
+    dox: &str,
+    sp: Option<rustc_span::Span>,
+    link_range: &Option<Range<usize>>,
+) {
+    let suggestion = disambiguator.suggestion();
+    let help = format!("to link to the {}, {}", disambiguator.descr(), suggestion.descr());
 
-                if dox.bytes().nth(link_range.start) == Some(b'`') {
-                    suggestion = format!("`{}`", suggestion);
-                }
+    if let Some(sp) = sp {
+        let link_range = link_range.as_ref().expect("must have a link range if we have a span");
+        let msg = if dox.bytes().nth(link_range.start) == Some(b'`') {
+            format!("`{}`", suggestion.as_help(path_str))
+        } else {
+            suggestion.as_help(path_str)
+        };
 
-                // FIXME: Create a version of this suggestion for when we don't have the span.
-                diag.span_suggestion(
-                    sp,
-                    &format!("to link to the {}, {}", res.descr(), action),
-                    suggestion,
-                    Applicability::MaybeIncorrect,
-                );
-            }
-        }
-    });
+        diag.span_suggestion(sp, &help, msg, Applicability::MaybeIncorrect);
+    } else {
+        diag.help(&format!("{}: {}", help, suggestion.as_help(path_str)));
+    }
 }
 
 fn privacy_error(
@@ -1335,7 +1800,7 @@ fn privacy_error(
     let msg =
         format!("public documentation for `{}` links to private item `{}`", item_name, path_str);
 
-    report_diagnostic(cx, &msg, item, dox, link_range, |diag, sp| {
+    report_diagnostic(cx, PRIVATE_INTRA_DOC_LINKS, &msg, item, dox, &link_range, |diag, sp| {
         if let Some(sp) = sp {
             diag.span_label(sp, "this item is private");
         }
@@ -1354,16 +1819,16 @@ fn handle_variant(
     cx: &DocContext<'_>,
     res: Res,
     extra_fragment: &Option<String>,
-) -> Result<(Res, Option<String>), ErrorKind> {
+) -> Result<(Res, Option<String>), ErrorKind<'static>> {
     use rustc_middle::ty::DefIdTree;
 
     if extra_fragment.is_some() {
-        return Err(ErrorKind::AnchorFailure(AnchorFailure::Variant));
+        return Err(ErrorKind::AnchorFailure(AnchorFailure::RustdocAnchorConflict(res)));
     }
     let parent = if let Some(parent) = cx.tcx.parent(res.def_id()) {
         parent
     } else {
-        return Err(ErrorKind::ResolutionFailure);
+        return Err(ResolutionFailure::NoParentItem.into());
     };
     let parent_def = Res::Def(DefKind::Enum, parent);
     let variant = cx.tcx.expect_variant_res(res);