1
Fork 0

Resolve documentation links in rustc and store the results in metadata

This commit implements MCP https://github.com/rust-lang/compiler-team/issues/584

It also removes code that is no longer used, and that includes code cloning resolver, so issue #83761 is fixed.
This commit is contained in:
Vadim Petrochenkov 2022-02-01 20:30:32 +08:00
parent a12d31d5a6
commit b62b82aef4
28 changed files with 653 additions and 853 deletions

View file

@ -7,6 +7,7 @@ edition = "2021"
[dependencies]
bitflags = "1.2.1"
pulldown-cmark = { version = "0.9.2", default-features = false }
rustc_arena = { path = "../rustc_arena" }
rustc_ast = { path = "../rustc_ast" }
rustc_ast_pretty = { path = "../rustc_ast_pretty" }

View file

@ -95,7 +95,7 @@ impl<'a> Resolver<'a> {
/// Reachable macros with block module parents exist due to `#[macro_export] macro_rules!`,
/// but they cannot use def-site hygiene, so the assumption holds
/// (<https://github.com/rust-lang/rust/pull/77984#issuecomment-712445508>).
pub fn get_nearest_non_block_module(&mut self, mut def_id: DefId) -> Module<'a> {
pub(crate) fn get_nearest_non_block_module(&mut self, mut def_id: DefId) -> Module<'a> {
loop {
match self.get_module(def_id) {
Some(module) => return module,
@ -104,7 +104,7 @@ impl<'a> Resolver<'a> {
}
}
pub fn expect_module(&mut self, def_id: DefId) -> Module<'a> {
pub(crate) fn expect_module(&mut self, def_id: DefId) -> Module<'a> {
self.get_module(def_id).expect("argument `DefId` is not a module")
}

View file

@ -8,7 +8,7 @@
use RibKind::*;
use crate::{path_names_to_string, BindingError, Finalize, LexicalScopeBinding};
use crate::{path_names_to_string, rustdoc, BindingError, Finalize, LexicalScopeBinding};
use crate::{Module, ModuleOrUniformRoot, NameBinding, ParentScope, PathResult};
use crate::{ResolutionError, Resolver, Segment, UseError};
@ -24,9 +24,10 @@ use rustc_hir::{BindingAnnotation, PrimTy, TraitCandidate};
use rustc_middle::middle::resolve_lifetime::Set1;
use rustc_middle::ty::DefIdTree;
use rustc_middle::{bug, span_bug};
use rustc_session::config::CrateType;
use rustc_session::lint;
use rustc_span::symbol::{kw, sym, Ident, Symbol};
use rustc_span::{BytePos, Span};
use rustc_span::{BytePos, Span, SyntaxContext};
use smallvec::{smallvec, SmallVec};
use rustc_span::source_map::{respan, Spanned};
@ -620,7 +621,9 @@ impl<'a: 'ast, 'ast> Visitor<'ast> for LateResolutionVisitor<'a, '_, 'ast> {
self.resolve_arm(arm);
}
fn visit_block(&mut self, block: &'ast Block) {
let old_macro_rules = self.parent_scope.macro_rules;
self.resolve_block(block);
self.parent_scope.macro_rules = old_macro_rules;
}
fn visit_anon_const(&mut self, constant: &'ast AnonConst) {
// We deal with repeat expressions explicitly in `resolve_expr`.
@ -771,6 +774,7 @@ impl<'a: 'ast, 'ast> Visitor<'ast> for LateResolutionVisitor<'a, '_, 'ast> {
);
}
fn visit_foreign_item(&mut self, foreign_item: &'ast ForeignItem) {
self.resolve_doc_links(&foreign_item.attrs);
match foreign_item.kind {
ForeignItemKind::TyAlias(box TyAlias { ref generics, .. }) => {
self.with_generic_param_rib(
@ -1159,6 +1163,16 @@ impl<'a: 'ast, 'ast> Visitor<'ast> for LateResolutionVisitor<'a, '_, 'ast> {
})
});
}
fn visit_variant(&mut self, v: &'ast Variant) {
self.resolve_doc_links(&v.attrs);
visit::walk_variant(self, v)
}
fn visit_field_def(&mut self, f: &'ast FieldDef) {
self.resolve_doc_links(&f.attrs);
visit::walk_field_def(self, f)
}
}
impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
@ -2185,6 +2199,8 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
}
fn resolve_item(&mut self, item: &'ast Item) {
self.resolve_doc_links(&item.attrs);
let name = item.ident.name;
debug!("(resolving item) resolving {} ({:?})", name, item.kind);
@ -2274,9 +2290,18 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
);
}
ItemKind::Mod(..) | ItemKind::ForeignMod(_) => {
ItemKind::Mod(..) => {
self.with_scope(item.id, |this| {
this.resolve_doc_links(&item.attrs);
let old_macro_rules = this.parent_scope.macro_rules;
visit::walk_item(this, item);
// Maintain macro_rules scopes in the same way as during early resolution
// for diagnostics and doc links.
if item.attrs.iter().all(|attr| {
!attr.has_name(sym::macro_use) && !attr.has_name(sym::macro_escape)
}) {
this.parent_scope.macro_rules = old_macro_rules;
}
});
}
@ -2309,14 +2334,22 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
self.future_proof_import(use_tree);
}
ItemKind::ExternCrate(..) | ItemKind::MacroDef(..) => {
// do nothing, these are just around to be encoded
ItemKind::MacroDef(ref macro_def) => {
// Maintain macro_rules scopes in the same way as during early resolution
// for diagnostics and doc links.
if macro_def.macro_rules {
let (macro_rules_scope, _) =
self.r.macro_rules_scope(self.r.local_def_id(item.id));
self.parent_scope.macro_rules = macro_rules_scope;
}
}
ItemKind::GlobalAsm(_) => {
ItemKind::ForeignMod(_) | ItemKind::GlobalAsm(_) => {
visit::walk_item(self, item);
}
ItemKind::ExternCrate(..) => {}
ItemKind::MacCall(_) => panic!("unexpanded macro in resolve!"),
}
}
@ -2544,6 +2577,7 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
};
for item in trait_items {
self.resolve_doc_links(&item.attrs);
match &item.kind {
AssocItemKind::Const(_, ty, default) => {
self.visit_ty(ty);
@ -2714,6 +2748,7 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
seen_trait_items: &mut FxHashMap<DefId, Span>,
) {
use crate::ResolutionError::*;
self.resolve_doc_links(&item.attrs);
match &item.kind {
AssocItemKind::Const(_, ty, default) => {
debug!("resolve_implementation AssocItemKind::Const");
@ -4116,6 +4151,86 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
self.r.extra_lifetime_params_map.insert(async_node_id, extra_lifetime_params);
}
}
fn resolve_and_cache_rustdoc_path(&mut self, path_str: &str, ns: Namespace) -> bool {
// FIXME: This caching may be incorrect in case of multiple `macro_rules`
// items with the same name in the same module.
// Also hygiene is not considered.
let mut doc_link_resolutions = std::mem::take(&mut self.r.doc_link_resolutions);
let res = doc_link_resolutions
.entry(self.parent_scope.module.nearest_parent_mod().expect_local())
.or_default()
.entry((Symbol::intern(path_str), ns))
.or_insert_with_key(|(path, ns)| {
let res = self.r.resolve_rustdoc_path(path.as_str(), *ns, self.parent_scope);
if let Some(res) = res
&& let Some(def_id) = res.opt_def_id()
&& !def_id.is_local()
&& self.r.session.crate_types().contains(&CrateType::ProcMacro) {
// Encoding foreign def ids in proc macro crate metadata will ICE.
return None;
}
res
})
.is_some();
self.r.doc_link_resolutions = doc_link_resolutions;
res
}
fn resolve_doc_links(&mut self, attrs: &[Attribute]) {
if !attrs.iter().any(|attr| attr.may_have_doc_links()) {
return;
}
let mut need_traits_in_scope = false;
for path_str in rustdoc::attrs_to_preprocessed_links(attrs) {
// Resolve all namespaces due to no disambiguator or for diagnostics.
let mut any_resolved = false;
let mut need_assoc = false;
for ns in [TypeNS, ValueNS, MacroNS] {
if self.resolve_and_cache_rustdoc_path(&path_str, ns) {
any_resolved = true;
} else if ns != MacroNS {
need_assoc = true;
}
}
// Resolve all prefixes for type-relative resolution or for diagnostics.
if need_assoc || !any_resolved {
let mut path = &path_str[..];
while let Some(idx) = path.rfind("::") {
path = &path[..idx];
need_traits_in_scope = true;
for ns in [TypeNS, ValueNS, MacroNS] {
self.resolve_and_cache_rustdoc_path(path, ns);
}
}
}
}
if need_traits_in_scope {
// FIXME: hygiene is not considered.
let mut doc_link_traits_in_scope = std::mem::take(&mut self.r.doc_link_traits_in_scope);
doc_link_traits_in_scope
.entry(self.parent_scope.module.nearest_parent_mod().expect_local())
.or_insert_with(|| {
self.r
.traits_in_scope(None, &self.parent_scope, SyntaxContext::root(), None)
.into_iter()
.filter_map(|tr| {
if !tr.def_id.is_local()
&& self.r.session.crate_types().contains(&CrateType::ProcMacro)
{
// Encoding foreign def ids in proc macro crate metadata will ICE.
return None;
}
Some(tr.def_id)
})
.collect()
});
self.r.doc_link_traits_in_scope = doc_link_traits_in_scope;
}
}
}
struct LifetimeCountVisitor<'a, 'b> {
@ -4162,6 +4277,7 @@ impl<'a> Resolver<'a> {
pub(crate) fn late_resolve_crate(&mut self, krate: &Crate) {
visit::walk_crate(&mut LifetimeCountVisitor { r: self }, krate);
let mut late_resolution_visitor = LateResolutionVisitor::new(self);
late_resolution_visitor.resolve_doc_links(&krate.attrs);
visit::walk_crate(&mut late_resolution_visitor, krate);
for (id, span) in late_resolution_visitor.diagnostic_metadata.unused_labels.iter() {
self.lint_buffer.buffer_lint(lint::builtin::UNUSED_LABELS, *id, *span, "unused label");

View file

@ -33,7 +33,7 @@ use rustc_data_structures::sync::{Lrc, RwLock};
use rustc_errors::{Applicability, DiagnosticBuilder, ErrorGuaranteed};
use rustc_expand::base::{DeriveResolutions, SyntaxExtension, SyntaxExtensionKind};
use rustc_hir::def::Namespace::*;
use rustc_hir::def::{self, CtorOf, DefKind, LifetimeRes, PartialRes};
use rustc_hir::def::{self, CtorOf, DefKind, DocLinkResMap, LifetimeRes, PartialRes};
use rustc_hir::def_id::{CrateNum, DefId, DefIdMap, LocalDefId};
use rustc_hir::def_id::{CRATE_DEF_ID, LOCAL_CRATE};
use rustc_hir::definitions::{DefPathData, Definitions};
@ -78,6 +78,7 @@ mod ident;
mod imports;
mod late;
mod macros;
pub mod rustdoc;
enum Weak {
Yes,
@ -138,17 +139,17 @@ enum ScopeSet<'a> {
/// This struct is currently used only for early resolution (imports and macros),
/// but not for late resolution yet.
#[derive(Clone, Copy, Debug)]
pub struct ParentScope<'a> {
pub module: Module<'a>,
struct ParentScope<'a> {
module: Module<'a>,
expansion: LocalExpnId,
pub macro_rules: MacroRulesScopeRef<'a>,
macro_rules: MacroRulesScopeRef<'a>,
derives: &'a [ast::Path],
}
impl<'a> ParentScope<'a> {
/// Creates a parent scope with the passed argument used as the module scope component,
/// and other scope components set to default empty values.
pub fn module(module: Module<'a>, resolver: &Resolver<'a>) -> ParentScope<'a> {
fn module(module: Module<'a>, resolver: &Resolver<'a>) -> ParentScope<'a> {
ParentScope {
module,
expansion: LocalExpnId::ROOT,
@ -1046,6 +1047,8 @@ pub struct Resolver<'a> {
lifetime_elision_allowed: FxHashSet<NodeId>,
effective_visibilities: EffectiveVisibilities,
doc_link_resolutions: FxHashMap<LocalDefId, DocLinkResMap>,
doc_link_traits_in_scope: FxHashMap<LocalDefId, Vec<DefId>>,
}
/// Nothing really interesting here; it just provides memory for the rest of the crate.
@ -1374,6 +1377,8 @@ impl<'a> Resolver<'a> {
confused_type_with_std_module: Default::default(),
lifetime_elision_allowed: Default::default(),
effective_visibilities: Default::default(),
doc_link_resolutions: Default::default(),
doc_link_traits_in_scope: Default::default(),
};
let root_parent_scope = ParentScope::module(graph_root, &resolver);
@ -1450,6 +1455,8 @@ impl<'a> Resolver<'a> {
proc_macros,
confused_type_with_std_module,
registered_tools: self.registered_tools,
doc_link_resolutions: self.doc_link_resolutions,
doc_link_traits_in_scope: self.doc_link_traits_in_scope,
};
let ast_lowering = ty::ResolverAstLowering {
legacy_const_generic_args: self.legacy_const_generic_args,
@ -1494,6 +1501,8 @@ impl<'a> Resolver<'a> {
confused_type_with_std_module: self.confused_type_with_std_module.clone(),
registered_tools: self.registered_tools.clone(),
effective_visibilities: self.effective_visibilities.clone(),
doc_link_resolutions: self.doc_link_resolutions.clone(),
doc_link_traits_in_scope: self.doc_link_traits_in_scope.clone(),
};
let ast_lowering = ty::ResolverAstLowering {
legacy_const_generic_args: self.legacy_const_generic_args.clone(),
@ -1575,7 +1584,7 @@ impl<'a> Resolver<'a> {
});
}
pub fn traits_in_scope(
fn traits_in_scope(
&mut self,
current_trait: Option<Module<'a>>,
parent_scope: &ParentScope<'a>,
@ -1927,7 +1936,7 @@ impl<'a> Resolver<'a> {
/// isn't something that can be returned because it can't be made to live that long,
/// and also it's a private type. Fortunately rustdoc doesn't need to know the error,
/// just that an error occurred.
pub fn resolve_rustdoc_path(
fn resolve_rustdoc_path(
&mut self,
path_str: &str,
ns: Namespace,
@ -1959,16 +1968,6 @@ impl<'a> Resolver<'a> {
}
}
/// For rustdoc.
/// For local modules returns only reexports, for external modules returns all children.
pub fn module_children_or_reexports(&self, def_id: DefId) -> Vec<ModChild> {
if let Some(def_id) = def_id.as_local() {
self.reexport_map.get(&def_id).cloned().unwrap_or_default()
} else {
self.cstore().module_children_untracked(def_id, self.session).collect()
}
}
/// For rustdoc.
pub fn macro_rules_scope(&self, def_id: LocalDefId) -> (MacroRulesScopeRef<'a>, Res) {
let scope = *self.macro_rules_scopes.get(&def_id).expect("not a `macro_rules` item");
@ -1978,11 +1977,6 @@ impl<'a> Resolver<'a> {
}
}
/// For rustdoc.
pub fn get_partial_res(&self, node_id: NodeId) -> Option<PartialRes> {
self.partial_res_map.get(&node_id).copied()
}
/// Retrieves the span of the given `DefId` if `DefId` is in the local crate.
#[inline]
pub fn opt_span(&self, def_id: DefId) -> Option<Span> {

View file

@ -568,7 +568,7 @@ impl<'a> Resolver<'a> {
Ok((ext, res))
}
pub fn resolve_macro_path(
pub(crate) fn resolve_macro_path(
&mut self,
path: &ast::Path,
kind: Option<MacroKind>,

View file

@ -0,0 +1,361 @@
use pulldown_cmark::{BrokenLink, Event, Options, Parser, Tag};
use rustc_ast as ast;
use rustc_ast::util::comments::beautify_doc_string;
use rustc_data_structures::fx::FxHashMap;
use rustc_span::def_id::DefId;
use rustc_span::symbol::{kw, Symbol};
use rustc_span::Span;
use std::cell::RefCell;
use std::{cmp, mem};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DocFragmentKind {
/// A doc fragment created from a `///` or `//!` doc comment.
SugaredDoc,
/// A doc fragment created from a "raw" `#[doc=""]` attribute.
RawDoc,
}
/// A portion of documentation, extracted from a `#[doc]` attribute.
///
/// Each variant contains the line number within the complete doc-comment where the fragment
/// starts, as well as the Span where the corresponding doc comment or attribute is located.
///
/// Included files are kept separate from inline doc comments so that proper line-number
/// information can be given when a doctest fails. Sugared doc comments and "raw" doc comments are
/// kept separate because of issue #42760.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct DocFragment {
pub span: Span,
/// The module this doc-comment came from.
///
/// This allows distinguishing between the original documentation and a pub re-export.
/// If it is `None`, the item was not re-exported.
pub parent_module: Option<DefId>,
pub doc: Symbol,
pub kind: DocFragmentKind,
pub indent: usize,
}
#[derive(Clone, Copy, Debug)]
pub 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,
}
/// Removes excess indentation on comments in order for the Markdown
/// to be parsed correctly. This is necessary because the convention for
/// writing documentation is to provide a space between the /// or //! marker
/// and the doc text, but Markdown is whitespace-sensitive. For example,
/// a block of text with four-space indentation is parsed as a code block,
/// so if we didn't unindent comments, these list items
///
/// /// A list:
/// ///
/// /// - Foo
/// /// - Bar
///
/// would be parsed as if they were in a code block, which is likely not what the user intended.
pub fn unindent_doc_fragments(docs: &mut [DocFragment]) {
// `add` is used in case the most common sugared doc syntax is used ("/// "). The other
// fragments kind's lines are never starting with a whitespace unless they are using some
// markdown formatting requiring it. Therefore, if the doc block have a mix between the two,
// we need to take into account the fact that the minimum indent minus one (to take this
// whitespace into account).
//
// For example:
//
// /// hello!
// #[doc = "another"]
//
// In this case, you want "hello! another" and not "hello! another".
let add = if docs.windows(2).any(|arr| arr[0].kind != arr[1].kind)
&& docs.iter().any(|d| d.kind == DocFragmentKind::SugaredDoc)
{
// In case we have a mix of sugared doc comments and "raw" ones, we want the sugared one to
// "decide" how much the minimum indent will be.
1
} else {
0
};
// `min_indent` is used to know how much whitespaces from the start of each lines must be
// removed. Example:
//
// /// hello!
// #[doc = "another"]
//
// In here, the `min_indent` is 1 (because non-sugared fragment are always counted with minimum
// 1 whitespace), meaning that "hello!" will be considered a codeblock because it starts with 4
// (5 - 1) whitespaces.
let Some(min_indent) = docs
.iter()
.map(|fragment| {
fragment.doc.as_str().lines().fold(usize::MAX, |min_indent, line| {
if line.chars().all(|c| c.is_whitespace()) {
min_indent
} else {
// Compare against either space or tab, ignoring whether they are
// mixed or not.
let whitespace = line.chars().take_while(|c| *c == ' ' || *c == '\t').count();
cmp::min(min_indent, whitespace)
+ if fragment.kind == DocFragmentKind::SugaredDoc { 0 } else { add }
}
})
})
.min()
else {
return;
};
for fragment in docs {
if fragment.doc == kw::Empty {
continue;
}
let min_indent = if fragment.kind != DocFragmentKind::SugaredDoc && min_indent > 0 {
min_indent - add
} else {
min_indent
};
fragment.indent = min_indent;
}
}
/// The goal of this function is to apply the `DocFragment` transformation that is required when
/// transforming into the final Markdown, which is applying the computed indent to each line in
/// each doc fragment (a `DocFragment` can contain multiple lines in case of `#[doc = ""]`).
///
/// Note: remove the trailing newline where appropriate
pub fn add_doc_fragment(out: &mut String, frag: &DocFragment) {
let s = frag.doc.as_str();
let mut iter = s.lines();
if s.is_empty() {
out.push('\n');
return;
}
while let Some(line) = iter.next() {
if line.chars().any(|c| !c.is_whitespace()) {
assert!(line.len() >= frag.indent);
out.push_str(&line[frag.indent..]);
} else {
out.push_str(line);
}
out.push('\n');
}
}
pub fn attrs_to_doc_fragments<'a>(
attrs: impl Iterator<Item = (&'a ast::Attribute, Option<DefId>)>,
doc_only: bool,
) -> (Vec<DocFragment>, ast::AttrVec) {
let mut doc_fragments = Vec::new();
let mut other_attrs = ast::AttrVec::new();
for (attr, parent_module) in attrs {
if let Some((doc_str, comment_kind)) = attr.doc_str_and_comment_kind() {
let doc = beautify_doc_string(doc_str, comment_kind);
let kind = if attr.is_doc_comment() {
DocFragmentKind::SugaredDoc
} else {
DocFragmentKind::RawDoc
};
let fragment = DocFragment { span: attr.span, doc, kind, parent_module, indent: 0 };
doc_fragments.push(fragment);
} else if !doc_only {
other_attrs.push(attr.clone());
}
}
unindent_doc_fragments(&mut doc_fragments);
(doc_fragments, other_attrs)
}
/// Return the doc-comments on this item, grouped by the module they came from.
/// The module can be different if this is a re-export with added documentation.
///
/// The last newline is not trimmed so the produced strings are reusable between
/// early and late doc link resolution regardless of their position.
pub fn prepare_to_doc_link_resolution(
doc_fragments: &[DocFragment],
) -> FxHashMap<Option<DefId>, String> {
let mut res = FxHashMap::default();
for fragment in doc_fragments {
let out_str = res.entry(fragment.parent_module).or_default();
add_doc_fragment(out_str, fragment);
}
res
}
/// Options for rendering Markdown in the main body of documentation.
pub fn main_body_opts() -> Options {
Options::ENABLE_TABLES
| Options::ENABLE_FOOTNOTES
| Options::ENABLE_STRIKETHROUGH
| Options::ENABLE_TASKLISTS
| Options::ENABLE_SMART_PUNCTUATION
}
fn strip_generics_from_path_segment(segment: Vec<char>) -> Result<String, MalformedGenerics> {
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.
// Give a helpful error message instead of completely ignoring the angle brackets.
return Err(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(MalformedGenerics::UnbalancedAngleBrackets)
}
}
pub fn strip_generics_from_path(path_str: &str) -> Result<String, MalformedGenerics> {
if !path_str.contains(['<', '>']) {
return Ok(path_str.to_string());
}
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(MalformedGenerics::InvalidPathSeparator);
}
}
'<' => {
segment.push(chr);
match path.next() {
Some('<') => {
return Err(MalformedGenerics::TooManyAngleBrackets);
}
Some('>') => {
return Err(MalformedGenerics::EmptyAngleBrackets);
}
Some(chr) => {
segment.push(chr);
while let Some(chr) = path.next_if(|c| *c != '>') {
segment.push(chr);
}
}
None => break,
}
}
_ => segment.push(chr),
}
trace!("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(MalformedGenerics::MissingType) }
}
/// Simplified version of the corresponding function in rustdoc.
/// If the rustdoc version returns a successful result, this function must return the same result.
/// Otherwise this function may return anything.
fn preprocess_link(link: &str) -> String {
let link = link.replace('`', "");
let link = link.split('#').next().unwrap();
let link = link.rsplit('@').next().unwrap();
let link = link.strip_suffix("()").unwrap_or(link);
let link = link.strip_suffix("{}").unwrap_or(link);
let link = link.strip_suffix("[]").unwrap_or(link);
let link = if link != "!" { link.strip_suffix("!").unwrap_or(link) } else { link };
strip_generics_from_path(link).unwrap_or_else(|_| link.to_string())
}
/// Simplified version of `preprocessed_markdown_links` from rustdoc.
/// Must return at least the same links as it, but may add some more links on top of that.
pub(crate) fn attrs_to_preprocessed_links(attrs: &[ast::Attribute]) -> Vec<String> {
let (doc_fragments, _) = attrs_to_doc_fragments(attrs.iter().map(|attr| (attr, None)), true);
let doc = prepare_to_doc_link_resolution(&doc_fragments).into_values().next().unwrap();
let links = RefCell::new(Vec::new());
let mut callback = |link: BrokenLink<'_>| {
links.borrow_mut().push(preprocess_link(&link.reference));
None
};
for event in Parser::new_with_broken_link_callback(&doc, main_body_opts(), Some(&mut callback))
{
if let Event::Start(Tag::Link(_, dest, _)) = event {
links.borrow_mut().push(preprocess_link(&dest));
}
}
links.into_inner()
}