Skip to content

Commit 3cba764

Browse files
Rollup merge of #96754 - notriddle:notriddle/impl-dups, r=GuillaumeGomez
rustdoc: ensure HTML/JS side implementors don't have dups Fixes #94641 Rendered: - https://notriddle.com/notriddle-rustdoc-test/impl-dups/std/iter/trait.Iterator.html - https://notriddle.com/notriddle-rustdoc-test/impl-dups/core/iter/trait.Iterator.html
2 parents 031a314 + bd11e22 commit 3cba764

File tree

7 files changed

+127
-17
lines changed

7 files changed

+127
-17
lines changed

src/librustdoc/html/render/print_item.rs

+90-12
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use clean::AttributesExt;
33
use std::cmp::Ordering;
44
use std::fmt;
55

6-
use rustc_data_structures::fx::FxHashMap;
6+
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
77
use rustc_hir as hir;
88
use rustc_hir::def::CtorKind;
99
use rustc_hir::def_id::DefId;
@@ -795,16 +795,18 @@ fn item_trait(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, t: &clean::Tra
795795
render_assoc_items(w, cx, it, it.item_id.expect_def_id(), AssocItemRender::All);
796796

797797
let cache = cx.cache();
798+
let mut extern_crates = FxHashSet::default();
798799
if let Some(implementors) = cache.implementors.get(&it.item_id.expect_def_id()) {
799800
// The DefId is for the first Type found with that name. The bool is
800801
// if any Types with the same name but different DefId have been found.
801802
let mut implementor_dups: FxHashMap<Symbol, (DefId, bool)> = FxHashMap::default();
802803
for implementor in implementors {
803-
match implementor.inner_impl().for_ {
804-
clean::Type::Path { ref path }
805-
| clean::BorrowedRef { type_: box clean::Type::Path { ref path }, .. }
806-
if !path.is_assoc_ty() =>
807-
{
804+
if let Some(did) = implementor.inner_impl().for_.without_borrowed_ref().def_id(cx.cache()) &&
805+
!did.is_local() {
806+
extern_crates.insert(did.krate);
807+
}
808+
match implementor.inner_impl().for_.without_borrowed_ref() {
809+
clean::Type::Path { ref path } if !path.is_assoc_ty() => {
808810
let did = path.def_id();
809811
let &mut (prev_did, ref mut has_duplicates) =
810812
implementor_dups.entry(path.last()).or_insert((did, false));
@@ -903,20 +905,96 @@ fn item_trait(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, t: &clean::Tra
903905
}
904906
}
905907

908+
// Include implementors in crates that depend on the current crate.
909+
//
910+
// This is complicated by the way rustdoc is invoked, which is basically
911+
// the same way rustc is invoked: it gets called, one at a time, for each
912+
// crate. When building the rustdocs for the current crate, rustdoc can
913+
// see crate metadata for its dependencies, but cannot see metadata for its
914+
// dependents.
915+
//
916+
// To make this work, we generate a "hook" at this stage, and our
917+
// dependents can "plug in" to it when they build. For simplicity's sake,
918+
// it's [JSONP]: a JavaScript file with the data we need (and can parse),
919+
// surrounded by a tiny wrapper that the Rust side ignores, but allows the
920+
// JavaScript side to include without having to worry about Same Origin
921+
// Policy. The code for *that* is in `write_shared.rs`.
922+
//
923+
// This is further complicated by `#[doc(inline)]`. We want all copies
924+
// of an inlined trait to reference the same JS file, to address complex
925+
// dependency graphs like this one (lower crates depend on higher crates):
926+
//
927+
// ```text
928+
// --------------------------------------------
929+
// | crate A: trait Foo |
930+
// --------------------------------------------
931+
// | |
932+
// -------------------------------- |
933+
// | crate B: impl A::Foo for Bar | |
934+
// -------------------------------- |
935+
// | |
936+
// ---------------------------------------------
937+
// | crate C: #[doc(inline)] use A::Foo as Baz |
938+
// | impl Baz for Quux |
939+
// ---------------------------------------------
940+
// ```
941+
//
942+
// Basically, we want `C::Baz` and `A::Foo` to show the same set of
943+
// impls, which is easier if they both treat `/implementors/A/trait.Foo.js`
944+
// as the Single Source of Truth.
945+
//
946+
// We also want the `impl Baz for Quux` to be written to
947+
// `trait.Foo.js`. However, when we generate plain HTML for `C::Baz`,
948+
// we're going to want to generate plain HTML for `impl Baz for Quux` too,
949+
// because that'll load faster, and it's better for SEO. And we don't want
950+
// the same impl to show up twice on the same page.
951+
//
952+
// To make this work, the implementors JS file has a structure kinda
953+
// like this:
954+
//
955+
// ```js
956+
// JSONP({
957+
// "B": {"impl A::Foo for Bar"},
958+
// "C": {"impl Baz for Quux"},
959+
// });
960+
// ```
961+
//
962+
// First of all, this means we can rebuild a crate, and it'll replace its own
963+
// data if something changes. That is, `rustdoc` is idempotent. The other
964+
// advantage is that we can list the crates that get included in the HTML,
965+
// and ignore them when doing the JavaScript-based part of rendering.
966+
// So C's HTML will have something like this:
967+
//
968+
// ```html
969+
// <script type="text/javascript" src="/implementors/A/trait.Foo.js"
970+
// data-ignore-extern-crates="A,B" async></script>
971+
// ```
972+
//
973+
// And, when the JS runs, anything in data-ignore-extern-crates is known
974+
// to already be in the HTML, and will be ignored.
975+
//
976+
// [JSONP]: https://en.wikipedia.org/wiki/JSONP
906977
let mut js_src_path: UrlPartsBuilder = std::iter::repeat("..")
907978
.take(cx.current.len())
908979
.chain(std::iter::once("implementors"))
909980
.collect();
910-
if it.item_id.is_local() {
911-
js_src_path.extend(cx.current.iter().copied());
981+
if let Some(did) = it.item_id.as_def_id() &&
982+
let get_extern = { || cache.external_paths.get(&did).map(|s| s.0.clone()) } &&
983+
let Some(fqp) = cache.exact_paths.get(&did).cloned().or_else(get_extern) {
984+
js_src_path.extend(fqp[..fqp.len() - 1].iter().copied());
985+
js_src_path.push_fmt(format_args!("{}.{}.js", it.type_(), fqp.last().unwrap()));
912986
} else {
913-
let (ref path, _) = cache.external_paths[&it.item_id.expect_def_id()];
914-
js_src_path.extend(path[..path.len() - 1].iter().copied());
987+
js_src_path.extend(cx.current.iter().copied());
988+
js_src_path.push_fmt(format_args!("{}.{}.js", it.type_(), it.name.unwrap()));
915989
}
916-
js_src_path.push_fmt(format_args!("{}.{}.js", it.type_(), it.name.unwrap()));
990+
let extern_crates = extern_crates
991+
.into_iter()
992+
.map(|cnum| cx.shared.tcx.crate_name(cnum).to_string())
993+
.collect::<Vec<_>>()
994+
.join(",");
917995
write!(
918996
w,
919-
"<script type=\"text/javascript\" src=\"{src}\" async></script>",
997+
"<script type=\"text/javascript\" src=\"{src}\" data-ignore-extern-crates=\"{extern_crates}\" async></script>",
920998
src = js_src_path.finish(),
921999
);
9221000
}

src/librustdoc/html/render/write_shared.rs

+6-3
Original file line numberDiff line numberDiff line change
@@ -501,10 +501,13 @@ pub(super) fn write_shared(
501501
//
502502
// FIXME: this is a vague explanation for why this can't be a `get`, in
503503
// theory it should be...
504-
let &(ref remote_path, remote_item_type) = match cache.paths.get(&did) {
505-
Some(p) => p,
504+
let (remote_path, remote_item_type) = match cache.exact_paths.get(&did) {
505+
Some(p) => match cache.paths.get(&did).or_else(|| cache.external_paths.get(&did)) {
506+
Some((_, t)) => (p, t),
507+
None => continue,
508+
},
506509
None => match cache.external_paths.get(&did) {
507-
Some(p) => p,
510+
Some((p, t)) => (p, t),
508511
None => continue,
509512
},
510513
};

src/librustdoc/html/static/js/main.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -759,8 +759,14 @@ function loadCss(cssFileName) {
759759
const traitName = document.querySelector("h1.fqn > .in-band > .trait").textContent;
760760
const baseIdName = "impl-" + traitName + "-";
761761
const libs = Object.getOwnPropertyNames(imp);
762+
// We don't want to include impls from this JS file, when the HTML already has them.
763+
// The current crate should always be ignored. Other crates that should also be
764+
// ignored are included in the attribute `data-ignore-extern-crates`.
765+
const ignoreExternCrates = document
766+
.querySelector("script[data-ignore-extern-crates]")
767+
.getAttribute("data-ignore-extern-crates");
762768
for (const lib of libs) {
763-
if (lib === window.currentCrate) {
769+
if (lib === window.currentCrate || ignoreExternCrates.indexOf(lib) !== -1) {
764770
continue;
765771
}
766772
const structs = imp[lib];

src/test/rustdoc-gui/implementors.goml

+7
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,10 @@ assert: "#implementors-list .impl:nth-child(2) > .code-header.in-band"
1818
goto: file://|DOC_PATH|/test_docs/struct.HasEmptyTraits.html
1919
compare-elements-position-near-false: ("#impl-EmptyTrait1", "#impl-EmptyTrait2", {"y": 30})
2020
compare-elements-position-near: ("#impl-EmptyTrait3 h3", "#impl-EmptyTrait3 .item-info", {"y": 30})
21+
22+
// Now check that re-exports work correctly.
23+
// There should be exactly one impl shown on both of these pages.
24+
goto: file://|DOC_PATH|/lib2/trait.TraitToReexport.html
25+
assert-count: ("#implementors-list .impl", 1)
26+
goto: file://|DOC_PATH|/implementors/trait.TraitToReexport.html
27+
assert-count: ("#implementors-list .impl", 1)

src/test/rustdoc-gui/src/lib2/implementors/lib.rs

+9
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,12 @@ pub struct Struct;
99
impl Whatever for Struct {
1010
type Foo = u8;
1111
}
12+
13+
mod traits {
14+
pub trait TraitToReexport {
15+
fn method() {}
16+
}
17+
}
18+
19+
#[doc(inline)]
20+
pub use traits::TraitToReexport;

src/test/rustdoc-gui/src/lib2/lib.rs

+7
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ impl implementors::Whatever for Foo {
4343
type Foo = u32;
4444
}
4545

46+
#[doc(inline)]
47+
pub use implementors::TraitToReexport;
48+
49+
pub struct StructToImplOnReexport;
50+
51+
impl TraitToReexport for StructToImplOnReexport {}
52+
4653
pub mod sub_mod {
4754
/// ```txt
4855
/// aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

src/test/rustdoc/hidden-impls.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ pub mod __hidden {
1212

1313
// @has foo/trait.Clone.html
1414
// @!has - 'Foo'
15-
// @has implementors/foo/trait.Clone.js
15+
// @has implementors/core/clone/trait.Clone.js
1616
// @!has - 'Foo'
1717
pub use std::clone::Clone;

0 commit comments

Comments
 (0)