Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 132 additions & 118 deletions src/librustdoc/passes/collect_intra_doc_links.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,24 +308,27 @@ impl<'tcx> LinkCollector<'_, 'tcx> {
let ty_res = self.resolve_path(path, TypeNS, item_id, module_id).ok_or_else(no_res)?;

match ty_res {
Res::Def(DefKind::Enum, did) => match tcx.type_of(did).instantiate_identity().kind() {
ty::Adt(def, _) if def.is_enum() => {
if let Some(variant) = def.variants().iter().find(|v| v.name == variant_name)
&& let Some(field) =
variant.fields.iter().find(|f| f.name == variant_field_name)
{
Ok((ty_res, field.did))
} else {
Err(UnresolvedPath {
item_id,
module_id,
partial_res: Some(Res::Def(DefKind::Enum, def.did())),
unresolved: variant_field_name.to_string().into(),
})
Res::Def(DefKind::Enum | DefKind::TyAlias, did) => {
match tcx.type_of(did).instantiate_identity().kind() {
ty::Adt(def, _) if def.is_enum() => {
if let Some(variant) =
def.variants().iter().find(|v| v.name == variant_name)
&& let Some(field) =
variant.fields.iter().find(|f| f.name == variant_field_name)
{
Ok((ty_res, field.did))
} else {
Err(UnresolvedPath {
item_id,
module_id,
partial_res: Some(Res::Def(DefKind::Enum, def.did())),
unresolved: variant_field_name.to_string().into(),
})
}
}
_ => unreachable!(),
}
_ => unreachable!(),
},
}
_ => Err(UnresolvedPath {
item_id,
module_id,
Expand Down Expand Up @@ -381,7 +384,7 @@ impl<'tcx> LinkCollector<'_, 'tcx> {
};

match tcx.def_kind(self_id) {
DefKind::Impl { .. } => self.def_id_to_res(self_id),
DefKind::Impl { .. } => self.ty_to_res(tcx.type_of(self_id).instantiate_identity()),
DefKind::Use => None,
def_kind => Some(Res::Def(def_kind, self_id)),
}
Expand Down Expand Up @@ -503,12 +506,12 @@ impl<'tcx> LinkCollector<'_, 'tcx> {
}
}

/// Convert a DefId to a Res, where possible.
/// Convert a Ty to a Res, where possible.
///
/// This is used for resolving type aliases.
fn def_id_to_res(&self, ty_id: DefId) -> Option<Res> {
fn ty_to_res(&self, ty: Ty<'tcx>) -> Option<Res> {
use PrimitiveType::*;
Some(match *self.cx.tcx.type_of(ty_id).instantiate_identity().kind() {
Some(match *ty.kind() {
ty::Bool => Res::Primitive(Bool),
ty::Char => Res::Primitive(Char),
ty::Int(ity) => Res::Primitive(ity.into()),
Expand Down Expand Up @@ -611,119 +614,130 @@ impl<'tcx> LinkCollector<'_, 'tcx> {
// Resolve the link on the type the alias points to.
// FIXME: if the associated item is defined directly on the type alias,
// it will show up on its documentation page, we should link there instead.
let Some(res) = self.def_id_to_res(did) else { return Vec::new() };
let Some(res) = self.ty_to_res(tcx.type_of(did).instantiate_identity()) else {
return Vec::new();
};
self.resolve_associated_item(res, item_name, ns, disambiguator, module_id)
}
Res::Def(
def_kind @ (DefKind::Struct | DefKind::Union | DefKind::Enum | DefKind::ForeignTy),
did,
) => {
debug!("looking for associated item named {item_name} for item {did:?}");
// Checks if item_name is a variant of the `SomeItem` enum
if ns == TypeNS && def_kind == DefKind::Enum {
match tcx.type_of(did).instantiate_identity().kind() {
ty::Adt(adt_def, _) => {
for variant in adt_def.variants() {
if variant.name == item_name {
return vec![(root_res, variant.def_id)];
}
}
) => self.resolve_assoc_on_adt(),
Res::Def(DefKind::Trait, did) => filter_assoc_items_by_name_and_namespace(
tcx,
did,
Ident::with_dummy_span(item_name),
ns,
)
.map(|item| (root_res, item.def_id))
.collect::<Vec<_>>(),
_ => Vec::new(),
}
}

fn resolve_assoc_on_adt(
&mut self,
adt_def_kind: DefKind,
adt_def_id: DefId,
item_name: Symbol,
ns: Namespace,
disambiguator: Option<Disambiguator>,
module_id: DefId,
) -> Vec<(Res, DefId)> {
let tcx = self.cx.tcx;
let root_res = Res::Def(adt_def_kind, adt_def_id);
debug!("looking for associated item named {item_name} for item {adt_def_id:?}");
// Checks if item_name is a variant of the `SomeItem` enum
if ns == TypeNS && adt_def_kind == DefKind::Enum {
match tcx.type_of(adt_def_id).instantiate_identity().kind() {
ty::Adt(adt_def, _) => {
for variant in adt_def.variants() {
if variant.name == item_name {
return vec![(root_res, variant.def_id)];
}
_ => unreachable!(),
}
}
_ => unreachable!(),
}
}

let search_for_field = || {
let (DefKind::Struct | DefKind::Union) = def_kind else { return vec![] };
debug!("looking for fields named {item_name} for {did:?}");
// FIXME: this doesn't really belong in `associated_item` (maybe `variant_field` is better?)
// NOTE: it's different from variant_field because it only resolves struct fields,
// not variant fields (2 path segments, not 3).
//
// We need to handle struct (and union) fields in this code because
// syntactically their paths are identical to associated item paths:
// `module::Type::field` and `module::Type::Assoc`.
//
// On the other hand, variant fields can't be mistaken for associated
// items because they look like this: `module::Type::Variant::field`.
//
// Variants themselves don't need to be handled here, even though
// they also look like associated items (`module::Type::Variant`),
// because they are real Rust syntax (unlike the intra-doc links
// field syntax) and are handled by the compiler's resolver.
let ty::Adt(def, _) = tcx.type_of(did).instantiate_identity().kind() else {
unreachable!()
};
def.non_enum_variant()
.fields
.iter()
.filter(|field| field.name == item_name)
.map(|field| (root_res, field.did))
.collect::<Vec<_>>()
};

if let Some(Disambiguator::Kind(DefKind::Field)) = disambiguator {
return search_for_field();
}
let search_for_field = || {
let (DefKind::Struct | DefKind::Union) = adt_def_kind else { return vec![] };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't work for enum's variants with fields I guess?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's handled separately in the variant_field function. The reason there's a distinction is that paths to struct and union fields look the same as paths to enum variants and associated items (two segments), while paths to enum variant fields look different (three segments).

I agree that it's confusing that they're handled separately though. I think it would be simpler if we don't do this sort of path length-based separation.

Copy link
Member Author

@camelid camelid Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this length-based logic means that re-exports of variants don't work properly (can't look up their fields). Although this is a pre-existing bug, we should definitely try to fix it in a future PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please open an issue. :)

debug!("looking for fields named {item_name} for {adt_def_id:?}");
// FIXME: this doesn't really belong in `associated_item` (maybe `variant_field` is better?)
// NOTE: it's different from variant_field because it only resolves struct fields,
// not variant fields (2 path segments, not 3).
//
// We need to handle struct (and union) fields in this code because
// syntactically their paths are identical to associated item paths:
// `module::Type::field` and `module::Type::Assoc`.
//
// On the other hand, variant fields can't be mistaken for associated
// items because they look like this: `module::Type::Variant::field`.
//
// Variants themselves don't need to be handled here, even though
// they also look like associated items (`module::Type::Variant`),
// because they are real Rust syntax (unlike the intra-doc links
// field syntax) and are handled by the compiler's resolver.
let ty::Adt(def, _) = tcx.type_of(adt_def_id).instantiate_identity().kind() else {
unreachable!()
};
def.non_enum_variant()
.fields
.iter()
.filter(|field| field.name == item_name)
.map(|field| (root_res, field.did))
.collect::<Vec<_>>()
};

// Checks if item_name belongs to `impl SomeItem`
let mut assoc_items: Vec<_> = tcx
.inherent_impls(did)
.iter()
.flat_map(|&imp| {
filter_assoc_items_by_name_and_namespace(
tcx,
imp,
Ident::with_dummy_span(item_name),
ns,
)
})
.map(|item| (root_res, item.def_id))
.collect();

if assoc_items.is_empty() {
// Check if item_name belongs to `impl SomeTrait for SomeItem`
// FIXME(#74563): This gives precedence to `impl SomeItem`:
// Although having both would be ambiguous, use impl version for compatibility's sake.
// To handle that properly resolve() would have to support
// something like [`ambi_fn`](<SomeStruct as SomeTrait>::ambi_fn)
assoc_items = resolve_associated_trait_item(
tcx.type_of(did).instantiate_identity(),
module_id,
item_name,
ns,
self.cx,
)
.into_iter()
.map(|item| (root_res, item.def_id))
.collect::<Vec<_>>();
}
if let Some(Disambiguator::Kind(DefKind::Field)) = disambiguator {
return search_for_field();
}

debug!("got associated item {assoc_items:?}");
// Checks if item_name belongs to `impl SomeItem`
let mut assoc_items: Vec<_> = tcx
.inherent_impls(adt_def_id)
.iter()
.flat_map(|&imp| {
filter_assoc_items_by_name_and_namespace(
tcx,
imp,
Ident::with_dummy_span(item_name),
ns,
)
})
.map(|item| (root_res, item.def_id))
.collect();

if assoc_items.is_empty() {
// Check if item_name belongs to `impl SomeTrait for SomeItem`
// FIXME(#74563): This gives precedence to `impl SomeItem`:
// Although having both would be ambiguous, use impl version for compatibility's sake.
// To handle that properly resolve() would have to support
// something like [`ambi_fn`](<SomeStruct as SomeTrait>::ambi_fn)
assoc_items = resolve_associated_trait_item(
tcx.type_of(adt_def_id).instantiate_identity(),
module_id,
item_name,
ns,
self.cx,
)
.into_iter()
.map(|item| (root_res, item.def_id))
.collect::<Vec<_>>();
}

if !assoc_items.is_empty() {
return assoc_items;
}
debug!("got associated item {assoc_items:?}");

if ns != Namespace::ValueNS {
return Vec::new();
}
if !assoc_items.is_empty() {
return assoc_items;
}

search_for_field()
}
Res::Def(DefKind::Trait, did) => filter_assoc_items_by_name_and_namespace(
tcx,
did,
Ident::with_dummy_span(item_name),
ns,
)
.map(|item| {
let res = Res::Def(item.as_def_kind(), item.def_id);
(res, item.def_id)
})
.collect::<Vec<_>>(),
_ => Vec::new(),
if ns != Namespace::ValueNS {
return Vec::new();
}

search_for_field()
}
}

Expand Down
25 changes: 25 additions & 0 deletions tests/rustdoc-html/intra-doc/adt-through-alias.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#![crate_name = "foo"]

//! [`TheStructAlias::the_field`]
//! [`TheEnumAlias::TheVariant`]
//! [`TheEnumAlias::TheVariant::the_field`]

// FIXME: this should resolve to the alias's version
//@ has foo/index.html '//a[@href="struct.TheStruct.html#structfield.the_field"]' 'TheStructAlias::the_field'
// FIXME: this should resolve to the alias's version
//@ has foo/index.html '//a[@href="enum.TheEnum.html#variant.TheVariant"]' 'TheEnumAlias::TheVariant'
//@ has foo/index.html '//a[@href="type.TheEnumAlias.html#variant.TheVariant.field.the_field"]' 'TheEnumAlias::TheVariant::the_field'

pub struct TheStruct {
pub the_field: i32,
}

pub type TheStructAlias = TheStruct;

pub enum TheEnum {
TheVariant { the_field: i32 },
}

pub type TheEnumAlias = TheEnum;

fn main() {}