1
Fork 0

Auto merge of #58824 - euclio:intra-link-ambiguity, r=petrochenkov

overhaul intra-doc-link ambiguity warning

Fixes #52784.

- Makes the warning part of the `intra_doc_link_resolution_failure`
lint.
- Tightens the span to just the ambiguous link.
- Reports ambiguities across all three namespaces.
- Uses structured suggestions for disambiguation.
- Adds a test for the warnings.

r? @QuietMisdreavus
This commit is contained in:
bors 2019-03-18 02:56:35 +00:00
commit 03dafa7da3
4 changed files with 296 additions and 153 deletions

View file

@ -1,7 +1,8 @@
use rustc::lint as lint; use errors::Applicability;
use rustc::hir; use rustc::hir::def::{Def, Namespace::{self, *}, PerNS};
use rustc::hir::def::Def;
use rustc::hir::def_id::DefId; use rustc::hir::def_id::DefId;
use rustc::hir;
use rustc::lint as lint;
use rustc::ty; use rustc::ty;
use syntax; use syntax;
use syntax::ast::{self, Ident}; use syntax::ast::{self, Ident};
@ -35,22 +36,9 @@ pub fn collect_intra_doc_links(krate: Crate, cx: &DocContext<'_>) -> Crate {
} }
} }
#[derive(Debug)]
enum PathKind {
/// Either a value or type, but not a macro
Unknown,
/// Macro
Macro,
/// Values, functions, consts, statics (everything in the value namespace)
Value,
/// Types, traits (everything in the type namespace)
Type,
}
struct LinkCollector<'a, 'tcx> { struct LinkCollector<'a, 'tcx> {
cx: &'a DocContext<'tcx>, cx: &'a DocContext<'tcx>,
mod_ids: Vec<ast::NodeId>, mod_ids: Vec<ast::NodeId>,
is_nightly_build: bool,
} }
impl<'a, 'tcx> LinkCollector<'a, 'tcx> { impl<'a, 'tcx> LinkCollector<'a, 'tcx> {
@ -58,16 +46,14 @@ impl<'a, 'tcx> LinkCollector<'a, 'tcx> {
LinkCollector { LinkCollector {
cx, cx,
mod_ids: Vec::new(), mod_ids: Vec::new(),
is_nightly_build: UnstableFeatures::from_environment().is_nightly_build(),
} }
} }
/// Resolves a given string as a path, along with whether or not it is /// Resolves a string as a path within a particular namespace. Also returns an optional
/// in the value namespace. Also returns an optional URL fragment in the case /// URL fragment in the case of variants and methods.
/// of variants and methods.
fn resolve(&self, fn resolve(&self,
path_str: &str, path_str: &str,
is_val: bool, ns: Namespace,
current_item: &Option<String>, current_item: &Option<String>,
parent_id: Option<ast::NodeId>) parent_id: Option<ast::NodeId>)
-> Result<(Def, Option<String>), ()> -> Result<(Def, Option<String>), ()>
@ -78,11 +64,11 @@ impl<'a, 'tcx> LinkCollector<'a, 'tcx> {
// path. // path.
if let Some(id) = parent_id.or(self.mod_ids.last().cloned()) { if let Some(id) = parent_id.or(self.mod_ids.last().cloned()) {
// FIXME: `with_scope` requires the `NodeId` of a module. // FIXME: `with_scope` requires the `NodeId` of a module.
let result = cx.enter_resolver(|resolver| resolver.with_scope(id, let result = cx.enter_resolver(|resolver| {
|resolver| { resolver.with_scope(id, |resolver| {
resolver.resolve_str_path_error(DUMMY_SP, resolver.resolve_str_path_error(DUMMY_SP, &path_str, ns == ValueNS)
&path_str, is_val) })
})); });
if let Ok(result) = result { if let Ok(result) = result {
// In case this is a trait item, skip the // In case this is a trait item, skip the
@ -95,16 +81,16 @@ impl<'a, 'tcx> LinkCollector<'a, 'tcx> {
_ => return Ok((result.def, None)) _ => return Ok((result.def, None))
}; };
if value != is_val { if value != (ns == ValueNS) {
return Err(()) return Err(())
} }
} else if let Some(prim) = is_primitive(path_str, is_val) { } else if let Some(prim) = is_primitive(path_str, ns) {
return Ok((prim, Some(path_str.to_owned()))) return Ok((prim, Some(path_str.to_owned())))
} else { } else {
// If resolution failed, it may still be a method // If resolution failed, it may still be a method
// because methods are not handled by the resolver // because methods are not handled by the resolver
// If so, bail when we're not looking for a value. // If so, bail when we're not looking for a value.
if !is_val { if ns != ValueNS {
return Err(()) return Err(())
} }
} }
@ -128,7 +114,7 @@ impl<'a, 'tcx> LinkCollector<'a, 'tcx> {
path = name.clone(); path = name.clone();
} }
} }
if let Some(prim) = is_primitive(&path, false) { if let Some(prim) = is_primitive(&path, TypeNS) {
let did = primitive_impl(cx, &path).ok_or(())?; let did = primitive_impl(cx, &path).ok_or(())?;
return cx.tcx.associated_items(did) return cx.tcx.associated_items(did)
.find(|item| item.ident.name == item_name) .find(|item| item.ident.name == item_name)
@ -152,8 +138,8 @@ impl<'a, 'tcx> LinkCollector<'a, 'tcx> {
.find(|item| item.ident.name == item_name); .find(|item| item.ident.name == item_name);
if let Some(item) = item { if let Some(item) = item {
let out = match item.kind { let out = match item.kind {
ty::AssociatedKind::Method if is_val => "method", ty::AssociatedKind::Method if ns == ValueNS => "method",
ty::AssociatedKind::Const if is_val => "associatedconstant", ty::AssociatedKind::Const if ns == ValueNS => "associatedconstant",
_ => return Err(()) _ => return Err(())
}; };
Ok((ty.def, Some(format!("{}.{}", out, item_name)))) Ok((ty.def, Some(format!("{}.{}", out, item_name))))
@ -190,9 +176,9 @@ impl<'a, 'tcx> LinkCollector<'a, 'tcx> {
.find(|item| item.ident.name == item_name); .find(|item| item.ident.name == item_name);
if let Some(item) = item { if let Some(item) = item {
let kind = match item.kind { let kind = match item.kind {
ty::AssociatedKind::Const if is_val => "associatedconstant", ty::AssociatedKind::Const if ns == ValueNS => "associatedconstant",
ty::AssociatedKind::Type if !is_val => "associatedtype", ty::AssociatedKind::Type if ns == TypeNS => "associatedtype",
ty::AssociatedKind::Method if is_val => { ty::AssociatedKind::Method if ns == ValueNS => {
if item.defaultness.has_value() { if item.defaultness.has_value() {
"method" "method"
} else { } else {
@ -279,10 +265,6 @@ impl<'a, 'tcx> DocFolder for LinkCollector<'a, 'tcx> {
look_for_tests(&cx, &dox, &item, true); look_for_tests(&cx, &dox, &item, true);
if !self.is_nightly_build {
return None;
}
for (ori_link, link_range) in markdown_links(&dox) { for (ori_link, link_range) in markdown_links(&dox) {
// Bail early for real links. // Bail early for real links.
if ori_link.contains('/') { if ori_link.contains('/') {
@ -296,28 +278,28 @@ impl<'a, 'tcx> DocFolder for LinkCollector<'a, 'tcx> {
let link = ori_link.replace("`", ""); let link = ori_link.replace("`", "");
let (def, fragment) = { let (def, fragment) = {
let mut kind = PathKind::Unknown; let mut kind = None;
let path_str = if let Some(prefix) = let path_str = if let Some(prefix) =
["struct@", "enum@", "type@", ["struct@", "enum@", "type@",
"trait@", "union@"].iter() "trait@", "union@"].iter()
.find(|p| link.starts_with(**p)) { .find(|p| link.starts_with(**p)) {
kind = PathKind::Type; kind = Some(TypeNS);
link.trim_start_matches(prefix) link.trim_start_matches(prefix)
} else if let Some(prefix) = } else if let Some(prefix) =
["const@", "static@", ["const@", "static@",
"value@", "function@", "mod@", "value@", "function@", "mod@",
"fn@", "module@", "method@"] "fn@", "module@", "method@"]
.iter().find(|p| link.starts_with(**p)) { .iter().find(|p| link.starts_with(**p)) {
kind = PathKind::Value; kind = Some(ValueNS);
link.trim_start_matches(prefix) link.trim_start_matches(prefix)
} else if link.ends_with("()") { } else if link.ends_with("()") {
kind = PathKind::Value; kind = Some(ValueNS);
link.trim_end_matches("()") link.trim_end_matches("()")
} else if link.starts_with("macro@") { } else if link.starts_with("macro@") {
kind = PathKind::Macro; kind = Some(MacroNS);
link.trim_start_matches("macro@") link.trim_start_matches("macro@")
} else if link.ends_with('!') { } else if link.ends_with('!') {
kind = PathKind::Macro; kind = Some(MacroNS);
link.trim_end_matches('!') link.trim_end_matches('!')
} else { } else {
&link[..] &link[..]
@ -329,8 +311,8 @@ impl<'a, 'tcx> DocFolder for LinkCollector<'a, 'tcx> {
} }
match kind { match kind {
PathKind::Value => { Some(ns @ ValueNS) => {
if let Ok(def) = self.resolve(path_str, true, &current_item, parent_node) { if let Ok(def) = self.resolve(path_str, ns, &current_item, parent_node) {
def def
} else { } else {
resolution_failure(cx, &item.attrs, path_str, &dox, link_range); resolution_failure(cx, &item.attrs, path_str, &dox, link_range);
@ -340,8 +322,8 @@ impl<'a, 'tcx> DocFolder for LinkCollector<'a, 'tcx> {
continue; continue;
} }
} }
PathKind::Type => { Some(ns @ TypeNS) => {
if let Ok(def) = self.resolve(path_str, false, &current_item, parent_node) { if let Ok(def) = self.resolve(path_str, ns, &current_item, parent_node) {
def def
} else { } else {
resolution_failure(cx, &item.attrs, path_str, &dox, link_range); resolution_failure(cx, &item.attrs, path_str, &dox, link_range);
@ -349,62 +331,49 @@ impl<'a, 'tcx> DocFolder for LinkCollector<'a, 'tcx> {
continue; continue;
} }
} }
PathKind::Unknown => { None => {
// Try everything! // Try everything!
if let Some(macro_def) = macro_resolve(cx, path_str) { let candidates = PerNS {
if let Ok(type_def) = macro_ns: macro_resolve(cx, path_str).map(|def| (def, None)),
self.resolve(path_str, false, &current_item, parent_node) type_ns: self
{ .resolve(path_str, TypeNS, &current_item, parent_node)
let (type_kind, article, type_disambig) .ok(),
= type_ns_kind(type_def.0, path_str); value_ns: self
ambiguity_error(cx, &item.attrs, path_str, .resolve(path_str, ValueNS, &current_item, parent_node)
article, type_kind, &type_disambig, .ok()
"a", "macro", &format!("macro@{}", path_str)); .and_then(|(def, fragment)| {
continue; // Constructors are picked up in the type namespace.
} else if let Ok(value_def) = match def {
self.resolve(path_str, true, &current_item, parent_node) Def::StructCtor(..)
{ | Def::VariantCtor(..)
let (value_kind, value_disambig) | Def::SelfCtor(..) => None,
= value_ns_kind(value_def.0, path_str) _ => Some((def, fragment))
.expect("struct and mod cases should have been \ }
caught in previous branch"); }),
ambiguity_error(cx, &item.attrs, path_str, };
"a", value_kind, &value_disambig,
"a", "macro", &format!("macro@{}", path_str)); if candidates.is_empty() {
}
(macro_def, None)
} else if let Ok(type_def) =
self.resolve(path_str, false, &current_item, parent_node)
{
// It is imperative we search for not-a-value first
// Otherwise we will find struct ctors for when we are looking
// for structs, and the link won't work if there is something in
// both namespaces.
if let Ok(value_def) =
self.resolve(path_str, true, &current_item, parent_node)
{
let kind = value_ns_kind(value_def.0, path_str);
if let Some((value_kind, value_disambig)) = kind {
let (type_kind, article, type_disambig)
= type_ns_kind(type_def.0, path_str);
ambiguity_error(cx, &item.attrs, path_str,
article, type_kind, &type_disambig,
"a", value_kind, &value_disambig);
continue;
}
}
type_def
} else if let Ok(value_def) =
self.resolve(path_str, true, &current_item, parent_node)
{
value_def
} else {
resolution_failure(cx, &item.attrs, path_str, &dox, link_range); resolution_failure(cx, &item.attrs, path_str, &dox, link_range);
// this could just be a normal link // this could just be a normal link
continue; continue;
} }
let is_unambiguous = candidates.clone().present_items().count() == 1;
if is_unambiguous {
candidates.present_items().next().unwrap()
} else {
ambiguity_error(
cx,
&item.attrs,
path_str,
&dox,
link_range,
candidates.map(|candidate| candidate.map(|(def, _)| def)),
);
continue;
}
} }
PathKind::Macro => { Some(MacroNS) => {
if let Some(def) = macro_resolve(cx, path_str) { if let Some(def) = macro_resolve(cx, path_str) {
(def, None) (def, None)
} else { } else {
@ -511,59 +480,114 @@ fn resolution_failure(
diag.emit(); diag.emit();
} }
fn ambiguity_error(cx: &DocContext<'_>, attrs: &Attributes, fn ambiguity_error(
path_str: &str, cx: &DocContext<'_>,
article1: &str, kind1: &str, disambig1: &str, attrs: &Attributes,
article2: &str, kind2: &str, disambig2: &str) { path_str: &str,
dox: &str,
link_range: Option<Range<usize>>,
candidates: PerNS<Option<Def>>,
) {
let sp = span_of_attrs(attrs); let sp = span_of_attrs(attrs);
cx.sess()
.struct_span_warn(sp,
&format!("`{}` is both {} {} and {} {}",
path_str, article1, kind1,
article2, kind2))
.help(&format!("try `{}` if you want to select the {}, \
or `{}` if you want to \
select the {}",
disambig1, kind1, disambig2,
kind2))
.emit();
}
/// Given a def, returns its name and disambiguator let mut msg = format!("`{}` is ", path_str);
/// for a value namespace.
/// let candidates = [TypeNS, ValueNS, MacroNS].iter().filter_map(|&ns| {
/// Returns `None` for things which cannot be ambiguous since candidates[ns].map(|def| (def, ns))
/// they exist in both namespaces (structs and modules). }).collect::<Vec<_>>();
fn value_ns_kind(def: Def, path_str: &str) -> Option<(&'static str, String)> { match candidates.as_slice() {
match def { [(first_def, _), (second_def, _)] => {
// Structs, variants, and mods exist in both namespaces; skip them. msg += &format!(
Def::StructCtor(..) | Def::Mod(..) | Def::Variant(..) | "both {} {} and {} {}",
Def::VariantCtor(..) | Def::SelfCtor(..) first_def.article(),
=> None, first_def.kind_name(),
Def::Fn(..) second_def.article(),
=> Some(("function", format!("{}()", path_str))), second_def.kind_name(),
Def::Method(..) );
=> Some(("method", format!("{}()", path_str))), }
Def::Const(..) _ => {
=> Some(("const", format!("const@{}", path_str))), let mut candidates = candidates.iter().peekable();
Def::Static(..) while let Some((def, _)) = candidates.next() {
=> Some(("static", format!("static@{}", path_str))), if candidates.peek().is_some() {
_ => Some(("value", format!("value@{}", path_str))), msg += &format!("{} {}, ", def.article(), def.kind_name());
} else {
msg += &format!("and {} {}", def.article(), def.kind_name());
}
}
}
} }
}
/// Given a def, returns its name, the article to be used, and a disambiguator let mut diag = cx.tcx.struct_span_lint_hir(
/// for the type namespace. lint::builtin::INTRA_DOC_LINK_RESOLUTION_FAILURE,
fn type_ns_kind(def: Def, path_str: &str) -> (&'static str, &'static str, String) { hir::CRATE_HIR_ID,
let (kind, article) = match def { sp,
// We can still have non-tuple structs. &msg,
Def::Struct(..) => ("struct", "a"), );
Def::Enum(..) => ("enum", "an"),
Def::Trait(..) => ("trait", "a"), if let Some(link_range) = link_range {
Def::Union(..) => ("union", "a"), if let Some(sp) = super::source_span_for_markdown_range(cx, dox, &link_range, attrs) {
_ => ("type", "a"), diag.set_span(sp);
}; diag.span_label(sp, "ambiguous link");
(kind, article, format!("{}@{}", kind, path_str))
for (def, ns) in candidates {
let (action, mut suggestion) = match def {
Def::Method(..) | Def::Fn(..) => {
("add parentheses", format!("{}()", path_str))
}
Def::Macro(..) => {
("add an exclamation mark", format!("{}!", path_str))
}
_ => {
let type_ = match (def, ns) {
(Def::Const(..), _) => "const",
(Def::Static(..), _) => "static",
(Def::Struct(..), _) => "struct",
(Def::Enum(..), _) => "enum",
(Def::Union(..), _) => "union",
(Def::Trait(..), _) => "trait",
(Def::Mod(..), _) => "module",
(_, TypeNS) => "type",
(_, ValueNS) => "value",
(_, MacroNS) => "macro",
};
// FIXME: if this is an implied shortcut link, it's bad style to suggest `@`
("prefix with the item type", format!("{}@{}", type_, path_str))
}
};
if dox.bytes().nth(link_range.start) == Some(b'`') {
suggestion = format!("`{}`", suggestion);
}
diag.span_suggestion(
sp,
&format!("to link to the {}, {}", def.kind_name(), action),
suggestion,
Applicability::MaybeIncorrect,
);
}
} else {
// blah blah blah\nblah\nblah [blah] blah blah\nblah blah
// ^ ~~~~
// | link_range
// last_new_line_offset
let last_new_line_offset = dox[..link_range.start].rfind('\n').map_or(0, |n| n + 1);
let line = dox[last_new_line_offset..].lines().next().unwrap_or("");
// 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$}",
line=line,
indicator="",
before=link_range.start - last_new_line_offset,
found=link_range.len(),
));
}
}
diag.emit();
} }
/// Given an enum variant's def, return the def of its enum and the associated fragment. /// Given an enum variant's def, return the def of its enum and the associated fragment.
@ -600,11 +624,11 @@ const PRIMITIVES: &[(&str, Def)] = &[
("char", Def::PrimTy(hir::PrimTy::Char)), ("char", Def::PrimTy(hir::PrimTy::Char)),
]; ];
fn is_primitive(path_str: &str, is_val: bool) -> Option<Def> { fn is_primitive(path_str: &str, ns: Namespace) -> Option<Def> {
if is_val { if ns == TypeNS {
None
} else {
PRIMITIVES.iter().find(|x| x.0 == path_str).map(|x| x.1) PRIMITIVES.iter().find(|x| x.0 == path_str).map(|x| x.1)
} else {
None
} }
} }

View file

@ -0,0 +1,36 @@
#![deny(intra_doc_link_resolution_failure)]
#![allow(non_camel_case_types)]
#![allow(non_upper_case_globals)]
pub fn ambiguous() {}
pub struct ambiguous {}
#[macro_export]
macro_rules! multi_conflict { () => {} }
#[allow(non_camel_case_types)]
pub struct multi_conflict {}
pub fn multi_conflict() {}
pub mod type_and_value {}
pub const type_and_value: i32 = 0;
pub mod foo {
pub enum bar {}
pub fn bar() {}
}
/// [`ambiguous`] is ambiguous. //~ERROR `ambiguous`
///
/// [ambiguous] is ambiguous. //~ERROR ambiguous
///
/// [`multi_conflict`] is a three-way conflict. //~ERROR `multi_conflict`
///
/// Ambiguous [type_and_value]. //~ERROR type_and_value
///
/// Ambiguous non-implied shortcut link [`foo::bar`]. //~ERROR `foo::bar`
pub struct Docs {}

View file

@ -0,0 +1,82 @@
error: `ambiguous` is both a struct and a function
--> $DIR/intra-links-ambiguity.rs:27:6
|
LL | /// [`ambiguous`] is ambiguous.
| ^^^^^^^^^^^ ambiguous link
|
note: lint level defined here
--> $DIR/intra-links-ambiguity.rs:1:9
|
LL | #![deny(intra_doc_link_resolution_failure)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: to link to the struct, prefix with the item type
|
LL | /// [`struct@ambiguous`] is ambiguous.
| ^^^^^^^^^^^^^^^^^^
help: to link to the function, add parentheses
|
LL | /// [`ambiguous()`] is ambiguous.
| ^^^^^^^^^^^^^
error: `ambiguous` is both a struct and a function
--> $DIR/intra-links-ambiguity.rs:29:6
|
LL | /// [ambiguous] is ambiguous.
| ^^^^^^^^^ ambiguous link
help: to link to the struct, prefix with the item type
|
LL | /// [struct@ambiguous] is ambiguous.
| ^^^^^^^^^^^^^^^^
help: to link to the function, add parentheses
|
LL | /// [ambiguous()] is ambiguous.
| ^^^^^^^^^^^
error: `multi_conflict` is a struct, a function, and a macro
--> $DIR/intra-links-ambiguity.rs:31:6
|
LL | /// [`multi_conflict`] is a three-way conflict.
| ^^^^^^^^^^^^^^^^ ambiguous link
help: to link to the struct, prefix with the item type
|
LL | /// [`struct@multi_conflict`] is a three-way conflict.
| ^^^^^^^^^^^^^^^^^^^^^^^
help: to link to the function, add parentheses
|
LL | /// [`multi_conflict()`] is a three-way conflict.
| ^^^^^^^^^^^^^^^^^^
help: to link to the macro, add an exclamation mark
|
LL | /// [`multi_conflict!`] is a three-way conflict.
| ^^^^^^^^^^^^^^^^^
error: `type_and_value` is both a module and a constant
--> $DIR/intra-links-ambiguity.rs:33:16
|
LL | /// Ambiguous [type_and_value].
| ^^^^^^^^^^^^^^ ambiguous link
help: to link to the module, prefix with the item type
|
LL | /// Ambiguous [module@type_and_value].
| ^^^^^^^^^^^^^^^^^^^^^
help: to link to the constant, prefix with the item type
|
LL | /// Ambiguous [const@type_and_value].
| ^^^^^^^^^^^^^^^^^^^^
error: `foo::bar` is both an enum and a function
--> $DIR/intra-links-ambiguity.rs:35:42
|
LL | /// Ambiguous non-implied shortcut link [`foo::bar`].
| ^^^^^^^^^^ ambiguous link
help: to link to the enum, prefix with the item type
|
LL | /// Ambiguous non-implied shortcut link [`enum@foo::bar`].
| ^^^^^^^^^^^^^^^
help: to link to the function, add parentheses
|
LL | /// Ambiguous non-implied shortcut link [`foo::bar()`].
| ^^^^^^^^^^^^
error: aborting due to 5 previous errors

View file

@ -22,6 +22,7 @@
//! * [`ThisType::this_method`](ThisType::this_method) //! * [`ThisType::this_method`](ThisType::this_method)
//! * [`ThisEnum`](ThisEnum) //! * [`ThisEnum`](ThisEnum)
//! * [`ThisEnum::ThisVariant`](ThisEnum::ThisVariant) //! * [`ThisEnum::ThisVariant`](ThisEnum::ThisVariant)
//! * [`ThisEnum::ThisVariantCtor`](ThisEnum::ThisVariantCtor)
//! * [`ThisTrait`](ThisTrait) //! * [`ThisTrait`](ThisTrait)
//! * [`ThisTrait::this_associated_method`](ThisTrait::this_associated_method) //! * [`ThisTrait::this_associated_method`](ThisTrait::this_associated_method)
//! * [`ThisTrait::ThisAssociatedType`](ThisTrait::ThisAssociatedType) //! * [`ThisTrait::ThisAssociatedType`](ThisTrait::ThisAssociatedType)
@ -50,7 +51,7 @@ pub struct ThisType;
impl ThisType { impl ThisType {
pub fn this_method() {} pub fn this_method() {}
} }
pub enum ThisEnum { ThisVariant, } pub enum ThisEnum { ThisVariant, ThisVariantCtor(u32), }
pub trait ThisTrait { pub trait ThisTrait {
type ThisAssociatedType; type ThisAssociatedType;
const THIS_ASSOCIATED_CONST: u8; const THIS_ASSOCIATED_CONST: u8;