Skip to content

[rustdoc] Render Deref methods on aliased items - #163571

Open
GuillaumeGomez wants to merge 6 commits into
rust-lang:mainfrom
GuillaumeGomez:deref-on-aliased
Open

GuillaumeGomez wants to merge 6 commits into
rust-lang:mainfrom
GuillaumeGomez:deref-on-aliased

Conversation

@GuillaumeGomez

@GuillaumeGomez GuillaumeGomez commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

View all comments

Fixes #134868.
Fixes #24686.

The biggest issue was to make it work only for the matching aliased type generics. Luckily for me, it was already done in write_shared.rs (where we generate the JS file containing the impls that we then put into the generated web pages, which makes me wonder why we don't put that directly into the page by default, but debate for another day...).

r? @lolbinarycat

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-rustdoc Relevant to the rustdoc team, which will review and decide on the PR/issue. T-rustdoc-frontend Relevant to the rustdoc-frontend team, which will review and decide on the web UI/UX output. labels Sep 30, 2026
@rustbot

rustbot commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator

lolbinarycat is currently at their maximum review capacity.
They may take a while to respond.

let mut assoc_types = Vec::new();
let mut assoc_fns = Vec::new();
let mut methods = Vec::new();
if let Some(v) = cache.impls.get(&did) {

@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.

Most of this diff comes from the removal of one level of indent which became (a few lines above):

    let v = match cache.impls.get(&item_def_id) {
        Some(impls) => impls.as_slice(),
        None if aliased_type.is_some() => &[],
        None => return,
    };

Since we don't render anything, we should have returned early. Now it's done, but ugly diff.

View changes since the review

@@ -54,7 +54,7 @@ impl std::ops::Deref for GenericStruct<u32> {
pub struct Wrap<T>(GenericStruct<T>);

//@ has 'foo/type.Alias.html'
//@ !has - '//h2' 'Methods from Deref<Target = u32>'
//@ has - '//h2' 'Methods from Deref<Target = u32>'

@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.

Now we have a Deref on type aliases, so we update here. :3

View changes since the review

let for_ty = self.cx.tcx().type_of(impl_did).skip_binder();
let reject_cx = DeepRejectCtxt::relate_infer_infer(self.cx.tcx());
if !reject_cx.types_may_unify(aliased_ty, for_ty) {
if !super::impl_may_apply_to_type_alias(self.cx.tcx(), self_did, impl_did) {

@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.

Like I mentioned in the first comment, took this code and re-used it to filter which Deref was matching the aliased ty.

View changes since the review

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

@lolbinarycat lolbinarycat left a comment •

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.

Only major issue I see is I think aliased_type_deref_impl is slightly the wrong level of encapsulation for a function (further explained below), otherwise the code looks solid.

View changes since this review

Comment thread src/librustdoc/html/render/mod.rs Outdated
) -> 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)

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.

I thinkj we can use types_may_unify in the type alias case of is_doc_subtype_of to improve #24686 significantly.

WIP diff of this idea
diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs
index a4ec40a0035..09f253ef643 100644
--- a/src/librustdoc/clean/types.rs
+++ b/src/librustdoc/clean/types.rs
@@ -1464,7 +1464,7 @@ fn is_type_alias(&self) -> bool {
     ///
     /// 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() {
@@ -1473,29 +1473,35 @@ pub(crate) fn is_doc_subtype_of(&self, other: &Self, cache: &Cache) -> bool {
             (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;
+            if let Some(self_did) = self_cleared.def_id(&cx.cache()) &&
+                let Some(other_did) = other_cleared.def_id(&cx.cache())
+            {
+                let tcx = cx.tcx();
+
+                    return crate::clean::ty::DeepRejectCtxt::relate_infer_infer(cx.tcx()).types_may_unify(tcx.type_of(self_did).skip_binder(), tcx.type_of(other_did).skip_binder());
+                }
+
         }
 
         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.
@@ -1509,13 +1515,13 @@ pub(crate) fn is_doc_subtype_of(&self, other: &Self, cache: &Cache) -> bool {
                 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),
         }
diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs
index a9ecf6d66d0..184591d350e 100644
--- a/src/librustdoc/html/render/mod.rs
+++ b/src/librustdoc/html/render/mod.rs
@@ -1506,7 +1506,7 @@ fn render_assoc_items_inner(
                 // 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)
+                    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()) {
@@ -1694,7 +1694,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()))
@@ -1724,7 +1724,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_
diff --git a/src/librustdoc/html/render/sidebar.rs b/src/librustdoc/html/render/sidebar.rs
index 6fa033f927c..cdd429da918 100644
--- a/src/librustdoc/html/render/sidebar.rs
+++ b/src/librustdoc/html/render/sidebar.rs
@@ -561,7 +561,7 @@ fn sidebar_deref_methods<'a>(
                 .iter()
                 .filter(|i| {
                     i.inner_impl().trait_.is_none()
-                        && real_target.is_doc_subtype_of(&i.inner_impl().for_, c)
+                        && real_target.is_doc_subtype_of(&i.inner_impl().for_, cx)
                 })
                 .flat_map(|i| {
                     get_methods(

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.

Added it.

Comment thread src/librustdoc/html/render/mod.rs Outdated
if !impl_.is_negative_trait_impl() && applies(impl_) {
deref_impl = Some(impl_);
}
} else if impl_.trait_did() == tcx.lang_items().deref_mut_trait() && applies(impl_) {

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.

Maybe Impl should have utility functions trait_is_deref and trait_is_deref_mut so we don't need to keep using this snippet everywhere?

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.

Great idea!

Comment thread src/librustdoc/html/render/sidebar.rs Outdated
Comment on lines +504 to +512
let deref = if let Some(impl_) = v.iter().find(|i| {
i.trait_did() == cx.tcx().lang_items().deref_trait() && !i.is_negative_trait_impl()
}) {
let deref_mut = v.iter().any(|i| i.trait_did() == cx.tcx().lang_items().deref_mut_trait());
Some((impl_, deref_mut))
} else {
aliased_type
.and_then(|aliased_did| super::aliased_type_deref_impl(cx, item_def_id, aliased_did))
};

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 exactly repeats a snippet used earlier in render/mod.rs

I think instead of aliased_type_deref_impl we should just have a get_deref_impl that works in both the general and aliased case (might need a doc comment to explain what all the parameters mean, or maybe just a renaming of the parameters).

@lolbinarycat

Copy link
Copy Markdown
Contributor

@rustbot author

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Oct 1, 2026
@lolbinarycat

Copy link
Copy Markdown
Contributor

one more thing: can you check locally if these change fixes the issues with ByteString?

@GuillaumeGomez

Copy link
Copy Markdown
Member Author

one more thing: can you check locally if these change fixes the issues with ByteString?

Confirmed it's fixed. Added the code from the first comment of the issue as regression test.

@GuillaumeGomez

Copy link
Copy Markdown
Member Author

Applied all suggestions.

@rustbot ready

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Oct 2, 2026
@rust-log-analyzer

This comment has been minimized.

@GuillaumeGomez

Copy link
Copy Markdown
Member Author

Had to remove the is_same_generic unit test because we can't call is_doc_subtype_of without Context and in particular TyCtxt anymore.

@lolbinarycat lolbinarycat left a comment •

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.

few more nits, but looks pretty good!

View changes since this review

Comment on lines +3107 to 3108
if trait_.as_ref().is_some_and(|t| t.is_deref_trait(tcx))
&& tcx.impl_polarity(def_id) != ty::ImplPolarity::Negative

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.

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. :')

@GuillaumeGomez

Copy link
Copy Markdown
Member Author

Added a test which illustrates the current limitations around type aliases.

@rust-bors

This comment has been minimized.

@rustbot

This comment has been minimized.

@GuillaumeGomez

Copy link
Copy Markdown
Member Author

CI passed.

@rust-bors

This comment has been minimized.

@rustbot

rustbot commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-rustdoc Relevant to the rustdoc team, which will review and decide on the PR/issue. T-rustdoc-frontend Relevant to the rustdoc-frontend team, which will review and decide on the web UI/UX output.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Methods from Deref<Target=[T]> doesnt appear for type aliases rustdoc: Methods from Deref<Target = T<U>> is imprecise (contains false positives)

4 participants