From 9ebe5ae3064af4f7f0f79c774e778ee26f36bdcb Mon Sep 17 00:00:00 2001 From: EtomicBomb Date: Wed, 24 Jul 2024 22:56:38 +0000 Subject: [PATCH 1/5] initial implementation of mergable rustdoc cci --- src/librustdoc/Cargo.toml | 2 +- src/librustdoc/clean/types.rs | 2 +- src/librustdoc/config.rs | 1 - src/librustdoc/html/render/context.rs | 10 +- src/librustdoc/html/render/mod.rs | 2 + src/librustdoc/html/render/search_index.rs | 34 +- src/librustdoc/html/render/sorted_json.rs | 82 + src/librustdoc/html/render/sorted_template.rs | 136 ++ src/librustdoc/html/render/tests.rs | 271 +++ src/librustdoc/html/render/write_shared.rs | 1462 +++++++++-------- 10 files changed, 1317 insertions(+), 685 deletions(-) create mode 100644 src/librustdoc/html/render/sorted_json.rs create mode 100644 src/librustdoc/html/render/sorted_template.rs diff --git a/src/librustdoc/Cargo.toml b/src/librustdoc/Cargo.toml index b3fccbf6456e0..67ba8c773175c 100644 --- a/src/librustdoc/Cargo.toml +++ b/src/librustdoc/Cargo.toml @@ -16,7 +16,7 @@ minifier = "0.3.0" pulldown-cmark-old = { version = "0.9.6", package = "pulldown-cmark", default-features = false } regex = "1" rustdoc-json-types = { path = "../rustdoc-json-types" } -serde_json = "1.0" +serde_json = { version = "1.0", features = ["preserve_order"] } serde = { version = "1.0", features = ["derive"] } smallvec = "1.8.1" tempfile = "3" diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index 4850500a1bfae..542e810b5cfa8 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -128,7 +128,7 @@ pub(crate) struct ExternalCrate { } impl ExternalCrate { - const LOCAL: Self = Self { crate_num: LOCAL_CRATE }; + pub(crate) const LOCAL: Self = Self { crate_num: LOCAL_CRATE }; #[inline] pub(crate) fn def_id(&self) -> DefId { diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index e4549796b3e83..2e54a22840bb5 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -730,7 +730,6 @@ impl Options { let extern_html_root_takes_precedence = matches.opt_present("extern-html-root-takes-precedence"); let html_no_source = matches.opt_present("html-no-source"); - if generate_link_to_definition && (show_coverage || output_format != OutputFormat::Html) { dcx.fatal( "--generate-link-to-definition option can only be used with HTML output format", diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs index 0334eacc16149..8e72dd6a864ae 100644 --- a/src/librustdoc/html/render/context.rs +++ b/src/librustdoc/html/render/context.rs @@ -14,7 +14,6 @@ use rustc_span::edition::Edition; use rustc_span::{sym, FileName, Symbol}; use super::print_item::{full_path, item_path, print_item}; -use super::search_index::build_index; use super::sidebar::{print_sidebar, sidebar_module_like, Sidebar}; use super::write_shared::write_shared; use super::{collect_spans_and_sources, scrape_examples_help, AllTypes, LinkFromSrc, StylePath}; @@ -573,13 +572,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { } if !no_emit_shared { - // Build our search index - let index = build_index(&krate, &mut Rc::get_mut(&mut cx.shared).unwrap().cache, tcx); - - // Write shared runs within a flock; disable thread dispatching of IO temporarily. - Rc::get_mut(&mut cx.shared).unwrap().fs.set_sync_only(true); - write_shared(&mut cx, &krate, index, &md_opts)?; - Rc::get_mut(&mut cx.shared).unwrap().fs.set_sync_only(false); + write_shared(&mut cx, &krate, &md_opts, tcx)?; } Ok((cx, krate)) @@ -729,6 +722,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { ); shared.fs.write(help_file, v)?; + // if to avoid writing files to doc root unless we're on the final invocation if shared.layout.scrape_examples_extension { page.title = "About scraped examples"; page.description = "How the scraped examples feature works in Rustdoc"; diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 9074e40a53614..4b1c9b4af474a 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -31,6 +31,8 @@ mod tests; mod context; mod print_item; pub(crate) mod sidebar; +mod sorted_json; +mod sorted_template; mod span_map; mod type_layout; mod write_shared; diff --git a/src/librustdoc/html/render/search_index.rs b/src/librustdoc/html/render/search_index.rs index 8a2f31f7413e1..184e5afba3c99 100644 --- a/src/librustdoc/html/render/search_index.rs +++ b/src/librustdoc/html/render/search_index.rs @@ -18,6 +18,7 @@ use crate::formats::cache::{Cache, OrphanImplItem}; use crate::formats::item_type::ItemType; use crate::html::format::join_with_double_colon; use crate::html::markdown::short_markdown_summary; +use crate::html::render::sorted_json::SortedJson; use crate::html::render::{self, IndexItem, IndexItemFunctionType, RenderType, RenderTypeId}; /// The serialized search description sharded version @@ -46,7 +47,7 @@ use crate::html::render::{self, IndexItem, IndexItemFunctionType, RenderType, Re /// [2]: https://en.wikipedia.org/wiki/Sliding_window_protocol#Basic_concept /// [3]: https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/description-tcp-features pub(crate) struct SerializedSearchIndex { - pub(crate) index: String, + pub(crate) index: SortedJson, pub(crate) desc: Vec<(usize, String)>, } @@ -683,24 +684,19 @@ pub(crate) fn build_index<'tcx>( // The index, which is actually used to search, is JSON // It uses `JSON.parse(..)` to actually load, since JSON // parses faster than the full JavaScript syntax. - let index = format!( - r#"["{}",{}]"#, - krate.name(tcx), - serde_json::to_string(&CrateData { - items: crate_items, - paths: crate_paths, - aliases: &aliases, - associated_item_disambiguators: &associated_item_disambiguators, - desc_index, - empty_desc, - }) - .expect("failed serde conversion") - // All these `replace` calls are because we have to go through JS string for JSON content. - .replace('\\', r"\\") - .replace('\'', r"\'") - // We need to escape double quotes for the JSON. - .replace("\\\"", "\\\\\"") - ); + let crate_name = krate.name(tcx); + let data = CrateData { + items: crate_items, + paths: crate_paths, + aliases: &aliases, + associated_item_disambiguators: &associated_item_disambiguators, + desc_index, + empty_desc, + }; + let index = SortedJson::array_unsorted([ + SortedJson::serialize(crate_name.as_str()), + SortedJson::serialize(data), + ]); SerializedSearchIndex { index, desc } } diff --git a/src/librustdoc/html/render/sorted_json.rs b/src/librustdoc/html/render/sorted_json.rs new file mode 100644 index 0000000000000..3a097733b8b20 --- /dev/null +++ b/src/librustdoc/html/render/sorted_json.rs @@ -0,0 +1,82 @@ +use itertools::Itertools as _; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::borrow::Borrow; +use std::fmt; + +/// Prerenedered json. +/// +/// Arrays are sorted by their stringified entries, and objects are sorted by their stringified +/// keys. +/// +/// Must use serde_json with the preserve_order feature. +/// +/// Both the Display and serde_json::to_string implementations write the serialized json +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(from = "Value")] +#[serde(into = "Value")] +pub(crate) struct SortedJson(String); + +impl SortedJson { + /// If you pass in an array, it will not be sorted. + pub(crate) fn serialize(item: T) -> Self { + SortedJson(serde_json::to_string(&item).unwrap()) + } + + /// Serializes and sorts + pub(crate) fn array, I: IntoIterator>(items: I) -> Self { + let items = items + .into_iter() + .sorted_unstable_by(|a, b| a.borrow().cmp(&b.borrow())) + .format_with(",", |item, f| f(item.borrow())); + SortedJson(format!("[{}]", items)) + } + + pub(crate) fn array_unsorted, I: IntoIterator>( + items: I, + ) -> Self { + let items = items.into_iter().format_with(",", |item, f| f(item.borrow())); + SortedJson(format!("[{items}]")) + } +} + +impl fmt::Display for SortedJson { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for SortedJson { + fn from(value: Value) -> Self { + SortedJson(serde_json::to_string(&value).unwrap()) + } +} + +impl From for Value { + fn from(json: SortedJson) -> Self { + serde_json::from_str(&json.0).unwrap() + } +} + +/// For use in JSON.parse('{...}'). +/// +/// JSON.parse supposedly loads faster than raw JS source, +/// so this is used for large objects. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct EscapedJson(SortedJson); + +impl From for EscapedJson { + fn from(json: SortedJson) -> Self { + EscapedJson(json) + } +} + +impl fmt::Display for EscapedJson { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // All these `replace` calls are because we have to go through JS string + // for JSON content. + // We need to escape double quotes for the JSON + let json = self.0.0.replace('\\', r"\\").replace('\'', r"\'").replace("\\\"", "\\\\\""); + write!(f, "{}", json) + } +} diff --git a/src/librustdoc/html/render/sorted_template.rs b/src/librustdoc/html/render/sorted_template.rs new file mode 100644 index 0000000000000..95240616b01dc --- /dev/null +++ b/src/librustdoc/html/render/sorted_template.rs @@ -0,0 +1,136 @@ +use std::collections::BTreeSet; +use std::fmt; +use std::marker::PhantomData; +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + +/// Append-only templates for sorted, deduplicated lists of items. +/// +/// Last line of the rendered output is a comment encoding the next insertion point. +#[derive(Debug, Clone)] +pub(crate) struct SortedTemplate { + format: PhantomData, + before: String, + after: String, + contents: BTreeSet, +} + +/// Written to last line of file to specify the location of each fragment +#[derive(Serialize, Deserialize, Debug, Clone)] +struct Offset { + /// Index of the first byte in the template + start: usize, + /// The length of each fragment in the encoded template, including the separator + delta: Vec, +} + +impl SortedTemplate { + /// Generate this template from arbitary text. + /// Will insert wherever the substring `magic` can be found. + /// Errors if it does not appear exactly once. + pub(crate) fn magic(template: &str, magic: &str) -> Result { + let mut split = template.split(magic); + let before = split.next().ok_or(Error)?; + let after = split.next().ok_or(Error)?; + if split.next().is_some() { + return Err(Error); + } + Ok(Self::before_after(before, after)) + } + + /// Template will insert contents between `before` and `after` + pub(crate) fn before_after(before: S, after: T) -> Self { + let before = before.to_string(); + let after = after.to_string(); + SortedTemplate { format: PhantomData, before, after, contents: Default::default() } + } +} + +impl SortedTemplate { + /// Adds this text to the template + pub(crate) fn append(&mut self, insert: String) { + self.contents.insert(insert); + } +} + +impl fmt::Display for SortedTemplate { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut delta = Vec::default(); + write!(f, "{}", self.before)?; + let contents: Vec<_> = self.contents.iter().collect(); + let mut sep = ""; + for content in contents { + delta.push(sep.len() + content.len()); + write!(f, "{}{}", sep, content)?; + sep = F::SEPARATOR; + } + let offset = Offset { start: self.before.len(), delta }; + let offset = serde_json::to_string(&offset).unwrap(); + write!(f, "{}\n{}{}{}", self.after, F::COMMENT_START, offset, F::COMMENT_END)?; + Ok(()) + } +} + +fn checked_split_at(s: &str, index: usize) -> Option<(&str, &str)> { + s.is_char_boundary(index).then(|| s.split_at(index)) +} + +impl FromStr for SortedTemplate { + type Err = Error; + fn from_str(s: &str) -> Result { + let (s, offset) = s.rsplit_once("\n").ok_or(Error)?; + let offset = offset.strip_prefix(F::COMMENT_START).ok_or(Error)?; + let offset = offset.strip_suffix(F::COMMENT_END).ok_or(Error)?; + let offset: Offset = serde_json::from_str(&offset).map_err(|_| Error)?; + let (before, mut s) = checked_split_at(s, offset.start).ok_or(Error)?; + let mut contents = BTreeSet::default(); + let mut sep = ""; + for &index in offset.delta.iter() { + let (content, rest) = checked_split_at(s, index).ok_or(Error)?; + s = rest; + let content = content.strip_prefix(sep).ok_or(Error)?; + contents.insert(content.to_string()); + sep = F::SEPARATOR; + } + Ok(SortedTemplate { + format: PhantomData, + before: before.to_string(), + after: s.to_string(), + contents, + }) + } +} + +pub(crate) trait FileFormat { + const COMMENT_START: &'static str; + const COMMENT_END: &'static str; + const SEPARATOR: &'static str; +} + +#[derive(Debug, Clone)] +pub(crate) struct Html; + +impl FileFormat for Html { + const COMMENT_START: &'static str = ""; + const SEPARATOR: &'static str = ""; +} + +#[derive(Debug, Clone)] +pub(crate) struct Js; + +impl FileFormat for Js { + const COMMENT_START: &'static str = "//"; + const COMMENT_END: &'static str = ""; + const SEPARATOR: &'static str = ","; +} + +#[derive(Debug, Clone)] +pub(crate) struct Error; + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "invalid template") + } +} diff --git a/src/librustdoc/html/render/tests.rs b/src/librustdoc/html/render/tests.rs index 4a9724a6f840f..16e67b0f1180e 100644 --- a/src/librustdoc/html/render/tests.rs +++ b/src/librustdoc/html/render/tests.rs @@ -52,3 +52,274 @@ fn test_all_types_prints_header_once() { assert_eq!(1, buffer.into_inner().matches("List of all items").count()); } + +mod sorted_json { + use super::super::sorted_json::*; + + fn check(json: SortedJson, serialized: &str) { + assert_eq!(json.to_string(), serialized); + assert_eq!(serde_json::to_string(&json).unwrap(), serialized); + + let json = json.to_string(); + let json: SortedJson = serde_json::from_str(&json).unwrap(); + + assert_eq!(json.to_string(), serialized); + assert_eq!(serde_json::to_string(&json).unwrap(), serialized); + + let json = serde_json::to_string(&json).unwrap(); + let json: SortedJson = serde_json::from_str(&json).unwrap(); + + assert_eq!(json.to_string(), serialized); + assert_eq!(serde_json::to_string(&json).unwrap(), serialized); + } + + #[test] + fn escape_json_number() { + let json = SortedJson::serialize(3); + let json = EscapedJson::from(json); + assert_eq!(format!("{json}"), "3"); + } + + #[test] + fn escape_json_single_quote() { + let json = SortedJson::serialize("he's"); + let json = EscapedJson::from(json); + assert_eq!(format!("{json}"), r#""he\'s""#); + } + + #[test] + fn escape_json_array() { + let json = SortedJson::serialize([1, 2, 3]); + let json = EscapedJson::from(json); + assert_eq!(format!("{json}"), r#"[1,2,3]"#); + } + + #[test] + fn escape_json_string() { + let json = SortedJson::serialize(r#"he"llo"#); + let json = EscapedJson::from(json); + assert_eq!(format!("{json}"), r#""he\\\"llo""#); + } + + #[test] + fn escape_json_string_escaped() { + let json = SortedJson::serialize(r#"he\"llo"#); + let json = EscapedJson::from(json); + assert_eq!(format!("{json}"), r#""he\\\\\\\"llo""#); + } + + #[test] + fn escape_json_string_escaped_escaped() { + let json = SortedJson::serialize(r#"he\\"llo"#); + let json = EscapedJson::from(json); + assert_eq!(format!("{json}"), r#""he\\\\\\\\\\\"llo""#); + } + + #[test] + fn number() { + let json = SortedJson::serialize(3); + let serialized = "3"; + check(json, serialized); + } + + #[test] + fn boolean() { + let json = SortedJson::serialize(true); + let serialized = "true"; + check(json, serialized); + } + + #[test] + fn string() { + let json = SortedJson::serialize("he\"llo"); + let serialized = r#""he\"llo""#; + check(json, serialized); + } + + #[test] + fn serialize_array() { + let json = SortedJson::serialize([3, 1, 2]); + let serialized = "[3,1,2]"; + check(json, serialized); + } + + #[test] + fn sorted_array() { + let items = ["c", "a", "b"]; + let serialized = r#"["a","b","c"]"#; + let items: Vec = items.into_iter().map(SortedJson::serialize).collect(); + let json = SortedJson::array(items); + check(json, serialized); + } + + #[test] + fn nested_array() { + let a = SortedJson::serialize(3); + let b = SortedJson::serialize(2); + let c = SortedJson::serialize(1); + let d = SortedJson::serialize([1, 3, 2]); + let json = SortedJson::array([a, b, c, d]); + let serialized = r#"[1,2,3,[1,3,2]]"#; + check(json, serialized); + } + + #[test] + fn array_unsorted() { + let items = ["c", "a", "b"]; + let serialized = r#"["c","a","b"]"#; + let items: Vec = items.into_iter().map(SortedJson::serialize).collect(); + let json = SortedJson::array_unsorted(items); + check(json, serialized); + } +} + +mod sorted_template { + use super::super::sorted_template::*; + use std::str::FromStr; + + fn is_comment_js(s: &str) -> bool { + s.starts_with("//") + } + + fn is_comment_html(s: &str) -> bool { + // not correct but good enough for these tests + s.starts_with("") + } + + #[test] + fn html_from_empty() { + let inserts = ["

hello

", "

kind

", "

hello

", "

world

"]; + let mut template = SortedTemplate::::before_after("", ""); + for insert in inserts { + template.append(insert.to_string()); + } + let template = format!("{template}"); + let (template, end) = template.rsplit_once("\n").unwrap(); + assert_eq!(template, "

hello

kind

world

"); + assert!(is_comment_html(end)); + assert!(!end.contains("\n")); + } + + #[test] + fn html_page() { + let inserts = ["

hello

", "

kind

", "

world

"]; + let before = ""; + let after = ""; + let mut template = SortedTemplate::::before_after(before, after); + for insert in inserts { + template.append(insert.to_string()); + } + let template = format!("{template}"); + let (template, end) = template.rsplit_once("\n").unwrap(); + assert_eq!(template, format!("{before}{}{after}", inserts.join(""))); + assert!(is_comment_html(end)); + assert!(!end.contains("\n")); + } + + #[test] + fn js_from_empty() { + let inserts = ["1", "2", "2", "2", "3", "1"]; + let mut template = SortedTemplate::::before_after("", ""); + for insert in inserts { + template.append(insert.to_string()); + } + let template = format!("{template}"); + let (template, end) = template.rsplit_once("\n").unwrap(); + assert_eq!(template, "1,2,3"); + assert!(is_comment_js(end)); + assert!(!end.contains("\n")); + } + + #[test] + fn js_empty_array() { + let template = SortedTemplate::::before_after("[", "]"); + let template = format!("{template}"); + let (template, end) = template.rsplit_once("\n").unwrap(); + assert_eq!(template, format!("[]")); + assert!(is_comment_js(end)); + assert!(!end.contains("\n")); + } + + #[test] + fn js_number_array() { + let inserts = ["1", "2", "3"]; + let mut template = SortedTemplate::::before_after("[", "]"); + for insert in inserts { + template.append(insert.to_string()); + } + let template = format!("{template}"); + let (template, end) = template.rsplit_once("\n").unwrap(); + assert_eq!(template, format!("[1,2,3]")); + assert!(is_comment_js(end)); + assert!(!end.contains("\n")); + } + + #[test] + fn magic_js_number_array() { + let inserts = ["1", "1"]; + let mut template = SortedTemplate::::magic("[#]", "#").unwrap(); + for insert in inserts { + template.append(insert.to_string()); + } + let template = format!("{template}"); + let (template, end) = template.rsplit_once("\n").unwrap(); + assert_eq!(template, format!("[1]")); + assert!(is_comment_js(end)); + assert!(!end.contains("\n")); + } + + #[test] + fn round_trip_js() { + let inserts = ["1", "2", "3"]; + let mut template = SortedTemplate::::before_after("[", "]"); + for insert in inserts { + template.append(insert.to_string()); + } + let template1 = format!("{template}"); + let mut template = SortedTemplate::::from_str(&template1).unwrap(); + assert_eq!(template1, format!("{template}")); + template.append("4".to_string()); + let template = format!("{template}"); + let (template, end) = template.rsplit_once("\n").unwrap(); + assert_eq!(template, "[1,2,3,4]"); + assert!(is_comment_js(end)); + } + + #[test] + fn round_trip_html() { + let inserts = ["

hello

", "

kind

", "

world

", "

kind

"]; + let before = ""; + let after = ""; + let mut template = SortedTemplate::::before_after(before, after); + template.append(inserts[0].to_string()); + template.append(inserts[1].to_string()); + let template = format!("{template}"); + let mut template = SortedTemplate::::from_str(&template).unwrap(); + template.append(inserts[2].to_string()); + let template = format!("{template}"); + let (template, end) = template.rsplit_once("\n").unwrap(); + assert_eq!(template, format!("{before}

hello

kind

world

{after}")); + assert!(is_comment_html(end)); + } + + #[test] + fn blank_js() { + let inserts = ["1", "2", "3"]; + let template = SortedTemplate::::before_after("", ""); + let template = format!("{template}"); + let (t, _) = template.rsplit_once("\n").unwrap(); + assert_eq!(t, ""); + let mut template = SortedTemplate::::from_str(&template).unwrap(); + for insert in inserts { + template.append(insert.to_string()); + } + let template1 = format!("{template}"); + let mut template = SortedTemplate::::from_str(&template1).unwrap(); + assert_eq!(template1, format!("{template}")); + template.append("4".to_string()); + let template = format!("{template}"); + let (template, end) = template.rsplit_once("\n").unwrap(); + assert_eq!(template, "1,2,3,4"); + assert!(is_comment_js(end)); + } +} diff --git a/src/librustdoc/html/render/write_shared.rs b/src/librustdoc/html/render/write_shared.rs index 8fd56eae37ffc..eaebeadd8817e 100644 --- a/src/librustdoc/html/render/write_shared.rs +++ b/src/librustdoc/html/render/write_shared.rs @@ -1,19 +1,43 @@ +//! Rustdoc writes out two kinds of shared files: +//! - Static files, which are embedded in the rustdoc binary and are written with a +//! filename that includes a hash of their contents. These will always have a new +//! URL if the contents change, so they are safe to cache with the +//! `Cache-Control: immutable` directive. They are written under the static.files/ +//! directory and are written when --emit-type is empty (default) or contains +//! "toolchain-specific". If using the --static-root-path flag, it should point +//! to a URL path prefix where each of these filenames can be fetched. +//! - Invocation specific files. These are generated based on the crate(s) being +//! documented. Their filenames need to be predictable without knowing their +//! contents, so they do not include a hash in their filename and are not safe to +//! cache with `Cache-Control: immutable`. They include the contents of the +//! --resource-suffix flag and are emitted when --emit-type is empty (default) +//! or contains "invocation-specific". + +use std::any::Any; use std::cell::RefCell; -use std::fs::{self, File}; -use std::io::prelude::*; -use std::io::{self, BufReader}; -use std::path::{Component, Path}; +use std::collections::hash_map::Entry; +use std::ffi::OsString; +use std::fs::File; +use std::io::BufWriter; +use std::io::Write as _; +use std::iter::once; +use std::marker::PhantomData; +use std::path::{Component, Path, PathBuf}; use std::rc::{Rc, Weak}; +use std::str::FromStr; +use std::{fmt, fs, io}; use indexmap::IndexMap; use itertools::Itertools; +use regex::Regex; use rustc_data_structures::flock; use rustc_data_structures::fx::{FxHashMap, FxHashSet}; use rustc_middle::ty::fast_reject::{DeepRejectCtxt, TreatParams}; +use rustc_middle::ty::TyCtxt; use rustc_span::def_id::DefId; use rustc_span::Symbol; use serde::ser::SerializeSeq; -use serde::{Serialize, Serializer}; +use serde::{de::DeserializeOwned, Deserialize, Serialize, Serializer}; use super::{collect_paths_for_type, ensure_trailing_slash, Context, RenderMode}; use crate::clean::{Crate, Item, ItemId, ItemKind}; @@ -24,53 +48,92 @@ use crate::formats::cache::Cache; use crate::formats::item_type::ItemType; use crate::formats::Impl; use crate::html::format::Buffer; +use crate::html::layout; +use crate::html::render::search_index::build_index; use crate::html::render::search_index::SerializedSearchIndex; +use crate::html::render::sorted_json::{EscapedJson, SortedJson}; +use crate::html::render::sorted_template::{self, SortedTemplate}; use crate::html::render::{AssocItemLink, ImplRenderingParameters}; -use crate::html::{layout, static_files}; +use crate::html::static_files::{self, suffix_path}; use crate::visit::DocVisitor; use crate::{try_err, try_none}; -/// Rustdoc writes out two kinds of shared files: -/// - Static files, which are embedded in the rustdoc binary and are written with a -/// filename that includes a hash of their contents. These will always have a new -/// URL if the contents change, so they are safe to cache with the -/// `Cache-Control: immutable` directive. They are written under the static.files/ -/// directory and are written when --emit-type is empty (default) or contains -/// "toolchain-specific". If using the --static-root-path flag, it should point -/// to a URL path prefix where each of these filenames can be fetched. -/// - Invocation specific files. These are generated based on the crate(s) being -/// documented. Their filenames need to be predictable without knowing their -/// contents, so they do not include a hash in their filename and are not safe to -/// cache with `Cache-Control: immutable`. They include the contents of the -/// --resource-suffix flag and are emitted when --emit-type is empty (default) -/// or contains "invocation-specific". -pub(super) fn write_shared( +/// Write crate-info.json cross-crate information, static files, invocation-specific files, etc. to disk +pub(crate) fn write_shared( cx: &mut Context<'_>, krate: &Crate, - search_index: SerializedSearchIndex, - options: &RenderOptions, + opt: &RenderOptions, + tcx: TyCtxt<'_>, ) -> Result<(), Error> { - // Write out the shared files. Note that these are shared among all rustdoc - // docs placed in the output directory, so this needs to be a synchronized - // operation with respect to all other rustdocs running around. + // NOTE(EtomicBomb): I don't think we need sync here because no read-after-write? + Rc::get_mut(&mut cx.shared).unwrap().fs.set_sync_only(true); let lock_file = cx.dst.join(".lock"); + // Write shared runs within a flock; disable thread dispatching of IO temporarily. let _lock = try_err!(flock::Lock::new(&lock_file, true, true, true), &lock_file); - // InvocationSpecific resources should always be dynamic. - let write_invocation_specific = |p: &str, make_content: &dyn Fn() -> Result, Error>| { - let content = make_content()?; - if options.emit.is_empty() || options.emit.contains(&EmitType::InvocationSpecific) { - let output_filename = static_files::suffix_path(p, &cx.shared.resource_suffix); - cx.shared.fs.write(cx.dst.join(output_filename), content) - } else { - Ok(()) - } + let SerializedSearchIndex { index, desc } = + build_index(&krate, &mut Rc::get_mut(&mut cx.shared).unwrap().cache, tcx); + write_search_desc(cx, &krate, &desc)?; // does not need to be merged; written unconditionally + + let crate_name = krate.name(cx.tcx()); + let crate_name = crate_name.as_str(); // rand + let crate_name_json = SortedJson::serialize(crate_name); // "rand" + let external_crates = hack_get_external_crate_names(cx)?; + let info = CrateInfo { + src_files_js: SourcesPart::get(cx, &crate_name_json)?, + search_index_js: SearchIndexPart::get(cx, index)?, + all_crates: AllCratesPart::get(crate_name_json.clone())?, + crates_index: CratesIndexPart::get(&crate_name, &external_crates)?, + trait_impl: TraitAliasPart::get(cx, &crate_name_json)?, + type_impl: TypeAliasPart::get(cx, krate, &crate_name_json)?, }; - cx.shared - .fs - .create_dir_all(cx.dst.join("static.files")) - .map_err(|e| PathError::new(e, "static.files"))?; + let crates_info = vec![info]; // we have info from just one crate + + write_static_files(cx, &opt)?; + let dst = &cx.dst; + if opt.emit.is_empty() || opt.emit.contains(&EmitType::InvocationSpecific) { + if cx.include_sources { + write_rendered_cci::(SourcesPart::blank, dst, &crates_info)?; + } + write_rendered_cci::( + SearchIndexPart::blank, + dst, + &crates_info, + )?; + write_rendered_cci::(AllCratesPart::blank, dst, &crates_info)?; + } + write_rendered_cci::(TraitAliasPart::blank, dst, &crates_info)?; + write_rendered_cci::(TypeAliasPart::blank, dst, &crates_info)?; + match &opt.index_page { + Some(index_page) if opt.enable_index_page => { + let mut md_opts = opt.clone(); + md_opts.output = cx.dst.clone(); + md_opts.external_html = cx.shared.layout.external_html.clone(); + try_err!( + crate::markdown::render(&index_page, md_opts, cx.shared.edition()), + &index_page + ); + } + None if opt.enable_index_page => { + write_rendered_cci::( + || CratesIndexPart::blank(cx), + dst, + &crates_info, + )?; + } + _ => {} // they don't want an index page + } + + Rc::get_mut(&mut cx.shared).unwrap().fs.set_sync_only(false); + Ok(()) +} + +/// Writes the static files, the style files, and the css extensions +fn write_static_files(cx: &mut Context<'_>, options: &RenderOptions) -> Result<(), Error> { + let static_dir = cx.dst.join("static.files"); + + cx.shared.fs.create_dir_all(&static_dir).map_err(|e| PathError::new(e, "static.files"))?; // Handle added third-party themes for entry in &cx.shared.style_files { @@ -97,680 +160,769 @@ pub(super) fn write_shared( } if options.emit.is_empty() || options.emit.contains(&EmitType::Toolchain) { - let static_dir = cx.dst.join(Path::new("static.files")); static_files::for_each(|f: &static_files::StaticFile| { let filename = static_dir.join(f.output_filename()); cx.shared.fs.write(filename, f.minified()) })?; } - /// Read a file and return all lines that match the `"{crate}":{data},` format, - /// and return a tuple `(Vec, Vec)`. - /// - /// This forms the payload of files that look like this: - /// - /// ```javascript - /// var data = { - /// "{crate1}":{data}, - /// "{crate2}":{data} - /// }; - /// use_data(data); - /// ``` - /// - /// The file needs to be formatted so that *only crate data lines start with `"`*. - fn collect(path: &Path, krate: &str) -> io::Result<(Vec, Vec)> { - let mut ret = Vec::new(); - let mut krates = Vec::new(); - - if path.exists() { - let prefix = format!("\"{krate}\""); - for line in BufReader::new(File::open(path)?).lines() { - let line = line?; - if !line.starts_with('"') { - continue; - } - if line.starts_with(&prefix) { - continue; - } - if line.ends_with(',') { - ret.push(line[..line.len() - 1].to_string()); - } else { - // No comma (it's the case for the last added crate line) - ret.push(line.to_string()); - } - krates.push( - line.split('"') - .find(|s| !s.is_empty()) - .map(|s| s.to_owned()) - .unwrap_or_else(String::new), - ); - } - } - Ok((ret, krates)) - } - - /// Read a file and return all lines that match the "{crate}":{data},\ format, - /// and return a tuple `(Vec, Vec)`. - /// - /// This forms the payload of files that look like this: - /// - /// ```javascript - /// var data = JSON.parse('{\ - /// "{crate1}":{data},\ - /// "{crate2}":{data}\ - /// }'); - /// use_data(data); - /// ``` - /// - /// The file needs to be formatted so that *only crate data lines start with `"`*. - fn collect_json(path: &Path, krate: &str) -> io::Result<(Vec, Vec)> { - let mut ret = Vec::new(); - let mut krates = Vec::new(); - - if path.exists() { - let prefix = format!("[\"{krate}\""); - for line in BufReader::new(File::open(path)?).lines() { - let line = line?; - if !line.starts_with("[\"") { - continue; - } - if line.starts_with(&prefix) { - continue; - } - if line.ends_with("],\\") { - ret.push(line[..line.len() - 2].to_string()); - } else { - // Ends with "\\" (it's the case for the last added crate line) - ret.push(line[..line.len() - 1].to_string()); - } - krates.push( - line[1..] // We skip the `[` parent at the beginning of the line. - .split('"') - .find(|s| !s.is_empty()) - .map(|s| s.to_owned()) - .unwrap_or_else(String::new), - ); - } - } - Ok((ret, krates)) + Ok(()) +} + +/// Write the search description shards to disk +fn write_search_desc( + cx: &mut Context<'_>, + krate: &Crate, + search_desc: &[(usize, String)], +) -> Result<(), Error> { + let crate_name = krate.name(cx.tcx()).to_string(); + let encoded_crate_name = SortedJson::serialize(&crate_name); + let path = PathBuf::from_iter([&cx.dst, Path::new("search.desc"), Path::new(&crate_name)]); + if Path::new(&path).exists() { + try_err!(fs::remove_dir_all(&path), &path); + } + for (i, (_, part)) in search_desc.iter().enumerate() { + let filename = static_files::suffix_path( + &format!("{crate_name}-desc-{i}-.js"), + &cx.shared.resource_suffix, + ); + let path = path.join(filename); + let part = SortedJson::serialize(&part); + let part = format!("searchState.loadedDescShard({encoded_crate_name}, {i}, {part})"); + write_create_parents(&path, part)?; } + Ok(()) +} - use std::ffi::OsString; +/// Written to `crate-info.json`. Contains pre-rendered contents to insert into the CCI template +#[derive(Serialize, Deserialize, Clone, Debug)] +struct CrateInfo { + src_files_js: PartsAndLocations, + search_index_js: PartsAndLocations, + all_crates: PartsAndLocations, + crates_index: PartsAndLocations, + trait_impl: PartsAndLocations, + type_impl: PartsAndLocations, +} - #[derive(Debug, Default)] - struct Hierarchy { - parent: Weak, - elem: OsString, - children: RefCell>>, - elems: RefCell>, +impl CrateInfo { + /// Gets a reference to the cross-crate information parts for `T` + fn get(&self) -> &PartsAndLocations { + (&self.src_files_js as &dyn Any) + .downcast_ref() + .or_else(|| (&self.search_index_js as &dyn Any).downcast_ref()) + .or_else(|| (&self.all_crates as &dyn Any).downcast_ref()) + .or_else(|| (&self.crates_index as &dyn Any).downcast_ref()) + .or_else(|| (&self.trait_impl as &dyn Any).downcast_ref()) + .or_else(|| (&self.type_impl as &dyn Any).downcast_ref()) + .expect("this should be an exhaustive list of `CciPart`s") } +} - impl Hierarchy { - fn with_parent(elem: OsString, parent: &Rc) -> Self { - Self { elem, parent: Rc::downgrade(parent), ..Self::default() } - } +/// Paths (relative to the doc root) and their pre-merge contents +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(transparent)] +struct PartsAndLocations

{ + parts: Vec<(PathBuf, P)>, +} - fn to_json_string(&self) -> String { - let borrow = self.children.borrow(); - let mut subs: Vec<_> = borrow.values().collect(); - subs.sort_unstable_by(|a, b| a.elem.cmp(&b.elem)); - let mut files = self - .elems - .borrow() - .iter() - .map(|s| format!("\"{}\"", s.to_str().expect("invalid osstring conversion"))) - .collect::>(); - files.sort_unstable(); - let subs = subs.iter().map(|s| s.to_json_string()).collect::>().join(","); - let dirs = if subs.is_empty() && files.is_empty() { - String::new() - } else { - format!(",[{subs}]") - }; - let files = files.join(","); - let files = if files.is_empty() { String::new() } else { format!(",[{files}]") }; - format!( - "[\"{name}\"{dirs}{files}]", - name = self.elem.to_str().expect("invalid osstring conversion"), - dirs = dirs, - files = files - ) - } +impl

Default for PartsAndLocations

{ + fn default() -> Self { + Self { parts: Vec::default() } + } +} - fn add_path(self: &Rc, path: &Path) { - let mut h = Rc::clone(&self); - let mut elems = path - .components() - .filter_map(|s| match s { - Component::Normal(s) => Some(s.to_owned()), - Component::ParentDir => Some(OsString::from("..")), - _ => None, - }) - .peekable(); - loop { - let cur_elem = elems.next().expect("empty file path"); - if cur_elem == ".." { - if let Some(parent) = h.parent.upgrade() { - h = parent; - } - continue; - } - if elems.peek().is_none() { - h.elems.borrow_mut().insert(cur_elem); - break; - } else { - let entry = Rc::clone( - h.children - .borrow_mut() - .entry(cur_elem.clone()) - .or_insert_with(|| Rc::new(Self::with_parent(cur_elem, &h))), - ); - h = entry; - } - } +impl PartsAndLocations> { + fn push(&mut self, path: PathBuf, item: U) { + self.parts.push((path, Part { _artifact: PhantomData, item })); + } + + /// Singleton part, one file + fn with(path: PathBuf, part: U) -> Self { + let mut ret = Self::default(); + ret.push(path, part); + ret + } +} + +/// A piece of one of the shared artifacts for documentation (search index, sources, alias list, etc.) +/// +/// Merged at a user specified time and written to the `doc/` directory +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(transparent)] +struct Part { + #[serde(skip)] + _artifact: PhantomData, + item: U, +} + +impl fmt::Display for Part { + /// Writes serialized JSON + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.item) + } +} + +/// Wrapper trait for `Part` +trait CciPart: Sized + fmt::Display + DeserializeOwned + 'static { + /// Identifies the file format of the cross-crate information + type FileFormat: sorted_template::FileFormat; +} + +#[derive(Serialize, Deserialize, Clone, Default, Debug)] +struct SearchIndex; +type SearchIndexPart = Part; +impl CciPart for SearchIndexPart { + type FileFormat = sorted_template::Js; +} + +impl SearchIndexPart { + fn blank() -> SortedTemplate<::FileFormat> { + SortedTemplate::before_after( + r"var searchIndex = new Map(JSON.parse('[", + r"]')); +if (typeof exports !== 'undefined') exports.searchIndex = searchIndex; +else if (window.initSearch) window.initSearch(searchIndex);", + ) + } + + fn get(cx: &Context<'_>, search_index: SortedJson) -> Result, Error> { + let path = suffix_path("search-index.js", &cx.shared.resource_suffix); + let search_index = EscapedJson::from(search_index); + Ok(PartsAndLocations::with(path, search_index)) + } +} + +#[derive(Serialize, Deserialize, Clone, Default, Debug)] +struct AllCrates; +type AllCratesPart = Part; +impl CciPart for AllCratesPart { + type FileFormat = sorted_template::Js; +} + +impl AllCratesPart { + fn blank() -> SortedTemplate<::FileFormat> { + SortedTemplate::before_after("window.ALL_CRATES = [", "];") + } + + fn get(crate_name_json: SortedJson) -> Result, Error> { + // external hack_get_external_crate_names not needed here, because + // there's no way that we write the search index but not crates.js + let path = PathBuf::from("crates.js"); + Ok(PartsAndLocations::with(path, crate_name_json)) + } +} + +/// Reads `crates.js`, which seems like the best +/// place to obtain the list of externally documented crates if the index +/// page was disabled when documenting the deps. +/// +/// This is to match the current behavior of rustdoc, which allows you to get all crates +/// on the index page, even if --enable-index-page is only passed to the last crate. +fn hack_get_external_crate_names(cx: &Context<'_>) -> Result, Error> { + let path = cx.dst.join("crates.js"); + let Ok(content) = fs::read_to_string(&path) else { + // they didn't emit invocation specific, so we just say there were no crates + return Ok(Vec::default()); + }; + // this is only run once so it's fine not to cache it + // !dot_matches_new_line: all crates on same line. greedy: match last bracket + let regex = Regex::new(r"\[.*\]").unwrap(); + let Some(content) = regex.find(&content) else { + return Err(Error::new("could not find crates list in crates.js", path)); + }; + let content: Vec = try_err!(serde_json::from_str(content.as_str()), &path); + Ok(content) +} + +#[derive(Serialize, Deserialize, Clone, Default, Debug)] +struct CratesIndex; +type CratesIndexPart = Part; +impl CciPart for CratesIndexPart { + type FileFormat = sorted_template::Html; +} + +impl CratesIndexPart { + fn blank(cx: &Context<'_>) -> SortedTemplate<::FileFormat> { + let page = layout::Page { + title: "Index of crates", + css_class: "mod sys", + root_path: "./", + static_root_path: cx.shared.static_root_path.as_deref(), + description: "List of crates", + resource_suffix: &cx.shared.resource_suffix, + rust_logo: true, + }; + let layout = &cx.shared.layout; + let style_files = &cx.shared.style_files; + const MAGIC: &str = "\u{FFFC}"; // users are being naughty if they have this + let content = format!("

List of all crates

    {MAGIC}
"); + let template = layout::render(layout, &page, "", content, &style_files); + match SortedTemplate::magic(&template, MAGIC) { + Ok(template) => template, + Err(e) => panic!( + "{e}: Object Replacement Character (U+FFFC) should not appear in the --index-page" + ), } } - if cx.include_sources { - let hierarchy = Rc::new(Hierarchy::default()); - for source in cx - .shared - .local_sources - .iter() - .filter_map(|p| p.0.strip_prefix(&cx.shared.src_root).ok()) - { - hierarchy.add_path(source); + /// Might return parts that are duplicate with ones in prexisting index.html + fn get(crate_name: &str, external_crates: &[String]) -> Result, Error> { + let mut ret = PartsAndLocations::default(); + let path = PathBuf::from("index.html"); + for crate_name in external_crates.iter().map(|s| s.as_str()).chain(once(crate_name)) { + let part = format!( + "
  • {crate_name}
  • ", + trailing_slash = ensure_trailing_slash(crate_name), + ); + ret.push(path.clone(), part); } - let hierarchy = Rc::try_unwrap(hierarchy).unwrap(); - let dst = cx.dst.join(&format!("src-files{}.js", cx.shared.resource_suffix)); - let make_sources = || { - let (mut all_sources, _krates) = - try_err!(collect_json(&dst, krate.name(cx.tcx()).as_str()), &dst); - all_sources.push(format!( - r#"["{}",{}]"#, - &krate.name(cx.tcx()), - hierarchy - .to_json_string() - // All these `replace` calls are because we have to go through JS string for JSON content. - .replace('\\', r"\\") - .replace('\'', r"\'") - // We need to escape double quotes for the JSON. - .replace("\\\"", "\\\\\"") - )); - all_sources.sort(); - // This needs to be `var`, not `const`. - // This variable needs declared in the current global scope so that if - // src-script.js loads first, it can pick it up. - let mut v = String::from("var srcIndex = new Map(JSON.parse('[\\\n"); - v.push_str(&all_sources.join(",\\\n")); - v.push_str("\\\n]'));\ncreateSrcSidebar();\n"); - Ok(v.into_bytes()) - }; - write_invocation_specific("src-files.js", &make_sources)?; + Ok(ret) } +} - // Update the search index and crate list. - let dst = cx.dst.join(&format!("search-index{}.js", cx.shared.resource_suffix)); - let (mut all_indexes, mut krates) = - try_err!(collect_json(&dst, krate.name(cx.tcx()).as_str()), &dst); - all_indexes.push(search_index.index); - krates.push(krate.name(cx.tcx()).to_string()); - krates.sort(); +#[derive(Serialize, Deserialize, Clone, Default, Debug)] +struct Sources; +type SourcesPart = Part; +impl CciPart for SourcesPart { + type FileFormat = sorted_template::Js; +} - // Sort the indexes by crate so the file will be generated identically even - // with rustdoc running in parallel. - all_indexes.sort(); - write_invocation_specific("search-index.js", &|| { +impl SourcesPart { + fn blank() -> SortedTemplate<::FileFormat> { // This needs to be `var`, not `const`. // This variable needs declared in the current global scope so that if - // search.js loads first, it can pick it up. - let mut v = String::from("var searchIndex = new Map(JSON.parse('[\\\n"); - v.push_str(&all_indexes.join(",\\\n")); - v.push_str( - r#"\ -]')); -if (typeof exports !== 'undefined') exports.searchIndex = searchIndex; -else if (window.initSearch) window.initSearch(searchIndex); -"#, - ); - Ok(v.into_bytes()) - })?; - - let search_desc_dir = cx.dst.join(format!("search.desc/{krate}", krate = krate.name(cx.tcx()))); - if Path::new(&search_desc_dir).exists() { - try_err!(std::fs::remove_dir_all(&search_desc_dir), &search_desc_dir); - } - try_err!(std::fs::create_dir_all(&search_desc_dir), &search_desc_dir); - let kratename = krate.name(cx.tcx()).to_string(); - for (i, (_, data)) in search_index.desc.into_iter().enumerate() { - let output_filename = static_files::suffix_path( - &format!("{kratename}-desc-{i}-.js"), - &cx.shared.resource_suffix, - ); - let path = search_desc_dir.join(output_filename); - try_err!( - std::fs::write( - &path, - &format!( - r##"searchState.loadedDescShard({kratename}, {i}, {data})"##, - kratename = serde_json::to_string(&kratename).unwrap(), - data = serde_json::to_string(&data).unwrap(), - ) - .into_bytes() - ), - &path - ); + // src-script.js loads first, it can pick it up. + SortedTemplate::before_after( + r"var srcIndex = new Map(JSON.parse('[", + r"]')); +createSrcSidebar();", + ) } - write_invocation_specific("crates.js", &|| { - let krates = krates.iter().map(|k| format!("\"{k}\"")).join(","); - Ok(format!("window.ALL_CRATES = [{krates}];").into_bytes()) - })?; + fn get(cx: &Context<'_>, crate_name: &SortedJson) -> Result, Error> { + let hierarchy = Rc::new(Hierarchy::default()); + cx.shared + .local_sources + .iter() + .filter_map(|p| p.0.strip_prefix(&cx.shared.src_root).ok()) + .for_each(|source| hierarchy.add_path(source)); + let path = suffix_path("src-files.js", &cx.shared.resource_suffix); + let hierarchy = hierarchy.to_json_string(); + let part = SortedJson::array_unsorted([crate_name, &hierarchy]); + let part = EscapedJson::from(part); + Ok(PartsAndLocations::with(path, part)) + } +} - if options.enable_index_page { - if let Some(index_page) = options.index_page.clone() { - let mut md_opts = options.clone(); - md_opts.output = cx.dst.clone(); - md_opts.external_html = (*cx.shared).layout.external_html.clone(); +/// Source files directory tree +#[derive(Debug, Default)] +struct Hierarchy { + parent: Weak, + elem: OsString, + children: RefCell>>, + elems: RefCell>, +} - crate::markdown::render(&index_page, md_opts, cx.shared.edition()) - .map_err(|e| Error::new(e, &index_page))?; - } else { - let shared = Rc::clone(&cx.shared); - let dst = cx.dst.join("index.html"); - let page = layout::Page { - title: "Index of crates", - css_class: "mod sys", - root_path: "./", - static_root_path: shared.static_root_path.as_deref(), - description: "List of crates", - resource_suffix: &shared.resource_suffix, - rust_logo: true, - }; +impl Hierarchy { + fn with_parent(elem: OsString, parent: &Rc) -> Self { + Self { elem, parent: Rc::downgrade(parent), ..Self::default() } + } - let content = format!( - "

    List of all crates

      {}
    ", - krates.iter().format_with("", |k, f| { - f(&format_args!( - "
  • {k}
  • ", - trailing_slash = ensure_trailing_slash(k), - )) - }) - ); - let v = layout::render(&shared.layout, &page, "", content, &shared.style_files); - shared.fs.write(dst, v)?; + fn to_json_string(&self) -> SortedJson { + let subs = self.children.borrow(); + let files = self.elems.borrow(); + let name = SortedJson::serialize(self.elem.to_str().expect("invalid osstring conversion")); + let mut out = Vec::from([name]); + if !subs.is_empty() || !files.is_empty() { + let subs = subs.iter().map(|(_, s)| s.to_json_string()); + out.push(SortedJson::array(subs)); } + if !files.is_empty() { + let files = + files.iter().map(|s| SortedJson::serialize(s.to_str().expect("invalid osstring"))); + out.push(SortedJson::array(files)); + } + SortedJson::array_unsorted(out) } - let cloned_shared = Rc::clone(&cx.shared); - let cache = &cloned_shared.cache; - - // Collect the list of aliased types and their aliases. - // - // - // The clean AST has type aliases that point at their types, but - // this visitor works to reverse that: `aliased_types` is a map - // from target to the aliases that reference it, and each one - // will generate one file. - struct TypeImplCollector<'cx, 'cache> { - // Map from DefId-of-aliased-type to its data. - aliased_types: IndexMap>, - visited_aliases: FxHashSet, - cache: &'cache Cache, - cx: &'cache mut Context<'cx>, - } - // Data for an aliased type. - // - // In the final file, the format will be roughly: - // - // ```json - // // type.impl/CRATE/TYPENAME.js - // JSONP( - // "CRATE": [ - // ["IMPL1 HTML", "ALIAS1", "ALIAS2", ...], - // ["IMPL2 HTML", "ALIAS3", "ALIAS4", ...], - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ struct AliasedType - // ... - // ] - // ) - // ``` - struct AliasedType<'cache> { - // This is used to generate the actual filename of this aliased type. - target_fqp: &'cache [Symbol], - target_type: ItemType, - // This is the data stored inside the file. - // ItemId is used to deduplicate impls. - impl_: IndexMap>, - } - // The `impl_` contains data that's used to figure out if an alias will work, - // and to generate the HTML at the end. - // - // The `type_aliases` list is built up with each type alias that matches. - struct AliasedTypeImpl<'cache> { - impl_: &'cache Impl, - type_aliases: Vec<(&'cache [Symbol], Item)>, - } - impl<'cx, 'cache> DocVisitor for TypeImplCollector<'cx, 'cache> { - fn visit_item(&mut self, it: &Item) { - self.visit_item_recur(it); - let cache = self.cache; - let ItemKind::TypeAliasItem(ref t) = *it.kind else { return }; - let Some(self_did) = it.item_id.as_def_id() else { return }; - if !self.visited_aliases.insert(self_did) { - return; - } - let Some(target_did) = t.type_.def_id(cache) else { return }; - let get_extern = { || cache.external_paths.get(&target_did) }; - let Some(&(ref target_fqp, target_type)) = - cache.paths.get(&target_did).or_else(get_extern) - else { - return; - }; - let aliased_type = self.aliased_types.entry(target_did).or_insert_with(|| { - let impl_ = cache - .impls - .get(&target_did) - .map(|v| &v[..]) - .unwrap_or_default() - .iter() - .map(|impl_| { - ( - impl_.impl_item.item_id, - AliasedTypeImpl { impl_, type_aliases: Vec::new() }, - ) - }) - .collect(); - AliasedType { target_fqp: &target_fqp[..], target_type, impl_ } - }); - let get_local = { || cache.paths.get(&self_did).map(|(p, _)| p) }; - let Some(self_fqp) = cache.exact_paths.get(&self_did).or_else(get_local) else { - return; - }; - let aliased_ty = self.cx.tcx().type_of(self_did).skip_binder(); - // Exclude impls that are directly on this type. They're already in the HTML. - // Some inlining scenarios can cause there to be two versions of the same - // impl: one on the type alias and one on the underlying target type. - let mut seen_impls: FxHashSet = cache - .impls - .get(&self_did) - .map(|s| &s[..]) - .unwrap_or_default() - .iter() - .map(|i| i.impl_item.item_id) - .collect(); - for (impl_item_id, aliased_type_impl) in &mut aliased_type.impl_ { - // Only include this impl if it actually unifies with this alias. - // Synthetic impls are not included; those are also included in the HTML. - // - // FIXME(lazy_type_alias): Once the feature is complete or stable, rewrite this - // to use type unification. - // Be aware of `tests/rustdoc/type-alias/deeply-nested-112515.rs` which might regress. - let Some(impl_did) = impl_item_id.as_def_id() else { continue }; - let for_ty = self.cx.tcx().type_of(impl_did).skip_binder(); - let reject_cx = DeepRejectCtxt::new(self.cx.tcx(), TreatParams::AsCandidateKey); - if !reject_cx.types_may_unify(aliased_ty, for_ty) { - continue; - } - // Avoid duplicates - if !seen_impls.insert(*impl_item_id) { - continue; + fn add_path(self: &Rc, path: &Path) { + let mut h = Rc::clone(&self); + let mut elems = path + .components() + .filter_map(|s| match s { + Component::Normal(s) => Some(s.to_owned()), + Component::ParentDir => Some(OsString::from("..")), + _ => None, + }) + .peekable(); + loop { + let cur_elem = elems.next().expect("empty file path"); + if cur_elem == ".." { + if let Some(parent) = h.parent.upgrade() { + h = parent; } - // This impl was not found in the set of rejected impls - aliased_type_impl.type_aliases.push((&self_fqp[..], it.clone())); + continue; } - } - } - let mut type_impl_collector = TypeImplCollector { - aliased_types: IndexMap::default(), - visited_aliases: FxHashSet::default(), - cache, - cx, - }; - DocVisitor::visit_crate(&mut type_impl_collector, &krate); - // Final serialized form of the alias impl - struct AliasSerializableImpl { - text: String, - trait_: Option, - aliases: Vec, - } - impl Serialize for AliasSerializableImpl { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut seq = serializer.serialize_seq(None)?; - seq.serialize_element(&self.text)?; - if let Some(trait_) = &self.trait_ { - seq.serialize_element(trait_)?; + if elems.peek().is_none() { + h.elems.borrow_mut().insert(cur_elem); + break; } else { - seq.serialize_element(&0)?; - } - for type_ in &self.aliases { - seq.serialize_element(type_)?; + let entry = Rc::clone( + h.children + .borrow_mut() + .entry(cur_elem.clone()) + .or_insert_with(|| Rc::new(Self::with_parent(cur_elem, &h))), + ); + h = entry; } - seq.end() } } - let cx = type_impl_collector.cx; - let dst = cx.dst.join("type.impl"); - let aliased_types = type_impl_collector.aliased_types; - for aliased_type in aliased_types.values() { - let impls = aliased_type - .impl_ - .values() - .flat_map(|AliasedTypeImpl { impl_, type_aliases }| { - let mut ret = Vec::new(); - let trait_ = impl_ - .inner_impl() - .trait_ - .as_ref() - .map(|trait_| format!("{:#}", trait_.print(cx))); - // render_impl will filter out "impossible-to-call" methods - // to make that functionality work here, it needs to be called with - // each type alias, and if it gives a different result, split the impl - for &(type_alias_fqp, ref type_alias_item) in type_aliases { - let mut buf = Buffer::html(); - cx.id_map = Default::default(); - cx.deref_id_map = Default::default(); - let target_did = impl_ +} + +#[derive(Serialize, Deserialize, Clone, Default, Debug)] +struct TypeAlias; +type TypeAliasPart = Part; +impl CciPart for TypeAliasPart { + type FileFormat = sorted_template::Js; +} + +impl TypeAliasPart { + fn blank() -> SortedTemplate<::FileFormat> { + SortedTemplate::before_after( + r"(function() { + var type_impls = Object.fromEntries([", + r"]); + if (window.register_type_impls) { + window.register_type_impls(type_impls); + } else { + window.pending_type_impls = type_impls; + } +})()", + ) + } + + fn get( + cx: &mut Context<'_>, + krate: &Crate, + crate_name_json: &SortedJson, + ) -> Result, Error> { + let cache = &Rc::clone(&cx.shared).cache; + let mut path_parts = PartsAndLocations::default(); + + let mut type_impl_collector = TypeImplCollector { + aliased_types: IndexMap::default(), + visited_aliases: FxHashSet::default(), + cache, + cx, + }; + DocVisitor::visit_crate(&mut type_impl_collector, &krate); + let cx = type_impl_collector.cx; + let aliased_types = type_impl_collector.aliased_types; + for aliased_type in aliased_types.values() { + let impls = aliased_type + .impl_ + .values() + .flat_map(|AliasedTypeImpl { impl_, type_aliases }| { + let mut ret = Vec::new(); + let trait_ = impl_ .inner_impl() .trait_ .as_ref() - .map(|trait_| trait_.def_id()) - .or_else(|| impl_.inner_impl().for_.def_id(cache)); - let provided_methods; - let assoc_link = if let Some(target_did) = target_did { - provided_methods = impl_.inner_impl().provided_trait_methods(cx.tcx()); - AssocItemLink::GotoSource(ItemId::DefId(target_did), &provided_methods) - } else { - AssocItemLink::Anchor(None) - }; - super::render_impl( - &mut buf, - cx, - *impl_, - &type_alias_item, - assoc_link, - RenderMode::Normal, - None, - &[], - ImplRenderingParameters { - show_def_docs: true, - show_default_items: true, - show_non_assoc_items: true, - toggle_open_by_default: true, - }, - ); - let text = buf.into_inner(); - let type_alias_fqp = (*type_alias_fqp).iter().join("::"); - if Some(&text) == ret.last().map(|s: &AliasSerializableImpl| &s.text) { - ret.last_mut() - .expect("already established that ret.last() is Some()") - .aliases - .push(type_alias_fqp); + .map(|trait_| format!("{:#}", trait_.print(cx))); + // render_impl will filter out "impossible-to-call" methods + // to make that functionality work here, it needs to be called with + // each type alias, and if it gives a different result, split the impl + for &(type_alias_fqp, ref type_alias_item) in type_aliases { + let mut buf = Buffer::html(); + cx.id_map = Default::default(); + cx.deref_id_map = Default::default(); + let target_did = impl_ + .inner_impl() + .trait_ + .as_ref() + .map(|trait_| trait_.def_id()) + .or_else(|| impl_.inner_impl().for_.def_id(cache)); + let provided_methods; + let assoc_link = if let Some(target_did) = target_did { + provided_methods = impl_.inner_impl().provided_trait_methods(cx.tcx()); + AssocItemLink::GotoSource(ItemId::DefId(target_did), &provided_methods) + } else { + AssocItemLink::Anchor(None) + }; + super::render_impl( + &mut buf, + cx, + *impl_, + &type_alias_item, + assoc_link, + RenderMode::Normal, + None, + &[], + ImplRenderingParameters { + show_def_docs: true, + show_default_items: true, + show_non_assoc_items: true, + toggle_open_by_default: true, + }, + ); + let text = buf.into_inner(); + let type_alias_fqp = (*type_alias_fqp).iter().join("::"); + if Some(&text) == ret.last().map(|s: &AliasSerializableImpl| &s.text) { + ret.last_mut() + .expect("already established that ret.last() is Some()") + .aliases + .push(type_alias_fqp); + } else { + ret.push(AliasSerializableImpl { + text, + trait_: trait_.clone(), + aliases: vec![type_alias_fqp], + }) + } + } + ret + }) + .collect::>(); + + let mut path = PathBuf::from("type.impl"); + for component in &aliased_type.target_fqp[..aliased_type.target_fqp.len() - 1] { + path.push(component.as_str()); + } + let aliased_item_type = aliased_type.target_type; + path.push(&format!( + "{aliased_item_type}.{}.js", + aliased_type.target_fqp[aliased_type.target_fqp.len() - 1] + )); + + let part = + SortedJson::array(impls.iter().map(SortedJson::serialize).collect::>()); + path_parts.push(path, SortedJson::array_unsorted([crate_name_json, &part])); + } + Ok(path_parts) + } +} + +#[derive(Serialize, Deserialize, Clone, Default, Debug)] +struct TraitAlias; +type TraitAliasPart = Part; +impl CciPart for TraitAliasPart { + type FileFormat = sorted_template::Js; +} + +impl TraitAliasPart { + fn blank() -> SortedTemplate<::FileFormat> { + SortedTemplate::before_after( + r"(function() { + var implementors = Object.fromEntries([", + r"]); + if (window.register_implementors) { + window.register_implementors(implementors); + } else { + window.pending_implementors = implementors; + } +})()", + ) + } + + fn get( + cx: &mut Context<'_>, + crate_name_json: &SortedJson, + ) -> Result, Error> { + let cache = &cx.shared.cache; + let mut path_parts = PartsAndLocations::default(); + // Update the list of all implementors for traits + // + for (&did, imps) in &cache.implementors { + // Private modules can leak through to this phase of rustdoc, which + // could contain implementations for otherwise private types. In some + // rare cases we could find an implementation for an item which wasn't + // indexed, so we just skip this step in that case. + // + // FIXME: this is a vague explanation for why this can't be a `get`, in + // theory it should be... + let (remote_path, remote_item_type) = match cache.exact_paths.get(&did) { + Some(p) => match cache.paths.get(&did).or_else(|| cache.external_paths.get(&did)) { + Some((_, t)) => (p, t), + None => continue, + }, + None => match cache.external_paths.get(&did) { + Some((p, t)) => (p, t), + None => continue, + }, + }; + + let implementors = imps + .iter() + .filter_map(|imp| { + // If the trait and implementation are in the same crate, then + // there's no need to emit information about it (there's inlining + // going on). If they're in different crates then the crate defining + // the trait will be interested in our implementation. + // + // If the implementation is from another crate then that crate + // should add it. + if imp.impl_item.item_id.krate() == did.krate + || !imp.impl_item.item_id.is_local() + { + None } else { - ret.push(AliasSerializableImpl { - text, - trait_: trait_.clone(), - aliases: vec![type_alias_fqp], + Some(Implementor { + text: imp.inner_impl().print(false, cx).to_string(), + synthetic: imp.inner_impl().kind.is_auto(), + types: collect_paths_for_type(imp.inner_impl().for_.clone(), cache), }) } - } - ret - }) - .collect::>(); + }) + .collect::>(); - // FIXME: this fixes only rustdoc part of instability of trait impls - // for js files, see #120371 - // Manually collect to string and sort to make list not depend on order - let mut impls = impls - .iter() - .map(|i| serde_json::to_string(i).expect("failed serde conversion")) - .collect::>(); - impls.sort(); + // Only create a js file if we have impls to add to it. If the trait is + // documented locally though we always create the file to avoid dead + // links. + if implementors.is_empty() && !cache.paths.contains_key(&did) { + continue; + } - let impls = format!(r#""{}":[{}]"#, krate.name(cx.tcx()), impls.join(",")); + let mut path = PathBuf::from("trait.impl"); + for component in &remote_path[..remote_path.len() - 1] { + path.push(component.as_str()); + } + path.push(&format!("{remote_item_type}.{}.js", remote_path[remote_path.len() - 1])); - let mut mydst = dst.clone(); - for part in &aliased_type.target_fqp[..aliased_type.target_fqp.len() - 1] { - mydst.push(part.to_string()); + let part = SortedJson::array( + implementors.iter().map(SortedJson::serialize).collect::>(), + ); + path_parts.push(path, SortedJson::array_unsorted([crate_name_json, &part])); } - cx.shared.ensure_dir(&mydst)?; - let aliased_item_type = aliased_type.target_type; - mydst.push(&format!( - "{aliased_item_type}.{}.js", - aliased_type.target_fqp[aliased_type.target_fqp.len() - 1] - )); - - let (mut all_impls, _) = try_err!(collect(&mydst, krate.name(cx.tcx()).as_str()), &mydst); - all_impls.push(impls); - // Sort the implementors by crate so the file will be generated - // identically even with rustdoc running in parallel. - all_impls.sort(); - - let mut v = String::from("(function() {var type_impls = {\n"); - v.push_str(&all_impls.join(",\n")); - v.push_str("\n};"); - v.push_str( - "if (window.register_type_impls) {\ - window.register_type_impls(type_impls);\ - } else {\ - window.pending_type_impls = type_impls;\ - }", - ); - v.push_str("})()"); - cx.shared.fs.write(mydst, v)?; - } - - // Update the list of all implementors for traits - // - let dst = cx.dst.join("trait.impl"); - for (&did, imps) in &cache.implementors { - // Private modules can leak through to this phase of rustdoc, which - // could contain implementations for otherwise private types. In some - // rare cases we could find an implementation for an item which wasn't - // indexed, so we just skip this step in that case. - // - // FIXME: this is a vague explanation for why this can't be a `get`, in - // theory it should be... - let (remote_path, remote_item_type) = match cache.exact_paths.get(&did) { - Some(p) => match cache.paths.get(&did).or_else(|| cache.external_paths.get(&did)) { - Some((_, t)) => (p, t), - None => continue, - }, - None => match cache.external_paths.get(&did) { - Some((p, t)) => (p, t), - None => continue, - }, - }; + Ok(path_parts) + } +} - struct Implementor { - text: String, - synthetic: bool, - types: Vec, +struct Implementor { + text: String, + synthetic: bool, + types: Vec, +} + +impl Serialize for Implementor { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut seq = serializer.serialize_seq(None)?; + seq.serialize_element(&self.text)?; + if self.synthetic { + seq.serialize_element(&1)?; + seq.serialize_element(&self.types)?; } + seq.end() + } +} - impl Serialize for Implementor { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut seq = serializer.serialize_seq(None)?; - seq.serialize_element(&self.text)?; - if self.synthetic { - seq.serialize_element(&1)?; - seq.serialize_element(&self.types)?; - } - seq.end() +/// Collect the list of aliased types and their aliases. +/// +/// +/// The clean AST has type aliases that point at their types, but +/// this visitor works to reverse that: `aliased_types` is a map +/// from target to the aliases that reference it, and each one +/// will generate one file. +struct TypeImplCollector<'cx, 'cache> { + /// Map from DefId-of-aliased-type to its data. + aliased_types: IndexMap>, + visited_aliases: FxHashSet, + cache: &'cache Cache, + cx: &'cache mut Context<'cx>, +} + +/// Data for an aliased type. +/// +/// In the final file, the format will be roughly: +/// +/// ```json +/// // type.impl/CRATE/TYPENAME.js +/// JSONP( +/// "CRATE": [ +/// ["IMPL1 HTML", "ALIAS1", "ALIAS2", ...], +/// ["IMPL2 HTML", "ALIAS3", "ALIAS4", ...], +/// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ struct AliasedType +/// ... +/// ] +/// ) +/// ``` +struct AliasedType<'cache> { + /// This is used to generate the actual filename of this aliased type. + target_fqp: &'cache [Symbol], + target_type: ItemType, + /// This is the data stored inside the file. + /// ItemId is used to deduplicate impls. + impl_: IndexMap>, +} + +/// The `impl_` contains data that's used to figure out if an alias will work, +/// and to generate the HTML at the end. +/// +/// The `type_aliases` list is built up with each type alias that matches. +struct AliasedTypeImpl<'cache> { + impl_: &'cache Impl, + type_aliases: Vec<(&'cache [Symbol], Item)>, +} + +impl<'cx, 'cache> DocVisitor for TypeImplCollector<'cx, 'cache> { + fn visit_item(&mut self, it: &Item) { + self.visit_item_recur(it); + let cache = self.cache; + let ItemKind::TypeAliasItem(ref t) = *it.kind else { return }; + let Some(self_did) = it.item_id.as_def_id() else { return }; + if !self.visited_aliases.insert(self_did) { + return; + } + let Some(target_did) = t.type_.def_id(cache) else { return }; + let get_extern = { || cache.external_paths.get(&target_did) }; + let Some(&(ref target_fqp, target_type)) = cache.paths.get(&target_did).or_else(get_extern) + else { + return; + }; + let aliased_type = self.aliased_types.entry(target_did).or_insert_with(|| { + let impl_ = cache + .impls + .get(&target_did) + .map(|v| &v[..]) + .unwrap_or_default() + .iter() + .map(|impl_| { + (impl_.impl_item.item_id, AliasedTypeImpl { impl_, type_aliases: Vec::new() }) + }) + .collect(); + AliasedType { target_fqp: &target_fqp[..], target_type, impl_ } + }); + let get_local = { || cache.paths.get(&self_did).map(|(p, _)| p) }; + let Some(self_fqp) = cache.exact_paths.get(&self_did).or_else(get_local) else { + return; + }; + let aliased_ty = self.cx.tcx().type_of(self_did).skip_binder(); + // Exclude impls that are directly on this type. They're already in the HTML. + // Some inlining scenarios can cause there to be two versions of the same + // impl: one on the type alias and one on the underlying target type. + let mut seen_impls: FxHashSet = cache + .impls + .get(&self_did) + .map(|s| &s[..]) + .unwrap_or_default() + .iter() + .map(|i| i.impl_item.item_id) + .collect(); + for (impl_item_id, aliased_type_impl) in &mut aliased_type.impl_ { + // Only include this impl if it actually unifies with this alias. + // Synthetic impls are not included; those are also included in the HTML. + // + // FIXME(lazy_type_alias): Once the feature is complete or stable, rewrite this + // to use type unification. + // Be aware of `tests/rustdoc/type-alias/deeply-nested-112515.rs` which might regress. + let Some(impl_did) = impl_item_id.as_def_id() else { continue }; + let for_ty = self.cx.tcx().type_of(impl_did).skip_binder(); + let reject_cx = DeepRejectCtxt::new(self.cx.tcx(), TreatParams::AsCandidateKey); + if !reject_cx.types_may_unify(aliased_ty, for_ty) { + continue; + } + // Avoid duplicates + if !seen_impls.insert(*impl_item_id) { + continue; } + // This impl was not found in the set of rejected impls + aliased_type_impl.type_aliases.push((&self_fqp[..], it.clone())); } + } +} - let implementors = imps - .iter() - .filter_map(|imp| { - // If the trait and implementation are in the same crate, then - // there's no need to emit information about it (there's inlining - // going on). If they're in different crates then the crate defining - // the trait will be interested in our implementation. - // - // If the implementation is from another crate then that crate - // should add it. - if imp.impl_item.item_id.krate() == did.krate || !imp.impl_item.item_id.is_local() { - None - } else { - Some(Implementor { - text: imp.inner_impl().print(false, cx).to_string(), - synthetic: imp.inner_impl().kind.is_auto(), - types: collect_paths_for_type(imp.inner_impl().for_.clone(), cache), - }) - } - }) - .collect::>(); +/// Final serialized form of the alias impl +struct AliasSerializableImpl { + text: String, + trait_: Option, + aliases: Vec, +} - // Only create a js file if we have impls to add to it. If the trait is - // documented locally though we always create the file to avoid dead - // links. - if implementors.is_empty() && !cache.paths.contains_key(&did) { - continue; +impl Serialize for AliasSerializableImpl { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut seq = serializer.serialize_seq(None)?; + seq.serialize_element(&self.text)?; + if let Some(trait_) = &self.trait_ { + seq.serialize_element(trait_)?; + } else { + seq.serialize_element(&0)?; + } + for type_ in &self.aliases { + seq.serialize_element(type_)?; } + seq.end() + } +} - // FIXME: this fixes only rustdoc part of instability of trait impls - // for js files, see #120371 - // Manually collect to string and sort to make list not depend on order - let mut implementors = implementors - .iter() - .map(|i| serde_json::to_string(i).expect("failed serde conversion")) - .collect::>(); - implementors.sort(); +/// Create all parents +fn create_parents(path: &Path) -> Result<(), Error> { + let parent = path.parent().expect("should not have an empty path here"); + try_err!(fs::create_dir_all(parent), parent); + Ok(()) +} - let implementors = format!(r#""{}":[{}]"#, krate.name(cx.tcx()), implementors.join(",")); +/// Create parents and then write +fn write_create_parents(path: &Path, content: String) -> Result<(), Error> { + create_parents(path)?; + try_err!(fs::write(path, content), path); + Ok(()) +} - let mut mydst = dst.clone(); - for part in &remote_path[..remote_path.len() - 1] { - mydst.push(part.to_string()); +/// Returns a blank template unless we could find one to append to +fn read_template_or_blank( + mut make_blank: F, + path: &Path, +) -> Result, Error> +where + F: FnMut() -> SortedTemplate, +{ + match fs::read_to_string(&path) { + Ok(template) => Ok(try_err!(SortedTemplate::from_str(&template), &path)), + Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(make_blank()), + Err(e) => Err(Error::new(e, &path)), + } +} + +/// info from this crate and the --include-info-json'd crates +fn write_rendered_cci( + mut make_blank: F, + dst: &Path, + crates_info: &[CrateInfo], +) -> Result<(), Error> +where + F: FnMut() -> SortedTemplate, +{ + // read parts from disk + let path_parts = + crates_info.iter().map(|crate_info| crate_info.get::().parts.iter()).flatten(); + // read previous rendered cci from storage, append to them + let mut templates: FxHashMap> = Default::default(); + for (path, part) in path_parts { + let part = format!("{part}"); + let path = dst.join(&path); + match templates.entry(path.clone()) { + Entry::Vacant(entry) => { + let template = read_template_or_blank::<_, T>(&mut make_blank, &path)?; + let template = entry.insert(template); + template.append(part); + } + Entry::Occupied(mut t) => t.get_mut().append(part), } - cx.shared.ensure_dir(&mydst)?; - mydst.push(&format!("{remote_item_type}.{}.js", remote_path[remote_path.len() - 1])); - - let (mut all_implementors, _) = - try_err!(collect(&mydst, krate.name(cx.tcx()).as_str()), &mydst); - all_implementors.push(implementors); - // Sort the implementors by crate so the file will be generated - // identically even with rustdoc running in parallel. - all_implementors.sort(); - - let mut v = String::from("(function() {var implementors = {\n"); - v.push_str(&all_implementors.join(",\n")); - v.push_str("\n};"); - v.push_str( - "if (window.register_implementors) {\ - window.register_implementors(implementors);\ - } else {\ - window.pending_implementors = implementors;\ - }", - ); - v.push_str("})()"); - cx.shared.fs.write(mydst, v)?; + } + + // write the merged cci to disk + for (path, template) in templates { + create_parents(&path)?; + let file = try_err!(File::create(&path), &path); + let mut file = BufWriter::new(file); + try_err!(write!(file, "{template}"), &path); + try_err!(file.flush(), &path); } Ok(()) } From f1a996c39b4accf958cb02028e9b8e5d4e3d796d Mon Sep 17 00:00:00 2001 From: EtomicBomb Date: Thu, 25 Jul 2024 00:05:32 +0000 Subject: [PATCH 2/5] add blank line, remove extraneous comment --- src/librustdoc/config.rs | 1 + src/librustdoc/html/render/context.rs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index 2e54a22840bb5..e4549796b3e83 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -730,6 +730,7 @@ impl Options { let extern_html_root_takes_precedence = matches.opt_present("extern-html-root-takes-precedence"); let html_no_source = matches.opt_present("html-no-source"); + if generate_link_to_definition && (show_coverage || output_format != OutputFormat::Html) { dcx.fatal( "--generate-link-to-definition option can only be used with HTML output format", diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs index 8e72dd6a864ae..0ed8921b1e8d7 100644 --- a/src/librustdoc/html/render/context.rs +++ b/src/librustdoc/html/render/context.rs @@ -722,7 +722,6 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { ); shared.fs.write(help_file, v)?; - // if to avoid writing files to doc root unless we're on the final invocation if shared.layout.scrape_examples_extension { page.title = "About scraped examples"; page.description = "How the scraped examples feature works in Rustdoc"; From 17c89239d92eca1e4e22ff6ba9d1be0f2f7b36fc Mon Sep 17 00:00:00 2001 From: EtomicBomb Date: Wed, 24 Jul 2024 23:36:49 +0000 Subject: [PATCH 3/5] move sorted_template and sorted_json tests --- src/librustdoc/clean/types.rs | 2 +- .../html/render/sorted_json/tests.rs | 119 ++++++++ src/librustdoc/html/render/sorted_template.rs | 3 + .../html/render/sorted_template/tests.rs | 148 ++++++++++ src/librustdoc/html/render/tests.rs | 271 ------------------ 5 files changed, 271 insertions(+), 272 deletions(-) create mode 100644 src/librustdoc/html/render/sorted_json/tests.rs create mode 100644 src/librustdoc/html/render/sorted_template/tests.rs diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index 542e810b5cfa8..4850500a1bfae 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -128,7 +128,7 @@ pub(crate) struct ExternalCrate { } impl ExternalCrate { - pub(crate) const LOCAL: Self = Self { crate_num: LOCAL_CRATE }; + const LOCAL: Self = Self { crate_num: LOCAL_CRATE }; #[inline] pub(crate) fn def_id(&self) -> DefId { diff --git a/src/librustdoc/html/render/sorted_json/tests.rs b/src/librustdoc/html/render/sorted_json/tests.rs new file mode 100644 index 0000000000000..1e72c6f614c38 --- /dev/null +++ b/src/librustdoc/html/render/sorted_json/tests.rs @@ -0,0 +1,119 @@ +use super::super::sorted_json::*; + +fn check(json: SortedJson, serialized: &str) { + assert_eq!(json.to_string(), serialized); + assert_eq!(serde_json::to_string(&json).unwrap(), serialized); + + let json = json.to_string(); + let json: SortedJson = serde_json::from_str(&json).unwrap(); + + assert_eq!(json.to_string(), serialized); + assert_eq!(serde_json::to_string(&json).unwrap(), serialized); + + let json = serde_json::to_string(&json).unwrap(); + let json: SortedJson = serde_json::from_str(&json).unwrap(); + + assert_eq!(json.to_string(), serialized); + assert_eq!(serde_json::to_string(&json).unwrap(), serialized); +} + +// Test this basic are needed because we are testing that our Display impl + serialize impl don't +// nest everything in extra level of string. We also are testing round trip. +#[test] +fn escape_json_number() { + let json = SortedJson::serialize(3); + let json = EscapedJson::from(json); + assert_eq!(format!("{json}"), "3"); +} + +#[test] +fn escape_json_single_quote() { + let json = SortedJson::serialize("he's"); + let json = EscapedJson::from(json); + assert_eq!(format!("{json}"), r#""he\'s""#); +} + +#[test] +fn escape_json_array() { + let json = SortedJson::serialize([1, 2, 3]); + let json = EscapedJson::from(json); + assert_eq!(format!("{json}"), r#"[1,2,3]"#); +} + +#[test] +fn escape_json_string() { + let json = SortedJson::serialize(r#"he"llo"#); + let json = EscapedJson::from(json); + assert_eq!(format!("{json}"), r#""he\\\"llo""#); +} + +#[test] +fn escape_json_string_escaped() { + let json = SortedJson::serialize(r#"he\"llo"#); + let json = EscapedJson::from(json); + assert_eq!(format!("{json}"), r#""he\\\\\\\"llo""#); +} + +#[test] +fn escape_json_string_escaped_escaped() { + let json = SortedJson::serialize(r#"he\\"llo"#); + let json = EscapedJson::from(json); + assert_eq!(format!("{json}"), r#""he\\\\\\\\\\\"llo""#); +} + +#[test] +fn number() { + let json = SortedJson::serialize(3); + let serialized = "3"; + check(json, serialized); +} + +#[test] +fn boolean() { + let json = SortedJson::serialize(true); + let serialized = "true"; + check(json, serialized); +} + +#[test] +fn string() { + let json = SortedJson::serialize("he\"llo"); + let serialized = r#""he\"llo""#; + check(json, serialized); +} + +#[test] +fn serialize_array() { + let json = SortedJson::serialize([3, 1, 2]); + let serialized = "[3,1,2]"; + check(json, serialized); +} + +#[test] +fn sorted_array() { + let items = ["c", "a", "b"]; + let serialized = r#"["a","b","c"]"#; + let items: Vec = items.into_iter().map(SortedJson::serialize).collect(); + let json = SortedJson::array(items); + check(json, serialized); +} + +#[test] +fn nested_array() { + let a = SortedJson::serialize(3); + let b = SortedJson::serialize(2); + let c = SortedJson::serialize(1); + let d = SortedJson::serialize([1, 3, 2]); + let json = SortedJson::array([a, b, c, d]); + let serialized = r#"[1,2,3,[1,3,2]]"#; + check(json, serialized); +} + +#[test] +fn array_unsorted() { + let items = ["c", "a", "b"]; + let serialized = r#"["c","a","b"]"#; + let items: Vec = items.into_iter().map(SortedJson::serialize).collect(); + let json = SortedJson::array_unsorted(items); + check(json, serialized); +} diff --git a/src/librustdoc/html/render/sorted_template.rs b/src/librustdoc/html/render/sorted_template.rs index 95240616b01dc..8e0a2ee0fd4d9 100644 --- a/src/librustdoc/html/render/sorted_template.rs +++ b/src/librustdoc/html/render/sorted_template.rs @@ -134,3 +134,6 @@ impl fmt::Display for Error { write!(f, "invalid template") } } + +#[cfg(test)] +mod tests; diff --git a/src/librustdoc/html/render/sorted_template/tests.rs b/src/librustdoc/html/render/sorted_template/tests.rs new file mode 100644 index 0000000000000..04553f65a2154 --- /dev/null +++ b/src/librustdoc/html/render/sorted_template/tests.rs @@ -0,0 +1,148 @@ +use super::super::sorted_template::*; +use std::str::FromStr; + +fn is_comment_js(s: &str) -> bool { + s.starts_with("//") +} + +fn is_comment_html(s: &str) -> bool { + // not correct but good enough for these tests + s.starts_with("") +} + +#[test] +fn html_from_empty() { + let inserts = ["

    hello

    ", "

    kind

    ", "

    hello

    ", "

    world

    "]; + let mut template = SortedTemplate::::before_after("", ""); + for insert in inserts { + template.append(insert.to_string()); + } + let template = format!("{template}"); + let (template, end) = template.rsplit_once("\n").unwrap(); + assert_eq!(template, "

    hello

    kind

    world

    "); + assert!(is_comment_html(end)); + assert!(!end.contains("\n")); +} + +#[test] +fn html_page() { + let inserts = ["

    hello

    ", "

    kind

    ", "

    world

    "]; + let before = ""; + let after = ""; + let mut template = SortedTemplate::::before_after(before, after); + for insert in inserts { + template.append(insert.to_string()); + } + let template = format!("{template}"); + let (template, end) = template.rsplit_once("\n").unwrap(); + assert_eq!(template, format!("{before}{}{after}", inserts.join(""))); + assert!(is_comment_html(end)); + assert!(!end.contains("\n")); +} + +#[test] +fn js_from_empty() { + let inserts = ["1", "2", "2", "2", "3", "1"]; + let mut template = SortedTemplate::::before_after("", ""); + for insert in inserts { + template.append(insert.to_string()); + } + let template = format!("{template}"); + let (template, end) = template.rsplit_once("\n").unwrap(); + assert_eq!(template, "1,2,3"); + assert!(is_comment_js(end)); + assert!(!end.contains("\n")); +} + +#[test] +fn js_empty_array() { + let template = SortedTemplate::::before_after("[", "]"); + let template = format!("{template}"); + let (template, end) = template.rsplit_once("\n").unwrap(); + assert_eq!(template, format!("[]")); + assert!(is_comment_js(end)); + assert!(!end.contains("\n")); +} + +#[test] +fn js_number_array() { + let inserts = ["1", "2", "3"]; + let mut template = SortedTemplate::::before_after("[", "]"); + for insert in inserts { + template.append(insert.to_string()); + } + let template = format!("{template}"); + let (template, end) = template.rsplit_once("\n").unwrap(); + assert_eq!(template, format!("[1,2,3]")); + assert!(is_comment_js(end)); + assert!(!end.contains("\n")); +} + +#[test] +fn magic_js_number_array() { + let inserts = ["1", "1"]; + let mut template = SortedTemplate::::magic("[#]", "#").unwrap(); + for insert in inserts { + template.append(insert.to_string()); + } + let template = format!("{template}"); + let (template, end) = template.rsplit_once("\n").unwrap(); + assert_eq!(template, format!("[1]")); + assert!(is_comment_js(end)); + assert!(!end.contains("\n")); +} + +#[test] +fn round_trip_js() { + let inserts = ["1", "2", "3"]; + let mut template = SortedTemplate::::before_after("[", "]"); + for insert in inserts { + template.append(insert.to_string()); + } + let template1 = format!("{template}"); + let mut template = SortedTemplate::::from_str(&template1).unwrap(); + assert_eq!(template1, format!("{template}")); + template.append("4".to_string()); + let template = format!("{template}"); + let (template, end) = template.rsplit_once("\n").unwrap(); + assert_eq!(template, "[1,2,3,4]"); + assert!(is_comment_js(end)); +} + +#[test] +fn round_trip_html() { + let inserts = ["

    hello

    ", "

    kind

    ", "

    world

    ", "

    kind

    "]; + let before = ""; + let after = ""; + let mut template = SortedTemplate::::before_after(before, after); + template.append(inserts[0].to_string()); + template.append(inserts[1].to_string()); + let template = format!("{template}"); + let mut template = SortedTemplate::::from_str(&template).unwrap(); + template.append(inserts[2].to_string()); + let template = format!("{template}"); + let (template, end) = template.rsplit_once("\n").unwrap(); + assert_eq!(template, format!("{before}

    hello

    kind

    world

    {after}")); + assert!(is_comment_html(end)); +} + +#[test] +fn blank_js() { + let inserts = ["1", "2", "3"]; + let template = SortedTemplate::::before_after("", ""); + let template = format!("{template}"); + let (t, _) = template.rsplit_once("\n").unwrap(); + assert_eq!(t, ""); + let mut template = SortedTemplate::::from_str(&template).unwrap(); + for insert in inserts { + template.append(insert.to_string()); + } + let template1 = format!("{template}"); + let mut template = SortedTemplate::::from_str(&template1).unwrap(); + assert_eq!(template1, format!("{template}")); + template.append("4".to_string()); + let template = format!("{template}"); + let (template, end) = template.rsplit_once("\n").unwrap(); + assert_eq!(template, "1,2,3,4"); + assert!(is_comment_js(end)); +} diff --git a/src/librustdoc/html/render/tests.rs b/src/librustdoc/html/render/tests.rs index 16e67b0f1180e..4a9724a6f840f 100644 --- a/src/librustdoc/html/render/tests.rs +++ b/src/librustdoc/html/render/tests.rs @@ -52,274 +52,3 @@ fn test_all_types_prints_header_once() { assert_eq!(1, buffer.into_inner().matches("List of all items").count()); } - -mod sorted_json { - use super::super::sorted_json::*; - - fn check(json: SortedJson, serialized: &str) { - assert_eq!(json.to_string(), serialized); - assert_eq!(serde_json::to_string(&json).unwrap(), serialized); - - let json = json.to_string(); - let json: SortedJson = serde_json::from_str(&json).unwrap(); - - assert_eq!(json.to_string(), serialized); - assert_eq!(serde_json::to_string(&json).unwrap(), serialized); - - let json = serde_json::to_string(&json).unwrap(); - let json: SortedJson = serde_json::from_str(&json).unwrap(); - - assert_eq!(json.to_string(), serialized); - assert_eq!(serde_json::to_string(&json).unwrap(), serialized); - } - - #[test] - fn escape_json_number() { - let json = SortedJson::serialize(3); - let json = EscapedJson::from(json); - assert_eq!(format!("{json}"), "3"); - } - - #[test] - fn escape_json_single_quote() { - let json = SortedJson::serialize("he's"); - let json = EscapedJson::from(json); - assert_eq!(format!("{json}"), r#""he\'s""#); - } - - #[test] - fn escape_json_array() { - let json = SortedJson::serialize([1, 2, 3]); - let json = EscapedJson::from(json); - assert_eq!(format!("{json}"), r#"[1,2,3]"#); - } - - #[test] - fn escape_json_string() { - let json = SortedJson::serialize(r#"he"llo"#); - let json = EscapedJson::from(json); - assert_eq!(format!("{json}"), r#""he\\\"llo""#); - } - - #[test] - fn escape_json_string_escaped() { - let json = SortedJson::serialize(r#"he\"llo"#); - let json = EscapedJson::from(json); - assert_eq!(format!("{json}"), r#""he\\\\\\\"llo""#); - } - - #[test] - fn escape_json_string_escaped_escaped() { - let json = SortedJson::serialize(r#"he\\"llo"#); - let json = EscapedJson::from(json); - assert_eq!(format!("{json}"), r#""he\\\\\\\\\\\"llo""#); - } - - #[test] - fn number() { - let json = SortedJson::serialize(3); - let serialized = "3"; - check(json, serialized); - } - - #[test] - fn boolean() { - let json = SortedJson::serialize(true); - let serialized = "true"; - check(json, serialized); - } - - #[test] - fn string() { - let json = SortedJson::serialize("he\"llo"); - let serialized = r#""he\"llo""#; - check(json, serialized); - } - - #[test] - fn serialize_array() { - let json = SortedJson::serialize([3, 1, 2]); - let serialized = "[3,1,2]"; - check(json, serialized); - } - - #[test] - fn sorted_array() { - let items = ["c", "a", "b"]; - let serialized = r#"["a","b","c"]"#; - let items: Vec = items.into_iter().map(SortedJson::serialize).collect(); - let json = SortedJson::array(items); - check(json, serialized); - } - - #[test] - fn nested_array() { - let a = SortedJson::serialize(3); - let b = SortedJson::serialize(2); - let c = SortedJson::serialize(1); - let d = SortedJson::serialize([1, 3, 2]); - let json = SortedJson::array([a, b, c, d]); - let serialized = r#"[1,2,3,[1,3,2]]"#; - check(json, serialized); - } - - #[test] - fn array_unsorted() { - let items = ["c", "a", "b"]; - let serialized = r#"["c","a","b"]"#; - let items: Vec = items.into_iter().map(SortedJson::serialize).collect(); - let json = SortedJson::array_unsorted(items); - check(json, serialized); - } -} - -mod sorted_template { - use super::super::sorted_template::*; - use std::str::FromStr; - - fn is_comment_js(s: &str) -> bool { - s.starts_with("//") - } - - fn is_comment_html(s: &str) -> bool { - // not correct but good enough for these tests - s.starts_with("") - } - - #[test] - fn html_from_empty() { - let inserts = ["

    hello

    ", "

    kind

    ", "

    hello

    ", "

    world

    "]; - let mut template = SortedTemplate::::before_after("", ""); - for insert in inserts { - template.append(insert.to_string()); - } - let template = format!("{template}"); - let (template, end) = template.rsplit_once("\n").unwrap(); - assert_eq!(template, "

    hello

    kind

    world

    "); - assert!(is_comment_html(end)); - assert!(!end.contains("\n")); - } - - #[test] - fn html_page() { - let inserts = ["

    hello

    ", "

    kind

    ", "

    world

    "]; - let before = ""; - let after = ""; - let mut template = SortedTemplate::::before_after(before, after); - for insert in inserts { - template.append(insert.to_string()); - } - let template = format!("{template}"); - let (template, end) = template.rsplit_once("\n").unwrap(); - assert_eq!(template, format!("{before}{}{after}", inserts.join(""))); - assert!(is_comment_html(end)); - assert!(!end.contains("\n")); - } - - #[test] - fn js_from_empty() { - let inserts = ["1", "2", "2", "2", "3", "1"]; - let mut template = SortedTemplate::::before_after("", ""); - for insert in inserts { - template.append(insert.to_string()); - } - let template = format!("{template}"); - let (template, end) = template.rsplit_once("\n").unwrap(); - assert_eq!(template, "1,2,3"); - assert!(is_comment_js(end)); - assert!(!end.contains("\n")); - } - - #[test] - fn js_empty_array() { - let template = SortedTemplate::::before_after("[", "]"); - let template = format!("{template}"); - let (template, end) = template.rsplit_once("\n").unwrap(); - assert_eq!(template, format!("[]")); - assert!(is_comment_js(end)); - assert!(!end.contains("\n")); - } - - #[test] - fn js_number_array() { - let inserts = ["1", "2", "3"]; - let mut template = SortedTemplate::::before_after("[", "]"); - for insert in inserts { - template.append(insert.to_string()); - } - let template = format!("{template}"); - let (template, end) = template.rsplit_once("\n").unwrap(); - assert_eq!(template, format!("[1,2,3]")); - assert!(is_comment_js(end)); - assert!(!end.contains("\n")); - } - - #[test] - fn magic_js_number_array() { - let inserts = ["1", "1"]; - let mut template = SortedTemplate::::magic("[#]", "#").unwrap(); - for insert in inserts { - template.append(insert.to_string()); - } - let template = format!("{template}"); - let (template, end) = template.rsplit_once("\n").unwrap(); - assert_eq!(template, format!("[1]")); - assert!(is_comment_js(end)); - assert!(!end.contains("\n")); - } - - #[test] - fn round_trip_js() { - let inserts = ["1", "2", "3"]; - let mut template = SortedTemplate::::before_after("[", "]"); - for insert in inserts { - template.append(insert.to_string()); - } - let template1 = format!("{template}"); - let mut template = SortedTemplate::::from_str(&template1).unwrap(); - assert_eq!(template1, format!("{template}")); - template.append("4".to_string()); - let template = format!("{template}"); - let (template, end) = template.rsplit_once("\n").unwrap(); - assert_eq!(template, "[1,2,3,4]"); - assert!(is_comment_js(end)); - } - - #[test] - fn round_trip_html() { - let inserts = ["

    hello

    ", "

    kind

    ", "

    world

    ", "

    kind

    "]; - let before = ""; - let after = ""; - let mut template = SortedTemplate::::before_after(before, after); - template.append(inserts[0].to_string()); - template.append(inserts[1].to_string()); - let template = format!("{template}"); - let mut template = SortedTemplate::::from_str(&template).unwrap(); - template.append(inserts[2].to_string()); - let template = format!("{template}"); - let (template, end) = template.rsplit_once("\n").unwrap(); - assert_eq!(template, format!("{before}

    hello

    kind

    world

    {after}")); - assert!(is_comment_html(end)); - } - - #[test] - fn blank_js() { - let inserts = ["1", "2", "3"]; - let template = SortedTemplate::::before_after("", ""); - let template = format!("{template}"); - let (t, _) = template.rsplit_once("\n").unwrap(); - assert_eq!(t, ""); - let mut template = SortedTemplate::::from_str(&template).unwrap(); - for insert in inserts { - template.append(insert.to_string()); - } - let template1 = format!("{template}"); - let mut template = SortedTemplate::::from_str(&template1).unwrap(); - assert_eq!(template1, format!("{template}")); - template.append("4".to_string()); - let template = format!("{template}"); - let (template, end) = template.rsplit_once("\n").unwrap(); - assert_eq!(template, "1,2,3,4"); - assert!(is_comment_js(end)); - } -} From 67663fc680428cf22267f974106d2805008a8568 Mon Sep 17 00:00:00 2001 From: EtomicBomb Date: Thu, 25 Jul 2024 15:27:29 +0000 Subject: [PATCH 4/5] added unit tests for write_shared --- src/librustdoc/html/render/sorted_json.rs | 3 + src/librustdoc/html/render/write_shared.rs | 99 ++++----- .../html/render/write_shared/tests.rs | 206 ++++++++++++++++++ 3 files changed, 257 insertions(+), 51 deletions(-) create mode 100644 src/librustdoc/html/render/write_shared/tests.rs diff --git a/src/librustdoc/html/render/sorted_json.rs b/src/librustdoc/html/render/sorted_json.rs index 3a097733b8b20..e937382f5b0a6 100644 --- a/src/librustdoc/html/render/sorted_json.rs +++ b/src/librustdoc/html/render/sorted_json.rs @@ -80,3 +80,6 @@ impl fmt::Display for EscapedJson { write!(f, "{}", json) } } + +#[cfg(test)] +mod tests; diff --git a/src/librustdoc/html/render/write_shared.rs b/src/librustdoc/html/render/write_shared.rs index eaebeadd8817e..c2d2b4cd7d9fb 100644 --- a/src/librustdoc/html/render/write_shared.rs +++ b/src/librustdoc/html/render/write_shared.rs @@ -15,7 +15,6 @@ use std::any::Any; use std::cell::RefCell; -use std::collections::hash_map::Entry; use std::ffi::OsString; use std::fs::File; use std::io::BufWriter; @@ -52,7 +51,7 @@ use crate::html::layout; use crate::html::render::search_index::build_index; use crate::html::render::search_index::SerializedSearchIndex; use crate::html::render::sorted_json::{EscapedJson, SortedJson}; -use crate::html::render::sorted_template::{self, SortedTemplate}; +use crate::html::render::sorted_template::{self, FileFormat, SortedTemplate}; use crate::html::render::{AssocItemLink, ImplRenderingParameters}; use crate::html::static_files::{self, suffix_path}; use crate::visit::DocVisitor; @@ -78,33 +77,29 @@ pub(crate) fn write_shared( let crate_name = krate.name(cx.tcx()); let crate_name = crate_name.as_str(); // rand let crate_name_json = SortedJson::serialize(crate_name); // "rand" - let external_crates = hack_get_external_crate_names(cx)?; + let external_crates = hack_get_external_crate_names(&cx.dst)?; let info = CrateInfo { src_files_js: SourcesPart::get(cx, &crate_name_json)?, - search_index_js: SearchIndexPart::get(cx, index)?, + search_index_js: SearchIndexPart::get(index, &cx.shared.resource_suffix)?, all_crates: AllCratesPart::get(crate_name_json.clone())?, crates_index: CratesIndexPart::get(&crate_name, &external_crates)?, trait_impl: TraitAliasPart::get(cx, &crate_name_json)?, type_impl: TypeAliasPart::get(cx, krate, &crate_name_json)?, }; - let crates_info = vec![info]; // we have info from just one crate + let crates = vec![info]; // we have info from just one crate. rest will found in out dir write_static_files(cx, &opt)?; let dst = &cx.dst; if opt.emit.is_empty() || opt.emit.contains(&EmitType::InvocationSpecific) { if cx.include_sources { - write_rendered_cci::(SourcesPart::blank, dst, &crates_info)?; + write_rendered_cci::(SourcesPart::blank, dst, &crates)?; } - write_rendered_cci::( - SearchIndexPart::blank, - dst, - &crates_info, - )?; - write_rendered_cci::(AllCratesPart::blank, dst, &crates_info)?; - } - write_rendered_cci::(TraitAliasPart::blank, dst, &crates_info)?; - write_rendered_cci::(TypeAliasPart::blank, dst, &crates_info)?; + write_rendered_cci::(SearchIndexPart::blank, dst, &crates)?; + write_rendered_cci::(AllCratesPart::blank, dst, &crates)?; + } + write_rendered_cci::(TraitAliasPart::blank, dst, &crates)?; + write_rendered_cci::(TypeAliasPart::blank, dst, &crates)?; match &opt.index_page { Some(index_page) if opt.enable_index_page => { let mut md_opts = opt.clone(); @@ -119,7 +114,7 @@ pub(crate) fn write_shared( write_rendered_cci::( || CratesIndexPart::blank(cx), dst, - &crates_info, + &crates, )?; } _ => {} // they don't want an index page @@ -189,7 +184,8 @@ fn write_search_desc( let path = path.join(filename); let part = SortedJson::serialize(&part); let part = format!("searchState.loadedDescShard({encoded_crate_name}, {i}, {part})"); - write_create_parents(&path, part)?; + create_parents(&path)?; + try_err!(fs::write(&path, part), &path); } Ok(()) } @@ -286,8 +282,11 @@ else if (window.initSearch) window.initSearch(searchIndex);", ) } - fn get(cx: &Context<'_>, search_index: SortedJson) -> Result, Error> { - let path = suffix_path("search-index.js", &cx.shared.resource_suffix); + fn get( + search_index: SortedJson, + resource_suffix: &str, + ) -> Result, Error> { + let path = suffix_path("search-index.js", resource_suffix); let search_index = EscapedJson::from(search_index); Ok(PartsAndLocations::with(path, search_index)) } @@ -319,8 +318,8 @@ impl AllCratesPart { /// /// This is to match the current behavior of rustdoc, which allows you to get all crates /// on the index page, even if --enable-index-page is only passed to the last crate. -fn hack_get_external_crate_names(cx: &Context<'_>) -> Result, Error> { - let path = cx.dst.join("crates.js"); +fn hack_get_external_crate_names(doc_root: &Path) -> Result, Error> { + let path = doc_root.join("crates.js"); let Ok(content) = fs::read_to_string(&path) else { // they didn't emit invocation specific, so we just say there were no crates return Ok(Vec::default()); @@ -361,7 +360,7 @@ impl CratesIndexPart { match SortedTemplate::magic(&template, MAGIC) { Ok(template) => template, Err(e) => panic!( - "{e}: Object Replacement Character (U+FFFC) should not appear in the --index-page" + "Object Replacement Character (U+FFFC) should not appear in the --index-page: {e}" ), } } @@ -860,6 +859,21 @@ impl Serialize for AliasSerializableImpl { } } +fn get_path_parts( + dst: &Path, + crates_info: &[CrateInfo], +) -> FxHashMap> { + let mut templates: FxHashMap> = FxHashMap::default(); + crates_info.iter().map(|crate_info| crate_info.get::().parts.iter()).flatten().for_each( + |(path, part)| { + let path = dst.join(&path); + let part = part.to_string(); + templates.entry(path).or_default().push(part); + }, + ); + templates +} + /// Create all parents fn create_parents(path: &Path) -> Result<(), Error> { let parent = path.parent().expect("should not have an empty path here"); @@ -867,20 +881,13 @@ fn create_parents(path: &Path) -> Result<(), Error> { Ok(()) } -/// Create parents and then write -fn write_create_parents(path: &Path, content: String) -> Result<(), Error> { - create_parents(path)?; - try_err!(fs::write(path, content), path); - Ok(()) -} - /// Returns a blank template unless we could find one to append to -fn read_template_or_blank( +fn read_template_or_blank( mut make_blank: F, path: &Path, -) -> Result, Error> +) -> Result, Error> where - F: FnMut() -> SortedTemplate, + F: FnMut() -> SortedTemplate, { match fs::read_to_string(&path) { Ok(template) => Ok(try_err!(SortedTemplate::from_str(&template), &path)), @@ -898,27 +905,14 @@ fn write_rendered_cci( where F: FnMut() -> SortedTemplate, { - // read parts from disk - let path_parts = - crates_info.iter().map(|crate_info| crate_info.get::().parts.iter()).flatten(); - // read previous rendered cci from storage, append to them - let mut templates: FxHashMap> = Default::default(); - for (path, part) in path_parts { - let part = format!("{part}"); - let path = dst.join(&path); - match templates.entry(path.clone()) { - Entry::Vacant(entry) => { - let template = read_template_or_blank::<_, T>(&mut make_blank, &path)?; - let template = entry.insert(template); - template.append(part); - } - Entry::Occupied(mut t) => t.get_mut().append(part), - } - } - // write the merged cci to disk - for (path, template) in templates { + for (path, parts) in get_path_parts::(dst, crates_info) { create_parents(&path)?; + // read previous rendered cci from storage, append to them + let mut template = read_template_or_blank::<_, T::FileFormat>(&mut make_blank, &path)?; + for part in parts { + template.append(part); + } let file = try_err!(File::create(&path), &path); let mut file = BufWriter::new(file); try_err!(write!(file, "{template}"), &path); @@ -926,3 +920,6 @@ where } Ok(()) } + +#[cfg(test)] +mod tests; diff --git a/src/librustdoc/html/render/write_shared/tests.rs b/src/librustdoc/html/render/write_shared/tests.rs new file mode 100644 index 0000000000000..000e233aec00f --- /dev/null +++ b/src/librustdoc/html/render/write_shared/tests.rs @@ -0,0 +1,206 @@ +use crate::html::render::sorted_json::{EscapedJson, SortedJson}; +use crate::html::render::sorted_template::{Html, SortedTemplate}; +use crate::html::render::write_shared::*; + +#[test] +fn hack_external_crate_names() { + let path = tempfile::TempDir::new().unwrap(); + let path = path.path(); + let crates = hack_get_external_crate_names(&path).unwrap(); + assert!(crates.is_empty()); + fs::write(path.join("crates.js"), r#"window.ALL_CRATES = ["a","b","c"];"#).unwrap(); + let crates = hack_get_external_crate_names(&path).unwrap(); + assert_eq!(crates, ["a".to_string(), "b".to_string(), "c".to_string()]); +} + +fn but_last_line(s: &str) -> &str { + let (before, _) = s.rsplit_once("\n").unwrap(); + before +} + +#[test] +fn sources_template() { + let mut template = SourcesPart::blank(); + assert_eq!( + but_last_line(&template.to_string()), + r"var srcIndex = new Map(JSON.parse('[]')); +createSrcSidebar();" + ); + template.append(EscapedJson::from(SortedJson::serialize("u")).to_string()); + assert_eq!( + but_last_line(&template.to_string()), + r#"var srcIndex = new Map(JSON.parse('["u"]')); +createSrcSidebar();"# + ); + template.append(EscapedJson::from(SortedJson::serialize("v")).to_string()); + assert_eq!( + but_last_line(&template.to_string()), + r#"var srcIndex = new Map(JSON.parse('["u","v"]')); +createSrcSidebar();"# + ); +} + +#[test] +fn sources_parts() { + let parts = SearchIndexPart::get(SortedJson::serialize(["foo", "bar"]), "suffix").unwrap(); + assert_eq!(&parts.parts[0].0, Path::new("search-indexsuffix.js")); + assert_eq!(&parts.parts[0].1.to_string(), r#"["foo","bar"]"#); +} + +#[test] +fn all_crates_template() { + let mut template = AllCratesPart::blank(); + assert_eq!(but_last_line(&template.to_string()), r"window.ALL_CRATES = [];"); + template.append(EscapedJson::from(SortedJson::serialize("b")).to_string()); + assert_eq!(but_last_line(&template.to_string()), r#"window.ALL_CRATES = ["b"];"#); + template.append(EscapedJson::from(SortedJson::serialize("a")).to_string()); + assert_eq!(but_last_line(&template.to_string()), r#"window.ALL_CRATES = ["a","b"];"#); +} + +#[test] +fn all_crates_parts() { + let parts = AllCratesPart::get(SortedJson::serialize("crate")).unwrap(); + assert_eq!(&parts.parts[0].0, Path::new("crates.js")); + assert_eq!(&parts.parts[0].1.to_string(), r#""crate""#); +} + +#[test] +fn search_index_template() { + let mut template = SearchIndexPart::blank(); + assert_eq!( + but_last_line(&template.to_string()), + r"var searchIndex = new Map(JSON.parse('[]')); +if (typeof exports !== 'undefined') exports.searchIndex = searchIndex; +else if (window.initSearch) window.initSearch(searchIndex);" + ); + template.append(EscapedJson::from(SortedJson::serialize([1, 2])).to_string()); + assert_eq!( + but_last_line(&template.to_string()), + r"var searchIndex = new Map(JSON.parse('[[1,2]]')); +if (typeof exports !== 'undefined') exports.searchIndex = searchIndex; +else if (window.initSearch) window.initSearch(searchIndex);" + ); + template.append(EscapedJson::from(SortedJson::serialize([4, 3])).to_string()); + assert_eq!( + but_last_line(&template.to_string()), + r"var searchIndex = new Map(JSON.parse('[[1,2],[4,3]]')); +if (typeof exports !== 'undefined') exports.searchIndex = searchIndex; +else if (window.initSearch) window.initSearch(searchIndex);" + ); +} + +#[test] +fn crates_index_part() { + let external_crates = ["bar".to_string(), "baz".to_string()]; + let mut parts = CratesIndexPart::get("foo", &external_crates).unwrap(); + parts.parts.sort_by(|a, b| a.1.to_string().cmp(&b.1.to_string())); + + assert_eq!(&parts.parts[0].0, Path::new("index.html")); + assert_eq!(&parts.parts[0].1.to_string(), r#"
  • bar
  • "#); + + assert_eq!(&parts.parts[1].0, Path::new("index.html")); + assert_eq!(&parts.parts[1].1.to_string(), r#"
  • baz
  • "#); + + assert_eq!(&parts.parts[2].0, Path::new("index.html")); + assert_eq!(&parts.parts[2].1.to_string(), r#"
  • foo
  • "#); +} + +#[test] +fn trait_alias_template() { + let mut template = TraitAliasPart::blank(); + assert_eq!( + but_last_line(&template.to_string()), + r#"(function() { + var implementors = Object.fromEntries([]); + if (window.register_implementors) { + window.register_implementors(implementors); + } else { + window.pending_implementors = implementors; + } +})()"#, + ); + template.append(SortedJson::serialize(["a"]).to_string()); + assert_eq!( + but_last_line(&template.to_string()), + r#"(function() { + var implementors = Object.fromEntries([["a"]]); + if (window.register_implementors) { + window.register_implementors(implementors); + } else { + window.pending_implementors = implementors; + } +})()"#, + ); + template.append(SortedJson::serialize(["b"]).to_string()); + assert_eq!( + but_last_line(&template.to_string()), + r#"(function() { + var implementors = Object.fromEntries([["a"],["b"]]); + if (window.register_implementors) { + window.register_implementors(implementors); + } else { + window.pending_implementors = implementors; + } +})()"#, + ); +} + +#[test] +fn type_alias_template() { + let mut template = TypeAliasPart::blank(); + assert_eq!( + but_last_line(&template.to_string()), + r#"(function() { + var type_impls = Object.fromEntries([]); + if (window.register_type_impls) { + window.register_type_impls(type_impls); + } else { + window.pending_type_impls = type_impls; + } +})()"#, + ); + template.append(SortedJson::serialize(["a"]).to_string()); + assert_eq!( + but_last_line(&template.to_string()), + r#"(function() { + var type_impls = Object.fromEntries([["a"]]); + if (window.register_type_impls) { + window.register_type_impls(type_impls); + } else { + window.pending_type_impls = type_impls; + } +})()"#, + ); + template.append(SortedJson::serialize(["b"]).to_string()); + assert_eq!( + but_last_line(&template.to_string()), + r#"(function() { + var type_impls = Object.fromEntries([["a"],["b"]]); + if (window.register_type_impls) { + window.register_type_impls(type_impls); + } else { + window.pending_type_impls = type_impls; + } +})()"#, + ); +} + +#[test] +fn read_template_test() { + let path = tempfile::TempDir::new().unwrap(); + let path = path.path().join("file.html"); + let make_blank = || SortedTemplate::::before_after("
    ", "
    "); + + let template = read_template_or_blank(make_blank, &path).unwrap(); + assert_eq!(but_last_line(&template.to_string()), "
    "); + fs::write(&path, template.to_string()).unwrap(); + let mut template = read_template_or_blank(make_blank, &path).unwrap(); + template.append("".to_string()); + fs::write(&path, template.to_string()).unwrap(); + let mut template = read_template_or_blank(make_blank, &path).unwrap(); + template.append("
    ".to_string()); + fs::write(&path, template.to_string()).unwrap(); + let template = read_template_or_blank(make_blank, &path).unwrap(); + + assert_eq!(but_last_line(&template.to_string()), "

    "); +} From 4b418cd4aa9b2b8c52e8029c13b0530abf97bce7 Mon Sep 17 00:00:00 2001 From: EtomicBomb Date: Fri, 26 Jul 2024 17:09:32 +0000 Subject: [PATCH 5/5] rename sortedjson -> orderedjson --- src/librustdoc/Cargo.toml | 2 +- src/librustdoc/html/render/mod.rs | 2 +- .../{sorted_json.rs => ordered_json.rs} | 56 +++---- .../{sorted_json => ordered_json}/tests.rs | 52 +++---- src/librustdoc/html/render/search_index.rs | 10 +- src/librustdoc/html/render/sorted_template.rs | 108 ++++++++------ .../html/render/sorted_template/tests.rs | 21 +-- src/librustdoc/html/render/write_shared.rs | 141 ++++++++++-------- .../html/render/write_shared/tests.rs | 29 ++-- 9 files changed, 228 insertions(+), 193 deletions(-) rename src/librustdoc/html/render/{sorted_json.rs => ordered_json.rs} (55%) rename src/librustdoc/html/render/{sorted_json => ordered_json}/tests.rs (55%) diff --git a/src/librustdoc/Cargo.toml b/src/librustdoc/Cargo.toml index 67ba8c773175c..b3fccbf6456e0 100644 --- a/src/librustdoc/Cargo.toml +++ b/src/librustdoc/Cargo.toml @@ -16,7 +16,7 @@ minifier = "0.3.0" pulldown-cmark-old = { version = "0.9.6", package = "pulldown-cmark", default-features = false } regex = "1" rustdoc-json-types = { path = "../rustdoc-json-types" } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } smallvec = "1.8.1" tempfile = "3" diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 4b1c9b4af474a..586c3c509b4ce 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -29,9 +29,9 @@ pub(crate) mod search_index; mod tests; mod context; +mod ordered_json; mod print_item; pub(crate) mod sidebar; -mod sorted_json; mod sorted_template; mod span_map; mod type_layout; diff --git a/src/librustdoc/html/render/sorted_json.rs b/src/librustdoc/html/render/ordered_json.rs similarity index 55% rename from src/librustdoc/html/render/sorted_json.rs rename to src/librustdoc/html/render/ordered_json.rs index e937382f5b0a6..3f76ff659d04e 100644 --- a/src/librustdoc/html/render/sorted_json.rs +++ b/src/librustdoc/html/render/ordered_json.rs @@ -1,72 +1,74 @@ +use std::borrow::Borrow; +use std::fmt; + use itertools::Itertools as _; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::borrow::Borrow; -use std::fmt; /// Prerenedered json. /// -/// Arrays are sorted by their stringified entries, and objects are sorted by their stringified -/// keys. -/// -/// Must use serde_json with the preserve_order feature. -/// /// Both the Display and serde_json::to_string implementations write the serialized json #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[serde(from = "Value")] #[serde(into = "Value")] -pub(crate) struct SortedJson(String); +pub(crate) struct OrderedJson(String); -impl SortedJson { +impl OrderedJson { /// If you pass in an array, it will not be sorted. - pub(crate) fn serialize(item: T) -> Self { - SortedJson(serde_json::to_string(&item).unwrap()) + pub(crate) fn serialize(item: T) -> Result { + Ok(OrderedJson(serde_json::to_string(&item)?)) } /// Serializes and sorts - pub(crate) fn array, I: IntoIterator>(items: I) -> Self { + pub(crate) fn array_sorted, I: IntoIterator>( + items: I, + ) -> Self { let items = items .into_iter() .sorted_unstable_by(|a, b| a.borrow().cmp(&b.borrow())) .format_with(",", |item, f| f(item.borrow())); - SortedJson(format!("[{}]", items)) + OrderedJson(format!("[{}]", items)) } - pub(crate) fn array_unsorted, I: IntoIterator>( + pub(crate) fn array_unsorted, I: IntoIterator>( items: I, ) -> Self { let items = items.into_iter().format_with(",", |item, f| f(item.borrow())); - SortedJson(format!("[{items}]")) + OrderedJson(format!("[{items}]")) } } -impl fmt::Display for SortedJson { +impl fmt::Display for OrderedJson { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) + self.0.fmt(f) } } -impl From for SortedJson { +impl From for OrderedJson { fn from(value: Value) -> Self { - SortedJson(serde_json::to_string(&value).unwrap()) + let serialized = + serde_json::to_string(&value).expect("Serializing a Value to String should never fail"); + OrderedJson(serialized) } } -impl From for Value { - fn from(json: SortedJson) -> Self { - serde_json::from_str(&json.0).unwrap() +impl From for Value { + fn from(json: OrderedJson) -> Self { + serde_json::from_str(&json.0).expect("OrderedJson should always store valid JSON") } } /// For use in JSON.parse('{...}'). /// -/// JSON.parse supposedly loads faster than raw JS source, +/// Assumes we are going to be wrapped in single quoted strings. +/// +/// JSON.parse loads faster than raw JS source, /// so this is used for large objects. #[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct EscapedJson(SortedJson); +pub(crate) struct EscapedJson(OrderedJson); -impl From for EscapedJson { - fn from(json: SortedJson) -> Self { +impl From for EscapedJson { + fn from(json: OrderedJson) -> Self { EscapedJson(json) } } @@ -77,7 +79,7 @@ impl fmt::Display for EscapedJson { // for JSON content. // We need to escape double quotes for the JSON let json = self.0.0.replace('\\', r"\\").replace('\'', r"\'").replace("\\\"", "\\\\\""); - write!(f, "{}", json) + json.fmt(f) } } diff --git a/src/librustdoc/html/render/sorted_json/tests.rs b/src/librustdoc/html/render/ordered_json/tests.rs similarity index 55% rename from src/librustdoc/html/render/sorted_json/tests.rs rename to src/librustdoc/html/render/ordered_json/tests.rs index 1e72c6f614c38..e0fe6446b9aff 100644 --- a/src/librustdoc/html/render/sorted_json/tests.rs +++ b/src/librustdoc/html/render/ordered_json/tests.rs @@ -1,90 +1,90 @@ -use super::super::sorted_json::*; +use super::super::ordered_json::*; -fn check(json: SortedJson, serialized: &str) { +fn check(json: OrderedJson, serialized: &str) { assert_eq!(json.to_string(), serialized); assert_eq!(serde_json::to_string(&json).unwrap(), serialized); let json = json.to_string(); - let json: SortedJson = serde_json::from_str(&json).unwrap(); + let json: OrderedJson = serde_json::from_str(&json).unwrap(); assert_eq!(json.to_string(), serialized); assert_eq!(serde_json::to_string(&json).unwrap(), serialized); let json = serde_json::to_string(&json).unwrap(); - let json: SortedJson = serde_json::from_str(&json).unwrap(); + let json: OrderedJson = serde_json::from_str(&json).unwrap(); assert_eq!(json.to_string(), serialized); assert_eq!(serde_json::to_string(&json).unwrap(), serialized); } -// Test this basic are needed because we are testing that our Display impl + serialize impl don't -// nest everything in extra level of string. We also are testing round trip. +// Make sure there is no extra level of string, plus number of escapes. #[test] fn escape_json_number() { - let json = SortedJson::serialize(3); + let json = OrderedJson::serialize(3).unwrap(); let json = EscapedJson::from(json); assert_eq!(format!("{json}"), "3"); } #[test] fn escape_json_single_quote() { - let json = SortedJson::serialize("he's"); + let json = OrderedJson::serialize("he's").unwrap(); let json = EscapedJson::from(json); assert_eq!(format!("{json}"), r#""he\'s""#); } #[test] fn escape_json_array() { - let json = SortedJson::serialize([1, 2, 3]); + let json = OrderedJson::serialize([1, 2, 3]).unwrap(); let json = EscapedJson::from(json); assert_eq!(format!("{json}"), r#"[1,2,3]"#); } #[test] fn escape_json_string() { - let json = SortedJson::serialize(r#"he"llo"#); + let json = OrderedJson::serialize(r#"he"llo"#).unwrap(); let json = EscapedJson::from(json); assert_eq!(format!("{json}"), r#""he\\\"llo""#); } #[test] fn escape_json_string_escaped() { - let json = SortedJson::serialize(r#"he\"llo"#); + let json = OrderedJson::serialize(r#"he\"llo"#).unwrap(); let json = EscapedJson::from(json); assert_eq!(format!("{json}"), r#""he\\\\\\\"llo""#); } #[test] fn escape_json_string_escaped_escaped() { - let json = SortedJson::serialize(r#"he\\"llo"#); + let json = OrderedJson::serialize(r#"he\\"llo"#).unwrap(); let json = EscapedJson::from(json); assert_eq!(format!("{json}"), r#""he\\\\\\\\\\\"llo""#); } +// Testing round trip + making sure there is no extra level of string #[test] fn number() { - let json = SortedJson::serialize(3); + let json = OrderedJson::serialize(3).unwrap(); let serialized = "3"; check(json, serialized); } #[test] fn boolean() { - let json = SortedJson::serialize(true); + let json = OrderedJson::serialize(true).unwrap(); let serialized = "true"; check(json, serialized); } #[test] fn string() { - let json = SortedJson::serialize("he\"llo"); + let json = OrderedJson::serialize("he\"llo").unwrap(); let serialized = r#""he\"llo""#; check(json, serialized); } #[test] fn serialize_array() { - let json = SortedJson::serialize([3, 1, 2]); + let json = OrderedJson::serialize([3, 1, 2]).unwrap(); let serialized = "[3,1,2]"; check(json, serialized); } @@ -93,18 +93,19 @@ fn serialize_array() { fn sorted_array() { let items = ["c", "a", "b"]; let serialized = r#"["a","b","c"]"#; - let items: Vec = items.into_iter().map(SortedJson::serialize).collect(); - let json = SortedJson::array(items); + let items: Vec = + items.into_iter().map(OrderedJson::serialize).collect::, _>>().unwrap(); + let json = OrderedJson::array_sorted(items); check(json, serialized); } #[test] fn nested_array() { - let a = SortedJson::serialize(3); - let b = SortedJson::serialize(2); - let c = SortedJson::serialize(1); - let d = SortedJson::serialize([1, 3, 2]); - let json = SortedJson::array([a, b, c, d]); + let a = OrderedJson::serialize(3).unwrap(); + let b = OrderedJson::serialize(2).unwrap(); + let c = OrderedJson::serialize(1).unwrap(); + let d = OrderedJson::serialize([1, 3, 2]).unwrap(); + let json = OrderedJson::array_sorted([a, b, c, d]); let serialized = r#"[1,2,3,[1,3,2]]"#; check(json, serialized); } @@ -113,7 +114,8 @@ fn nested_array() { fn array_unsorted() { let items = ["c", "a", "b"]; let serialized = r#"["c","a","b"]"#; - let items: Vec = items.into_iter().map(SortedJson::serialize).collect(); - let json = SortedJson::array_unsorted(items); + let items: Vec = + items.into_iter().map(OrderedJson::serialize).collect::, _>>().unwrap(); + let json = OrderedJson::array_unsorted(items); check(json, serialized); } diff --git a/src/librustdoc/html/render/search_index.rs b/src/librustdoc/html/render/search_index.rs index 184e5afba3c99..8a12bdef69bf4 100644 --- a/src/librustdoc/html/render/search_index.rs +++ b/src/librustdoc/html/render/search_index.rs @@ -18,7 +18,7 @@ use crate::formats::cache::{Cache, OrphanImplItem}; use crate::formats::item_type::ItemType; use crate::html::format::join_with_double_colon; use crate::html::markdown::short_markdown_summary; -use crate::html::render::sorted_json::SortedJson; +use crate::html::render::ordered_json::OrderedJson; use crate::html::render::{self, IndexItem, IndexItemFunctionType, RenderType, RenderTypeId}; /// The serialized search description sharded version @@ -47,7 +47,7 @@ use crate::html::render::{self, IndexItem, IndexItemFunctionType, RenderType, Re /// [2]: https://en.wikipedia.org/wiki/Sliding_window_protocol#Basic_concept /// [3]: https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/description-tcp-features pub(crate) struct SerializedSearchIndex { - pub(crate) index: SortedJson, + pub(crate) index: OrderedJson, pub(crate) desc: Vec<(usize, String)>, } @@ -693,9 +693,9 @@ pub(crate) fn build_index<'tcx>( desc_index, empty_desc, }; - let index = SortedJson::array_unsorted([ - SortedJson::serialize(crate_name.as_str()), - SortedJson::serialize(data), + let index = OrderedJson::array_unsorted([ + OrderedJson::serialize(crate_name.as_str()).unwrap(), + OrderedJson::serialize(data).unwrap(), ]); SerializedSearchIndex { index, desc } } diff --git a/src/librustdoc/html/render/sorted_template.rs b/src/librustdoc/html/render/sorted_template.rs index 8e0a2ee0fd4d9..1dc70408f013d 100644 --- a/src/librustdoc/html/render/sorted_template.rs +++ b/src/librustdoc/html/render/sorted_template.rs @@ -1,8 +1,9 @@ use std::collections::BTreeSet; -use std::fmt; +use std::fmt::{self, Write as _}; use std::marker::PhantomData; use std::str::FromStr; +use itertools::{Itertools as _, Position}; use serde::{Deserialize, Serialize}; /// Append-only templates for sorted, deduplicated lists of items. @@ -13,7 +14,7 @@ pub(crate) struct SortedTemplate { format: PhantomData, before: String, after: String, - contents: BTreeSet, + fragments: BTreeSet, } /// Written to last line of file to specify the location of each fragment @@ -22,82 +23,88 @@ struct Offset { /// Index of the first byte in the template start: usize, /// The length of each fragment in the encoded template, including the separator - delta: Vec, + fragment_lengths: Vec, } impl SortedTemplate { /// Generate this template from arbitary text. /// Will insert wherever the substring `magic` can be found. /// Errors if it does not appear exactly once. - pub(crate) fn magic(template: &str, magic: &str) -> Result { - let mut split = template.split(magic); - let before = split.next().ok_or(Error)?; - let after = split.next().ok_or(Error)?; + pub(crate) fn from_template(template: &str, delimiter: &str) -> Result { + let mut split = template.split(delimiter); + let before = split.next().ok_or(Error("delimiter should appear at least once"))?; + let after = split.next().ok_or(Error("delimiter should appear at least once"))?; + // not `split_once` because we want to check for too many occurrences if split.next().is_some() { - return Err(Error); + return Err(Error("delimiter should appear at most once")); } - Ok(Self::before_after(before, after)) + Ok(Self::from_before_after(before, after)) } - /// Template will insert contents between `before` and `after` - pub(crate) fn before_after(before: S, after: T) -> Self { + /// Template will insert fragments between `before` and `after` + pub(crate) fn from_before_after(before: S, after: T) -> Self { let before = before.to_string(); let after = after.to_string(); - SortedTemplate { format: PhantomData, before, after, contents: Default::default() } + SortedTemplate { format: PhantomData, before, after, fragments: Default::default() } } } -impl SortedTemplate { +impl SortedTemplate { /// Adds this text to the template pub(crate) fn append(&mut self, insert: String) { - self.contents.insert(insert); + self.fragments.insert(insert); } } impl fmt::Display for SortedTemplate { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut delta = Vec::default(); + fn fmt(&self, mut f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut fragment_lengths = Vec::default(); write!(f, "{}", self.before)?; - let contents: Vec<_> = self.contents.iter().collect(); - let mut sep = ""; - for content in contents { - delta.push(sep.len() + content.len()); - write!(f, "{}{}", sep, content)?; - sep = F::SEPARATOR; + for (p, fragment) in self.fragments.iter().with_position() { + let mut f = DeltaWriter { inner: &mut f, delta: 0 }; + let sep = if matches!(p, Position::First | Position::Only) { "" } else { F::SEPARATOR }; + write!(f, "{}{}", sep, fragment)?; + fragment_lengths.push(f.delta); } - let offset = Offset { start: self.before.len(), delta }; + let offset = Offset { start: self.before.len(), fragment_lengths }; let offset = serde_json::to_string(&offset).unwrap(); - write!(f, "{}\n{}{}{}", self.after, F::COMMENT_START, offset, F::COMMENT_END)?; - Ok(()) + write!(f, "{}\n{}{}{}", self.after, F::COMMENT_START, offset, F::COMMENT_END) } } -fn checked_split_at(s: &str, index: usize) -> Option<(&str, &str)> { - s.is_char_boundary(index).then(|| s.split_at(index)) -} - impl FromStr for SortedTemplate { type Err = Error; fn from_str(s: &str) -> Result { - let (s, offset) = s.rsplit_once("\n").ok_or(Error)?; - let offset = offset.strip_prefix(F::COMMENT_START).ok_or(Error)?; - let offset = offset.strip_suffix(F::COMMENT_END).ok_or(Error)?; - let offset: Offset = serde_json::from_str(&offset).map_err(|_| Error)?; - let (before, mut s) = checked_split_at(s, offset.start).ok_or(Error)?; - let mut contents = BTreeSet::default(); - let mut sep = ""; - for &index in offset.delta.iter() { - let (content, rest) = checked_split_at(s, index).ok_or(Error)?; + let (s, offset) = s + .rsplit_once("\n") + .ok_or(Error("invalid format: should have a newline on the last line"))?; + let offset = offset + .strip_prefix(F::COMMENT_START) + .ok_or(Error("last line expected to start with a comment"))?; + let offset = offset + .strip_suffix(F::COMMENT_END) + .ok_or(Error("last line expected to end with a comment"))?; + let offset: Offset = serde_json::from_str(&offset).map_err(|_| { + Error("could not find insertion location descriptor object on last line") + })?; + let (before, mut s) = + s.split_at_checked(offset.start).ok_or(Error("invalid start: out of bounds"))?; + let mut fragments = BTreeSet::default(); + for (p, &index) in offset.fragment_lengths.iter().with_position() { + let (fragment, rest) = + s.split_at_checked(index).ok_or(Error("invalid fragment length: out of bounds"))?; s = rest; - let content = content.strip_prefix(sep).ok_or(Error)?; - contents.insert(content.to_string()); - sep = F::SEPARATOR; + let sep = if matches!(p, Position::First | Position::Only) { "" } else { F::SEPARATOR }; + let fragment = fragment + .strip_prefix(sep) + .ok_or(Error("invalid fragment length: expected to find separator here"))?; + fragments.insert(fragment.to_string()); } Ok(SortedTemplate { format: PhantomData, before: before.to_string(), after: s.to_string(), - contents, + fragments, }) } } @@ -127,11 +134,24 @@ impl FileFormat for Js { } #[derive(Debug, Clone)] -pub(crate) struct Error; +pub(crate) struct Error(&'static str); impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "invalid template") + write!(f, "invalid template: {}", self.0) + } +} + +struct DeltaWriter { + inner: W, + delta: usize, +} + +impl fmt::Write for DeltaWriter { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.inner.write_str(s)?; + self.delta += s.len(); + Ok(()) } } diff --git a/src/librustdoc/html/render/sorted_template/tests.rs b/src/librustdoc/html/render/sorted_template/tests.rs index 04553f65a2154..db057463005c9 100644 --- a/src/librustdoc/html/render/sorted_template/tests.rs +++ b/src/librustdoc/html/render/sorted_template/tests.rs @@ -1,6 +1,7 @@ -use super::super::sorted_template::*; use std::str::FromStr; +use super::super::sorted_template::*; + fn is_comment_js(s: &str) -> bool { s.starts_with("//") } @@ -13,7 +14,7 @@ fn is_comment_html(s: &str) -> bool { #[test] fn html_from_empty() { let inserts = ["

    hello

    ", "

    kind

    ", "

    hello

    ", "

    world

    "]; - let mut template = SortedTemplate::::before_after("", ""); + let mut template = SortedTemplate::::from_before_after("", ""); for insert in inserts { template.append(insert.to_string()); } @@ -29,7 +30,7 @@ fn html_page() { let inserts = ["

    hello

    ", "

    kind

    ", "

    world

    "]; let before = ""; let after = ""; - let mut template = SortedTemplate::::before_after(before, after); + let mut template = SortedTemplate::::from_before_after(before, after); for insert in inserts { template.append(insert.to_string()); } @@ -43,7 +44,7 @@ fn html_page() { #[test] fn js_from_empty() { let inserts = ["1", "2", "2", "2", "3", "1"]; - let mut template = SortedTemplate::::before_after("", ""); + let mut template = SortedTemplate::::from_before_after("", ""); for insert in inserts { template.append(insert.to_string()); } @@ -56,7 +57,7 @@ fn js_from_empty() { #[test] fn js_empty_array() { - let template = SortedTemplate::::before_after("[", "]"); + let template = SortedTemplate::::from_before_after("[", "]"); let template = format!("{template}"); let (template, end) = template.rsplit_once("\n").unwrap(); assert_eq!(template, format!("[]")); @@ -67,7 +68,7 @@ fn js_empty_array() { #[test] fn js_number_array() { let inserts = ["1", "2", "3"]; - let mut template = SortedTemplate::::before_after("[", "]"); + let mut template = SortedTemplate::::from_before_after("[", "]"); for insert in inserts { template.append(insert.to_string()); } @@ -81,7 +82,7 @@ fn js_number_array() { #[test] fn magic_js_number_array() { let inserts = ["1", "1"]; - let mut template = SortedTemplate::::magic("[#]", "#").unwrap(); + let mut template = SortedTemplate::::from_template("[#]", "#").unwrap(); for insert in inserts { template.append(insert.to_string()); } @@ -95,7 +96,7 @@ fn magic_js_number_array() { #[test] fn round_trip_js() { let inserts = ["1", "2", "3"]; - let mut template = SortedTemplate::::before_after("[", "]"); + let mut template = SortedTemplate::::from_before_after("[", "]"); for insert in inserts { template.append(insert.to_string()); } @@ -114,7 +115,7 @@ fn round_trip_html() { let inserts = ["

    hello

    ", "

    kind

    ", "

    world

    ", "

    kind

    "]; let before = ""; let after = ""; - let mut template = SortedTemplate::::before_after(before, after); + let mut template = SortedTemplate::::from_before_after(before, after); template.append(inserts[0].to_string()); template.append(inserts[1].to_string()); let template = format!("{template}"); @@ -129,7 +130,7 @@ fn round_trip_html() { #[test] fn blank_js() { let inserts = ["1", "2", "3"]; - let template = SortedTemplate::::before_after("", ""); + let template = SortedTemplate::::from_before_after("", ""); let template = format!("{template}"); let (t, _) = template.rsplit_once("\n").unwrap(); assert_eq!(t, ""); diff --git a/src/librustdoc/html/render/write_shared.rs b/src/librustdoc/html/render/write_shared.rs index c2d2b4cd7d9fb..ef3c35c20ab01 100644 --- a/src/librustdoc/html/render/write_shared.rs +++ b/src/librustdoc/html/render/write_shared.rs @@ -1,4 +1,4 @@ -//! Rustdoc writes out two kinds of shared files: +//! Rustdoc writes aut two kinds of shared files: //! - Static files, which are embedded in the rustdoc binary and are written with a //! filename that includes a hash of their contents. These will always have a new //! URL if the contents change, so they are safe to cache with the @@ -13,18 +13,16 @@ //! --resource-suffix flag and are emitted when --emit-type is empty (default) //! or contains "invocation-specific". -use std::any::Any; use std::cell::RefCell; use std::ffi::OsString; use std::fs::File; -use std::io::BufWriter; -use std::io::Write as _; +use std::io::{self, BufWriter, Write as _}; use std::iter::once; use std::marker::PhantomData; use std::path::{Component, Path, PathBuf}; use std::rc::{Rc, Weak}; use std::str::FromStr; -use std::{fmt, fs, io}; +use std::{fmt, fs}; use indexmap::IndexMap; use itertools::Itertools; @@ -35,8 +33,9 @@ use rustc_middle::ty::fast_reject::{DeepRejectCtxt, TreatParams}; use rustc_middle::ty::TyCtxt; use rustc_span::def_id::DefId; use rustc_span::Symbol; +use serde::de::DeserializeOwned; use serde::ser::SerializeSeq; -use serde::{de::DeserializeOwned, Deserialize, Serialize, Serializer}; +use serde::{Deserialize, Serialize, Serializer}; use super::{collect_paths_for_type, ensure_trailing_slash, Context, RenderMode}; use crate::clean::{Crate, Item, ItemId, ItemKind}; @@ -48,9 +47,8 @@ use crate::formats::item_type::ItemType; use crate::formats::Impl; use crate::html::format::Buffer; use crate::html::layout; -use crate::html::render::search_index::build_index; -use crate::html::render::search_index::SerializedSearchIndex; -use crate::html::render::sorted_json::{EscapedJson, SortedJson}; +use crate::html::render::ordered_json::{EscapedJson, OrderedJson}; +use crate::html::render::search_index::{build_index, SerializedSearchIndex}; use crate::html::render::sorted_template::{self, FileFormat, SortedTemplate}; use crate::html::render::{AssocItemLink, ImplRenderingParameters}; use crate::html::static_files::{self, suffix_path}; @@ -76,7 +74,7 @@ pub(crate) fn write_shared( let crate_name = krate.name(cx.tcx()); let crate_name = crate_name.as_str(); // rand - let crate_name_json = SortedJson::serialize(crate_name); // "rand" + let crate_name_json = OrderedJson::serialize(crate_name).unwrap(); // "rand" let external_crates = hack_get_external_crate_names(&cx.dst)?; let info = CrateInfo { src_files_js: SourcesPart::get(cx, &crate_name_json)?, @@ -111,11 +109,7 @@ pub(crate) fn write_shared( ); } None if opt.enable_index_page => { - write_rendered_cci::( - || CratesIndexPart::blank(cx), - dst, - &crates, - )?; + write_rendered_cci::(|| CratesIndexPart::blank(cx), dst, &crates)?; } _ => {} // they don't want an index page } @@ -171,9 +165,9 @@ fn write_search_desc( search_desc: &[(usize, String)], ) -> Result<(), Error> { let crate_name = krate.name(cx.tcx()).to_string(); - let encoded_crate_name = SortedJson::serialize(&crate_name); + let encoded_crate_name = OrderedJson::serialize(&crate_name).unwrap(); let path = PathBuf::from_iter([&cx.dst, Path::new("search.desc"), Path::new(&crate_name)]); - if Path::new(&path).exists() { + if path.exists() { try_err!(fs::remove_dir_all(&path), &path); } for (i, (_, part)) in search_desc.iter().enumerate() { @@ -182,7 +176,7 @@ fn write_search_desc( &cx.shared.resource_suffix, ); let path = path.join(filename); - let part = SortedJson::serialize(&part); + let part = OrderedJson::serialize(&part).unwrap(); let part = format!("searchState.loadedDescShard({encoded_crate_name}, {i}, {part})"); create_parents(&path)?; try_err!(fs::write(&path, part), &path); @@ -201,20 +195,6 @@ struct CrateInfo { type_impl: PartsAndLocations, } -impl CrateInfo { - /// Gets a reference to the cross-crate information parts for `T` - fn get(&self) -> &PartsAndLocations { - (&self.src_files_js as &dyn Any) - .downcast_ref() - .or_else(|| (&self.search_index_js as &dyn Any).downcast_ref()) - .or_else(|| (&self.all_crates as &dyn Any).downcast_ref()) - .or_else(|| (&self.crates_index as &dyn Any).downcast_ref()) - .or_else(|| (&self.trait_impl as &dyn Any).downcast_ref()) - .or_else(|| (&self.type_impl as &dyn Any).downcast_ref()) - .expect("this should be an exhaustive list of `CciPart`s") - } -} - /// Paths (relative to the doc root) and their pre-merge contents #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(transparent)] @@ -263,6 +243,7 @@ impl fmt::Display for Part { trait CciPart: Sized + fmt::Display + DeserializeOwned + 'static { /// Identifies the file format of the cross-crate information type FileFormat: sorted_template::FileFormat; + fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations; } #[derive(Serialize, Deserialize, Clone, Default, Debug)] @@ -270,11 +251,14 @@ struct SearchIndex; type SearchIndexPart = Part; impl CciPart for SearchIndexPart { type FileFormat = sorted_template::Js; + fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations { + &crate_info.search_index_js + } } impl SearchIndexPart { fn blank() -> SortedTemplate<::FileFormat> { - SortedTemplate::before_after( + SortedTemplate::from_before_after( r"var searchIndex = new Map(JSON.parse('[", r"]')); if (typeof exports !== 'undefined') exports.searchIndex = searchIndex; @@ -283,7 +267,7 @@ else if (window.initSearch) window.initSearch(searchIndex);", } fn get( - search_index: SortedJson, + search_index: OrderedJson, resource_suffix: &str, ) -> Result, Error> { let path = suffix_path("search-index.js", resource_suffix); @@ -294,17 +278,20 @@ else if (window.initSearch) window.initSearch(searchIndex);", #[derive(Serialize, Deserialize, Clone, Default, Debug)] struct AllCrates; -type AllCratesPart = Part; +type AllCratesPart = Part; impl CciPart for AllCratesPart { type FileFormat = sorted_template::Js; + fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations { + &crate_info.all_crates + } } impl AllCratesPart { fn blank() -> SortedTemplate<::FileFormat> { - SortedTemplate::before_after("window.ALL_CRATES = [", "];") + SortedTemplate::from_before_after("window.ALL_CRATES = [", "];") } - fn get(crate_name_json: SortedJson) -> Result, Error> { + fn get(crate_name_json: OrderedJson) -> Result, Error> { // external hack_get_external_crate_names not needed here, because // there's no way that we write the search index but not crates.js let path = PathBuf::from("crates.js"); @@ -339,6 +326,9 @@ struct CratesIndex; type CratesIndexPart = Part; impl CciPart for CratesIndexPart { type FileFormat = sorted_template::Html; + fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations { + &crate_info.crates_index + } } impl CratesIndexPart { @@ -354,10 +344,11 @@ impl CratesIndexPart { }; let layout = &cx.shared.layout; let style_files = &cx.shared.style_files; - const MAGIC: &str = "\u{FFFC}"; // users are being naughty if they have this - let content = format!("

    List of all crates

      {MAGIC}
    "); + const DELIMITER: &str = "\u{FFFC}"; // users are being naughty if they have this + let content = + format!("

    List of all crates

      {DELIMITER}
    "); let template = layout::render(layout, &page, "", content, &style_files); - match SortedTemplate::magic(&template, MAGIC) { + match SortedTemplate::from_template(&template, DELIMITER) { Ok(template) => template, Err(e) => panic!( "Object Replacement Character (U+FFFC) should not appear in the --index-page: {e}" @@ -385,6 +376,9 @@ struct Sources; type SourcesPart = Part; impl CciPart for SourcesPart { type FileFormat = sorted_template::Js; + fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations { + &crate_info.src_files_js + } } impl SourcesPart { @@ -392,14 +386,14 @@ impl SourcesPart { // This needs to be `var`, not `const`. // This variable needs declared in the current global scope so that if // src-script.js loads first, it can pick it up. - SortedTemplate::before_after( + SortedTemplate::from_before_after( r"var srcIndex = new Map(JSON.parse('[", r"]')); createSrcSidebar();", ) } - fn get(cx: &Context<'_>, crate_name: &SortedJson) -> Result, Error> { + fn get(cx: &Context<'_>, crate_name: &OrderedJson) -> Result, Error> { let hierarchy = Rc::new(Hierarchy::default()); cx.shared .local_sources @@ -408,7 +402,7 @@ createSrcSidebar();", .for_each(|source| hierarchy.add_path(source)); let path = suffix_path("src-files.js", &cx.shared.resource_suffix); let hierarchy = hierarchy.to_json_string(); - let part = SortedJson::array_unsorted([crate_name, &hierarchy]); + let part = OrderedJson::array_unsorted([crate_name, &hierarchy]); let part = EscapedJson::from(part); Ok(PartsAndLocations::with(path, part)) } @@ -428,21 +422,23 @@ impl Hierarchy { Self { elem, parent: Rc::downgrade(parent), ..Self::default() } } - fn to_json_string(&self) -> SortedJson { + fn to_json_string(&self) -> OrderedJson { let subs = self.children.borrow(); let files = self.elems.borrow(); - let name = SortedJson::serialize(self.elem.to_str().expect("invalid osstring conversion")); + let name = OrderedJson::serialize(self.elem.to_str().expect("invalid osstring conversion")) + .unwrap(); let mut out = Vec::from([name]); if !subs.is_empty() || !files.is_empty() { let subs = subs.iter().map(|(_, s)| s.to_json_string()); - out.push(SortedJson::array(subs)); + out.push(OrderedJson::array_sorted(subs)); } if !files.is_empty() { - let files = - files.iter().map(|s| SortedJson::serialize(s.to_str().expect("invalid osstring"))); - out.push(SortedJson::array(files)); + let files = files + .iter() + .map(|s| OrderedJson::serialize(s.to_str().expect("invalid osstring")).unwrap()); + out.push(OrderedJson::array_sorted(files)); } - SortedJson::array_unsorted(out) + OrderedJson::array_unsorted(out) } fn add_path(self: &Rc, path: &Path) { @@ -481,14 +477,17 @@ impl Hierarchy { #[derive(Serialize, Deserialize, Clone, Default, Debug)] struct TypeAlias; -type TypeAliasPart = Part; +type TypeAliasPart = Part; impl CciPart for TypeAliasPart { type FileFormat = sorted_template::Js; + fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations { + &crate_info.type_impl + } } impl TypeAliasPart { fn blank() -> SortedTemplate<::FileFormat> { - SortedTemplate::before_after( + SortedTemplate::from_before_after( r"(function() { var type_impls = Object.fromEntries([", r"]); @@ -504,7 +503,7 @@ impl TypeAliasPart { fn get( cx: &mut Context<'_>, krate: &Crate, - crate_name_json: &SortedJson, + crate_name_json: &OrderedJson, ) -> Result, Error> { let cache = &Rc::clone(&cx.shared).cache; let mut path_parts = PartsAndLocations::default(); @@ -594,9 +593,10 @@ impl TypeAliasPart { aliased_type.target_fqp[aliased_type.target_fqp.len() - 1] )); - let part = - SortedJson::array(impls.iter().map(SortedJson::serialize).collect::>()); - path_parts.push(path, SortedJson::array_unsorted([crate_name_json, &part])); + let part = OrderedJson::array_sorted( + impls.iter().map(OrderedJson::serialize).collect::, _>>().unwrap(), + ); + path_parts.push(path, OrderedJson::array_unsorted([crate_name_json, &part])); } Ok(path_parts) } @@ -604,14 +604,17 @@ impl TypeAliasPart { #[derive(Serialize, Deserialize, Clone, Default, Debug)] struct TraitAlias; -type TraitAliasPart = Part; +type TraitAliasPart = Part; impl CciPart for TraitAliasPart { type FileFormat = sorted_template::Js; + fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations { + &crate_info.trait_impl + } } impl TraitAliasPart { fn blank() -> SortedTemplate<::FileFormat> { - SortedTemplate::before_after( + SortedTemplate::from_before_after( r"(function() { var implementors = Object.fromEntries([", r"]); @@ -626,7 +629,7 @@ impl TraitAliasPart { fn get( cx: &mut Context<'_>, - crate_name_json: &SortedJson, + crate_name_json: &OrderedJson, ) -> Result, Error> { let cache = &cx.shared.cache; let mut path_parts = PartsAndLocations::default(); @@ -688,10 +691,14 @@ impl TraitAliasPart { } path.push(&format!("{remote_item_type}.{}.js", remote_path[remote_path.len() - 1])); - let part = SortedJson::array( - implementors.iter().map(SortedJson::serialize).collect::>(), + let part = OrderedJson::array_sorted( + implementors + .iter() + .map(OrderedJson::serialize) + .collect::, _>>() + .unwrap(), ); - path_parts.push(path, SortedJson::array_unsorted([crate_name_json, &part])); + path_parts.push(path, OrderedJson::array_unsorted([crate_name_json, &part])); } Ok(path_parts) } @@ -864,13 +871,15 @@ fn get_path_parts( crates_info: &[CrateInfo], ) -> FxHashMap> { let mut templates: FxHashMap> = FxHashMap::default(); - crates_info.iter().map(|crate_info| crate_info.get::().parts.iter()).flatten().for_each( - |(path, part)| { + crates_info + .iter() + .map(|crate_info| T::from_crate_info(crate_info).parts.iter()) + .flatten() + .for_each(|(path, part)| { let path = dst.join(&path); let part = part.to_string(); templates.entry(path).or_default().push(part); - }, - ); + }); templates } diff --git a/src/librustdoc/html/render/write_shared/tests.rs b/src/librustdoc/html/render/write_shared/tests.rs index 000e233aec00f..4d1874b7df5f9 100644 --- a/src/librustdoc/html/render/write_shared/tests.rs +++ b/src/librustdoc/html/render/write_shared/tests.rs @@ -1,4 +1,4 @@ -use crate::html::render::sorted_json::{EscapedJson, SortedJson}; +use crate::html::render::ordered_json::{EscapedJson, OrderedJson}; use crate::html::render::sorted_template::{Html, SortedTemplate}; use crate::html::render::write_shared::*; @@ -26,13 +26,13 @@ fn sources_template() { r"var srcIndex = new Map(JSON.parse('[]')); createSrcSidebar();" ); - template.append(EscapedJson::from(SortedJson::serialize("u")).to_string()); + template.append(EscapedJson::from(OrderedJson::serialize("u").unwrap()).to_string()); assert_eq!( but_last_line(&template.to_string()), r#"var srcIndex = new Map(JSON.parse('["u"]')); createSrcSidebar();"# ); - template.append(EscapedJson::from(SortedJson::serialize("v")).to_string()); + template.append(EscapedJson::from(OrderedJson::serialize("v").unwrap()).to_string()); assert_eq!( but_last_line(&template.to_string()), r#"var srcIndex = new Map(JSON.parse('["u","v"]')); @@ -42,7 +42,8 @@ createSrcSidebar();"# #[test] fn sources_parts() { - let parts = SearchIndexPart::get(SortedJson::serialize(["foo", "bar"]), "suffix").unwrap(); + let parts = + SearchIndexPart::get(OrderedJson::serialize(["foo", "bar"]).unwrap(), "suffix").unwrap(); assert_eq!(&parts.parts[0].0, Path::new("search-indexsuffix.js")); assert_eq!(&parts.parts[0].1.to_string(), r#"["foo","bar"]"#); } @@ -51,15 +52,15 @@ fn sources_parts() { fn all_crates_template() { let mut template = AllCratesPart::blank(); assert_eq!(but_last_line(&template.to_string()), r"window.ALL_CRATES = [];"); - template.append(EscapedJson::from(SortedJson::serialize("b")).to_string()); + template.append(EscapedJson::from(OrderedJson::serialize("b").unwrap()).to_string()); assert_eq!(but_last_line(&template.to_string()), r#"window.ALL_CRATES = ["b"];"#); - template.append(EscapedJson::from(SortedJson::serialize("a")).to_string()); + template.append(EscapedJson::from(OrderedJson::serialize("a").unwrap()).to_string()); assert_eq!(but_last_line(&template.to_string()), r#"window.ALL_CRATES = ["a","b"];"#); } #[test] fn all_crates_parts() { - let parts = AllCratesPart::get(SortedJson::serialize("crate")).unwrap(); + let parts = AllCratesPart::get(OrderedJson::serialize("crate").unwrap()).unwrap(); assert_eq!(&parts.parts[0].0, Path::new("crates.js")); assert_eq!(&parts.parts[0].1.to_string(), r#""crate""#); } @@ -73,14 +74,14 @@ fn search_index_template() { if (typeof exports !== 'undefined') exports.searchIndex = searchIndex; else if (window.initSearch) window.initSearch(searchIndex);" ); - template.append(EscapedJson::from(SortedJson::serialize([1, 2])).to_string()); + template.append(EscapedJson::from(OrderedJson::serialize([1, 2]).unwrap()).to_string()); assert_eq!( but_last_line(&template.to_string()), r"var searchIndex = new Map(JSON.parse('[[1,2]]')); if (typeof exports !== 'undefined') exports.searchIndex = searchIndex; else if (window.initSearch) window.initSearch(searchIndex);" ); - template.append(EscapedJson::from(SortedJson::serialize([4, 3])).to_string()); + template.append(EscapedJson::from(OrderedJson::serialize([4, 3]).unwrap()).to_string()); assert_eq!( but_last_line(&template.to_string()), r"var searchIndex = new Map(JSON.parse('[[1,2],[4,3]]')); @@ -119,7 +120,7 @@ fn trait_alias_template() { } })()"#, ); - template.append(SortedJson::serialize(["a"]).to_string()); + template.append(OrderedJson::serialize(["a"]).unwrap().to_string()); assert_eq!( but_last_line(&template.to_string()), r#"(function() { @@ -131,7 +132,7 @@ fn trait_alias_template() { } })()"#, ); - template.append(SortedJson::serialize(["b"]).to_string()); + template.append(OrderedJson::serialize(["b"]).unwrap().to_string()); assert_eq!( but_last_line(&template.to_string()), r#"(function() { @@ -159,7 +160,7 @@ fn type_alias_template() { } })()"#, ); - template.append(SortedJson::serialize(["a"]).to_string()); + template.append(OrderedJson::serialize(["a"]).unwrap().to_string()); assert_eq!( but_last_line(&template.to_string()), r#"(function() { @@ -171,7 +172,7 @@ fn type_alias_template() { } })()"#, ); - template.append(SortedJson::serialize(["b"]).to_string()); + template.append(OrderedJson::serialize(["b"]).unwrap().to_string()); assert_eq!( but_last_line(&template.to_string()), r#"(function() { @@ -189,7 +190,7 @@ fn type_alias_template() { fn read_template_test() { let path = tempfile::TempDir::new().unwrap(); let path = path.path().join("file.html"); - let make_blank = || SortedTemplate::::before_after("
    ", "
    "); + let make_blank = || SortedTemplate::::from_before_after("
    ", "
    "); let template = read_template_or_blank(make_blank, &path).unwrap(); assert_eq!(but_last_line(&template.to_string()), "
    ");