Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow generic parameters in intra-doc links #76934

Merged
merged 7 commits into from
Oct 10, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions library/core/src/option.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@
//! ```
//!
//! [`Box<T>`]: ../../std/boxed/struct.Box.html
//! [`Box<U>`]: ../../std/boxed/struct.Box.html
//! [`num::NonZero*`]: crate::num
//! [`ptr::NonNull<U>`]: crate::ptr::NonNull

#![stable(feature = "rust1", since = "1.0.0")]

Expand Down
2 changes: 1 addition & 1 deletion src/doc/rustdoc/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
- [Linking to items by name](linking-to-items-by-name.md)
- [Lints](lints.md)
- [Passes](passes.md)
- [Advanced Features](advanced-features.md)
- [Advanced features](advanced-features.md)
jyn514 marked this conversation as resolved.
Show resolved Hide resolved
- [Unstable features](unstable-features.md)
2 changes: 1 addition & 1 deletion src/doc/rustdoc/src/advanced-features.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Advanced Features
# Advanced features

The features listed on this page fall outside the rest of the main categories.

Expand Down
41 changes: 35 additions & 6 deletions src/doc/rustdoc/src/linking-to-items-by-name.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Linking to items by name

Rustdoc is capable of directly linking to other rustdoc pages in Markdown documentation using the path of item as a link.
Rustdoc is capable of directly linking to other rustdoc pages using the path of
the item as a link.

For example, in the following code all of the links will link to the rustdoc page for `Bar`:

Expand All @@ -19,15 +20,26 @@ pub struct Foo3;
/// This struct is also not [`Bar`]
pub struct Foo4;

/// This struct *is* [`Bar`]!
pub struct Bar;
```

You can refer to anything in scope, and use paths, including `Self`, `self`, `super`, and `crate`. You may also use `foo()` and `foo!()` to refer to methods/functions and macros respectively. Backticks around the link will be stripped.
Backticks around the link will be stripped, so ``[`Option`]`` will correctly
link to `Option`.

You can refer to anything in scope, and use paths, including `Self`, `self`,
`super`, and `crate`. You may also use `foo()` and `foo!()` to refer to methods/functions and macros, respectively.

You can also refer to items with generic parameters like `Vec<T>`. The link will
resolve as if you had written ``[`Vec<T>`](Vec)``. Fully-qualified syntax (for example,
`<Vec as IntoIterator>::into_iter()`) is [not yet supported][fqs-issue], however.

[fqs-issue]: https://github.com/rust-lang/rust/issues/74563

```rust,edition2018
use std::sync::mpsc::Receiver;

/// This is an version of [`Receiver`], with support for [`std::future`].
/// This is a version of [`Receiver<T>`] with support for [`std::future`].
///
/// You can obtain a [`std::future::Future`] by calling [`Self::recv()`].
pub struct AsyncReceiver<T> {
Expand All @@ -44,13 +56,15 @@ impl<T> AsyncReceiver<T> {
You can also link to sections using URL fragment specifiers:

```rust
/// This is a special implementation of [positional parameters]
/// This is a special implementation of [positional parameters].
///
/// [positional parameters]: std::fmt#formatting-parameters
struct MySpecialFormatter;
```

Paths in Rust have three namespaces: type, value, and macro. Items from these namespaces are allowed to overlap. In case of ambiguity, rustdoc will warn about the ambiguity and ask you to disambiguate, which can be done by using a prefix like `struct@`, `enum@`, `type@`, `trait@`, `union@`, `const@`, `static@`, `value@`, `function@`, `mod@`, `fn@`, `module@`, `method@`, `prim@`, `primitive@`, `macro@`, or `derive@`:
Paths in Rust have three namespaces: type, value, and macro. Item names must be
unique within their namespace, but can overlap with items outside of their
namespace. In case of ambiguity, rustdoc will warn about the ambiguity and ask you to disambiguate, which can be done by using a prefix like `struct@`, `enum@`, `type@`, `trait@`, `union@`, `const@`, `static@`, `value@`, `fn@`, `function@`, `mod@`, `module@`, `method@`, `prim@`, `primitive@`, `macro@`, or `derive@`:

```rust
/// See also: [`Foo`](struct@Foo)
Expand All @@ -62,4 +76,19 @@ struct Foo {}
fn Foo() {}
```

Note: Because of how `macro_rules` macros are scoped in Rust, the intra-doc links of a `macro_rules` macro will be resolved relative to the crate root, as opposed to the module it is defined in.
You can also disambiguate for functions by adding `()` after the function name,
jyn514 marked this conversation as resolved.
Show resolved Hide resolved
or for macros by adding `!` after the macro name:

```rust
/// See also: [`Foo`](struct@Foo)
struct Bar;

/// This is different from [`Foo()`]
struct Foo {}

fn Foo() {}
```

Note: Because of how `macro_rules!` macros are scoped in Rust, the intra-doc links of a `macro_rules!` macro will be resolved [relative to the crate root][#72243], as opposed to the module it is defined in.

[#72243]: https://github.com/rust-lang/rust/issues/72243
10 changes: 5 additions & 5 deletions src/doc/rustdoc/src/lints.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@
can use them like any other lints by doing this:

```rust,ignore
#![allow(missing_docs)] // allowing the lint, no message
#![warn(missing_docs)] // warn if there is missing docs
#![deny(missing_docs)] // rustdoc will fail if there is missing docs
#![allow(missing_docs)] // allows the lint, no diagnostics will be reported
#![warn(missing_docs)] // warn if there are missing docs
#![deny(missing_docs)] // error if there are missing docs
```

Here is the list of the lints provided by `rustdoc`:

## broken_intra_doc_links

This lint **warns by default**. This lint detects when an [intra-doc link] fails to get resolved. For example:
This lint **warns by default**. This lint detects when an [intra-doc link] fails to be resolved. For example:

[intra-doc link]: linking-to-items-by-name.html
[intra-doc link]: linking-to-items-by-name.md

```rust
/// I want to link to [`Nonexistent`] but it doesn't exist!
Expand Down
2 changes: 2 additions & 0 deletions src/librustdoc/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
html_playground_url = "https://play.rust-lang.org/"
)]
#![feature(rustc_private)]
#![feature(array_methods)]
#![feature(box_patterns)]
#![feature(box_syntax)]
#![feature(in_band_lifetimes)]
#![feature(nll)]
#![feature(or_patterns)]
#![feature(peekable_next_if)]
#![feature(test)]
#![feature(crate_visibility_modifier)]
#![feature(never_type)]
Expand Down
203 changes: 202 additions & 1 deletion src/librustdoc/passes/collect_intra_doc_links.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use smallvec::{smallvec, SmallVec};

use std::borrow::Cow;
use std::cell::Cell;
use std::mem;
use std::ops::Range;

use crate::clean::*;
Expand Down Expand Up @@ -65,10 +66,53 @@ enum ResolutionFailure<'a> {
NotResolved { module_id: DefId, partial_res: Option<Res>, unresolved: Cow<'a, str> },
/// should not ever happen
NoParentItem,
/// This link has malformed generic parameters; e.g., the angle brackets are unbalanced.
MalformedGenerics(MalformedGenerics),
/// used to communicate that this should be ignored, but shouldn't be reported to the user
Dummy,
}

#[derive(Debug)]
enum MalformedGenerics {
/// This link has unbalanced angle brackets.
///
/// For example, `Vec<T` should trigger this, as should `Vec<T>>`.
UnbalancedAngleBrackets,
/// The generics are not attached to a type.
///
/// For example, `<T>` should trigger this.
///
/// This is detected by checking if the path is empty after the generics are stripped.
MissingType,
/// The link uses fully-qualified syntax, which is currently unsupported.
///
/// For example, `<Vec as IntoIterator>::into_iter` should trigger this.
///
/// This is detected by checking if ` as ` (the keyword `as` with spaces around it) is inside
/// angle brackets.
HasFullyQualifiedSyntax,
/// The link has an invalid path separator.
///
/// For example, `Vec:<T>:new()` should trigger this. Note that `Vec:new()` will **not**
/// trigger this because it has no generics and thus [`strip_generics_from_path`] will not be
/// called.
///
/// Note that this will also **not** be triggered if the invalid path separator is inside angle
/// brackets because rustdoc mostly ignores what's inside angle brackets (except for
/// [`HasFullyQualifiedSyntax`](MalformedGenerics::HasFullyQualifiedSyntax)).
///
/// This is detected by checking if there is a colon followed by a non-colon in the link.
InvalidPathSeparator,
/// The link has too many angle brackets.
///
/// For example, `Vec<<T>>` should trigger this.
TooManyAngleBrackets,
/// The link has empty angle brackets.
///
/// For example, `Vec<>` should trigger this.
EmptyAngleBrackets,
}

impl ResolutionFailure<'a> {
// This resolved fully (not just partially) but is erroneous for some other reason
fn full_res(&self) -> Option<Res> {
Expand Down Expand Up @@ -912,6 +956,7 @@ impl LinkCollector<'_, '_> {
let link_text;
let mut path_str;
let disambiguator;
let stripped_path_string;
let (mut res, mut fragment) = {
path_str = if let Ok((d, path)) = Disambiguator::from_str(&link) {
disambiguator = Some(d);
Expand All @@ -922,7 +967,7 @@ impl LinkCollector<'_, '_> {
}
.trim();

if path_str.contains(|ch: char| !(ch.is_alphanumeric() || ch == ':' || ch == '_')) {
if path_str.contains(|ch: char| !(ch.is_alphanumeric() || ":_<>, ".contains(ch))) {
return None;
}

Expand Down Expand Up @@ -985,6 +1030,36 @@ impl LinkCollector<'_, '_> {
module_id = DefId { krate, index: CRATE_DEF_INDEX };
}

// Strip generics from the path.
if path_str.contains(['<', '>'].as_slice()) {
stripped_path_string = match strip_generics_from_path(path_str) {
Ok(path) => path,
Err(err_kind) => {
debug!("link has malformed generics: {}", path_str);
resolution_failure(
camelid marked this conversation as resolved.
Show resolved Hide resolved
self,
&item,
path_str,
disambiguator,
dox,
link_range,
smallvec![err_kind],
);
return None;
}
};
path_str = &stripped_path_string;
}

// Sanity check to make sure we don't have any angle brackets after stripping generics.
assert!(!path_str.contains(['<', '>'].as_slice()));

// The link is not an intra-doc link if it still contains commas or spaces after
// stripping generics.
if path_str.contains([',', ' '].as_slice()) {
return None;
}

match self.resolve_with_disambiguator(
disambiguator,
item,
Expand Down Expand Up @@ -1718,6 +1793,27 @@ fn resolution_failure(
diag.level = rustc_errors::Level::Bug;
"all intra doc links should have a parent item".to_owned()
}
ResolutionFailure::MalformedGenerics(variant) => match variant {
MalformedGenerics::UnbalancedAngleBrackets => {
String::from("unbalanced angle brackets")
}
MalformedGenerics::MissingType => {
String::from("missing type for generic parameters")
}
MalformedGenerics::HasFullyQualifiedSyntax => {
diag.note("see https://github.com/rust-lang/rust/issues/74563 for more information");
String::from("fully-qualified syntax is unsupported")
}
MalformedGenerics::InvalidPathSeparator => {
String::from("has invalid path separator")
}
MalformedGenerics::TooManyAngleBrackets => {
String::from("too many angle brackets")
}
MalformedGenerics::EmptyAngleBrackets => {
String::from("empty angle brackets")
}
},
};
if let Some(span) = sp {
diag.span_label(span, &note);
Expand Down Expand Up @@ -1908,3 +2004,108 @@ fn is_primitive(path_str: &str, ns: Namespace) -> Option<(&'static str, Res)> {
fn primitive_impl(cx: &DocContext<'_>, path_str: &str) -> Option<&'static SmallVec<[DefId; 4]>> {
Some(PrimitiveType::from_symbol(Symbol::intern(path_str))?.impls(cx.tcx))
}

fn strip_generics_from_path(path_str: &str) -> Result<String, ResolutionFailure<'static>> {
let mut stripped_segments = vec![];
let mut path = path_str.chars().peekable();
let mut segment = Vec::new();

while let Some(chr) = path.next() {
match chr {
':' => {
if path.next_if_eq(&':').is_some() {
let stripped_segment =
strip_generics_from_path_segment(mem::take(&mut segment))?;
if !stripped_segment.is_empty() {
stripped_segments.push(stripped_segment);
}
} else {
return Err(ResolutionFailure::MalformedGenerics(
MalformedGenerics::InvalidPathSeparator,
));
}
}
'<' => {
segment.push(chr);

match path.next() {
Some('<') => {
return Err(ResolutionFailure::MalformedGenerics(
MalformedGenerics::TooManyAngleBrackets,
));
}
Some('>') => {
return Err(ResolutionFailure::MalformedGenerics(
MalformedGenerics::EmptyAngleBrackets,
));
}
Some(chr) => {
segment.push(chr);

while let Some(chr) = path.next_if(|c| *c != '>') {
segment.push(chr);
}
}
None => break,
}
}
_ => segment.push(chr),
}
debug!("raw segment: {:?}", segment);
}

if !segment.is_empty() {
let stripped_segment = strip_generics_from_path_segment(segment)?;
if !stripped_segment.is_empty() {
stripped_segments.push(stripped_segment);
}
}

debug!("path_str: {:?}\nstripped segments: {:?}", path_str, &stripped_segments);

let stripped_path = stripped_segments.join("::");

if !stripped_path.is_empty() {
Ok(stripped_path)
} else {
Err(ResolutionFailure::MalformedGenerics(MalformedGenerics::MissingType))
}
}

fn strip_generics_from_path_segment(
segment: Vec<char>,
) -> Result<String, ResolutionFailure<'static>> {
let mut stripped_segment = String::new();
let mut param_depth = 0;

let mut latest_generics_chunk = String::new();

for c in segment {
if c == '<' {
param_depth += 1;
latest_generics_chunk.clear();
} else if c == '>' {
param_depth -= 1;
if latest_generics_chunk.contains(" as ") {
// The segment tries to use fully-qualified syntax, which is currently unsupported.
camelid marked this conversation as resolved.
Show resolved Hide resolved
// Give a helpful error message instead of completely ignoring the angle brackets.
return Err(ResolutionFailure::MalformedGenerics(
MalformedGenerics::HasFullyQualifiedSyntax,
));
}
} else {
if param_depth == 0 {
stripped_segment.push(c);
} else {
latest_generics_chunk.push(c);
}
}
}

if param_depth == 0 {
Ok(stripped_segment)
} else {
// The segment has unbalanced angle brackets, e.g. `Vec<T` or `Vec<T>>`
Err(ResolutionFailure::MalformedGenerics(MalformedGenerics::UnbalancedAngleBrackets))
}
}
2 changes: 1 addition & 1 deletion src/test/rustdoc-ui/intra-link-errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//~^ NOTE lint level is defined

// FIXME: this should say that it was skipped (maybe an allowed by default lint?)
/// [<invalid syntax>]
/// [invalid intra-doc syntax!!]

camelid marked this conversation as resolved.
Show resolved Hide resolved
/// [path::to::nonexistent::module]
//~^ ERROR unresolved link
Expand Down
Loading