1//! This module implements [RFC 1946]: Intra-rustdoc-links2//!3//! [RFC 1946]: https://github.com/rust-lang/rfcs/blob/master/text/1946-intra-rustdoc-links.md45use std::borrow::Cow;6use std::fmt::Display;7use std::mem;8use std::ops::Range;910use rustc_ast::util::comments::may_have_doc_links;11use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexMap, FxIndexSet};12use rustc_data_structures::intern::Interned;13use rustc_errors::{Applicability, Diag, DiagMessage};14use rustc_hir::attrs::AttributeKind;15use rustc_hir::def::Namespace::*;16use rustc_hir::def::{DefKind, MacroKinds, Namespace, PerNS};17use rustc_hir::def_id::{CRATE_DEF_ID, DefId, LOCAL_CRATE};18use rustc_hir::{Attribute, Mutability, Safety, find_attr};19use rustc_middle::ty::{Ty, TyCtxt};20use rustc_middle::{bug, span_bug, ty};21use rustc_resolve::rustdoc::pulldown_cmark::LinkType;22use rustc_resolve::rustdoc::{23 MalformedGenerics, has_primitive_or_keyword_or_attribute_docs, prepare_to_doc_link_resolution,24 source_span_for_markdown_range, strip_generics_from_path,25};26use rustc_session::config::CrateType;27use rustc_session::lint::Lint;28use rustc_span::BytePos;29use rustc_span::symbol::{Ident, Symbol, sym};30use smallvec::{SmallVec, smallvec};31use tracing::{debug, info, instrument, trace};3233use crate::clean::utils::find_nearest_parent_module;34use crate::clean::{self, Crate, Item, ItemId, ItemLink, PrimitiveType};35use crate::core::DocContext;36use crate::html::markdown::{MarkdownLink, MarkdownLinkRange, markdown_links};37use crate::lint::{BROKEN_INTRA_DOC_LINKS, PRIVATE_INTRA_DOC_LINKS};38use crate::passes::Pass;39use crate::visit::DocVisitor;4041pub(crate) const COLLECT_INTRA_DOC_LINKS: Pass =42 Pass { name: "collect-intra-doc-links", run: None, description: "resolves intra-doc links" };4344pub(crate) fn collect_intra_doc_links<'a, 'tcx>(45 krate: Crate,46 cx: &'a mut DocContext<'tcx>,47) -> (Crate, LinkCollector<'a, 'tcx>) {48 let mut collector = LinkCollector {49 cx,50 visited_links: FxHashMap::default(),51 ambiguous_links: FxIndexMap::default(),52 };53 collector.visit_crate(&krate);54 (krate, collector)55}5657fn filter_assoc_items_by_name_and_namespace(58 tcx: TyCtxt<'_>,59 assoc_items_of: DefId,60 ident: Ident,61 ns: Namespace,62) -> impl Iterator<Item = &ty::AssocItem> {63 tcx.associated_items(assoc_items_of).filter_by_name_unhygienic(ident.name).filter(move |item| {64 item.namespace() == ns && tcx.hygienic_eq(ident, item.ident(tcx), assoc_items_of)65 })66}6768#[derive(Copy, Clone, Debug, Hash, PartialEq)]69pub(crate) enum Res {70 Def(DefKind, DefId),71 Primitive(PrimitiveType),72}7374type ResolveRes = rustc_hir::def::Res<rustc_ast::NodeId>;7576impl Res {77 fn descr(self) -> &'static str {78 match self {79 Res::Def(kind, id) => ResolveRes::Def(kind, id).descr(),80 Res::Primitive(_) => "primitive type",81 }82 }8384 fn article(self) -> &'static str {85 match self {86 Res::Def(kind, id) => ResolveRes::Def(kind, id).article(),87 Res::Primitive(_) => "a",88 }89 }9091 fn name(self, tcx: TyCtxt<'_>) -> Symbol {92 match self {93 Res::Def(_, id) => tcx.item_name(id),94 Res::Primitive(prim) => prim.as_sym(),95 }96 }9798 fn def_id(self, tcx: TyCtxt<'_>) -> Option<DefId> {99 match self {100 Res::Def(_, id) => Some(id),101 Res::Primitive(prim) => PrimitiveType::primitive_locations(tcx).get(&prim).copied(),102 }103 }104105 fn from_def_id(tcx: TyCtxt<'_>, def_id: DefId) -> Res {106 Res::Def(tcx.def_kind(def_id), def_id)107 }108109 /// Used for error reporting.110 fn disambiguator_suggestion(self) -> Suggestion {111 let kind = match self {112 Res::Primitive(_) => return Suggestion::Prefix("prim"),113 Res::Def(kind, _) => kind,114 };115116 let prefix = match kind {117 DefKind::Fn | DefKind::AssocFn => return Suggestion::Function,118 // FIXME: handle macros with multiple kinds, and attribute/derive macros that aren't119 // proc macros120 DefKind::Macro(MacroKinds::ATTR) => "attribute",121 DefKind::Macro(MacroKinds::DERIVE) => "derive",122 DefKind::Macro(_) => return Suggestion::Macro,123 DefKind::Struct => "struct",124 DefKind::Enum => "enum",125 DefKind::Trait => "trait",126 DefKind::Union => "union",127 DefKind::Mod => "mod",128 DefKind::Const { .. }129 | DefKind::ConstParam130 | DefKind::AssocConst { .. }131 | DefKind::AnonConst => "const",132 DefKind::Static { .. } => "static",133 DefKind::Field => "field",134 DefKind::Variant | DefKind::Ctor(..) => "variant",135 DefKind::TyAlias => "tyalias",136 // Now handle things that don't have a specific disambiguator137 _ => match kind138 .ns()139 .expect("tried to calculate a disambiguator for a def without a namespace?")140 {141 Namespace::TypeNS => "type",142 Namespace::ValueNS => "value",143 Namespace::MacroNS => "macro",144 },145 };146147 Suggestion::Prefix(prefix)148 }149}150151impl TryFrom<ResolveRes> for Res {152 type Error = ();153154 fn try_from(res: ResolveRes) -> Result<Self, ()> {155 use rustc_hir::def::Res::*;156 match res {157 Def(kind, id) => Ok(Res::Def(kind, id)),158 PrimTy(prim) => Ok(Res::Primitive(PrimitiveType::from_hir(prim))),159 // e.g. `#[derive]`160 ToolMod | NonMacroAttr(..) | Err => Result::Err(()),161 other => bug!("unrecognized res {other:?}"),162 }163 }164}165166/// The link failed to resolve. [`resolution_failure`] should look to see if there's167/// a more helpful error that can be given.168#[derive(Debug)]169struct UnresolvedPath<'a> {170 /// Item on which the link is resolved, used for resolving `Self`.171 item_id: DefId,172 /// The scope the link was resolved in.173 module_id: DefId,174 /// If part of the link resolved, this has the `Res`.175 ///176 /// In `[std::io::Error::x]`, `std::io::Error` would be a partial resolution.177 partial_res: Option<Res>,178 /// The remaining unresolved path segments.179 ///180 /// In `[std::io::Error::x]`, `x` would be unresolved.181 unresolved: Cow<'a, str>,182}183184#[derive(Debug)]185enum ResolutionFailure<'a> {186 /// This resolved, but with the wrong namespace.187 WrongNamespace {188 /// What the link resolved to.189 res: Res,190 /// The expected namespace for the resolution, determined from the link's disambiguator.191 ///192 /// E.g., for `[fn@Result]` this is [`Namespace::ValueNS`],193 /// even though `Result`'s actual namespace is [`Namespace::TypeNS`].194 expected_ns: Namespace,195 },196 NotResolved(UnresolvedPath<'a>),197}198199#[derive(Clone, Debug, Hash, PartialEq, Eq)]200pub(crate) enum UrlFragment {201 Item(DefId),202 /// A part of a page that isn't a rust item.203 ///204 /// Eg: `[Vector Examples](std::vec::Vec#examples)`205 UserWritten(String),206}207208#[derive(Clone, Debug, Hash, PartialEq, Eq)]209pub(crate) struct ResolutionInfo {210 item_id: DefId,211 module_id: DefId,212 dis: Option<Disambiguator>,213 path_str: Box<str>,214 extra_fragment: Option<String>,215}216217#[derive(Clone)]218pub(crate) struct DiagnosticInfo<'a> {219 item: &'a Item,220 dox: &'a str,221 ori_link: &'a str,222 link_range: MarkdownLinkRange,223}224225pub(crate) struct OwnedDiagnosticInfo {226 item: Item,227 dox: String,228 ori_link: String,229 link_range: MarkdownLinkRange,230}231232impl From<DiagnosticInfo<'_>> for OwnedDiagnosticInfo {233 fn from(f: DiagnosticInfo<'_>) -> Self {234 Self {235 item: f.item.clone(),236 dox: f.dox.to_string(),237 ori_link: f.ori_link.to_string(),238 link_range: f.link_range.clone(),239 }240 }241}242243impl OwnedDiagnosticInfo {244 pub(crate) fn as_info(&self) -> DiagnosticInfo<'_> {245 DiagnosticInfo {246 item: &self.item,247 ori_link: &self.ori_link,248 dox: &self.dox,249 link_range: self.link_range.clone(),250 }251 }252}253254pub(crate) struct LinkCollector<'a, 'tcx> {255 pub(crate) cx: &'a mut DocContext<'tcx>,256 /// Cache the resolved links so we can avoid resolving (and emitting errors for) the same link.257 /// The link will be `None` if it could not be resolved (i.e. the error was cached).258 pub(crate) visited_links: FxHashMap<ResolutionInfo, Option<(Res, Option<UrlFragment>)>>,259 /// According to `rustc_resolve`, these links are ambiguous.260 ///261 /// However, we cannot link to an item that has been stripped from the documentation. If all262 /// but one of the "possibilities" are stripped, then there is no real ambiguity. To determine263 /// if an ambiguity is real, we delay resolving them until after `Cache::populate`, then filter264 /// every item that doesn't have a cached path.265 ///266 /// We could get correct results by simply delaying everything. This would have fewer happy267 /// codepaths, but we want to distinguish different kinds of error conditions, and this is easy268 /// to do by resolving links as soon as possible.269 pub(crate) ambiguous_links: FxIndexMap<(ItemId, String), Vec<AmbiguousLinks>>,270}271272pub(crate) struct AmbiguousLinks {273 link_text: Box<str>,274 diag_info: OwnedDiagnosticInfo,275 resolved: Vec<(Res, Option<UrlFragment>)>,276}277278impl<'tcx> LinkCollector<'_, 'tcx> {279 /// Given a full link, parse it as an [enum struct variant].280 ///281 /// In particular, this will return an error whenever there aren't three282 /// full path segments left in the link.283 ///284 /// [enum struct variant]: rustc_hir::VariantData::Struct285 fn variant_field<'path>(286 &self,287 path_str: &'path str,288 item_id: DefId,289 module_id: DefId,290 ) -> Result<(Res, DefId), UnresolvedPath<'path>> {291 let tcx = self.cx.tcx;292 let no_res = || UnresolvedPath {293 item_id,294 module_id,295 partial_res: None,296 unresolved: path_str.into(),297 };298299 debug!("looking for enum variant {path_str}");300 let mut split = path_str.rsplitn(3, "::");301 let variant_field_name = Symbol::intern(split.next().unwrap());302 // We're not sure this is a variant at all, so use the full string.303 // If there's no second component, the link looks like `[path]`.304 // So there's no partial res and we should say the whole link failed to resolve.305 let variant_name = Symbol::intern(split.next().ok_or_else(no_res)?);306307 // If there's no third component, we saw `[a::b]` before and it failed to resolve.308 // So there's no partial res.309 let path = split.next().ok_or_else(no_res)?;310 let ty_res = self.resolve_path(path, TypeNS, item_id, module_id).ok_or_else(no_res)?;311312 match ty_res {313 Res::Def(DefKind::Enum | DefKind::TyAlias, did) => {314 match tcx.type_of(did).instantiate_identity().skip_norm_wip().kind() {315 ty::Adt(def, _) if def.is_enum() => {316 if let Some(variant) =317 def.variants().iter().find(|v| v.name == variant_name)318 && let Some(field) =319 variant.fields.iter().find(|f| f.name == variant_field_name)320 {321 Ok((ty_res, field.did))322 } else {323 Err(UnresolvedPath {324 item_id,325 module_id,326 partial_res: Some(Res::Def(DefKind::Enum, def.did())),327 unresolved: variant_field_name.to_string().into(),328 })329 }330 }331 _ => Err(UnresolvedPath {332 item_id,333 module_id,334 partial_res: Some(Res::Def(DefKind::TyAlias, did)),335 unresolved: variant_name.to_string().into(),336 }),337 }338 }339 _ => Err(UnresolvedPath {340 item_id,341 module_id,342 partial_res: Some(ty_res),343 unresolved: variant_name.to_string().into(),344 }),345 }346 }347348 /// Convenience wrapper around `doc_link_resolutions`.349 ///350 /// This also handles resolving `true` and `false` as booleans.351 /// NOTE: `doc_link_resolutions` knows only about paths, not about types.352 /// Associated items will never be resolved by this function.353 fn resolve_path(354 &self,355 path_str: &str,356 ns: Namespace,357 item_id: DefId,358 module_id: DefId,359 ) -> Option<Res> {360 if let res @ Some(..) = resolve_self_ty(self.cx.tcx, path_str, ns, item_id) {361 return res;362 }363364 // Resolver doesn't know about true, false, and types that aren't paths (e.g. `()`).365 let result = self366 .cx367 .tcx368 .doc_link_resolutions(module_id)369 .get(&(Symbol::intern(path_str), ns))370 .copied()371 // NOTE: do not remove this panic! Missing links should be recorded as `Res::Err`; if372 // `doc_link_resolutions` is missing a `path_str`, that means that there are valid links373 // that are being missed. To fix the ICE, change374 // `rustc_resolve::rustdoc::attrs_to_preprocessed_links` to cache the link.375 .unwrap_or_else(|| {376 span_bug!(377 self.cx.tcx.def_span(item_id),378 "no resolution for {path_str:?} {ns:?} {module_id:?}",379 )380 })381 .and_then(|res| res.try_into().ok())382 .or_else(|| resolve_primitive(path_str, ns));383 debug!("{path_str} resolved to {result:?} in namespace {ns:?}");384 result385 }386387 /// Resolves a string as a path within a particular namespace. Returns an388 /// optional URL fragment in the case of variants and methods.389 fn resolve<'path>(390 &self,391 path_str: &'path str,392 ns: Namespace,393 disambiguator: Option<Disambiguator>,394 item_id: DefId,395 module_id: DefId,396 ) -> Result<Vec<(Res, Option<DefId>)>, UnresolvedPath<'path>> {397 let tcx = self.cx.tcx;398399 if let Some(res) = self.resolve_path(path_str, ns, item_id, module_id) {400 return Ok(match res {401 Res::Def(402 DefKind::AssocFn403 | DefKind::AssocConst { .. }404 | DefKind::AssocTy405 | DefKind::Variant,406 def_id,407 ) => {408 vec![(Res::from_def_id(self.cx.tcx, self.cx.tcx.parent(def_id)), Some(def_id))]409 }410 _ => vec![(res, None)],411 });412 } else if ns == MacroNS {413 return Err(UnresolvedPath {414 item_id,415 module_id,416 partial_res: None,417 unresolved: path_str.into(),418 });419 }420421 // Try looking for methods and associated items.422 // NB: `path_root` could be empty when resolving in the root namespace (e.g. `::std`).423 let (path_root, item_str) = match path_str.rsplit_once("::") {424 Some(res @ (_path_root, item_str)) if !item_str.is_empty() => res,425 _ => {426 // If there's no `::`, or the `::` is at the end (e.g. `String::`) it's not an427 // associated item. So we can be sure that `rustc_resolve` was accurate when it428 // said it wasn't resolved.429 debug!("`::` missing or at end, assuming {path_str} was not in scope");430 return Err(UnresolvedPath {431 item_id,432 module_id,433 partial_res: None,434 unresolved: path_str.into(),435 });436 }437 };438 let item_name = Symbol::intern(item_str);439440 // FIXME(#83862): this arbitrarily gives precedence to primitives over modules to support441 // links to primitives when `#[rustc_doc_primitive]` is present. It should give an ambiguity442 // error instead and special case *only* modules with `#[rustc_doc_primitive]`, not all443 // primitives.444 match resolve_primitive(path_root, TypeNS)445 .or_else(|| self.resolve_path(path_root, TypeNS, item_id, module_id))446 .map(|ty_res| {447 resolve_associated_item(tcx, ty_res, item_name, ns, disambiguator, module_id)448 .into_iter()449 .map(|(res, def_id)| (res, Some(def_id)))450 .collect::<Vec<_>>()451 }) {452 Some(r) if !r.is_empty() => Ok(r),453 _ => {454 if ns == Namespace::ValueNS {455 self.variant_field(path_str, item_id, module_id)456 .map(|(res, def_id)| vec![(res, Some(def_id))])457 } else {458 Err(UnresolvedPath {459 item_id,460 module_id,461 partial_res: None,462 unresolved: path_root.into(),463 })464 }465 }466 }467 }468}469470fn full_res(tcx: TyCtxt<'_>, (base, assoc_item): (Res, Option<DefId>)) -> Res {471 assoc_item.map_or(base, |def_id| Res::from_def_id(tcx, def_id))472}473474/// Given a primitive type, try to resolve an associated item.475fn resolve_primitive_inherent_assoc_item<'tcx>(476 tcx: TyCtxt<'tcx>,477 prim_ty: PrimitiveType,478 ns: Namespace,479 item_ident: Ident,480) -> Vec<(Res, DefId)> {481 prim_ty482 .impls(tcx)483 .flat_map(|impl_| {484 filter_assoc_items_by_name_and_namespace(tcx, impl_, item_ident, ns)485 .map(|item| (Res::Primitive(prim_ty), item.def_id))486 })487 .collect::<Vec<_>>()488}489490fn resolve_self_ty<'tcx>(491 tcx: TyCtxt<'tcx>,492 path_str: &str,493 ns: Namespace,494 item_id: DefId,495) -> Option<Res> {496 if ns != TypeNS || path_str != "Self" {497 return None;498 }499500 let self_id = match tcx.def_kind(item_id) {501 def_kind @ (DefKind::AssocFn502 | DefKind::AssocConst { .. }503 | DefKind::AssocTy504 | DefKind::Variant505 | DefKind::Field) => {506 let parent_def_id = tcx.parent(item_id);507 if def_kind == DefKind::Field && tcx.def_kind(parent_def_id) == DefKind::Variant {508 tcx.parent(parent_def_id)509 } else {510 parent_def_id511 }512 }513 _ => item_id,514 };515516 match tcx.def_kind(self_id) {517 DefKind::Impl { .. } => {518 ty_to_res(tcx, tcx.type_of(self_id).instantiate_identity().skip_norm_wip())519 }520 DefKind::Use => None,521 def_kind => Some(Res::Def(def_kind, self_id)),522 }523}524525/// Convert a Ty to a Res, where possible.526///527/// This is used for resolving type aliases.528fn ty_to_res<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> Option<Res> {529 use PrimitiveType::*;530 Some(match *ty.kind() {531 ty::Bool => Res::Primitive(Bool),532 ty::Char => Res::Primitive(Char),533 ty::Int(ity) => Res::Primitive(ity.into()),534 ty::Uint(uty) => Res::Primitive(uty.into()),535 ty::Float(fty) => Res::Primitive(fty.into()),536 ty::Str => Res::Primitive(Str),537 ty::Tuple(tys) if tys.is_empty() => Res::Primitive(Unit),538 ty::Tuple(_) => Res::Primitive(Tuple),539 ty::Pat(..) => Res::Primitive(Pat),540 ty::Array(..) => Res::Primitive(Array),541 ty::Slice(_) => Res::Primitive(Slice),542 ty::RawPtr(_, _) => Res::Primitive(RawPointer),543 ty::Ref(..) => Res::Primitive(Reference),544 ty::FnDef(..) => panic!("type alias to a function definition"),545 ty::FnPtr(..) => Res::Primitive(Fn),546 ty::Never => Res::Primitive(Never),547 ty::Adt(ty::AdtDef(Interned(&ty::AdtDefData { did, .. }, _)), _) | ty::Foreign(did) => {548 Res::from_def_id(tcx, did)549 }550 ty::Alias(..)551 | ty::Closure(..)552 | ty::CoroutineClosure(..)553 | ty::Coroutine(..)554 | ty::CoroutineWitness(..)555 | ty::Dynamic(..)556 | ty::UnsafeBinder(_)557 | ty::Param(_)558 | ty::Bound(..)559 | ty::Placeholder(_)560 | ty::Infer(_)561 | ty::Error(_) => return None,562 })563}564565/// Convert a PrimitiveType to a Ty, where possible.566///567/// This is used for resolving trait impls for primitives568fn primitive_type_to_ty<'tcx>(tcx: TyCtxt<'tcx>, prim: PrimitiveType) -> Option<Ty<'tcx>> {569 use PrimitiveType::*;570571 // FIXME: Only simple types are supported here, see if we can support572 // other types such as Tuple, Array, Slice, etc.573 // See https://github.com/rust-lang/rust/issues/90703#issuecomment-1004263455574 Some(match prim {575 Bool => tcx.types.bool,576 Str => tcx.types.str_,577 Char => tcx.types.char,578 Never => tcx.types.never,579 I8 => tcx.types.i8,580 I16 => tcx.types.i16,581 I32 => tcx.types.i32,582 I64 => tcx.types.i64,583 I128 => tcx.types.i128,584 Isize => tcx.types.isize,585 F16 => tcx.types.f16,586 F32 => tcx.types.f32,587 F64 => tcx.types.f64,588 F128 => tcx.types.f128,589 U8 => tcx.types.u8,590 U16 => tcx.types.u16,591 U32 => tcx.types.u32,592 U64 => tcx.types.u64,593 U128 => tcx.types.u128,594 Usize => tcx.types.usize,595 _ => return None,596 })597}598599/// Resolve an associated item, returning its containing page's `Res`600/// and the fragment targeting the associated item on its page.601fn resolve_associated_item<'tcx>(602 tcx: TyCtxt<'tcx>,603 root_res: Res,604 item_name: Symbol,605 ns: Namespace,606 disambiguator: Option<Disambiguator>,607 module_id: DefId,608) -> Vec<(Res, DefId)> {609 let item_ident = Ident::with_dummy_span(item_name);610611 match root_res {612 Res::Def(DefKind::TyAlias, alias_did) => {613 // Resolve the link on the type the alias points to.614 // FIXME: if the associated item is defined directly on the type alias,615 // it will show up on its documentation page, we should link there instead.616 let Some(aliased_res) =617 ty_to_res(tcx, tcx.type_of(alias_did).instantiate_identity().skip_norm_wip())618 else {619 return vec![];620 };621 let aliased_items =622 resolve_associated_item(tcx, aliased_res, item_name, ns, disambiguator, module_id);623 aliased_items624 .into_iter()625 .map(|(res, assoc_did)| {626 if is_assoc_item_on_alias_page(tcx, assoc_did) {627 (root_res, assoc_did)628 } else {629 (res, assoc_did)630 }631 })632 .collect()633 }634 Res::Primitive(prim) => resolve_assoc_on_primitive(tcx, prim, ns, item_ident, module_id),635 Res::Def(DefKind::Struct | DefKind::Union | DefKind::Enum, did) => {636 resolve_assoc_on_adt(tcx, did, item_ident, ns, disambiguator, module_id)637 }638 Res::Def(DefKind::ForeignTy, did) => {639 resolve_assoc_on_simple_type(tcx, did, item_ident, ns, module_id)640 }641 Res::Def(DefKind::Trait, did) => filter_assoc_items_by_name_and_namespace(642 tcx,643 did,644 Ident::with_dummy_span(item_name),645 ns,646 )647 .map(|item| (root_res, item.def_id))648 .collect::<Vec<_>>(),649 _ => Vec::new(),650 }651}652653// FIXME: make this fully complete by also including ALL inherent impls654// and trait impls BUT ONLY if on alias directly655fn is_assoc_item_on_alias_page<'tcx>(tcx: TyCtxt<'tcx>, assoc_did: DefId) -> bool {656 match tcx.def_kind(assoc_did) {657 // Variants and fields always have docs on the alias page.658 DefKind::Variant | DefKind::Field => true,659 _ => false,660 }661}662663fn resolve_assoc_on_primitive<'tcx>(664 tcx: TyCtxt<'tcx>,665 prim: PrimitiveType,666 ns: Namespace,667 item_ident: Ident,668 module_id: DefId,669) -> Vec<(Res, DefId)> {670 let root_res = Res::Primitive(prim);671 let items = resolve_primitive_inherent_assoc_item(tcx, prim, ns, item_ident);672 if !items.is_empty() {673 items674 // Inherent associated items take precedence over items that come from trait impls.675 } else {676 primitive_type_to_ty(tcx, prim)677 .map(|ty| {678 resolve_associated_trait_item(ty, module_id, item_ident, ns, tcx)679 .iter()680 .map(|item| (root_res, item.def_id))681 .collect::<Vec<_>>()682 })683 .unwrap_or_default()684 }685}686687fn resolve_assoc_on_adt<'tcx>(688 tcx: TyCtxt<'tcx>,689 adt_def_id: DefId,690 item_ident: Ident,691 ns: Namespace,692 disambiguator: Option<Disambiguator>,693 module_id: DefId,694) -> Vec<(Res, DefId)> {695 debug!("looking for associated item named {item_ident} for item {adt_def_id:?}");696 let root_res = Res::from_def_id(tcx, adt_def_id);697 let adt_ty = tcx.type_of(adt_def_id).instantiate_identity().skip_norm_wip();698 let adt_def = adt_ty.ty_adt_def().expect("must be ADT");699 // Checks if item_name is a variant of the `SomeItem` enum700 if ns == TypeNS && adt_def.is_enum() {701 for variant in adt_def.variants() {702 if variant.name == item_ident.name {703 return vec![(root_res, variant.def_id)];704 }705 }706 }707708 if let Some(Disambiguator::Kind(DefKind::Field)) = disambiguator709 && (adt_def.is_struct() || adt_def.is_union())710 {711 return resolve_structfield(adt_def, item_ident.name)712 .into_iter()713 .map(|did| (root_res, did))714 .collect();715 }716717 let assoc_items = resolve_assoc_on_simple_type(tcx, adt_def_id, item_ident, ns, module_id);718 if !assoc_items.is_empty() {719 return assoc_items;720 }721722 if ns == Namespace::ValueNS && (adt_def.is_struct() || adt_def.is_union()) {723 return resolve_structfield(adt_def, item_ident.name)724 .into_iter()725 .map(|did| (root_res, did))726 .collect();727 }728729 vec![]730}731732/// "Simple" i.e. an ADT, foreign type, etc. -- not a type alias, primitive type, or other trickier type.733fn resolve_assoc_on_simple_type<'tcx>(734 tcx: TyCtxt<'tcx>,735 ty_def_id: DefId,736 item_ident: Ident,737 ns: Namespace,738 module_id: DefId,739) -> Vec<(Res, DefId)> {740 let root_res = Res::from_def_id(tcx, ty_def_id);741 // Checks if item_name belongs to `impl SomeItem`742 let inherent_assoc_items: Vec<_> = tcx743 .inherent_impls(ty_def_id)744 .iter()745 .flat_map(|&imp| filter_assoc_items_by_name_and_namespace(tcx, imp, item_ident, ns))746 .map(|item| (root_res, item.def_id))747 .collect();748 debug!("got inherent assoc items {inherent_assoc_items:?}");749 if !inherent_assoc_items.is_empty() {750 return inherent_assoc_items;751 }752753 // Check if item_name belongs to `impl SomeTrait for SomeItem`754 // FIXME(#74563): This gives precedence to `impl SomeItem`:755 // Although having both would be ambiguous, use impl version for compatibility's sake.756 // To handle that properly resolve() would have to support757 // something like [`ambi_fn`](<SomeStruct as SomeTrait>::ambi_fn)758 let ty = tcx.type_of(ty_def_id).instantiate_identity().skip_norm_wip();759 let trait_assoc_items = resolve_associated_trait_item(ty, module_id, item_ident, ns, tcx)760 .into_iter()761 .map(|item| (root_res, item.def_id))762 .collect::<Vec<_>>();763 debug!("got trait assoc items {trait_assoc_items:?}");764 trait_assoc_items765}766767fn resolve_structfield<'tcx>(adt_def: ty::AdtDef<'tcx>, item_name: Symbol) -> Option<DefId> {768 debug!("looking for fields named {item_name} for {adt_def:?}");769 adt_def770 .non_enum_variant()771 .fields772 .iter()773 .find(|field| field.name == item_name)774 .map(|field| field.did)775}776777/// Look to see if a resolved item has an associated item named `item_name`.778///779/// Given `[std::io::Error::source]`, where `source` is unresolved, this would780/// find `std::error::Error::source` and return781/// `<io::Error as error::Error>::source`.782fn resolve_associated_trait_item<'tcx>(783 ty: Ty<'tcx>,784 module: DefId,785 item_ident: Ident,786 ns: Namespace,787 tcx: TyCtxt<'tcx>,788) -> Vec<ty::AssocItem> {789 // FIXME: this should also consider blanket impls (`impl<T> X for T`). Unfortunately790 // `get_auto_trait_and_blanket_impls` is broken because the caching behavior is wrong. In the791 // meantime, just don't look for these blanket impls.792793 // Next consider explicit impls: `impl MyTrait for MyType`794 // Give precedence to inherent impls.795 let traits = trait_impls_for(tcx, ty, module);796 debug!("considering traits {traits:?}");797 let candidates = traits798 .iter()799 .flat_map(|&(impl_, trait_)| {800 filter_assoc_items_by_name_and_namespace(tcx, trait_, item_ident, ns).map(801 move |trait_assoc| {802 trait_assoc_to_impl_assoc_item(tcx, impl_, trait_assoc.def_id)803 .unwrap_or(*trait_assoc)804 },805 )806 })807 .collect::<Vec<_>>();808 // FIXME(#74563): warn about ambiguity809 debug!("the candidates were {candidates:?}");810 candidates811}812813/// Find the associated item in the impl `impl_id` that corresponds to the814/// trait associated item `trait_assoc_id`.815///816/// This function returns `None` if no associated item was found in the impl.817/// This can occur when the trait associated item has a default value that is818/// not overridden in the impl.819///820/// This is just a wrapper around [`TyCtxt::impl_item_implementor_ids()`] and821/// [`TyCtxt::associated_item()`] (with some helpful logging added).822#[instrument(level = "debug", skip(tcx), ret)]823fn trait_assoc_to_impl_assoc_item<'tcx>(824 tcx: TyCtxt<'tcx>,825 impl_id: DefId,826 trait_assoc_id: DefId,827) -> Option<ty::AssocItem> {828 let trait_to_impl_assoc_map = tcx.impl_item_implementor_ids(impl_id);829 debug!(?trait_to_impl_assoc_map);830 let impl_assoc_id = *trait_to_impl_assoc_map.get(&trait_assoc_id)?;831 debug!(?impl_assoc_id);832 Some(tcx.associated_item(impl_assoc_id))833}834835/// Given a type, return all trait impls in scope in `module` for that type.836/// Returns a set of pairs of `(impl_id, trait_id)`.837///838/// NOTE: this cannot be a query because more traits could be available when more crates are compiled!839/// So it is not stable to serialize cross-crate.840#[instrument(level = "debug", skip(tcx))]841fn trait_impls_for<'tcx>(842 tcx: TyCtxt<'tcx>,843 ty: Ty<'tcx>,844 module: DefId,845) -> FxIndexSet<(DefId, DefId)> {846 let mut impls = FxIndexSet::default();847848 for &trait_ in tcx.doc_link_traits_in_scope(module) {849 tcx.for_each_relevant_impl(trait_, ty, |impl_| {850 let trait_ref = tcx.impl_trait_ref(impl_);851 // Check if these are the same type.852 let impl_type = trait_ref.skip_binder().self_ty();853 trace!(854 "comparing type {impl_type} with kind {kind:?} against type {ty:?}",855 kind = impl_type.kind(),856 );857 // Fast path: if this is a primitive simple `==` will work858 // NOTE: the `match` is necessary; see #92662.859 // this allows us to ignore generics because the user input860 // may not include the generic placeholders861 // e.g. this allows us to match Foo (user comment) with Foo<T> (actual type)862 let saw_impl = impl_type == ty863 || match (impl_type.kind(), ty.kind()) {864 (ty::Adt(impl_def, _), ty::Adt(ty_def, _)) => {865 debug!("impl def_id: {:?}, ty def_id: {:?}", impl_def.did(), ty_def.did());866 impl_def.did() == ty_def.did()867 }868 _ => false,869 };870871 if saw_impl {872 impls.insert((impl_, trait_));873 }874 });875 }876877 impls878}879880/// Check for resolve collisions between a trait and its derive.881///882/// These are common and we should just resolve to the trait in that case.883fn is_derive_trait_collision<T>(ns: &PerNS<Result<Vec<(Res, T)>, ResolutionFailure<'_>>>) -> bool {884 if let (Ok(type_ns), Ok(macro_ns)) = (&ns.type_ns, &ns.macro_ns) {885 type_ns.iter().any(|(res, _)| matches!(res, Res::Def(DefKind::Trait, _)))886 && macro_ns.iter().any(|(res, _)| {887 matches!(888 res,889 Res::Def(DefKind::Macro(kinds), _) if kinds.contains(MacroKinds::DERIVE)890 )891 })892 } else {893 false894 }895}896897impl DocVisitor<'_> for LinkCollector<'_, '_> {898 fn visit_item(&mut self, item: &Item) {899 self.resolve_links(item);900 self.visit_item_recur(item)901 }902}903904enum PreprocessingError {905 /// User error: `[std#x#y]` is not valid906 MultipleAnchors,907 Disambiguator(MarkdownLinkRange, String),908 MalformedGenerics(MalformedGenerics, String),909}910911impl PreprocessingError {912 fn report(&self, cx: &DocContext<'_>, diag_info: DiagnosticInfo<'_>) {913 match self {914 PreprocessingError::MultipleAnchors => report_multiple_anchors(cx, diag_info),915 PreprocessingError::Disambiguator(range, msg) => {916 disambiguator_error(cx, diag_info, range.clone(), msg.clone())917 }918 PreprocessingError::MalformedGenerics(err, path_str) => {919 report_malformed_generics(cx, diag_info, *err, path_str)920 }921 }922 }923}924925#[derive(Clone)]926struct PreprocessingInfo {927 path_str: Box<str>,928 disambiguator: Option<Disambiguator>,929 extra_fragment: Option<String>,930 link_text: Box<str>,931}932933// Not a typedef to avoid leaking several private structures from this module.934pub(crate) struct PreprocessedMarkdownLink(935 Result<PreprocessingInfo, PreprocessingError>,936 MarkdownLink,937);938939/// Returns:940/// - `None` if the link should be ignored.941/// - `Some(Err(_))` if the link should emit an error942/// - `Some(Ok(_))` if the link is valid943///944/// `link_buffer` is needed for lifetime reasons; it will always be overwritten and the contents ignored.945fn preprocess_link(946 ori_link: &MarkdownLink,947 dox: &str,948) -> Option<Result<PreprocessingInfo, PreprocessingError>> {949 // IMPORTANT: To be kept in sync with the corresponding function in `rustc_resolve::rustdoc`.950 // Namely, whenever this function returns a successful result for a given input,951 // the rustc counterpart *MUST* return a link that's equal to `PreprocessingInfo.path_str`!952953 // certain link kinds cannot have their path be urls,954 // so they should not be ignored, no matter how much they look like urls.955 // e.g. [https://example.com/] is not a link to example.com.956 let can_be_url = !matches!(957 ori_link.kind,958 LinkType::ShortcutUnknown | LinkType::CollapsedUnknown | LinkType::ReferenceUnknown959 );960961 // [] is mostly likely not supposed to be a link962 if ori_link.link.is_empty() {963 return None;964 }965966 // Bail early for real links.967 if can_be_url && ori_link.link.contains('/') {968 return None;969 }970971 let stripped = ori_link.link.replace('`', "");972 let mut parts = stripped.split('#');973974 let link = parts.next().unwrap();975 let link = link.trim();976 if link.is_empty() {977 // This is an anchor to an element of the current page, nothing to do in here!978 return None;979 }980 let extra_fragment = parts.next();981 if parts.next().is_some() {982 // A valid link can't have multiple #'s983 return Some(Err(PreprocessingError::MultipleAnchors));984 }985986 // Parse and strip the disambiguator from the link, if present.987 let (disambiguator, path_str, link_text) = match Disambiguator::from_str(link) {988 Ok(Some((d, path, link_text))) => (Some(d), path.trim(), link_text.trim()),989 Ok(None) => (None, link, link),990 Err((err_msg, relative_range)) => {991 // Only report error if we would not have ignored this link. See issue #83859.992 if !(can_be_url && should_ignore_link_with_disambiguators(link)) {993 let disambiguator_range = match range_between_backticks(&ori_link.range, dox) {994 MarkdownLinkRange::Destination(no_backticks_range) => {995 MarkdownLinkRange::Destination(996 (no_backticks_range.start + relative_range.start)997 ..(no_backticks_range.start + relative_range.end),998 )999 }1000 mdlr @ MarkdownLinkRange::WholeLink(_) => mdlr,1001 };1002 return Some(Err(PreprocessingError::Disambiguator(disambiguator_range, err_msg)));1003 } else {1004 return None;1005 }1006 }1007 };10081009 let is_shortcut_style = ori_link.kind == LinkType::ShortcutUnknown;1010 // If there's no backticks, be lenient and revert to the old behavior.1011 // This is to prevent churn by linting on stuff that isn't meant to be a link.1012 // only shortcut links have simple enough syntax that they1013 // are likely to be written accidentally, collapsed and reference links1014 // need 4 metachars, and reference links will not usually use1015 // backticks in the reference name.1016 // therefore, only shortcut syntax gets the lenient behavior.1017 //1018 // here's a truth table for how link kinds that cannot be urls are handled:1019 //1020 // |-------------------------------------------------------|1021 // | | is shortcut link | not shortcut link |1022 // |--------------|--------------------|-------------------|1023 // | has backtick | never ignore | never ignore |1024 // | no backtick | ignore if url-like | never ignore |1025 // |-------------------------------------------------------|1026 let ignore_urllike = can_be_url || (is_shortcut_style && !ori_link.link.contains('`'));1027 if ignore_urllike && should_ignore_link(path_str) {1028 return None;1029 }1030 // If we have an intra-doc link starting with `!` (which isn't `[!]` because this is the never type), we ignore it1031 // as it is never valid.1032 //1033 // The case is common enough because of cases like `#[doc = include_str!("../README.md")]` which often1034 // uses GitHub-flavored Markdown (GFM) admonitions, such as `[!NOTE]`.1035 if is_shortcut_style1036 && let Some(suffix) = ori_link.link.strip_prefix('!')1037 && !suffix.is_empty()1038 && suffix.chars().all(|c| c.is_ascii_alphabetic())1039 {1040 return None;1041 }10421043 // Strip generics from the path.1044 let path_str = match strip_generics_from_path(path_str) {1045 Ok(path) => path,1046 Err(err) => {1047 debug!("link has malformed generics: {path_str}");1048 return Some(Err(PreprocessingError::MalformedGenerics(err, path_str.to_owned())));1049 }1050 };10511052 // Sanity check to make sure we don't have any angle brackets after stripping generics.1053 assert!(!path_str.contains(['<', '>'].as_slice()));10541055 // The link is not an intra-doc link if it still contains spaces after stripping generics.1056 if path_str.contains(' ') {1057 return None;1058 }10591060 Some(Ok(PreprocessingInfo {1061 path_str,1062 disambiguator,1063 extra_fragment: extra_fragment.map(|frag| frag.to_owned()),1064 link_text: Box::<str>::from(link_text),1065 }))1066}10671068fn preprocessed_markdown_links(s: &str) -> Vec<PreprocessedMarkdownLink> {1069 markdown_links(s, |link| {1070 preprocess_link(&link, s).map(|pp_link| PreprocessedMarkdownLink(pp_link, link))1071 })1072}10731074impl LinkCollector<'_, '_> {1075 #[instrument(level = "debug", skip_all)]1076 fn resolve_links(&mut self, item: &Item) {1077 let tcx = self.cx.tcx;1078 let document_private = self.cx.document_private();1079 let effective_visibilities = tcx.effective_visibilities(());1080 let should_skip_link_resolution = |item_id: DefId| {1081 !document_private1082 && item_id1083 .as_local()1084 .is_some_and(|local_def_id| !effective_visibilities.is_exported(local_def_id))1085 && !has_primitive_or_keyword_or_attribute_docs(&item.attrs.other_attrs)1086 };10871088 if let Some(def_id) = item.item_id.as_def_id()1089 && should_skip_link_resolution(def_id)1090 {1091 // Skip link resolution for non-exported items.1092 return;1093 }10941095 let mut try_insert_links = |item_id, doc: &str| {1096 if should_skip_link_resolution(item_id) {1097 return;1098 }1099 let module_id = match tcx.def_kind(item_id) {1100 DefKind::Mod if item.inner_docs(tcx) => item_id,1101 _ => find_nearest_parent_module(tcx, item_id).unwrap(),1102 };1103 for md_link in preprocessed_markdown_links(&doc) {1104 let link = self.resolve_link(&doc, item, item_id, module_id, &md_link);1105 if let Some(link) = link {1106 self.cx1107 .cache1108 .intra_doc_links1109 .entry(item.item_or_reexport_id())1110 .or_default()1111 .insert(link);1112 }1113 }1114 };11151116 // We want to resolve in the lexical scope of the documentation.1117 // In the presence of re-exports, this is not the same as the module of the item.1118 // Rather than merging all documentation into one, resolve it one attribute at a time1119 // so we know which module it came from.1120 for (item_id, doc) in prepare_to_doc_link_resolution(&item.attrs.doc_strings) {1121 if !may_have_doc_links(&doc) {1122 continue;1123 }11241125 debug!("combined_docs={doc}");1126 // NOTE: if there are links that start in one crate and end in another, this will not resolve them.1127 // This is a degenerate case and it's not supported by rustdoc.1128 let item_id = item_id.unwrap_or_else(|| item.item_id.expect_def_id());1129 try_insert_links(item_id, &doc)1130 }11311132 // Also resolve links in the note text of `#[deprecated]`.1133 for attr in &item.attrs.other_attrs {1134 let Attribute::Parsed(AttributeKind::Deprecated { span: depr_span, deprecation }) =1135 attr1136 else {1137 continue;1138 };1139 let Some(note_sym) = deprecation.note else { continue };1140 let note = note_sym.as_str();11411142 if !may_have_doc_links(note) {1143 continue;1144 }11451146 debug!("deprecated_note={note}");1147 // When resolving an intra-doc link inside a deprecation note that is on an inlined1148 // `use` statement, we need to use the `def_id` of the `use` statement, not the1149 // inlined item.1150 // <https://github.com/rust-lang/rust/pull/151120>1151 let item_id = if let Some(inline_stmt_id) = item.inline_stmt_id1152 && find_attr!(tcx, inline_stmt_id, Deprecated { span, ..} if span == depr_span)1153 {1154 inline_stmt_id.to_def_id()1155 } else {1156 item.item_id.expect_def_id()1157 };1158 try_insert_links(item_id, note)1159 }1160 }11611162 pub(crate) fn save_link(&mut self, item_id: ItemId, link: ItemLink) {1163 self.cx.cache.intra_doc_links.entry(item_id).or_default().insert(link);1164 }11651166 /// This is the entry point for resolving an intra-doc link.1167 ///1168 /// FIXME(jynelson): this is way too many arguments1169 fn resolve_link(1170 &mut self,1171 dox: &str,1172 item: &Item,1173 item_id: DefId,1174 module_id: DefId,1175 PreprocessedMarkdownLink(pp_link, ori_link): &PreprocessedMarkdownLink,1176 ) -> Option<ItemLink> {1177 trace!("considering link '{}'", ori_link.link);11781179 let diag_info = DiagnosticInfo {1180 item,1181 dox,1182 ori_link: &ori_link.link,1183 link_range: ori_link.range.clone(),1184 };1185 let PreprocessingInfo { path_str, disambiguator, extra_fragment, link_text } =1186 pp_link.as_ref().map_err(|err| err.report(self.cx, diag_info.clone())).ok()?;1187 let disambiguator = *disambiguator;11881189 let mut resolved = self.resolve_with_disambiguator_cached(1190 ResolutionInfo {1191 item_id,1192 module_id,1193 dis: disambiguator,1194 path_str: path_str.clone(),1195 extra_fragment: extra_fragment.clone(),1196 },1197 diag_info.clone(), // this struct should really be Copy, but Range is not :(1198 // For reference-style links we want to report only one error so unsuccessful1199 // resolutions are cached, for other links we want to report an error every1200 // time so they are not cached.1201 matches!(ori_link.kind, LinkType::Reference | LinkType::Shortcut),1202 )?;12031204 if resolved.len() > 1 {1205 let links = AmbiguousLinks {1206 link_text: link_text.clone(),1207 diag_info: diag_info.into(),1208 resolved,1209 };12101211 self.ambiguous_links1212 .entry((item.item_id, path_str.to_string()))1213 .or_default()1214 .push(links);1215 None1216 } else if let Some((res, fragment)) = resolved.pop() {1217 self.compute_link(res, fragment, path_str, disambiguator, diag_info, link_text)1218 } else {1219 None1220 }1221 }12221223 /// Returns `true` if a link could be generated from the given intra-doc information.1224 ///1225 /// This is a very light version of `format::href_with_root_path` since we're only interested1226 /// about whether we can generate a link to an item or not.1227 ///1228 /// * If `original_did` is local, then we check if the item is reexported or public.1229 /// * If `original_did` is not local, then we check if the crate it comes from is a direct1230 /// public dependency.1231 fn validate_link(&self, original_did: DefId) -> bool {1232 let tcx = self.cx.tcx;1233 let def_kind = tcx.def_kind(original_did);1234 let did = match def_kind {1235 DefKind::AssocTy | DefKind::AssocFn | DefKind::AssocConst { .. } | DefKind::Variant => {1236 // documented on their parent's page1237 tcx.parent(original_did)1238 }1239 // If this a constructor, we get the parent (either a struct or a variant) and then1240 // generate the link for this item.1241 DefKind::Ctor(..) => return self.validate_link(tcx.parent(original_did)),1242 DefKind::ExternCrate => {1243 // Link to the crate itself, not the `extern crate` item.1244 if let Some(local_did) = original_did.as_local() {1245 tcx.extern_mod_stmt_cnum(local_did).unwrap_or(LOCAL_CRATE).as_def_id()1246 } else {1247 original_did1248 }1249 }1250 _ => original_did,1251 };12521253 let cache = &self.cx.cache;1254 if !original_did.is_local()1255 && !cache.effective_visibilities.is_directly_public(tcx, did)1256 && !cache.document_private1257 && !cache.primitive_locations.values().any(|&id| id == did)1258 {1259 return false;1260 }12611262 cache.paths.get(&did).is_some()1263 || cache.external_paths.contains_key(&did)1264 || !did.is_local()1265 }12661267 pub(crate) fn resolve_ambiguities(&mut self) {1268 let mut ambiguous_links = mem::take(&mut self.ambiguous_links);1269 for ((item_id, path_str), info_items) in ambiguous_links.iter_mut() {1270 for info in info_items {1271 info.resolved.retain(|(res, _)| match res {1272 Res::Def(_, def_id) => self.validate_link(*def_id),1273 // Primitive types are always valid.1274 Res::Primitive(_) => true,1275 });1276 let diag_info = info.diag_info.as_info();1277 match info.resolved.len() {1278 1 => {1279 let (res, fragment) = info.resolved.pop().unwrap();1280 if let Some(link) = self.compute_link(1281 res,1282 fragment,1283 path_str,1284 None,1285 diag_info,1286 &info.link_text,1287 ) {1288 self.save_link(*item_id, link);1289 }1290 }1291 0 => {1292 report_diagnostic(1293 self.cx.tcx,1294 BROKEN_INTRA_DOC_LINKS,1295 format!("all items matching `{path_str}` are private or doc(hidden)"),1296 &diag_info,1297 |diag, sp, _| {1298 if let Some(sp) = sp {1299 diag.span_label(sp, "unresolved link");1300 } else {1301 diag.note("unresolved link");1302 }1303 },1304 );1305 }1306 _ => {1307 let candidates = info1308 .resolved1309 .iter()1310 .map(|(res, fragment)| {1311 let def_id = if let Some(UrlFragment::Item(def_id)) = fragment {1312 Some(*def_id)1313 } else {1314 None1315 };1316 (*res, def_id)1317 })1318 .collect::<Vec<_>>();1319 ambiguity_error(self.cx, &diag_info, path_str, &candidates, true);1320 }1321 }1322 }1323 }1324 }13251326 fn compute_link(1327 &mut self,1328 mut res: Res,1329 fragment: Option<UrlFragment>,1330 path_str: &str,1331 disambiguator: Option<Disambiguator>,1332 diag_info: DiagnosticInfo<'_>,1333 link_text: &Box<str>,1334 ) -> Option<ItemLink> {1335 // Check for a primitive which might conflict with a module1336 // Report the ambiguity and require that the user specify which one they meant.1337 // FIXME: could there ever be a primitive not in the type namespace?1338 if matches!(1339 disambiguator,1340 None | Some(Disambiguator::Namespace(Namespace::TypeNS) | Disambiguator::Primitive)1341 ) && !matches!(res, Res::Primitive(_))1342 && let Some(prim) = resolve_primitive(path_str, TypeNS)1343 {1344 // `prim@char`1345 if matches!(disambiguator, Some(Disambiguator::Primitive)) {1346 res = prim;1347 } else {1348 // `[char]` when a `char` module is in scope1349 let candidates = &[(res, res.def_id(self.cx.tcx)), (prim, None)];1350 ambiguity_error(self.cx, &diag_info, path_str, candidates, true);1351 return None;1352 }1353 }13541355 match res {1356 Res::Primitive(_) => {1357 if let Some(UrlFragment::Item(id)) = fragment {1358 // We're actually resolving an associated item of a primitive, so we need to1359 // verify the disambiguator (if any) matches the type of the associated item.1360 // This case should really follow the same flow as the `Res::Def` branch below,1361 // but attempting to add a call to `clean::register_res` causes an ICE. @jyn5141362 // thinks `register_res` is only needed for cross-crate re-exports, but Rust1363 // doesn't allow statements like `use str::trim;`, making this a (hopefully)1364 // valid omission. See https://github.com/rust-lang/rust/pull/80660#discussion_r5515856771365 // for discussion on the matter.1366 let kind = self.cx.tcx.def_kind(id);1367 self.verify_disambiguator(path_str, kind, id, disambiguator, &diag_info)?;1368 } else {1369 match disambiguator {1370 Some(Disambiguator::Primitive | Disambiguator::Namespace(_)) | None => {}1371 Some(other) => {1372 self.report_disambiguator_mismatch(path_str, other, res, &diag_info);1373 return None;1374 }1375 }1376 }13771378 res.def_id(self.cx.tcx).map(|page_id| ItemLink {1379 link: Box::<str>::from(diag_info.ori_link),1380 link_text: link_text.clone(),1381 page_id,1382 fragment,1383 })1384 }1385 Res::Def(kind, id) => {1386 let (kind_for_dis, id_for_dis) = if let Some(UrlFragment::Item(id)) = fragment {1387 (self.cx.tcx.def_kind(id), id)1388 } else {1389 (kind, id)1390 };1391 self.verify_disambiguator(1392 path_str,1393 kind_for_dis,1394 id_for_dis,1395 disambiguator,1396 &diag_info,1397 )?;13981399 let page_id = clean::register_res(self.cx, rustc_hir::def::Res::Def(kind, id));1400 Some(ItemLink {1401 link: Box::<str>::from(diag_info.ori_link),1402 link_text: link_text.clone(),1403 page_id,1404 fragment,1405 })1406 }1407 }1408 }14091410 fn verify_disambiguator(1411 &self,1412 path_str: &str,1413 kind: DefKind,1414 id: DefId,1415 disambiguator: Option<Disambiguator>,1416 diag_info: &DiagnosticInfo<'_>,1417 ) -> Option<()> {1418 debug!("intra-doc link to {path_str} resolved to {:?}", (kind, id));14191420 // Disallow e.g. linking to enums with `struct@`1421 debug!("saw kind {kind:?} with disambiguator {disambiguator:?}");1422 match (kind, disambiguator) {1423 | (1424 DefKind::Const { .. }1425 | DefKind::ConstParam1426 | DefKind::AssocConst { .. }1427 | DefKind::AnonConst,1428 Some(Disambiguator::Kind(DefKind::Const { .. })),1429 )1430 // NOTE: this allows 'method' to mean both normal functions and associated functions1431 // This can't cause ambiguity because both are in the same namespace.1432 | (DefKind::Fn | DefKind::AssocFn, Some(Disambiguator::Kind(DefKind::Fn)))1433 // These are namespaces; allow anything in the namespace to match1434 | (_, Some(Disambiguator::Namespace(_)))1435 // If no disambiguator given, allow anything1436 | (_, None)1437 // All of these are valid, so do nothing1438 => {}1439 (actual, Some(Disambiguator::Kind(expected))) if actual == expected => {}1440 (_, Some(specified @ Disambiguator::Kind(_) | specified @ Disambiguator::Primitive)) => {1441 self.report_disambiguator_mismatch(path_str, specified, Res::Def(kind, id), diag_info);1442 return None;1443 }1444 }14451446 // item can be non-local e.g. when using `#[rustc_doc_primitive = "pointer"]`1447 if let Some(dst_id) = id.as_local()1448 && let Some(src_id) = diag_info.item.item_id.expect_def_id().as_local()1449 && self.cx.tcx.effective_visibilities(()).is_exported(src_id)1450 && !self.cx.tcx.effective_visibilities(()).is_exported(dst_id)1451 {1452 privacy_error(self.cx, diag_info, path_str);1453 }14541455 Some(())1456 }14571458 fn report_disambiguator_mismatch(1459 &self,1460 path_str: &str,1461 specified: Disambiguator,1462 resolved: Res,1463 diag_info: &DiagnosticInfo<'_>,1464 ) {1465 // The resolved item did not match the disambiguator; give a better error than 'not found'1466 let msg = format!("incompatible link kind for `{path_str}`");1467 let callback = |diag: &mut Diag<'_, ()>, sp: Option<rustc_span::Span>, link_range| {1468 let note = format!(1469 "this link resolved to {} {}, which is not {} {}",1470 resolved.article(),1471 resolved.descr(),1472 specified.article(),1473 specified.descr(),1474 );1475 if let Some(sp) = sp {1476 diag.span_label(sp, note);1477 } else {1478 diag.note(note);1479 }1480 suggest_disambiguator(resolved, diag, path_str, link_range, sp, diag_info);1481 };1482 report_diagnostic(self.cx.tcx, BROKEN_INTRA_DOC_LINKS, msg, diag_info, callback);1483 }14841485 fn report_rawptr_assoc_feature_gate(1486 &self,1487 dox: &str,1488 ori_link: &MarkdownLinkRange,1489 item: &Item,1490 ) {1491 let span = match source_span_for_markdown_range(1492 self.cx.tcx,1493 dox,1494 ori_link.inner_range(),1495 &item.attrs.doc_strings,1496 ) {1497 Some((sp, _)) => sp,1498 None => item.attr_span(self.cx.tcx),1499 };1500 rustc_session::errors::feature_err(1501 self.cx.tcx.sess,1502 sym::intra_doc_pointers,1503 span,1504 "linking to associated items of raw pointers is experimental",1505 )1506 .with_note("rustdoc does not allow disambiguating between `*const` and `*mut`, and pointers are unstable until it does")1507 .emit();1508 }15091510 fn resolve_with_disambiguator_cached(1511 &mut self,1512 key: ResolutionInfo,1513 diag: DiagnosticInfo<'_>,1514 // If errors are cached then they are only reported on first occurrence1515 // which we want in some cases but not in others.1516 cache_errors: bool,1517 ) -> Option<Vec<(Res, Option<UrlFragment>)>> {1518 if let Some(res) = self.visited_links.get(&key)1519 && (res.is_some() || cache_errors)1520 {1521 return res.clone().map(|r| vec![r]);1522 }15231524 let mut candidates = self.resolve_with_disambiguator(&key, diag.clone());15251526 // FIXME: it would be nice to check that the feature gate was enabled in the original crate, not just ignore it altogether.1527 // However I'm not sure how to check that across crates.1528 if let Some(candidate) = candidates.first()1529 && candidate.0 == Res::Primitive(PrimitiveType::RawPointer)1530 && key.path_str.contains("::")1531 // We only want to check this if this is an associated item.1532 {1533 if key.item_id.is_local() && !self.cx.tcx.features().intra_doc_pointers() {1534 self.report_rawptr_assoc_feature_gate(diag.dox, &diag.link_range, diag.item);1535 return None;1536 } else {1537 candidates = vec![*candidate];1538 }1539 }15401541 // If there are multiple items with the same "kind" (for example, both "associated types")1542 // and after removing duplicated kinds, only one remains, the `ambiguity_error` function1543 // won't emit an error. So at this point, we can just take the first candidate as it was1544 // the first retrieved and use it to generate the link.1545 if let [candidate, _candidate2, ..] = *candidates1546 && !ambiguity_error(self.cx, &diag, &key.path_str, &candidates, false)1547 {1548 candidates = vec![candidate];1549 }15501551 let mut out = Vec::with_capacity(candidates.len());1552 for (res, def_id) in candidates {1553 let fragment = match (&key.extra_fragment, def_id) {1554 (Some(_), Some(def_id)) => {1555 report_anchor_conflict(self.cx, diag, def_id);1556 return None;1557 }1558 (Some(u_frag), None) => Some(UrlFragment::UserWritten(u_frag.clone())),1559 (None, Some(def_id)) => Some(UrlFragment::Item(def_id)),1560 (None, None) => None,1561 };1562 out.push((res, fragment));1563 }1564 if let [r] = out.as_slice() {1565 self.visited_links.insert(key, Some(r.clone()));1566 } else if cache_errors {1567 self.visited_links.insert(key, None);1568 }1569 Some(out)1570 }15711572 /// After parsing the disambiguator, resolve the main part of the link.1573 fn resolve_with_disambiguator(1574 &mut self,1575 key: &ResolutionInfo,1576 diag: DiagnosticInfo<'_>,1577 ) -> Vec<(Res, Option<DefId>)> {1578 let disambiguator = key.dis;1579 let path_str = &key.path_str;1580 let item_id = key.item_id;1581 let module_id = key.module_id;15821583 match disambiguator.map(Disambiguator::ns) {1584 Some(expected_ns) => {1585 match self.resolve(path_str, expected_ns, disambiguator, item_id, module_id) {1586 Ok(candidates) => candidates,1587 Err(err) => {1588 // We only looked in one namespace. Try to give a better error if possible.1589 // FIXME: really it should be `resolution_failure` that does this, not `resolve_with_disambiguator`.1590 // See https://github.com/rust-lang/rust/pull/76955#discussion_r493953382 for a good approach.1591 let mut err = ResolutionFailure::NotResolved(err);1592 for other_ns in [TypeNS, ValueNS, MacroNS] {1593 if other_ns != expected_ns1594 && let Ok(&[res, ..]) = self1595 .resolve(path_str, other_ns, None, item_id, module_id)1596 .as_deref()1597 {1598 err = ResolutionFailure::WrongNamespace {1599 res: full_res(self.cx.tcx, res),1600 expected_ns,1601 };1602 break;1603 }1604 }1605 resolution_failure(self, diag, path_str, disambiguator, smallvec![err]);1606 vec![]1607 }1608 }1609 }1610 None => {1611 // Try everything!1612 let candidate = |ns| {1613 self.resolve(path_str, ns, None, item_id, module_id)1614 .map_err(ResolutionFailure::NotResolved)1615 };16161617 let candidates = PerNS {1618 macro_ns: candidate(MacroNS),1619 type_ns: candidate(TypeNS),1620 value_ns: candidate(ValueNS).and_then(|v_res| {1621 for (res, _) in v_res.iter() {1622 // Constructors are picked up in the type namespace.1623 if let Res::Def(DefKind::Ctor(..), _) = res {1624 return Err(ResolutionFailure::WrongNamespace {1625 res: *res,1626 expected_ns: TypeNS,1627 });1628 }1629 }1630 Ok(v_res)1631 }),1632 };16331634 let len = candidates1635 .iter()1636 .fold(0, |acc, res| if let Ok(res) = res { acc + res.len() } else { acc });16371638 if len == 0 {1639 resolution_failure(1640 self,1641 diag,1642 path_str,1643 disambiguator,1644 candidates.into_iter().filter_map(|res| res.err()).collect(),1645 );1646 vec![]1647 } else if len == 1 {1648 candidates.into_iter().filter_map(|res| res.ok()).flatten().collect::<Vec<_>>()1649 } else {1650 let has_derive_trait_collision = is_derive_trait_collision(&candidates);1651 if len == 2 && has_derive_trait_collision {1652 candidates.type_ns.unwrap()1653 } else {1654 // If we're reporting an ambiguity, don't mention the namespaces that failed1655 let mut candidates = candidates.map(|candidate| candidate.ok());1656 // If there a collision between a trait and a derive, we ignore the derive.1657 if has_derive_trait_collision {1658 candidates.macro_ns = None;1659 }1660 candidates.into_iter().flatten().flatten().collect::<Vec<_>>()1661 }1662 }1663 }1664 }1665 }1666}16671668/// Get the section of a link between the backticks,1669/// or the whole link if there aren't any backticks.1670///1671/// For example:1672///1673/// ```text1674/// [`Foo`]1675/// ^^^1676/// ```1677///1678/// This function does nothing if `ori_link.range` is a `MarkdownLinkRange::WholeLink`.1679fn range_between_backticks(ori_link_range: &MarkdownLinkRange, dox: &str) -> MarkdownLinkRange {1680 let range = match ori_link_range {1681 mdlr @ MarkdownLinkRange::WholeLink(_) => return mdlr.clone(),1682 MarkdownLinkRange::Destination(inner) => inner.clone(),1683 };1684 let ori_link_text = &dox[range.clone()];1685 let after_first_backtick_group = ori_link_text.bytes().position(|b| b != b'`').unwrap_or(0);1686 let before_second_backtick_group = ori_link_text1687 .bytes()1688 .skip(after_first_backtick_group)1689 .position(|b| b == b'`')1690 .unwrap_or(ori_link_text.len());1691 MarkdownLinkRange::Destination(1692 (range.start + after_first_backtick_group)..(range.start + before_second_backtick_group),1693 )1694}16951696/// Returns true if we should ignore `link` due to it being unlikely1697/// that it is an intra-doc link. `link` should still have disambiguators1698/// if there were any.1699///1700/// The difference between this and [`should_ignore_link()`] is that this1701/// check should only be used on links that still have disambiguators.1702fn should_ignore_link_with_disambiguators(link: &str) -> bool {1703 link.contains(|ch: char| !(ch.is_alphanumeric() || ":_<>, !*&;@()".contains(ch)))1704}17051706/// Returns true if we should ignore `path_str` due to it being unlikely1707/// that it is an intra-doc link.1708fn should_ignore_link(path_str: &str) -> bool {1709 path_str.contains(|ch: char| !(ch.is_alphanumeric() || ":_<>, !*&;".contains(ch)))1710}17111712#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]1713/// Disambiguators for a link.1714enum Disambiguator {1715 /// `prim@`1716 ///1717 /// This is buggy, see <https://github.com/rust-lang/rust/pull/77875#discussion_r503583103>1718 Primitive,1719 /// `struct@` or `f()`1720 Kind(DefKind),1721 /// `type@`1722 Namespace(Namespace),1723}17241725impl Disambiguator {1726 /// Given a link, parse and return `(disambiguator, path_str, link_text)`.1727 ///1728 /// This returns `Ok(Some(...))` if a disambiguator was found,1729 /// `Ok(None)` if no disambiguator was found, or `Err(...)`1730 /// if there was a problem with the disambiguator.1731 fn from_str(link: &str) -> Result<Option<(Self, &str, &str)>, (String, Range<usize>)> {1732 use Disambiguator::{Kind, Namespace as NS, Primitive};17331734 let suffixes = [1735 // If you update this list, please also update the relevant rustdoc book section!1736 ("!()", DefKind::Macro(MacroKinds::BANG)),1737 ("!{}", DefKind::Macro(MacroKinds::BANG)),1738 ("![]", DefKind::Macro(MacroKinds::BANG)),1739 ("()", DefKind::Fn),1740 ("!", DefKind::Macro(MacroKinds::BANG)),1741 ];17421743 if let Some(idx) = link.find('@') {1744 let (prefix, rest) = link.split_at(idx);1745 let d = match prefix {1746 // If you update this list, please also update the relevant rustdoc book section!1747 "struct" => Kind(DefKind::Struct),1748 "enum" => Kind(DefKind::Enum),1749 "trait" => Kind(DefKind::Trait),1750 "union" => Kind(DefKind::Union),1751 "module" | "mod" => Kind(DefKind::Mod),1752 "const" | "constant" => Kind(DefKind::Const { is_type_const: false }),1753 "static" => Kind(DefKind::Static {1754 mutability: Mutability::Not,1755 nested: false,1756 safety: Safety::Safe,1757 }),1758 "function" | "fn" | "method" => Kind(DefKind::Fn),1759 "derive" => Kind(DefKind::Macro(MacroKinds::DERIVE)),1760 "field" => Kind(DefKind::Field),1761 "variant" => Kind(DefKind::Variant),1762 "type" => NS(Namespace::TypeNS),1763 "value" => NS(Namespace::ValueNS),1764 "macro" => NS(Namespace::MacroNS),1765 "prim" | "primitive" => Primitive,1766 "tyalias" | "typealias" => Kind(DefKind::TyAlias),1767 _ => return Err((format!("unknown disambiguator `{prefix}`"), 0..idx)),1768 };17691770 for (suffix, kind) in suffixes {1771 if let Some(path_str) = rest.strip_suffix(suffix) {1772 if d.ns() != Kind(kind).ns() {1773 return Err((1774 format!("unmatched disambiguator `{prefix}` and suffix `{suffix}`"),1775 0..idx,1776 ));1777 } else if path_str.len() > 1 {1778 // path_str != "@"1779 return Ok(Some((d, &path_str[1..], &rest[1..])));1780 }1781 }1782 }17831784 Ok(Some((d, &rest[1..], &rest[1..])))1785 } else {1786 for (suffix, kind) in suffixes {1787 // Avoid turning `!` or `()` into an empty string1788 if let Some(path_str) = link.strip_suffix(suffix)1789 && !path_str.is_empty()1790 {1791 return Ok(Some((Kind(kind), path_str, link)));1792 }1793 }1794 Ok(None)1795 }1796 }17971798 fn ns(self) -> Namespace {1799 match self {1800 Self::Namespace(n) => n,1801 // for purposes of link resolution, fields are in the value namespace.1802 Self::Kind(DefKind::Field) => ValueNS,1803 Self::Kind(k) => {1804 k.ns().expect("only DefKinds with a valid namespace can be disambiguators")1805 }1806 Self::Primitive => TypeNS,1807 }1808 }18091810 fn article(self) -> &'static str {1811 match self {1812 Self::Namespace(_) => panic!("article() doesn't make sense for namespaces"),1813 Self::Kind(k) => k.article(),1814 Self::Primitive => "a",1815 }1816 }18171818 fn descr(self) -> &'static str {1819 match self {1820 Self::Namespace(n) => n.descr(),1821 // HACK(jynelson): the source of `DefKind::descr` only uses the DefId for1822 // printing "module" vs "crate" so using the wrong ID is not a huge problem1823 Self::Kind(k) => k.descr(CRATE_DEF_ID.to_def_id()),1824 Self::Primitive => "builtin type",1825 }1826 }1827}18281829/// A suggestion to show in a diagnostic.1830enum Suggestion {1831 /// `struct@`1832 Prefix(&'static str),1833 /// `f()`1834 Function,1835 /// `m!`1836 Macro,1837}18381839impl Suggestion {1840 fn descr(&self) -> Cow<'static, str> {1841 match self {1842 Self::Prefix(x) => format!("prefix with `{x}@`").into(),1843 Self::Function => "add parentheses".into(),1844 Self::Macro => "add an exclamation mark".into(),1845 }1846 }18471848 fn as_help(&self, path_str: &str) -> String {1849 // FIXME: if this is an implied shortcut link, it's bad style to suggest `@`1850 match self {1851 Self::Prefix(prefix) => format!("{prefix}@{path_str}"),1852 Self::Function => format!("{path_str}()"),1853 Self::Macro => format!("{path_str}!"),1854 }1855 }18561857 fn as_help_span(1858 &self,1859 ori_link: &str,1860 sp: rustc_span::Span,1861 ) -> Vec<(rustc_span::Span, String)> {1862 let inner_sp = match ori_link.find('(') {1863 Some(index) if index != 0 && ori_link.as_bytes()[index - 1] == b'\\' => {1864 sp.with_hi(sp.lo() + BytePos((index - 1) as _))1865 }1866 Some(index) => sp.with_hi(sp.lo() + BytePos(index as _)),1867 None => sp,1868 };1869 let inner_sp = match ori_link.find('!') {1870 Some(index) if index != 0 && ori_link.as_bytes()[index - 1] == b'\\' => {1871 sp.with_hi(sp.lo() + BytePos((index - 1) as _))1872 }1873 Some(index) => inner_sp.with_hi(inner_sp.lo() + BytePos(index as _)),1874 None => inner_sp,1875 };1876 let inner_sp = match ori_link.find('@') {1877 Some(index) if index != 0 && ori_link.as_bytes()[index - 1] == b'\\' => {1878 sp.with_hi(sp.lo() + BytePos((index - 1) as _))1879 }1880 Some(index) => inner_sp.with_lo(inner_sp.lo() + BytePos(index as u32 + 1)),1881 None => inner_sp,1882 };1883 match self {1884 Self::Prefix(prefix) => {1885 // FIXME: if this is an implied shortcut link, it's bad style to suggest `@`1886 let mut sugg = vec![(sp.with_hi(inner_sp.lo()), format!("{prefix}@"))];1887 if sp.hi() != inner_sp.hi() {1888 sugg.push((inner_sp.shrink_to_hi().with_hi(sp.hi()), String::new()));1889 }1890 sugg1891 }1892 Self::Function => {1893 let mut sugg = vec![(inner_sp.shrink_to_hi().with_hi(sp.hi()), "()".to_string())];1894 if sp.lo() != inner_sp.lo() {1895 sugg.push((inner_sp.shrink_to_lo().with_lo(sp.lo()), String::new()));1896 }1897 sugg1898 }1899 Self::Macro => {1900 let mut sugg = vec![(inner_sp.shrink_to_hi(), "!".to_string())];1901 if sp.lo() != inner_sp.lo() {1902 sugg.push((inner_sp.shrink_to_lo().with_lo(sp.lo()), String::new()));1903 }1904 sugg1905 }1906 }1907 }1908}19091910/// Reports a diagnostic for an intra-doc link.1911///1912/// If no link range is provided, or the source span of the link cannot be determined, the span of1913/// the entire documentation block is used for the lint. If a range is provided but the span1914/// calculation fails, a note is added to the diagnostic pointing to the link in the markdown.1915///1916/// The `decorate` callback is invoked in all cases to allow further customization of the1917/// diagnostic before emission. If the span of the link was able to be determined, the second1918/// parameter of the callback will contain it, and the primary span of the diagnostic will be set1919/// to it.1920fn report_diagnostic(1921 tcx: TyCtxt<'_>,1922 lint: &'static Lint,1923 msg: impl Into<DiagMessage> + Display,1924 DiagnosticInfo { item, ori_link: _, dox, link_range }: &DiagnosticInfo<'_>,1925 decorate: impl FnOnce(&mut Diag<'_, ()>, Option<rustc_span::Span>, MarkdownLinkRange),1926) {1927 let Some(hir_id) = DocContext::as_local_hir_id(tcx, item.item_id) else {1928 // If non-local, no need to check anything.1929 info!("ignoring warning from parent crate: {msg}");1930 return;1931 };19321933 let sp = item.attr_span(tcx);19341935 tcx.emit_node_span_lint(1936 lint,1937 hir_id,1938 sp,1939 rustc_errors::DiagDecorator(|lint| {1940 lint.primary_message(msg);19411942 let (span, link_range) = match link_range {1943 MarkdownLinkRange::Destination(md_range) => {1944 let mut md_range = md_range.clone();1945 let sp = source_span_for_markdown_range(1946 tcx,1947 dox,1948 &md_range,1949 &item.attrs.doc_strings,1950 )1951 .map(|(mut sp, _)| {1952 while dox.as_bytes().get(md_range.start) == Some(&b' ')1953 || dox.as_bytes().get(md_range.start) == Some(&b'`')1954 {1955 md_range.start += 1;1956 sp = sp.with_lo(sp.lo() + BytePos(1));1957 }1958 while dox.as_bytes().get(md_range.end - 1) == Some(&b' ')1959 || dox.as_bytes().get(md_range.end - 1) == Some(&b'`')1960 {1961 md_range.end -= 1;1962 sp = sp.with_hi(sp.hi() - BytePos(1));1963 }1964 sp1965 });1966 (sp, MarkdownLinkRange::Destination(md_range))1967 }1968 MarkdownLinkRange::WholeLink(md_range) => (1969 source_span_for_markdown_range(tcx, dox, md_range, &item.attrs.doc_strings)1970 .map(|(sp, _)| sp),1971 link_range.clone(),1972 ),1973 };19741975 if let Some(sp) = span {1976 lint.span(sp);1977 } else {1978 // blah blah blah\nblah\nblah [blah] blah blah\nblah blah1979 // ^ ~~~~1980 // | link_range1981 // last_new_line_offset1982 let md_range = link_range.inner_range().clone();1983 let last_new_line_offset = dox[..md_range.start].rfind('\n').map_or(0, |n| n + 1);1984 let line = dox[last_new_line_offset..].lines().next().unwrap_or("");19851986 // Print the line containing the `md_range` and manually mark it with '^'s.1987 lint.note(format!(1988 "the link appears in this line:\n\n{line}\n\1989 {indicator: <before$}{indicator:^<found$}",1990 indicator = "",1991 before = md_range.start - last_new_line_offset,1992 found = md_range.len(),1993 ));1994 }19951996 decorate(lint, span, link_range);1997 }),1998 );1999}
Findings
✓ No findings reported for this file.