Skip to content

Commit 4c765f6

Browse files
committed
Allow generic parameters in intra-doc links
The contents of the generics will be mostly ignored (except for warning if fully-qualified syntax is used, which is currently unsupported in intra-doc links - see issue rust-lang#74563). * Allow links like `Vec<T>`, `Result<T, E>`, and `Option<Box<T>>` * Allow links like `Vec::<T>::new()` * Warn on * Unbalanced angle brackets (e.g. `Vec<T` or `Vec<T>>`) * Missing type to apply generics to (`<T>` or `<Box<T>>`) * Use of fully-qualified syntax (`<Vec as IntoIterator>::into_iter`) * Invalid path separator (`Vec:<T>:new`) * Too many angle brackets (`Vec<<T>>`) * Empty angle brackets (`Vec<>`) Note that this implementation *does* allow some constructs that aren't valid in the actual Rust syntax, for example `Box::<T>new()`. That may not be supported in rustdoc in the future; it is an implementation detail.
1 parent 9ba1d21 commit 4c765f6

6 files changed

+381
-2
lines changed

src/librustdoc/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
html_playground_url = "https://play.rust-lang.org/"
44
)]
55
#![feature(rustc_private)]
6+
#![feature(array_methods)]
67
#![feature(box_patterns)]
78
#![feature(box_syntax)]
89
#![feature(in_band_lifetimes)]
910
#![feature(nll)]
1011
#![feature(or_patterns)]
12+
#![feature(peekable_next_if)]
1113
#![feature(test)]
1214
#![feature(crate_visibility_modifier)]
1315
#![feature(never_type)]

src/librustdoc/passes/collect_intra_doc_links.rs

+202-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use smallvec::{smallvec, SmallVec};
2323

2424
use std::borrow::Cow;
2525
use std::cell::Cell;
26+
use std::mem;
2627
use std::ops::Range;
2728

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

75+
#[derive(Debug)]
76+
enum MalformedGenerics {
77+
/// This link has unbalanced angle brackets.
78+
///
79+
/// For example, `Vec<T` should trigger this, as should `Vec<T>>`.
80+
UnbalancedAngleBrackets,
81+
/// The generics are not attached to a type.
82+
///
83+
/// For example, `<T>` should trigger this.
84+
///
85+
/// This is detected by checking if the path is empty after the generics are stripped.
86+
MissingType,
87+
/// The link uses fully-qualified syntax, which is currently unsupported.
88+
///
89+
/// For example, `<Vec as IntoIterator>::into_iter` should trigger this.
90+
///
91+
/// This is detected by checking if ` as ` (the keyword `as` with spaces around it) is inside
92+
/// angle brackets.
93+
HasFullyQualifiedSyntax,
94+
/// The link has an invalid path separator.
95+
///
96+
/// For example, `Vec:<T>:new()` should trigger this. Note that `Vec:new()` will **not**
97+
/// trigger this because it has no generics and thus [`strip_generics_from_path`] will not be
98+
/// called.
99+
///
100+
/// Note that this will also **not** be triggered if the invalid path separator is inside angle
101+
/// brackets because rustdoc mostly ignores what's inside angle brackets (except for
102+
/// [`HasFullyQualifiedSyntax`](MalformedGenerics::HasFullyQualifiedSyntax)).
103+
///
104+
/// This is detected by checking if there is a colon followed by a non-colon in the link.
105+
InvalidPathSeparator,
106+
/// The link has too many angle brackets.
107+
///
108+
/// For example, `Vec<<T>>` should trigger this.
109+
TooManyAngleBrackets,
110+
/// The link has empty angle brackets.
111+
///
112+
/// For example, `Vec<>` should trigger this.
113+
EmptyAngleBrackets,
114+
}
115+
72116
impl ResolutionFailure<'a> {
73117
// This resolved fully (not just partially) but is erroneous for some other reason
74118
fn full_res(&self) -> Option<Res> {
@@ -912,6 +956,7 @@ impl LinkCollector<'_, '_> {
912956
let link_text;
913957
let mut path_str;
914958
let disambiguator;
959+
let stripped_path_string;
915960
let (mut res, mut fragment) = {
916961
path_str = if let Ok((d, path)) = Disambiguator::from_str(&link) {
917962
disambiguator = Some(d);
@@ -922,7 +967,7 @@ impl LinkCollector<'_, '_> {
922967
}
923968
.trim();
924969

925-
if path_str.contains(|ch: char| !(ch.is_alphanumeric() || ch == ':' || ch == '_')) {
970+
if path_str.contains(|ch: char| !(ch.is_alphanumeric() || ":_<>, ".contains(ch))) {
926971
return None;
927972
}
928973

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

1033+
// Strip generics from the path.
1034+
if path_str.contains(['<', '>'].as_slice()) {
1035+
stripped_path_string = match strip_generics_from_path(path_str) {
1036+
Ok(path) => path,
1037+
Err(err_kind) => {
1038+
debug!("link has malformed generics: {}", path_str);
1039+
resolution_failure(
1040+
self,
1041+
&item,
1042+
path_str,
1043+
disambiguator,
1044+
dox,
1045+
link_range,
1046+
smallvec![err_kind],
1047+
);
1048+
return None;
1049+
}
1050+
};
1051+
path_str = &stripped_path_string;
1052+
}
1053+
1054+
// Sanity check to make sure we don't have any angle brackets after stripping generics.
1055+
assert!(!path_str.contains(['<', '>'].as_slice()));
1056+
1057+
// The link is not an intra-doc link if it still contains commas or spaces after
1058+
// stripping generics.
1059+
if path_str.contains([',', ' '].as_slice()) {
1060+
return None;
1061+
}
1062+
9881063
match self.resolve_with_disambiguator(
9891064
disambiguator,
9901065
item,
@@ -1718,6 +1793,27 @@ fn resolution_failure(
17181793
diag.level = rustc_errors::Level::Bug;
17191794
"all intra doc links should have a parent item".to_owned()
17201795
}
1796+
ResolutionFailure::MalformedGenerics(variant) => match variant {
1797+
MalformedGenerics::UnbalancedAngleBrackets => {
1798+
String::from("unbalanced angle brackets")
1799+
}
1800+
MalformedGenerics::MissingType => {
1801+
String::from("missing type for generic parameters")
1802+
}
1803+
MalformedGenerics::HasFullyQualifiedSyntax => {
1804+
diag.note("see https://github.com/rust-lang/rust/issues/74563 for more information");
1805+
String::from("fully-qualified syntax is unsupported")
1806+
}
1807+
MalformedGenerics::InvalidPathSeparator => {
1808+
String::from("has invalid path separator")
1809+
}
1810+
MalformedGenerics::TooManyAngleBrackets => {
1811+
String::from("too many angle brackets")
1812+
}
1813+
MalformedGenerics::EmptyAngleBrackets => {
1814+
String::from("empty angle brackets")
1815+
}
1816+
},
17211817
};
17221818
if let Some(span) = sp {
17231819
diag.span_label(span, &note);
@@ -1908,3 +2004,108 @@ fn is_primitive(path_str: &str, ns: Namespace) -> Option<(&'static str, Res)> {
19082004
fn primitive_impl(cx: &DocContext<'_>, path_str: &str) -> Option<&'static SmallVec<[DefId; 4]>> {
19092005
Some(PrimitiveType::from_symbol(Symbol::intern(path_str))?.impls(cx.tcx))
19102006
}
2007+
2008+
fn strip_generics_from_path(path_str: &str) -> Result<String, ResolutionFailure<'static>> {
2009+
let mut stripped_segments = vec![];
2010+
let mut path = path_str.chars().peekable();
2011+
let mut segment = Vec::new();
2012+
2013+
while let Some(chr) = path.next() {
2014+
match chr {
2015+
':' => {
2016+
if path.next_if_eq(&':').is_some() {
2017+
let stripped_segment =
2018+
strip_generics_from_path_segment(mem::take(&mut segment))?;
2019+
if !stripped_segment.is_empty() {
2020+
stripped_segments.push(stripped_segment);
2021+
}
2022+
} else {
2023+
return Err(ResolutionFailure::MalformedGenerics(
2024+
MalformedGenerics::InvalidPathSeparator,
2025+
));
2026+
}
2027+
}
2028+
'<' => {
2029+
segment.push(chr);
2030+
2031+
match path.peek() {
2032+
Some('<') => {
2033+
return Err(ResolutionFailure::MalformedGenerics(
2034+
MalformedGenerics::TooManyAngleBrackets,
2035+
));
2036+
}
2037+
Some('>') => {
2038+
return Err(ResolutionFailure::MalformedGenerics(
2039+
MalformedGenerics::EmptyAngleBrackets,
2040+
));
2041+
}
2042+
Some(_) => {
2043+
segment.push(path.next().unwrap());
2044+
2045+
while let Some(chr) = path.next_if(|c| *c != '>') {
2046+
segment.push(chr);
2047+
}
2048+
}
2049+
None => break,
2050+
}
2051+
}
2052+
_ => segment.push(chr),
2053+
}
2054+
debug!("raw segment: {:?}", segment);
2055+
}
2056+
2057+
if !segment.is_empty() {
2058+
let stripped_segment = strip_generics_from_path_segment(segment)?;
2059+
if !stripped_segment.is_empty() {
2060+
stripped_segments.push(stripped_segment);
2061+
}
2062+
}
2063+
2064+
debug!("path_str: {:?}\nstripped segments: {:?}", path_str, &stripped_segments);
2065+
2066+
let stripped_path = stripped_segments.join("::");
2067+
2068+
if !stripped_path.is_empty() {
2069+
Ok(stripped_path)
2070+
} else {
2071+
Err(ResolutionFailure::MalformedGenerics(MalformedGenerics::MissingType))
2072+
}
2073+
}
2074+
2075+
fn strip_generics_from_path_segment(
2076+
segment: Vec<char>,
2077+
) -> Result<String, ResolutionFailure<'static>> {
2078+
let mut stripped_segment = String::new();
2079+
let mut param_depth = 0;
2080+
2081+
let mut latest_generics_chunk = String::new();
2082+
2083+
for c in segment {
2084+
if c == '<' {
2085+
param_depth += 1;
2086+
latest_generics_chunk.clear();
2087+
} else if c == '>' {
2088+
param_depth -= 1;
2089+
if latest_generics_chunk.contains(" as ") {
2090+
// The segment tries to use fully-qualified syntax, which is currently unsupported.
2091+
// Give a helpful error message instead of completely ignoring the angle brackets.
2092+
return Err(ResolutionFailure::MalformedGenerics(
2093+
MalformedGenerics::HasFullyQualifiedSyntax,
2094+
));
2095+
}
2096+
} else {
2097+
if param_depth == 0 {
2098+
stripped_segment.push(c);
2099+
} else {
2100+
latest_generics_chunk.push(c);
2101+
}
2102+
}
2103+
}
2104+
2105+
if param_depth == 0 {
2106+
Ok(stripped_segment)
2107+
} else {
2108+
// The segment has unbalanced angle brackets, e.g. `Vec<T` or `Vec<T>>`
2109+
Err(ResolutionFailure::MalformedGenerics(MalformedGenerics::UnbalancedAngleBrackets))
2110+
}
2111+
}

src/test/rustdoc-ui/intra-link-errors.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//~^ NOTE lint level is defined
33

44
// FIXME: this should say that it was skipped (maybe an allowed by default lint?)
5-
/// [<invalid syntax>]
5+
/// [invalid intra-doc syntax!!]
66
77
/// [path::to::nonexistent::module]
88
//~^ ERROR unresolved link
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#![deny(broken_intra_doc_links)]
2+
3+
//! [Vec<] //~ ERROR
4+
//! [Vec<Box<T] //~ ERROR
5+
//! [Vec<Box<T>] //~ ERROR
6+
//! [Vec<Box<T>>>] //~ ERROR
7+
//! [Vec<T>>>] //~ ERROR
8+
//! [<Vec] //~ ERROR
9+
//! [Vec::<] //~ ERROR
10+
//! [<T>] //~ ERROR
11+
//! [<invalid syntax>] //~ ERROR
12+
//! [Vec:<T>:new()] //~ ERROR
13+
//! [Vec<<T>>] //~ ERROR
14+
//! [Vec<>] //~ ERROR
15+
//! [Vec<<>>] //~ ERROR
16+
17+
// FIXME(#74563) support UFCS
18+
//! [<Vec as IntoIterator>::into_iter] //~ ERROR
19+
//! [<Vec<T> as IntoIterator>::iter] //~ ERROR
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
error: unresolved link to `Vec<`
2+
--> $DIR/intra-link-malformed-generics.rs:3:6
3+
|
4+
LL | //! [Vec<]
5+
| ^^^^ unbalanced angle brackets
6+
|
7+
note: the lint level is defined here
8+
--> $DIR/intra-link-malformed-generics.rs:1:9
9+
|
10+
LL | #![deny(broken_intra_doc_links)]
11+
| ^^^^^^^^^^^^^^^^^^^^^^
12+
13+
error: unresolved link to `Vec<Box<T`
14+
--> $DIR/intra-link-malformed-generics.rs:4:6
15+
|
16+
LL | //! [Vec<Box<T]
17+
| ^^^^^^^^^ unbalanced angle brackets
18+
19+
error: unresolved link to `Vec<Box<T>`
20+
--> $DIR/intra-link-malformed-generics.rs:5:6
21+
|
22+
LL | //! [Vec<Box<T>]
23+
| ^^^^^^^^^^ unbalanced angle brackets
24+
25+
error: unresolved link to `Vec<Box<T>>>`
26+
--> $DIR/intra-link-malformed-generics.rs:6:6
27+
|
28+
LL | //! [Vec<Box<T>>>]
29+
| ^^^^^^^^^^^^ unbalanced angle brackets
30+
31+
error: unresolved link to `Vec<T>>>`
32+
--> $DIR/intra-link-malformed-generics.rs:7:6
33+
|
34+
LL | //! [Vec<T>>>]
35+
| ^^^^^^^^ unbalanced angle brackets
36+
37+
error: unresolved link to `<Vec`
38+
--> $DIR/intra-link-malformed-generics.rs:8:6
39+
|
40+
LL | //! [<Vec]
41+
| ^^^^ unbalanced angle brackets
42+
43+
error: unresolved link to `Vec::<`
44+
--> $DIR/intra-link-malformed-generics.rs:9:6
45+
|
46+
LL | //! [Vec::<]
47+
| ^^^^^^ unbalanced angle brackets
48+
49+
error: unresolved link to `<T>`
50+
--> $DIR/intra-link-malformed-generics.rs:10:6
51+
|
52+
LL | //! [<T>]
53+
| ^^^ missing type for generic parameters
54+
55+
error: unresolved link to `<invalid syntax>`
56+
--> $DIR/intra-link-malformed-generics.rs:11:6
57+
|
58+
LL | //! [<invalid syntax>]
59+
| ^^^^^^^^^^^^^^^^ missing type for generic parameters
60+
61+
error: unresolved link to `Vec:<T>:new`
62+
--> $DIR/intra-link-malformed-generics.rs:12:6
63+
|
64+
LL | //! [Vec:<T>:new()]
65+
| ^^^^^^^^^^^^^ has invalid path separator
66+
67+
error: unresolved link to `Vec<<T>>`
68+
--> $DIR/intra-link-malformed-generics.rs:13:6
69+
|
70+
LL | //! [Vec<<T>>]
71+
| ^^^^^^^^ too many angle brackets
72+
73+
error: unresolved link to `Vec<>`
74+
--> $DIR/intra-link-malformed-generics.rs:14:6
75+
|
76+
LL | //! [Vec<>]
77+
| ^^^^^ empty angle brackets
78+
79+
error: unresolved link to `Vec<<>>`
80+
--> $DIR/intra-link-malformed-generics.rs:15:6
81+
|
82+
LL | //! [Vec<<>>]
83+
| ^^^^^^^ too many angle brackets
84+
85+
error: unresolved link to `<Vec as IntoIterator>::into_iter`
86+
--> $DIR/intra-link-malformed-generics.rs:18:6
87+
|
88+
LL | //! [<Vec as IntoIterator>::into_iter]
89+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ fully-qualified syntax is unsupported
90+
|
91+
= note: see https://github.com/rust-lang/rust/issues/74563 for more information
92+
93+
error: unresolved link to `<Vec<T> as IntoIterator>::iter`
94+
--> $DIR/intra-link-malformed-generics.rs:19:6
95+
|
96+
LL | //! [<Vec<T> as IntoIterator>::iter]
97+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ fully-qualified syntax is unsupported
98+
|
99+
= note: see https://github.com/rust-lang/rust/issues/74563 for more information
100+
101+
error: aborting due to 15 previous errors
102+

0 commit comments

Comments
 (0)