diff options
Diffstat (limited to 'compiler/rustc_resolve/src')
| -rw-r--r-- | compiler/rustc_resolve/src/build_reduced_graph.rs | 4 | ||||
| -rw-r--r-- | compiler/rustc_resolve/src/late.rs | 191 | ||||
| -rw-r--r-- | compiler/rustc_resolve/src/lib.rs | 38 | ||||
| -rw-r--r-- | compiler/rustc_resolve/src/macros.rs | 2 | ||||
| -rw-r--r-- | compiler/rustc_resolve/src/rustdoc.rs | 369 |
5 files changed, 570 insertions, 34 deletions
diff --git a/compiler/rustc_resolve/src/build_reduced_graph.rs b/compiler/rustc_resolve/src/build_reduced_graph.rs index 2fb62ce53ba..e74bb0a9a4f 100644 --- a/compiler/rustc_resolve/src/build_reduced_graph.rs +++ b/compiler/rustc_resolve/src/build_reduced_graph.rs @@ -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") } diff --git a/compiler/rustc_resolve/src/late.rs b/compiler/rustc_resolve/src/late.rs index b28aee40b16..bd74a010fa3 100644 --- a/compiler/rustc_resolve/src/late.rs +++ b/compiler/rustc_resolve/src/late.rs @@ -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,12 +24,13 @@ 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, ResolveDocLinks}; use rustc_session::lint; +use rustc_span::source_map::{respan, Spanned}; 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}; use std::assert_matches::debug_assert_matches; use std::borrow::Cow; use std::collections::{hash_map::Entry, BTreeSet}; @@ -493,6 +494,30 @@ impl<'a> PathSource<'a> { } } +/// At this point for most items we can answer whether that item is exported or not, +/// but some items like impls require type information to determine exported-ness, so we make a +/// conservative estimate for them (e.g. based on nominal visibility). +#[derive(Clone, Copy)] +enum MaybeExported<'a> { + Ok(NodeId), + Impl(Option<DefId>), + ImplItem(Result<DefId, &'a Visibility>), +} + +impl MaybeExported<'_> { + fn eval(self, r: &Resolver<'_>) -> bool { + let def_id = match self { + MaybeExported::Ok(node_id) => Some(r.local_def_id(node_id)), + MaybeExported::Impl(Some(trait_def_id)) | MaybeExported::ImplItem(Ok(trait_def_id)) => { + trait_def_id.as_local() + } + MaybeExported::Impl(None) => return true, + MaybeExported::ImplItem(Err(vis)) => return vis.kind.is_pub(), + }; + def_id.map_or(true, |def_id| r.effective_visibilities.is_exported(def_id)) + } +} + #[derive(Default)] struct DiagnosticMetadata<'ast> { /// The current trait's associated items' ident, used for diagnostic suggestions. @@ -620,7 +645,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 +798,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, MaybeExported::Ok(foreign_item.id)); match foreign_item.kind { ForeignItemKind::TyAlias(box TyAlias { ref generics, .. }) => { self.with_generic_param_rib( @@ -1159,6 +1187,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, MaybeExported::Ok(v.id)); + visit::walk_variant(self, v) + } + + fn visit_field_def(&mut self, f: &'ast FieldDef) { + self.resolve_doc_links(&f.attrs, MaybeExported::Ok(f.id)); + visit::walk_field_def(self, f) + } } impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> { @@ -2184,6 +2222,12 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> { } fn resolve_item(&mut self, item: &'ast Item) { + let mod_inner_docs = + matches!(item.kind, ItemKind::Mod(..)) && rustdoc::inner_docs(&item.attrs); + if !mod_inner_docs && !matches!(item.kind, ItemKind::Impl(..)) { + self.resolve_doc_links(&item.attrs, MaybeExported::Ok(item.id)); + } + let name = item.ident.name; debug!("(resolving item) resolving {} ({:?})", name, item.kind); @@ -2228,7 +2272,14 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> { .. }) => { self.diagnostic_metadata.current_impl_items = Some(impl_items); - self.resolve_implementation(generics, of_trait, &self_ty, item.id, impl_items); + self.resolve_implementation( + &item.attrs, + generics, + of_trait, + &self_ty, + item.id, + impl_items, + ); self.diagnostic_metadata.current_impl_items = None; } @@ -2273,9 +2324,20 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> { ); } - ItemKind::Mod(..) | ItemKind::ForeignMod(_) => { + ItemKind::Mod(..) => { self.with_scope(item.id, |this| { + if mod_inner_docs { + this.resolve_doc_links(&item.attrs, MaybeExported::Ok(item.id)); + } + 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; + } }); } @@ -2308,14 +2370,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!"), } } @@ -2543,6 +2613,7 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> { }; for item in trait_items { + self.resolve_doc_links(&item.attrs, MaybeExported::Ok(item.id)); match &item.kind { AssocItemKind::Const(_, ty, default) => { self.visit_ty(ty); @@ -2630,6 +2701,7 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> { fn resolve_implementation( &mut self, + attrs: &[ast::Attribute], generics: &'ast Generics, opt_trait_reference: &'ast Option<TraitRef>, self_type: &'ast Ty, @@ -2660,6 +2732,8 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> { opt_trait_reference.as_ref(), self_type, |this, trait_id| { + this.resolve_doc_links(attrs, MaybeExported::Impl(trait_id)); + let item_def_id = this.r.local_def_id(item_id); // Register the trait definitions from here. @@ -2693,7 +2767,7 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> { debug!("resolve_implementation with_self_rib_ns(ValueNS, ...)"); let mut seen_trait_items = Default::default(); for item in impl_items { - this.resolve_impl_item(&**item, &mut seen_trait_items); + this.resolve_impl_item(&**item, &mut seen_trait_items, trait_id); } }); }); @@ -2711,8 +2785,10 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> { &mut self, item: &'ast AssocItem, seen_trait_items: &mut FxHashMap<DefId, Span>, + trait_id: Option<DefId>, ) { use crate::ResolutionError::*; + self.resolve_doc_links(&item.attrs, MaybeExported::ImplItem(trait_id.ok_or(&item.vis))); match &item.kind { AssocItemKind::Const(_, ty, default) => { debug!("resolve_implementation AssocItemKind::Const"); @@ -4115,6 +4191,102 @@ 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], maybe_exported: MaybeExported<'_>) { + match self.r.session.opts.resolve_doc_links { + ResolveDocLinks::None => return, + ResolveDocLinks::ExportedMetadata + if !self.r.session.crate_types().iter().copied().any(CrateType::has_metadata) + || !maybe_exported.eval(self.r) => + { + return; + } + ResolveDocLinks::Exported if !maybe_exported.eval(self.r) => { + return; + } + ResolveDocLinks::ExportedMetadata + | ResolveDocLinks::Exported + | ResolveDocLinks::All => {} + } + + 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> { @@ -4161,6 +4333,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, MaybeExported::Ok(CRATE_NODE_ID)); 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"); diff --git a/compiler/rustc_resolve/src/lib.rs b/compiler/rustc_resolve/src/lib.rs index 1b181b71400..e61e83189c3 100644 --- a/compiler/rustc_resolve/src/lib.rs +++ b/compiler/rustc_resolve/src/lib.rs @@ -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, @@ -1960,16 +1969,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"); match scope.get() { @@ -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> { diff --git a/compiler/rustc_resolve/src/macros.rs b/compiler/rustc_resolve/src/macros.rs index b5b1602c5e0..0c2e8be0498 100644 --- a/compiler/rustc_resolve/src/macros.rs +++ b/compiler/rustc_resolve/src/macros.rs @@ -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>, diff --git a/compiler/rustc_resolve/src/rustdoc.rs b/compiler/rustc_resolve/src/rustdoc.rs new file mode 100644 index 00000000000..a967f4b940c --- /dev/null +++ b/compiler/rustc_resolve/src/rustdoc.rs @@ -0,0 +1,369 @@ +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) } +} + +/// Returns whether the first doc-comment is an inner attribute. +/// +//// If there are no doc-comments, return true. +/// FIXME(#78591): Support both inner and outer attributes on the same item. +pub fn inner_docs(attrs: &[ast::Attribute]) -> bool { + attrs.iter().find(|a| a.doc_str().is_some()).map_or(true, |a| a.style == ast::AttrStyle::Inner) +} + +/// 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() +} |
