Skip to content

Put negative implementors first and apply same ordering logic to foreign implementors #142380

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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
4 changes: 4 additions & 0 deletions src/librustdoc/formats/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,8 @@ impl Impl {
};
true
}

pub(crate) fn is_negative_trait_impl(&self) -> bool {
self.inner_impl().is_negative_trait_impl()
}
}
43 changes: 40 additions & 3 deletions src/librustdoc/html/render/print_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,27 @@ fn item_function(cx: &Context<'_>, it: &clean::Item, f: &clean::Function) -> imp
})
}

/// Struct used to handle insertion of "negative impl" marker in the generated DOM.
///
/// This marker appears once in all trait impl lists to divide negative impls from positive impls.
struct NegativeMarker {
inserted_negative_marker: bool,
}

impl NegativeMarker {
fn new() -> Self {
Self { inserted_negative_marker: false }
}

fn insert_if_needed(&mut self, w: &mut fmt::Formatter<'_>, implementor: &Impl) -> fmt::Result {
if !self.inserted_negative_marker && !implementor.is_negative_trait_impl() {
write!(w, "<div class=\"negative-marker\"></div>")?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
write!(w, "<div class=\"negative-marker\"></div>")?;
write!(w, "<span class=\"negative-marker\"></span>")?;

Using a span feels more idiomatic, as it's less likely to cause weird extra linebreaks in unstyled environments.

Copy link
Member Author

Choose a reason for hiding this comment

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

Based on the docs/spec I read, it's exactly what div is for. From MDN:

The <div> HTML element is the generic container for flow content. It has no effect on the content or layout until styled in some way using CSS (e.g., styling is directly applied to it, or some kind of layout model like Flexbox is applied to its parent element).

As for linebreaks, I couldn't find information about how div is supposed to handle them by default.

Copy link
Contributor

Choose a reason for hiding this comment

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

div has a default style of display: block; so its presence can in fact change layout quite easily, as it will cause line/paragraph breaks the same way any other block element will. I'm not sure why MDN says it can't impact layout.

span is essentially the equivalent of div except it is used for inline content, which I believe a zero size marker with no impact on layout technically is?

self.inserted_negative_marker = true;
}
Ok(())
}
}

fn item_trait(cx: &Context<'_>, it: &clean::Item, t: &clean::Trait) -> impl fmt::Display {
fmt::from_fn(|w| {
let tcx = cx.tcx();
Expand Down Expand Up @@ -1069,7 +1090,9 @@ fn item_trait(cx: &Context<'_>, it: &clean::Item, t: &clean::Trait) -> impl fmt:
"<div id=\"implementors-list\">",
)
)?;
let mut negative_marker = NegativeMarker::new();
for implementor in concrete {
negative_marker.insert_if_needed(w, implementor)?;
write!(w, "{}", render_implementor(cx, implementor, it, &implementor_dups, &[]))?;
}
w.write_str("</div>")?;
Expand All @@ -1085,7 +1108,9 @@ fn item_trait(cx: &Context<'_>, it: &clean::Item, t: &clean::Trait) -> impl fmt:
"<div id=\"synthetic-implementors-list\">",
)
)?;
let mut negative_marker = NegativeMarker::new();
for implementor in synthetic {
negative_marker.insert_if_needed(w, implementor)?;
write!(
w,
"{}",
Expand Down Expand Up @@ -2302,11 +2327,18 @@ where
}

#[derive(PartialEq, Eq)]
struct ImplString(String);
struct ImplString {
rendered: String,
is_negative: bool,
}

impl ImplString {
fn new(i: &Impl, cx: &Context<'_>) -> ImplString {
ImplString(format!("{}", i.inner_impl().print(false, cx)))
let impl_ = i.inner_impl();
ImplString {
is_negative: impl_.is_negative_trait_impl(),
rendered: format!("{}", impl_.print(false, cx)),
}
}
}

Expand All @@ -2318,7 +2350,12 @@ impl PartialOrd for ImplString {

impl Ord for ImplString {
fn cmp(&self, other: &Self) -> Ordering {
compare_names(&self.0, &other.0)
// We sort negative impls first.
match (self.is_negative, other.is_negative) {
(false, true) => Ordering::Greater,
(true, false) => Ordering::Less,
_ => compare_names(&self.rendered, &other.rendered),
}
}
}

Expand Down
34 changes: 30 additions & 4 deletions src/librustdoc/html/render/write_shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
//! or contains "invocation-specific".

use std::cell::RefCell;
use std::cmp::Ordering;
use std::ffi::OsString;
use std::fs::File;
use std::io::{self, Write as _};
Expand Down Expand Up @@ -46,6 +47,7 @@ use crate::formats::Impl;
use crate::formats::item_type::ItemType;
use crate::html::layout;
use crate::html::render::ordered_json::{EscapedJson, OrderedJson};
use crate::html::render::print_item::compare_names;
use crate::html::render::search_index::{SerializedSearchIndex, build_index};
use crate::html::render::sorted_template::{self, FileFormat, SortedTemplate};
use crate::html::render::{AssocItemLink, ImplRenderingParameters, StylePath};
Expand Down Expand Up @@ -703,7 +705,7 @@ impl TraitAliasPart {
fn blank() -> SortedTemplate<<Self as CciPart>::FileFormat> {
SortedTemplate::from_before_after(
r"(function() {
var implementors = Object.fromEntries([",
const implementors = Object.fromEntries([",
r"]);
if (window.register_implementors) {
window.register_implementors(implementors);
Expand Down Expand Up @@ -741,7 +743,7 @@ impl TraitAliasPart {
},
};

let implementors = imps
let mut implementors = imps
.iter()
.filter_map(|imp| {
// If the trait and implementation are in the same crate, then
Expand All @@ -756,10 +758,12 @@ impl TraitAliasPart {
{
None
} else {
let impl_ = imp.inner_impl();
Some(Implementor {
text: imp.inner_impl().print(false, cx).to_string(),
text: impl_.print(false, cx).to_string(),
synthetic: imp.inner_impl().kind.is_auto(),
types: collect_paths_for_type(&imp.inner_impl().for_, cache),
is_negative: impl_.is_negative_trait_impl(),
})
}
})
Expand All @@ -778,7 +782,9 @@ impl TraitAliasPart {
}
path.push(format!("{remote_item_type}.{}.js", remote_path[remote_path.len() - 1]));

let part = OrderedJson::array_sorted(
implementors.sort_unstable();

let part = OrderedJson::array_unsorted(
Copy link
Contributor

Choose a reason for hiding this comment

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

This is definitely an improvement, as it eliminates an allocation, but we're still allocating an intermediate string for each element.

OrderedJson could be much more efficient if it had a method that appended using serde_json::to_writer.

I can open an issue for improving the OrderedJson interface, if you want.

Copy link
Member Author

Choose a reason for hiding this comment

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

An issue would be welcome indeed.

implementors
.iter()
.map(OrderedJson::serialize)
Expand All @@ -791,10 +797,12 @@ impl TraitAliasPart {
}
}

#[derive(PartialEq, Eq)]
Copy link
Contributor

Choose a reason for hiding this comment

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

logic error: Eq and Ord impls disagree on the definition of equality, as Ord only checks the name.

this can easily be fixed by writing a PartialEq impl that defers to Ord:

impl PartialEq for Implementor {
    fn eq(&self, other: &Self) -> bool {
        self.cmp(other) == Ordering::Equal
    }
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Interesting approach, I like it. Adding it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Wait, in which would it be an issue to have this difference? Ordering and equality don't have to be equivalent.

Copy link
Contributor

Choose a reason for hiding this comment

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

it's a correctness requirement on PartialEq.

it doesn't cause any issue with the current code, bur it could cause issues with generic datastructures like BTreeMap, so I think it would be best to try to avoid something that can cause this type of sneaky logic error.

I think this is technically the same class of error as if you had Copy and Clone diverge, not unsound, just incorrect.

struct Implementor {
text: String,
synthetic: bool,
types: Vec<String>,
is_negative: bool,
}

impl Serialize for Implementor {
Expand All @@ -804,6 +812,7 @@ impl Serialize for Implementor {
{
let mut seq = serializer.serialize_seq(None)?;
seq.serialize_element(&self.text)?;
seq.serialize_element(if self.is_negative { &1 } else { &0 })?;
if self.synthetic {
seq.serialize_element(&1)?;
seq.serialize_element(&self.types)?;
Expand All @@ -812,6 +821,23 @@ impl Serialize for Implementor {
}
}

impl PartialOrd for Implementor {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(Ord::cmp(self, other))
}
}

impl Ord for Implementor {
fn cmp(&self, other: &Self) -> Ordering {
// We sort negative impls first.
match (self.is_negative, other.is_negative) {
(false, true) => Ordering::Greater,
(true, false) => Ordering::Less,
_ => compare_names(&self.text, &other.text),
}
}
}

/// Collect the list of aliased types and their aliases.
/// <https://github.com/search?q=repo%3Arust-lang%2Frust+[RUSTDOCIMPL]+type.impl&type=code>
///
Expand Down
6 changes: 3 additions & 3 deletions src/librustdoc/html/render/write_shared/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ fn trait_alias_template() {
assert_eq!(
but_last_line(&template.to_string()),
r#"(function() {
var implementors = Object.fromEntries([]);
const implementors = Object.fromEntries([]);
if (window.register_implementors) {
window.register_implementors(implementors);
} else {
Expand All @@ -125,7 +125,7 @@ fn trait_alias_template() {
assert_eq!(
but_last_line(&template.to_string()),
r#"(function() {
var implementors = Object.fromEntries([["a"]]);
const implementors = Object.fromEntries([["a"]]);
if (window.register_implementors) {
window.register_implementors(implementors);
} else {
Expand All @@ -137,7 +137,7 @@ fn trait_alias_template() {
assert_eq!(
but_last_line(&template.to_string()),
r#"(function() {
var implementors = Object.fromEntries([["a"],["b"]]);
const implementors = Object.fromEntries([["a"],["b"]]);
if (window.register_implementors) {
window.register_implementors(implementors);
} else {
Expand Down
3 changes: 3 additions & 0 deletions src/librustdoc/html/static/css/rustdoc.css
Original file line number Diff line number Diff line change
Expand Up @@ -2776,6 +2776,9 @@ in src-script.js and main.js
{
margin-bottom: 0.75em;
}
.negative-marker {
display: none;
}
Comment on lines +2779 to +2781
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any meaningful difference between an inline span with no content and the same span but hidden? if not, then I don't think this is needed.

Copy link
Member Author

Choose a reason for hiding this comment

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

It makes the rendering engine completely discard the element. So close to no difference. But still better to have.


.variants > .docblock,
.implementors-toggle > .docblock,
Expand Down
39 changes: 30 additions & 9 deletions src/librustdoc/html/static/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -674,21 +674,34 @@ function preLoadCss(cssUrl) {

// <https://github.com/search?q=repo%3Arust-lang%2Frust+[RUSTDOCIMPL]+trait.impl&type=code>
window.register_implementors = imp => {
const implementors = document.getElementById("implementors-list");
const synthetic_implementors = document.getElementById("synthetic-implementors-list");
/** Takes an ID as input and returns a list of two elements. The first element is the DOM
* element with the given ID and the second is the "negative marker", meaning the location
* between the negative and non-negative impls.
*
* @param {string} id: ID of the DOM element.
*
* @return {[HTMLElement|null, HTMLElement|null]}
*/
function implementorsElems(id) {
const elem = document.getElementById(id);
return [elem, elem ? elem.querySelector(".negative-marker") : null];
}
const implementors = implementorsElems("implementors-list");
const synthetic_implementors = implementorsElems("synthetic-implementors-list");
const inlined_types = new Set();

const TEXT_IDX = 0;
const SYNTHETIC_IDX = 1;
const TYPES_IDX = 2;
const IS_NEG_IDX = 1;
const SYNTHETIC_IDX = 2;
const TYPES_IDX = 3;

if (synthetic_implementors) {
if (synthetic_implementors[0]) {
// This `inlined_types` variable is used to avoid having the same implementation
// showing up twice. For example "String" in the "Sync" doc page.
//
// By the way, this is only used by and useful for traits implemented automatically
// (like "Send" and "Sync").
onEachLazy(synthetic_implementors.getElementsByClassName("impl"), el => {
onEachLazy(synthetic_implementors[0].getElementsByClassName("impl"), el => {
const aliases = el.getAttribute("data-aliases");
if (!aliases) {
return;
Expand All @@ -701,7 +714,7 @@ function preLoadCss(cssUrl) {
}

// @ts-expect-error
let currentNbImpls = implementors.getElementsByClassName("impl").length;
let currentNbImpls = implementors[0].getElementsByClassName("impl").length;
// @ts-expect-error
const traitName = document.querySelector(".main-heading h1 > .trait").textContent;
const baseIdName = "impl-" + traitName + "-";
Expand Down Expand Up @@ -758,8 +771,16 @@ function preLoadCss(cssUrl) {
addClass(display, "impl");
display.appendChild(anchor);
display.appendChild(code);
// @ts-expect-error
list.appendChild(display);

// If this is a negative implementor, we put it into the right location (just
// before the negative impl marker).
if (struct[IS_NEG_IDX]) {
// @ts-expect-error
list[1].before(display);
} else {
// @ts-expect-error
list[0].appendChild(display);
}
currentNbImpls += 1;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/librustdoc/html/static/js/rustdoc.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ declare namespace rustdoc {
* Provied by generated `trait.impl` files.
*/
type Implementors = {
[key: string]: Array<[string, number, Array<string>]>
[key: string]: Array<[string, 0|1, number, Array<string>]>
}

type TypeImpls = {
Expand Down
47 changes: 37 additions & 10 deletions tests/rustdoc-gui/implementors.goml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,45 @@
// have the same display than the "local" ones.
go-to: "file://" + |DOC_PATH| + "/implementors/trait.Whatever.html"
assert: "#implementors-list"
// There are supposed to be two implementors listed.
assert-count: ("#implementors-list .impl", 2)
// There are supposed to be four implementors listed.
assert-count: ("#implementors-list .impl", 4)
// There are supposed to be two non-negative implementors.
assert-count: ("#implementors-list .negative-marker ~ *", 2)
// Now we check that both implementors have an anchor, an ID and a similar DOM.
assert: ("#implementors-list .impl:nth-child(1) > a.anchor")
assert-attribute: ("#implementors-list .impl:nth-child(1)", {"id": "impl-Whatever-for-Struct"})
assert-attribute: ("#implementors-list .impl:nth-child(1) > a.anchor", {"href": "#impl-Whatever-for-Struct"})
assert: "#implementors-list .impl:nth-child(1) > .code-header"
define-function: (
"check-dom",
[id],
block {
assert-attribute: (|id| + " > a.anchor", {"href": |id|})
assert: |id| + " > .code-header"
},
)

call-function: ("check-dom", {"id": "#impl-Whatever-for-Struct2"})
call-function: ("check-dom", {"id": "#impl-Whatever-2"})
call-function: ("check-dom", {"id": "#impl-Whatever-for-Struct"})
call-function: ("check-dom", {"id": "#impl-Whatever-3"})

assert: ("#implementors-list .impl:nth-child(2) > a.anchor")
assert-attribute: ("#implementors-list .impl:nth-child(2)", {"id": "impl-Whatever-1"})
assert-attribute: ("#implementors-list .impl:nth-child(2) > a.anchor", {"href": "#impl-Whatever-1"})
assert: "#implementors-list .impl:nth-child(2) > .code-header"
// Ensure that negative impl are sorted first.
assert-property: (
"#implementors-list > *:nth-child(1) > h3",
{"textContent": "impl !Whatever for Struct2"},
)
assert-property: (
"#implementors-list > *:nth-child(2) > h3",
{"textContent": "impl !Whatever for StructToImplOnReexport"},
)
// Third one is the negative marker.
assert-attribute: ("#implementors-list > *:nth-child(3)", {"class": "negative-marker"})
// This one is a `<detail>` so the selector is a bit different.
assert-property: (
"#implementors-list > *:nth-child(4) section > h3",
{"textContent": "impl Whatever for Struct"},
)
assert-property: (
"#implementors-list > *:nth-child(5) > h3",
{"textContent": "impl Whatever for Foo"},
)

go-to: "file://" + |DOC_PATH| + "/test_docs/struct.HasEmptyTraits.html"
compare-elements-position-near-false: (
Expand Down
5 changes: 5 additions & 0 deletions tests/rustdoc-gui/src/lib2/implementors/lib.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
#![feature(negative_impls)]

pub trait Whatever {
type Foo;

fn method() {}
}

pub struct Struct;
pub struct Struct2;

impl Whatever for Struct {
type Foo = u8;
}

impl !Whatever for Struct2 {}

impl http::HttpTrait for Struct {}

mod traits {
Expand Down
Loading
Loading