diff --git a/src/librustdoc/clean/cfg.rs b/src/librustdoc/clean/cfg.rs index b576f28176e5d..bec7fbe8f52bd 100644 --- a/src/librustdoc/clean/cfg.rs +++ b/src/librustdoc/clean/cfg.rs @@ -13,8 +13,8 @@ use rustc_session::parse::ParseSess; use rustc_span::Span; use rustc_span::symbol::{Symbol, sym}; +use crate::display::Joined as _; use crate::html::escape::Escape; -use crate::joined::Joined as _; #[cfg(test)] mod tests; diff --git a/src/librustdoc/joined.rs b/src/librustdoc/display.rs similarity index 67% rename from src/librustdoc/joined.rs rename to src/librustdoc/display.rs index f369c6cf2371c..ee8dde013ee93 100644 --- a/src/librustdoc/joined.rs +++ b/src/librustdoc/display.rs @@ -1,3 +1,5 @@ +//! Various utilities for working with [`fmt::Display`] implementations. + use std::fmt::{self, Display, Formatter}; pub(crate) trait Joined: IntoIterator { @@ -27,3 +29,19 @@ where Ok(()) } } + +pub(crate) trait MaybeDisplay { + /// For a given `Option`, returns a `Display` implementation that will display `t` if `Some(t)`, or nothing if `None`. + fn maybe_display(self) -> impl Display; +} + +impl MaybeDisplay for Option { + fn maybe_display(self) -> impl Display { + fmt::from_fn(move |f| { + if let Some(t) = self.as_ref() { + t.fmt(f)?; + } + Ok(()) + }) + } +} diff --git a/src/librustdoc/html/format.rs b/src/librustdoc/html/format.rs index 086a85aa616f6..91b4b3ba1ebac 100644 --- a/src/librustdoc/html/format.rs +++ b/src/librustdoc/html/format.rs @@ -30,11 +30,11 @@ use super::url_parts_builder::{UrlPartsBuilder, estimate_item_path_byte_length}; use crate::clean::types::ExternalLocation; use crate::clean::utils::find_nearest_parent_module; use crate::clean::{self, ExternalCrate, PrimitiveType}; +use crate::display::Joined as _; use crate::formats::cache::Cache; use crate::formats::item_type::ItemType; use crate::html::escape::{Escape, EscapeBodyText}; use crate::html::render::Context; -use crate::joined::Joined as _; use crate::passes::collect_intra_doc_links::UrlFragment; pub(crate) fn write_str(s: &mut String, f: fmt::Arguments<'_>) { @@ -709,19 +709,22 @@ fn resolved_path( if w.alternate() { write!(w, "{}{:#}", last.name, last.args.print(cx))?; } else { - let path = if use_absolute { - if let Ok((_, _, fqp)) = href(did, cx) { - format!( - "{path}::{anchor}", - path = join_with_double_colon(&fqp[..fqp.len() - 1]), - anchor = anchor(did, *fqp.last().unwrap(), cx) - ) + let path = fmt::from_fn(|f| { + if use_absolute { + if let Ok((_, _, fqp)) = href(did, cx) { + write!( + f, + "{path}::{anchor}", + path = join_with_double_colon(&fqp[..fqp.len() - 1]), + anchor = anchor(did, *fqp.last().unwrap(), cx) + ) + } else { + write!(f, "{}", last.name) + } } else { - last.name.to_string() + write!(f, "{}", anchor(did, last.name, cx)) } - } else { - anchor(did, last.name, cx).to_string() - }; + }); write!(w, "{path}{args}", args = last.args.print(cx))?; } Ok(()) @@ -749,16 +752,20 @@ fn primitive_link_fragment( match m.primitive_locations.get(&prim) { Some(&def_id) if def_id.is_local() => { let len = cx.current.len(); - let path = if len == 0 { - let cname_sym = ExternalCrate { crate_num: def_id.krate }.name(cx.tcx()); - format!("{cname_sym}/") - } else { - "../".repeat(len - 1) - }; + let path = fmt::from_fn(|f| { + if len == 0 { + let cname_sym = ExternalCrate { crate_num: def_id.krate }.name(cx.tcx()); + write!(f, "{cname_sym}/")?; + } else { + for _ in 0..(len - 1) { + f.write_str("../")?; + } + } + Ok(()) + }); write!( f, - "", - path, + "", prim.as_sym() )?; needs_termination = true; diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs index b774e60c62d3b..146bdd340697b 100644 --- a/src/librustdoc/html/render/context.rs +++ b/src/librustdoc/html/render/context.rs @@ -1,8 +1,9 @@ use std::cell::RefCell; use std::collections::BTreeMap; +use std::fmt::{self, Write as _}; +use std::io; use std::path::{Path, PathBuf}; use std::sync::mpsc::{Receiver, channel}; -use std::{fmt, io}; use rinja::Template; use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexMap, FxIndexSet}; @@ -265,12 +266,12 @@ impl<'tcx> Context<'tcx> { // preventing an infinite redirection loop in the generated // documentation. - let mut path = String::new(); - for name in &names[..names.len() - 1] { - path.push_str(name.as_str()); - path.push('/'); - } - path.push_str(&item_path(ty, names.last().unwrap().as_str())); + let path = fmt::from_fn(|f| { + for name in &names[..names.len() - 1] { + write!(f, "{name}/")?; + } + write!(f, "{}", item_path(ty, names.last().unwrap().as_str())) + }); match self.shared.redirections { Some(ref redirections) => { let mut current_path = String::new(); @@ -278,8 +279,12 @@ impl<'tcx> Context<'tcx> { current_path.push_str(name.as_str()); current_path.push('/'); } - current_path.push_str(&item_path(ty, names.last().unwrap().as_str())); - redirections.borrow_mut().insert(current_path, path); + let _ = write!( + current_path, + "{}", + item_path(ty, names.last().unwrap().as_str()) + ); + redirections.borrow_mut().insert(current_path, path.to_string()); } None => { return layout::redirect(&format!( @@ -854,9 +859,9 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { if !buf.is_empty() { let name = item.name.as_ref().unwrap(); let item_type = item.type_(); - let file_name = &item_path(item_type, name.as_str()); + let file_name = item_path(item_type, name.as_str()).to_string(); self.shared.ensure_dir(&self.dst)?; - let joint_dst = self.dst.join(file_name); + let joint_dst = self.dst.join(&file_name); self.shared.fs.write(joint_dst, buf)?; if !self.info.render_redirect_pages { @@ -873,7 +878,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { format!("{crate_name}/{file_name}"), ); } else { - let v = layout::redirect(file_name); + let v = layout::redirect(&file_name); let redir_dst = self.dst.join(redir_name); self.shared.fs.write(redir_dst, v)?; } diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 2f52a38c58173..204631063a23a 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -63,6 +63,7 @@ pub(crate) use self::context::*; pub(crate) use self::span_map::{LinkFromSrc, collect_spans_and_sources}; pub(crate) use self::write_shared::*; use crate::clean::{self, ItemId, RenderedLink}; +use crate::display::{Joined as _, MaybeDisplay as _}; use crate::error::Error; use crate::formats::Impl; use crate::formats::cache::Cache; @@ -568,17 +569,27 @@ fn document_short<'a, 'cx: 'a>( let (mut summary_html, has_more_content) = MarkdownSummaryLine(&s, &item.links(cx)).into_string_with_has_more_content(); - if has_more_content { - let link = format!(" Read more", assoc_href_attr(item, link, cx)); + let link = if has_more_content { + let link = fmt::from_fn(|f| { + write!( + f, + " Read more", + assoc_href_attr(item, link, cx).maybe_display() + ) + }); if let Some(idx) = summary_html.rfind("

") { - summary_html.insert_str(idx, &link); + summary_html.insert_str(idx, &link.to_string()); + None } else { - summary_html.push_str(&link); + Some(link) } + } else { + None } + .maybe_display(); - write!(f, "
{summary_html}
")?; + write!(f, "
{summary_html}{link}
")?; } Ok(()) }) @@ -788,13 +799,23 @@ pub(crate) fn render_impls( } /// Build a (possibly empty) `href` attribute (a key-value pair) for the given associated item. -fn assoc_href_attr(it: &clean::Item, link: AssocItemLink<'_>, cx: &Context<'_>) -> String { +fn assoc_href_attr<'a, 'tcx>( + it: &clean::Item, + link: AssocItemLink<'a>, + cx: &Context<'tcx>, +) -> Option> { let name = it.name.unwrap(); let item_type = it.type_(); + enum Href<'a> { + AnchorId(&'a str), + Anchor(ItemType), + Url(String, ItemType), + } + let href = match link { - AssocItemLink::Anchor(Some(ref id)) => Some(format!("#{id}")), - AssocItemLink::Anchor(None) => Some(format!("#{item_type}.{name}")), + AssocItemLink::Anchor(Some(ref id)) => Href::AnchorId(id), + AssocItemLink::Anchor(None) => Href::Anchor(item_type), AssocItemLink::GotoSource(did, provided_methods) => { // We're creating a link from the implementation of an associated item to its // declaration in the trait declaration. @@ -814,7 +835,7 @@ fn assoc_href_attr(it: &clean::Item, link: AssocItemLink<'_>, cx: &Context<'_>) }; match href(did.expect_def_id(), cx) { - Ok((url, ..)) => Some(format!("{url}#{item_type}.{name}")), + Ok((url, ..)) => Href::Url(url, item_type), // The link is broken since it points to an external crate that wasn't documented. // Do not create any link in such case. This is better than falling back to a // dummy anchor like `#{item_type}.{name}` representing the `id` of *this* impl item @@ -826,15 +847,25 @@ fn assoc_href_attr(it: &clean::Item, link: AssocItemLink<'_>, cx: &Context<'_>) // those two items are distinct! // In this scenario, the actual `id` of this impl item would be // `#{item_type}.{name}-{n}` for some number `n` (a disambiguator). - Err(HrefError::DocumentationNotBuilt) => None, - Err(_) => Some(format!("#{item_type}.{name}")), + Err(HrefError::DocumentationNotBuilt) => return None, + Err(_) => Href::Anchor(item_type), } } }; + let href = fmt::from_fn(move |f| match &href { + Href::AnchorId(id) => write!(f, "#{id}"), + Href::Url(url, item_type) => { + write!(f, "{url}#{item_type}.{name}") + } + Href::Anchor(item_type) => { + write!(f, "#{item_type}.{name}") + } + }); + // If there is no `href` for the reason explained above, simply do not render it which is valid: // https://html.spec.whatwg.org/multipage/links.html#links-created-by-a-and-area-elements - href.map(|href| format!(" href=\"{href}\"")).unwrap_or_default() + Some(fmt::from_fn(move |f| write!(f, " href=\"{href}\""))) } #[derive(Debug)] @@ -865,7 +896,7 @@ fn assoc_const( "{indent}{vis}const {name}{generics}: {ty}", indent = " ".repeat(indent), vis = visibility_print_with_space(it, cx), - href = assoc_href_attr(it, link, cx), + href = assoc_href_attr(it, link, cx).maybe_display(), name = it.name.as_ref().unwrap(), generics = generics.print(cx), ty = ty.print(cx), @@ -905,7 +936,7 @@ fn assoc_type( "{indent}{vis}type {name}{generics}", indent = " ".repeat(indent), vis = visibility_print_with_space(it, cx), - href = assoc_href_attr(it, link, cx), + href = assoc_href_attr(it, link, cx).maybe_display(), name = it.name.as_ref().unwrap(), generics = generics.print(cx), ), @@ -948,7 +979,7 @@ fn assoc_method( let asyncness = header.asyncness.print_with_space(); let safety = header.safety.print_with_space(); let abi = print_abi_with_space(header.abi).to_string(); - let href = assoc_href_attr(meth, link, cx); + let href = assoc_href_attr(meth, link, cx).maybe_display(); // NOTE: `{:#}` does not print HTML formatting, `{}` does. So `g.print` can't be reused between the length calculation and `write!`. let generics_len = format!("{:#}", g.print(cx)).len(); @@ -962,7 +993,7 @@ fn assoc_method( + name.as_str().len() + generics_len; - let notable_traits = notable_traits_button(&d.output, cx); + let notable_traits = notable_traits_button(&d.output, cx).maybe_display(); let (indent, indent_str, end_newline) = if parent == ItemType::Trait { header_len += 4; @@ -990,7 +1021,6 @@ fn assoc_method( name = name, generics = g.print(cx), decl = d.full_print(header_len, indent, cx), - notable_traits = notable_traits.unwrap_or_default(), where_clause = print_where_clause(g, cx, indent, end_newline), ), ); @@ -1438,7 +1468,10 @@ fn should_render_item(item: &clean::Item, deref_mut_: bool, tcx: TyCtxt<'_>) -> } } -pub(crate) fn notable_traits_button(ty: &clean::Type, cx: &Context<'_>) -> Option { +pub(crate) fn notable_traits_button<'a, 'tcx>( + ty: &'a clean::Type, + cx: &'a Context<'tcx>, +) -> Option> { let mut has_notable_trait = false; if ty.is_unit() { @@ -1480,15 +1513,16 @@ pub(crate) fn notable_traits_button(ty: &clean::Type, cx: &Context<'_>) -> Optio } } - if has_notable_trait { + has_notable_trait.then(|| { cx.types_with_notable_traits.borrow_mut().insert(ty.clone()); - Some(format!( - " ", - ty = Escape(&format!("{:#}", ty.print(cx))), - )) - } else { - None - } + fmt::from_fn(|f| { + write!( + f, + " ", + ty = Escape(&format!("{:#}", ty.print(cx))), + ) + }) + }) } fn notable_traits_decl(ty: &clean::Type, cx: &Context<'_>) -> (String, String) { @@ -2117,11 +2151,11 @@ pub(crate) fn render_impl_summary( ) { let inner_impl = i.inner_impl(); let id = cx.derive_id(get_id_for_impl(cx.tcx(), i.impl_item.item_id)); - let aliases = if aliases.is_empty() { - String::new() - } else { - format!(" data-aliases=\"{}\"", aliases.join(",")) - }; + let aliases = (!aliases.is_empty()) + .then_some(fmt::from_fn(|f| { + write!(f, " data-aliases=\"{}\"", fmt::from_fn(|f| aliases.iter().joined(",", f))) + })) + .maybe_display(); write_str(w, format_args!("
")); render_rightside(w, cx, &i.impl_item, RenderMode::Normal); write_str( diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs index 9c1afef75ef3b..d2d7415261b9f 100644 --- a/src/librustdoc/html/render/print_item.rs +++ b/src/librustdoc/html/render/print_item.rs @@ -1,6 +1,6 @@ use std::cmp::Ordering; use std::fmt; -use std::fmt::{Display, Write}; +use std::fmt::Display; use itertools::Itertools; use rinja::Template; @@ -27,6 +27,7 @@ use super::{ }; use crate::clean; use crate::config::ModuleSorting; +use crate::display::{Joined as _, MaybeDisplay as _}; use crate::formats::Impl; use crate::formats::item_type::ItemType; use crate::html::escape::{Escape, EscapeBodyTextWithWbr}; @@ -37,7 +38,6 @@ use crate::html::format::{ use crate::html::markdown::{HeadingOffset, MarkdownSummaryLine}; use crate::html::render::{document_full, document_item_info}; use crate::html::url_parts_builder::UrlPartsBuilder; -use crate::joined::Joined as _; /// Generates a Rinja template struct for rendering items with common methods. /// @@ -619,7 +619,7 @@ fn item_function(w: &mut String, cx: &Context<'_>, it: &clean::Item, f: &clean:: + name.as_str().len() + generics_len; - let notable_traits = notable_traits_button(&f.decl.output, cx); + let notable_traits = notable_traits_button(&f.decl.output, cx).maybe_display(); wrap_item(w, |w| { w.reserve(header_len); @@ -638,7 +638,6 @@ fn item_function(w: &mut String, cx: &Context<'_>, it: &clean::Item, f: &clean:: generics = f.generics.print(cx), where_clause = print_where_clause(&f.generics, cx, 0, Ending::Newline), decl = f.decl.full_print(header_len, 0, cx), - notable_traits = notable_traits.unwrap_or_default(), ), ); }); @@ -2116,34 +2115,33 @@ pub(super) fn full_path(cx: &Context<'_>, item: &clean::Item) -> String { s } -pub(super) fn item_path(ty: ItemType, name: &str) -> String { - match ty { - ItemType::Module => format!("{}index.html", ensure_trailing_slash(name)), - _ => format!("{ty}.{name}.html"), - } +pub(super) fn item_path(ty: ItemType, name: &str) -> impl Display + '_ { + fmt::from_fn(move |f| match ty { + ItemType::Module => write!(f, "{}index.html", ensure_trailing_slash(name)), + _ => write!(f, "{ty}.{name}.html"), + }) } -fn bounds(t_bounds: &[clean::GenericBound], trait_alias: bool, cx: &Context<'_>) -> String { - let mut bounds = String::new(); - if t_bounds.is_empty() { - return bounds; - } - let has_lots_of_bounds = t_bounds.len() > 2; - let inter_str = if has_lots_of_bounds { "\n + " } else { " + " }; - if !trait_alias { - if has_lots_of_bounds { - bounds.push_str(":\n "); - } else { - bounds.push_str(": "); - } - } - write!( - bounds, - "{}", - fmt::from_fn(|f| t_bounds.iter().map(|p| p.print(cx)).joined(inter_str, f)) - ) - .unwrap(); - bounds +fn bounds<'a, 'tcx>( + bounds: &'a [clean::GenericBound], + trait_alias: bool, + cx: &'a Context<'tcx>, +) -> impl Display + 'a + Captures<'tcx> { + (!bounds.is_empty()) + .then_some(fmt::from_fn(move |f| { + let has_lots_of_bounds = bounds.len() > 2; + let inter_str = if has_lots_of_bounds { "\n + " } else { " + " }; + if !trait_alias { + if has_lots_of_bounds { + f.write_str(":\n ")?; + } else { + f.write_str(": ")?; + } + } + + bounds.iter().map(|p| p.print(cx)).joined(inter_str, f) + })) + .maybe_display() } fn wrap_item(w: &mut W, f: F) diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs index 4e4cff40686ef..e4acbcf2c626f 100644 --- a/src/librustdoc/lib.rs +++ b/src/librustdoc/lib.rs @@ -106,6 +106,7 @@ macro_rules! map { mod clean; mod config; mod core; +mod display; mod docfs; mod doctest; mod error; @@ -114,7 +115,6 @@ mod fold; mod formats; // used by the error-index generator, so it needs to be public pub mod html; -mod joined; mod json; pub(crate) mod lint; mod markdown;