Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 3 additions & 2 deletions src/librustdoc/clean/inline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -598,8 +598,9 @@ pub(crate) fn build_impl(
};
let trait_ = associated_trait
.map(|t| clean_trait_ref_with_constraints(cx, ty::Binder::dummy(t), ThinVec::new()));
if trait_.as_ref().map(|t| t.def_id()) == tcx.lang_items().deref_trait()
&& polarity != ty::ImplPolarity::Negative
if trait_
.as_ref()
.is_some_and(|t| polarity != ty::ImplPolarity::Negative && t.is_deref_trait(tcx))
{
super::build_deref_target_impls(cx, &trait_items, ret);
}
Expand Down
2 changes: 1 addition & 1 deletion src/librustdoc/clean/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3104,7 +3104,7 @@ fn clean_impl<'tcx>(

// If this impl block is a positive implementation of the Deref trait, then we
// need to try inlining the target's inherent impl blocks as well.
if trait_.as_ref().is_some_and(|t| tcx.lang_items().deref_trait() == Some(t.def_id()))
if trait_.as_ref().is_some_and(|t| t.is_deref_trait(tcx))
&& tcx.impl_polarity(def_id) != ty::ImplPolarity::Negative
Comment on lines +3107 to 3108

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the impl polarity test maybe should go in is_deref_trait, as we'll usually care about that, but then it might need a new name? subjective.

@GuillaumeGomez GuillaumeGomez Oct 5, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hum... Fair point, gonna rename it is_non_negative_deref_trait.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

On second thought, in some cases the the trait polarity is queried ahead of time, so I'll them them separate for now.

{
build_deref_target_impls(cx, &items, &mut ret);
Expand Down
56 changes: 40 additions & 16 deletions src/librustdoc/clean/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use rustc_hir::def_id::{CrateNum, DefId, LOCAL_CRATE, LocalDefId};
use rustc_hir::{BodyId, Mutability};
use rustc_index::IndexVec;
use rustc_metadata::rendered_const;
use rustc_middle::ty::fast_reject::SimplifiedType;
use rustc_middle::ty::fast_reject::{DeepRejectCtxt, SimplifiedType};
use rustc_middle::ty::{self, Ty, TyCtxt, Visibility};
use rustc_resolve::rustdoc::{
DocFragment, add_doc_fragment, attrs_to_doc_fragments, span_of_fragments,
Expand Down Expand Up @@ -1380,6 +1380,22 @@ pub(crate) struct PolyTrait {
pub(crate) generic_params: Vec<GenericParamDef>,
}

// Also used in `write_shared.rs` when generating the JS `impl` file for aliased types.
//
// FIXME(checked_type_alias): Once the feature is complete or stable, rewrite this
// to use type unification.
// Be aware of `tests/rustdoc-html/type-alias/deeply-nested-112515.rs` which might
// regress.
pub(crate) fn impl_may_apply_to_type_alias(
tcx: TyCtxt<'_>,
alias_def_id: DefId,
impl_def_id: DefId,
) -> bool {
let alias_ty = tcx.type_of(alias_def_id).skip_binder();
let for_ty = tcx.type_of(impl_def_id).skip_binder();
DeepRejectCtxt::relate_infer_infer(tcx).types_may_unify(alias_ty, for_ty)
}

/// Rustdoc's representation of types, mostly based on the [`hir::Ty`].
#[derive(Clone, PartialEq, Eq, Debug, Hash)]
pub(crate) enum Type {
Expand Down Expand Up @@ -1469,7 +1485,7 @@ impl Type {
///
/// An owned type is also the same as its borrowed variants (this is commutative),
/// but `&T` is not the same as `&mut T`.
pub(crate) fn is_doc_subtype_of(&self, other: &Self, cache: &Cache) -> bool {
pub(crate) fn is_doc_subtype_of(&self, other: &Self, cx: &Context<'_>) -> bool {
// Strip the references so that it can compare the actual types, unless both are references.
// If both are references, leave them alone and compare the mutabilities later.
let (self_cleared, other_cleared) = if !self.is_borrowed_ref() || !other.is_borrowed_ref() {
Expand All @@ -1478,29 +1494,33 @@ impl Type {
(self, other)
};

// FIXME: `Cache` does not have the data required to unwrap type aliases,
// so we just assume they are equal.
// This is only remotely acceptable because we were previously
// assuming all types were equal when used
// as a generic parameter of a type in `Deref::Target`.
// FIXME(checked_type_alias): Once the feature is complete or stable, rewrite this
// to use type unification.
// Be aware of `tests/rustdoc-html/type-alias/deeply-nested-112515.rs` which might
// regress.
if self_cleared.is_type_alias() || other_cleared.is_type_alias() {
return true;
let cache = cx.cache();
if let Some(self_def_id) = self_cleared.def_id(&cache)
&& let Some(other_def_id) = other_cleared.def_id(&cache)
{
return impl_may_apply_to_type_alias(cx.tcx(), self_def_id, other_def_id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this (and my patch) are still subtly wrong, as the special rustdoc-specific rules is_doc_subtype_of implements won't apply when type aliases are involved. still an improvement over the status quo, but worth a note.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

How so?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

if i understand the code correctly, Option<u8> and Option<&u8> will compare as doc subtypes, but Option<AliasOfRefU8> and Option<u8> won't.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Might be worth a test to confirm that. Gonna add one.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Nope, it works. Adding test. =D

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ah no nevermind, the filtering is not working. Too laxist. Oh well, that will be a follow-up. :')

}
}

match (self_cleared, other_cleared) {
// Recursive cases.
(Type::Tuple(a), Type::Tuple(b)) => {
a.iter().eq_by(b, |a, b| a.is_doc_subtype_of(b, cache))
a.iter().eq_by(b, |a, b| a.is_doc_subtype_of(b, cx))
}
(Type::Slice(a), Type::Slice(b)) => a.is_doc_subtype_of(b, cache),
(Type::Array(a, al), Type::Array(b, bl)) => al == bl && a.is_doc_subtype_of(b, cache),
(Type::Slice(a), Type::Slice(b)) => a.is_doc_subtype_of(b, cx),
(Type::Array(a, al), Type::Array(b, bl)) => al == bl && a.is_doc_subtype_of(b, cx),
(Type::RawPointer(mutability, type_), Type::RawPointer(b_mutability, b_type_)) => {
mutability == b_mutability && type_.is_doc_subtype_of(b_type_, cache)
mutability == b_mutability && type_.is_doc_subtype_of(b_type_, cx)
}
(
Type::BorrowedRef { mutability, type_, .. },
Type::BorrowedRef { mutability: b_mutability, type_: b_type_, .. },
) => mutability == b_mutability && type_.is_doc_subtype_of(b_type_, cache),
) => mutability == b_mutability && type_.is_doc_subtype_of(b_type_, cx),
// Placeholders are equal to all other types.
(Type::Infer, _) | (_, Type::Infer) => true,
// Generics match everything on the right, but not on the left.
Expand All @@ -1514,13 +1534,13 @@ impl Type {
a.def_id() == b.def_id()
&& a.generics()
.zip(b.generics())
.map(|(ag, bg)| ag.zip(bg).all(|(at, bt)| at.is_doc_subtype_of(bt, cache)))
.map(|(ag, bg)| ag.zip(bg).all(|(at, bt)| at.is_doc_subtype_of(bt, cx)))
.unwrap_or(true)
}
// Other cases, such as primitives, just use recursion.
(a, b) => a
.def_id(cache)
.and_then(|a| Some((a, b.def_id(cache)?)))
.def_id(cx.cache())
.and_then(|a| Some((a, b.def_id(cx.cache())?)))
.map(|(a, b)| a == b)
.unwrap_or(false),
}
Expand Down Expand Up @@ -2149,6 +2169,10 @@ impl Path {
}
})
}

pub(crate) fn is_deref_trait(&self, tcx: TyCtxt<'_>) -> bool {
tcx.lang_items().deref_trait() == Some(self.def_id())
}
}

#[derive(Clone, PartialEq, Eq, Debug, Hash)]
Expand Down
13 changes: 0 additions & 13 deletions src/librustdoc/clean/types/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,3 @@ fn should_not_trim() {
run_test("\t line1 \n\t line2", "line1 \nline2");
run_test(" \tline1 \n \tline2", "line1 \nline2");
}

#[test]
fn is_same_generic() {
use crate::clean::types::{PrimitiveType, Type};
use crate::formats::cache::Cache;
create_default_session_globals_then(|| {
let cache = Cache::new(false, false);
let generic = Type::Generic(sym::character('T'));
let unit = Type::Primitive(PrimitiveType::Unit);
assert!(!generic.is_doc_subtype_of(&unit, &cache));
assert!(unit.is_doc_subtype_of(&generic, &cache));
})
}
9 changes: 9 additions & 0 deletions src/librustdoc/formats/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub(crate) mod renderer;

pub(crate) use renderer::{FormatRenderer, run_format};
use rustc_hir::def_id::DefId;
use rustc_middle::ty::TyCtxt;

use crate::clean::{self, ItemId};
use crate::html::render::Context;
Expand All @@ -26,6 +27,14 @@ impl Impl {
self.inner_impl().trait_.as_ref().map(|t| t.def_id())
}

pub(crate) fn is_deref_trait(&self, tcx: TyCtxt<'_>) -> bool {
self.trait_did() == tcx.lang_items().deref_trait()
}

pub(crate) fn is_deref_mut_trait(&self, tcx: TyCtxt<'_>) -> bool {
self.trait_did() == tcx.lang_items().deref_mut_trait()
}

/// This function is used to extract a `DefId` to be used as a key for the `Cache::impls` field.
///
/// It allows to prevent having duplicated implementations showing up (the biggest issue was
Expand Down
99 changes: 79 additions & 20 deletions src/librustdoc/html/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ mod sorted_template;
mod type_layout;
mod write_shared;

use std::borrow::Cow;
use std::borrow::{Borrow, Cow};
use std::cmp::Ordering;
use std::collections::{BTreeMap, VecDeque};
use std::fmt::{self, Display as _, Write};
Expand Down Expand Up @@ -1467,16 +1467,77 @@ fn render_all_impls(
Ok(())
}

fn aliased_type_deref_impl<'a>(
cx: &'a Context<'_>,
alias_def_id: DefId,
aliased_def_id: DefId,
) -> Option<(&'a Impl, bool)> {
let tcx = cx.tcx();
let applies = |impl_: &Impl| {
impl_.impl_item.item_id.as_def_id().is_some_and(|impl_def_id| {
clean::types::impl_may_apply_to_type_alias(tcx, alias_def_id, impl_def_id)
})
};
let impls = cx.cache().impls.get(&aliased_def_id)?;
let mut deref_impl = None;
let mut has_deref_mut = false;

for impl_ in impls {

@GuillaumeGomez GuillaumeGomez Sep 30, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

To avoid doing two loops (one to get Deref and the other to check if we also implement DerefMut), inlined it into one.

View changes since the review

if impl_.is_deref_trait(tcx) {
if !impl_.is_negative_trait_impl() && applies(impl_) {
deref_impl = Some(impl_);
}
} else if impl_.is_deref_mut_trait(tcx) && applies(impl_) {
has_deref_mut = true;
}
}
Some((deref_impl?, has_deref_mut))
}

/// Try to get the `Deref` trait `Impl` for `item`. If it cannot be found, it will try to do the
/// same with `aliased_type`. Returns a tuple containing the `Deref` `Impl`, and a bool set to
/// `true` if `DerefMut` is also implemented.
fn get_deref_impls<'a, I: Borrow<Impl>>(
cx: &'a Context<'_>,
trait_impls: &'a [I],
item: DefId,
aliased_type: Option<DefId>,
) -> Option<(&'a Impl, bool)> {
let tcx = cx.tcx();
let mut impls = trait_impls.iter().map(|t| t.borrow());
if let Some(impl_) =
impls.clone().find(|t| t.is_deref_trait(tcx) && !t.is_negative_trait_impl())
{
let has_deref_mut = impls.any(|t| t.is_deref_mut_trait(tcx));
Some((impl_, has_deref_mut))
} else {
aliased_type.and_then(|aliased_did| aliased_type_deref_impl(cx, item, aliased_did))
}
}

fn render_assoc_items(
cx: &Context<'_>,
containing_item: &clean::Item,
it: DefId,
what: AssocItemRender<'_>,
) -> impl fmt::Display {
render_assoc_items_with_aliased_type(cx, containing_item, it, what, None)
}

fn render_assoc_items_with_aliased_type(
cx: &Context<'_>,
containing_item: &clean::Item,
it: DefId,
what: AssocItemRender<'_>,
aliased_type: Option<DefId>,
) -> impl fmt::Display {
fmt::from_fn(move |f| {
let mut derefs = DefIdSet::default();
derefs.insert(it);
render_assoc_items_inner(f, cx, containing_item, it, what, &mut derefs)
if let Some(aliased_type) = aliased_type {
derefs.insert(aliased_type);
}
render_assoc_items_inner(f, cx, containing_item, it, what, &mut derefs, aliased_type)
})
}

Expand All @@ -1487,10 +1548,15 @@ fn render_assoc_items_inner(
it: DefId,
what: AssocItemRender<'_>,
derefs: &mut DefIdSet,
aliased_type: Option<DefId>,
) -> fmt::Result {
info!("Documenting associated items of {:?}", containing_item.name);
let cache = &cx.shared.cache;
let Some(impls) = cache.impls.get(&it) else { return Ok(()) };
let impls = match cache.impls.get(&it) {
Some(impls) => impls.as_slice(),
None if aliased_type.is_some() => &[],
None => return Ok(()),
};
let (mut inherent_impls, trait_impls): (Vec<_>, _) =
impls.iter().partition(|i| i.inner_impl().trait_.is_none());
if !inherent_impls.is_empty() {
Expand All @@ -1516,9 +1582,8 @@ fn render_assoc_items_inner(
// we should not show methods from `[MaybeUninit<u8>]`.
// this `retain` filters out any instances where
// the types do not line up perfectly.
inherent_impls.retain(|impl_| {
type_.is_doc_subtype_of(&impl_.inner_impl().for_, &cx.shared.cache)
});
inherent_impls
.retain(|impl_| type_.is_doc_subtype_of(&impl_.inner_impl().for_, &cx));
let derived_id = cx.derive_id(&id);
if let Some(def_id) = type_.def_id(cx.cache()) {
cx.deref_id_map.borrow_mut().insert(def_id, id.clone());
Expand Down Expand Up @@ -1578,17 +1643,11 @@ fn render_assoc_items_inner(
}
}

if !trait_impls.is_empty() {
let deref_impl = trait_impls.iter().find(|t| {
t.trait_did() == cx.tcx().lang_items().deref_trait() && !t.is_negative_trait_impl()
});
if let Some(impl_) = deref_impl {
let has_deref_mut = trait_impls
.iter()
.any(|t| t.trait_did() == cx.tcx().lang_items().deref_mut_trait());
render_deref_methods(&mut w, cx, impl_, containing_item, has_deref_mut, derefs)?;
}
if let Some((impl_, has_deref_mut)) = get_deref_impls(cx, &trait_impls, it, aliased_type) {
render_deref_methods(&mut w, cx, impl_, containing_item, has_deref_mut, derefs)?;
}

if !trait_impls.is_empty() {
// If we were already one level into rendering deref methods, we don't want to render
// anything after recursing into any further deref methods above.
if let AssocItemRender::DerefFor { .. } = what {
Expand Down Expand Up @@ -1646,11 +1705,11 @@ fn render_deref_methods(
return Ok(());
}
}
render_assoc_items_inner(&mut w, cx, container_item, did, what, derefs)?;
render_assoc_items_inner(&mut w, cx, container_item, did, what, derefs, None)?;
} else if let Some(prim) = target.primitive_type()
&& let Some(&did) = cache.primitive_locations.get(&prim)
{
render_assoc_items_inner(&mut w, cx, container_item, did, what, derefs)?;
render_assoc_items_inner(&mut w, cx, container_item, did, what, derefs, None)?;
}
Ok(())
}
Expand Down Expand Up @@ -1711,7 +1770,7 @@ fn notable_traits_button(ty: &clean::Type, cx: &Context<'_>) -> Option<impl fmt:
impl_.polarity == ty::ImplPolarity::Positive
// Two different types might have the same did,
// without actually being the same.
&& ty.is_doc_subtype_of(&impl_.for_, cx.cache())
&& ty.is_doc_subtype_of(&impl_.for_, cx)
})
.filter_map(|impl_| impl_.trait_.as_ref())
.filter_map(|trait_| cx.cache().traits.get(&trait_.def_id()))
Expand Down Expand Up @@ -1741,7 +1800,7 @@ fn notable_traits_decl(ty: &clean::Type, cx: &Context<'_>) -> (String, String) {
.filter(|impl_| impl_.polarity == ty::ImplPolarity::Positive)
.filter(|impl_| {
// Two different types might have the same did, without actually being the same.
ty.is_doc_subtype_of(&impl_.for_, cx.cache())
ty.is_doc_subtype_of(&impl_.for_, cx)
})
.filter_map(|impl_| {
if let Some(trait_) = &impl_.trait_
Expand Down
Loading
Loading