Auto merge of #136244 - yotamofek:pr/rustdoc-join-iter, r=GuillaumeGomez

librustdoc: create a helper for separating elements of an iterator instead of implementing it multiple times

This implements something similar to [`Itertools::format`](https://docs.rs/itertools/latest/itertools/trait.Itertools.html#method.format), but on `Fn`s returning iterators instead of directly on iterators, to allow implementing `Display` without the use of a `Cell` (to handle the possibility of `fmt` being called multiple times while receiving `&self`).

~This is WIP, I just want to get a perf run first to see if the regression I saw in #135494 is fixed~

This was originally part of #135494 , but originally caused a perf regression that was since fixed:
7d5ae1863a/src/librustdoc/html/format.rs (L507)
This commit is contained in:
bors 2025-02-05 02:51:17 +00:00
commit 8df89d1cb0
5 changed files with 203 additions and 206 deletions

View File

@ -14,6 +14,7 @@ use rustc_span::Span;
use rustc_span::symbol::{Symbol, sym}; use rustc_span::symbol::{Symbol, sym};
use crate::html::escape::Escape; use crate::html::escape::Escape;
use crate::joined::Joined as _;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@ -396,6 +397,8 @@ impl Display<'_> {
sub_cfgs: &[Cfg], sub_cfgs: &[Cfg],
separator: &str, separator: &str,
) -> fmt::Result { ) -> fmt::Result {
use fmt::Display as _;
let short_longhand = self.1.is_long() && { let short_longhand = self.1.is_long() && {
let all_crate_features = let all_crate_features =
sub_cfgs.iter().all(|sub_cfg| matches!(sub_cfg, Cfg::Cfg(sym::feature, Some(_)))); sub_cfgs.iter().all(|sub_cfg| matches!(sub_cfg, Cfg::Cfg(sym::feature, Some(_))));
@ -414,11 +417,14 @@ impl Display<'_> {
} }
}; };
for (i, sub_cfg) in sub_cfgs.iter().enumerate() { fmt::from_fn(|f| {
if i != 0 { sub_cfgs
fmt.write_str(separator)?; .iter()
} .map(|sub_cfg| {
if let (true, Cfg::Cfg(_, Some(feat))) = (short_longhand, sub_cfg) { fmt::from_fn(move |fmt| {
if let Cfg::Cfg(_, Some(feat)) = sub_cfg
&& short_longhand
{
if self.1.is_html() { if self.1.is_html() {
write!(fmt, "<code>{feat}</code>")?; write!(fmt, "<code>{feat}</code>")?;
} else { } else {
@ -427,7 +433,13 @@ impl Display<'_> {
} else { } else {
write_with_opt_paren(fmt, !sub_cfg.is_all(), Display(sub_cfg, self.1))?; write_with_opt_paren(fmt, !sub_cfg.is_all(), Display(sub_cfg, self.1))?;
} }
} Ok(())
})
})
.joined(separator, f)
})
.fmt(fmt)?;
Ok(()) Ok(())
} }
} }
@ -439,11 +451,20 @@ impl fmt::Display for Display<'_> {
Cfg::Any(ref sub_cfgs) => { Cfg::Any(ref sub_cfgs) => {
let separator = let separator =
if sub_cfgs.iter().all(Cfg::is_simple) { " nor " } else { ", nor " }; if sub_cfgs.iter().all(Cfg::is_simple) { " nor " } else { ", nor " };
for (i, sub_cfg) in sub_cfgs.iter().enumerate() { fmt.write_str("neither ")?;
fmt.write_str(if i == 0 { "neither " } else { separator })?;
write_with_opt_paren(fmt, !sub_cfg.is_all(), Display(sub_cfg, self.1))?; sub_cfgs
} .iter()
Ok(()) .map(|sub_cfg| {
fmt::from_fn(|fmt| {
write_with_opt_paren(
fmt,
!sub_cfg.is_all(),
Display(sub_cfg, self.1),
)
})
})
.joined(separator, fmt)
} }
ref simple @ Cfg::Cfg(..) => write!(fmt, "non-{}", Display(simple, self.1)), ref simple @ Cfg::Cfg(..) => write!(fmt, "non-{}", Display(simple, self.1)),
ref c => write!(fmt, "not ({})", Display(c, self.1)), ref c => write!(fmt, "not ({})", Display(c, self.1)),

View File

@ -8,12 +8,11 @@
//! not be used external to this module. //! not be used external to this module.
use std::borrow::Cow; use std::borrow::Cow;
use std::cell::Cell;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::fmt::{self, Display, Write}; use std::fmt::{self, Display, Write};
use std::iter::{self, once}; use std::iter::{self, once};
use itertools::Itertools; use itertools::Either;
use rustc_abi::ExternAbi; use rustc_abi::ExternAbi;
use rustc_attr_parsing::{ConstStability, StabilityLevel, StableSince}; use rustc_attr_parsing::{ConstStability, StabilityLevel, StableSince};
use rustc_data_structures::captures::Captures; use rustc_data_structures::captures::Captures;
@ -35,6 +34,7 @@ use crate::formats::cache::Cache;
use crate::formats::item_type::ItemType; use crate::formats::item_type::ItemType;
use crate::html::escape::{Escape, EscapeBodyText}; use crate::html::escape::{Escape, EscapeBodyText};
use crate::html::render::Context; use crate::html::render::Context;
use crate::joined::Joined as _;
use crate::passes::collect_intra_doc_links::UrlFragment; use crate::passes::collect_intra_doc_links::UrlFragment;
pub(crate) trait Print { pub(crate) trait Print {
@ -146,22 +146,6 @@ impl Buffer {
} }
} }
pub(crate) fn comma_sep<T: Display>(
items: impl Iterator<Item = T>,
space_after_comma: bool,
) -> impl Display {
let items = Cell::new(Some(items));
fmt::from_fn(move |f| {
for (i, item) in items.take().unwrap().enumerate() {
if i != 0 {
write!(f, ",{}", if space_after_comma { " " } else { "" })?;
}
item.fmt(f)?;
}
Ok(())
})
}
pub(crate) fn print_generic_bounds<'a, 'tcx: 'a>( pub(crate) fn print_generic_bounds<'a, 'tcx: 'a>(
bounds: &'a [clean::GenericBound], bounds: &'a [clean::GenericBound],
cx: &'a Context<'tcx>, cx: &'a Context<'tcx>,
@ -169,13 +153,11 @@ pub(crate) fn print_generic_bounds<'a, 'tcx: 'a>(
fmt::from_fn(move |f| { fmt::from_fn(move |f| {
let mut bounds_dup = FxHashSet::default(); let mut bounds_dup = FxHashSet::default();
for (i, bound) in bounds.iter().filter(|b| bounds_dup.insert(*b)).enumerate() { bounds
if i > 0 { .iter()
f.write_str(" + ")?; .filter(move |b| bounds_dup.insert(*b))
} .map(|bound| bound.print(cx))
bound.print(cx).fmt(f)?; .joined(" + ", f)
}
Ok(())
}) })
} }
@ -190,12 +172,7 @@ impl clean::GenericParamDef {
if !outlives.is_empty() { if !outlives.is_empty() {
f.write_str(": ")?; f.write_str(": ")?;
for (i, lt) in outlives.iter().enumerate() { outlives.iter().map(|lt| lt.print()).joined(" + ", f)?;
if i != 0 {
f.write_str(" + ")?;
}
write!(f, "{}", lt.print())?;
}
} }
Ok(()) Ok(())
@ -245,10 +222,12 @@ impl clean::Generics {
return Ok(()); return Ok(());
} }
let real_params =
fmt::from_fn(|f| real_params.clone().map(|g| g.print(cx)).joined(", ", f));
if f.alternate() { if f.alternate() {
write!(f, "<{:#}>", comma_sep(real_params.map(|g| g.print(cx)), true)) write!(f, "<{:#}>", real_params)
} else { } else {
write!(f, "&lt;{}&gt;", comma_sep(real_params.map(|g| g.print(cx)), true)) write!(f, "&lt;{}&gt;", real_params)
} }
}) })
} }
@ -260,31 +239,14 @@ pub(crate) enum Ending {
NoNewline, NoNewline,
} }
/// * The Generics from which to emit a where-clause. fn print_where_predicate<'a, 'tcx: 'a>(
/// * The number of spaces to indent each line with. predicate: &'a clean::WherePredicate,
/// * Whether the where-clause needs to add a comma and newline after the last bound.
pub(crate) fn print_where_clause<'a, 'tcx: 'a>(
gens: &'a clean::Generics,
cx: &'a Context<'tcx>, cx: &'a Context<'tcx>,
indent: usize,
ending: Ending,
) -> impl Display + 'a + Captures<'tcx> { ) -> impl Display + 'a + Captures<'tcx> {
fmt::from_fn(move |f| { fmt::from_fn(move |f| {
let mut where_predicates = gens match predicate {
.where_predicates
.iter()
.map(|pred| {
fmt::from_fn(move |f| {
if f.alternate() {
f.write_str(" ")?;
} else {
f.write_str("\n")?;
}
match pred {
clean::WherePredicate::BoundPredicate { ty, bounds, bound_params } => { clean::WherePredicate::BoundPredicate { ty, bounds, bound_params } => {
print_higher_ranked_params_with_space(bound_params, cx, "for") print_higher_ranked_params_with_space(bound_params, cx, "for").fmt(f)?;
.fmt(f)?;
ty.print(cx).fmt(f)?; ty.print(cx).fmt(f)?;
f.write_str(":")?; f.write_str(":")?;
if !bounds.is_empty() { if !bounds.is_empty() {
@ -311,14 +273,38 @@ pub(crate) fn print_where_clause<'a, 'tcx: 'a>(
} }
} }
}) })
}) }
.peekable();
if where_predicates.peek().is_none() { /// * The Generics from which to emit a where-clause.
/// * The number of spaces to indent each line with.
/// * Whether the where-clause needs to add a comma and newline after the last bound.
pub(crate) fn print_where_clause<'a, 'tcx: 'a>(
gens: &'a clean::Generics,
cx: &'a Context<'tcx>,
indent: usize,
ending: Ending,
) -> impl Display + 'a + Captures<'tcx> {
fmt::from_fn(move |f| {
if gens.where_predicates.is_empty() {
return Ok(()); return Ok(());
} }
let where_preds = comma_sep(where_predicates, false); let where_preds = fmt::from_fn(|f| {
gens.where_predicates
.iter()
.map(|predicate| {
fmt::from_fn(|f| {
if f.alternate() {
f.write_str(" ")?;
} else {
f.write_str("\n")?;
}
print_where_predicate(predicate, cx).fmt(f)
})
})
.joined(",", f)
});
let clause = if f.alternate() { let clause = if f.alternate() {
if ending == Ending::Newline { if ending == Ending::Newline {
format!(" where{where_preds},") format!(" where{where_preds},")
@ -415,12 +401,7 @@ impl clean::GenericBound {
} else { } else {
f.write_str("use&lt;")?; f.write_str("use&lt;")?;
} }
for (i, arg) in args.iter().enumerate() { args.iter().joined(", ", f)?;
if i > 0 {
write!(f, ", ")?;
}
arg.fmt(f)?;
}
if f.alternate() { f.write_str(">") } else { f.write_str("&gt;") } if f.alternate() { f.write_str(">") } else { f.write_str("&gt;") }
} }
}) })
@ -438,29 +419,18 @@ impl clean::GenericArgs {
} else { } else {
f.write_str("&lt;")?; f.write_str("&lt;")?;
} }
let mut comma = false;
for arg in args.iter() { [Either::Left(args), Either::Right(constraints)]
if comma { .into_iter()
f.write_str(", ")?; .flat_map(Either::factor_into_iter)
} .map(|either| {
comma = true; either.map_either(
if f.alternate() { |arg| arg.print(cx),
write!(f, "{:#}", arg.print(cx))?; |constraint| constraint.print(cx),
} else { )
write!(f, "{}", arg.print(cx))?; })
} .joined(", ", f)?;
}
for constraint in constraints.iter() {
if comma {
f.write_str(", ")?;
}
comma = true;
if f.alternate() {
write!(f, "{:#}", constraint.print(cx))?;
} else {
write!(f, "{}", constraint.print(cx))?;
}
}
if f.alternate() { if f.alternate() {
f.write_str(">")?; f.write_str(">")?;
} else { } else {
@ -470,14 +440,7 @@ impl clean::GenericArgs {
} }
clean::GenericArgs::Parenthesized { inputs, output } => { clean::GenericArgs::Parenthesized { inputs, output } => {
f.write_str("(")?; f.write_str("(")?;
let mut comma = false; inputs.iter().map(|ty| ty.print(cx)).joined(", ", f)?;
for ty in inputs.iter() {
if comma {
f.write_str(", ")?;
}
comma = true;
ty.print(cx).fmt(f)?;
}
f.write_str(")")?; f.write_str(")")?;
if let Some(ref ty) = *output { if let Some(ref ty) = *output {
if f.alternate() { if f.alternate() {
@ -524,6 +487,7 @@ pub(crate) enum HrefError {
// Panics if `syms` is empty. // Panics if `syms` is empty.
pub(crate) fn join_with_double_colon(syms: &[Symbol]) -> String { pub(crate) fn join_with_double_colon(syms: &[Symbol]) -> String {
let mut s = String::with_capacity(estimate_item_path_byte_length(syms.len())); let mut s = String::with_capacity(estimate_item_path_byte_length(syms.len()));
// NOTE: using `Joined::joined` here causes a noticeable perf regression
s.push_str(syms[0].as_str()); s.push_str(syms[0].as_str());
for sym in &syms[1..] { for sym in &syms[1..] {
s.push_str("::"); s.push_str("::");
@ -572,20 +536,20 @@ fn generate_macro_def_id_path(
} }
if let Some(last) = path.last_mut() { if let Some(last) = path.last_mut() {
*last = Symbol::intern(&format!("macro.{}.html", last.as_str())); *last = Symbol::intern(&format!("macro.{last}.html"));
} }
let url = match cache.extern_locations[&def_id.krate] { let url = match cache.extern_locations[&def_id.krate] {
ExternalLocation::Remote(ref s) => { ExternalLocation::Remote(ref s) => {
// `ExternalLocation::Remote` always end with a `/`. // `ExternalLocation::Remote` always end with a `/`.
format!("{s}{path}", path = path.iter().map(|p| p.as_str()).join("/")) format!("{s}{path}", path = fmt::from_fn(|f| path.iter().joined("/", f)))
} }
ExternalLocation::Local => { ExternalLocation::Local => {
// `root_path` always end with a `/`. // `root_path` always end with a `/`.
format!( format!(
"{root_path}{path}", "{root_path}{path}",
root_path = root_path.unwrap_or(""), root_path = root_path.unwrap_or(""),
path = path.iter().map(|p| p.as_str()).join("/") path = fmt::from_fn(|f| path.iter().joined("/", f))
) )
} }
ExternalLocation::Unknown => { ExternalLocation::Unknown => {
@ -682,9 +646,8 @@ fn make_href(
url_parts.push("index.html"); url_parts.push("index.html");
} }
_ => { _ => {
let prefix = shortty.as_str();
let last = fqp.last().unwrap(); let last = fqp.last().unwrap();
url_parts.push_fmt(format_args!("{prefix}.{last}.html")); url_parts.push_fmt(format_args!("{shortty}.{last}.html"));
} }
} }
Ok((url_parts.finish(), shortty, fqp.to_vec())) Ok((url_parts.finish(), shortty, fqp.to_vec()))
@ -950,12 +913,7 @@ fn tybounds<'a, 'tcx: 'a>(
cx: &'a Context<'tcx>, cx: &'a Context<'tcx>,
) -> impl Display + 'a + Captures<'tcx> { ) -> impl Display + 'a + Captures<'tcx> {
fmt::from_fn(move |f| { fmt::from_fn(move |f| {
for (i, bound) in bounds.iter().enumerate() { bounds.iter().map(|bound| bound.print(cx)).joined(" + ", f)?;
if i > 0 {
write!(f, " + ")?;
}
bound.print(cx).fmt(f)?;
}
if let Some(lt) = lt { if let Some(lt) = lt {
// We don't need to check `alternate` since we can be certain that // We don't need to check `alternate` since we can be certain that
// the lifetime doesn't contain any characters which need escaping. // the lifetime doesn't contain any characters which need escaping.
@ -974,7 +932,7 @@ fn print_higher_ranked_params_with_space<'a, 'tcx: 'a>(
if !params.is_empty() { if !params.is_empty() {
f.write_str(keyword)?; f.write_str(keyword)?;
f.write_str(if f.alternate() { "<" } else { "&lt;" })?; f.write_str(if f.alternate() { "<" } else { "&lt;" })?;
comma_sep(params.iter().map(|lt| lt.print(cx)), true).fmt(f)?; params.iter().map(|lt| lt.print(cx)).joined(", ", f)?;
f.write_str(if f.alternate() { "> " } else { "&gt; " })?; f.write_str(if f.alternate() { "> " } else { "&gt; " })?;
} }
Ok(()) Ok(())
@ -1025,9 +983,7 @@ fn fmt_type(
clean::Primitive(clean::PrimitiveType::Never) => { clean::Primitive(clean::PrimitiveType::Never) => {
primitive_link(f, PrimitiveType::Never, format_args!("!"), cx) primitive_link(f, PrimitiveType::Never, format_args!("!"), cx)
} }
clean::Primitive(prim) => { clean::Primitive(prim) => primitive_link(f, prim, format_args!("{}", prim.as_sym()), cx),
primitive_link(f, prim, format_args!("{}", prim.as_sym().as_str()), cx)
}
clean::BareFunction(ref decl) => { clean::BareFunction(ref decl) => {
print_higher_ranked_params_with_space(&decl.generic_params, cx, "for").fmt(f)?; print_higher_ranked_params_with_space(&decl.generic_params, cx, "for").fmt(f)?;
decl.safety.print_with_space().fmt(f)?; decl.safety.print_with_space().fmt(f)?;
@ -1067,18 +1023,16 @@ fn fmt_type(
primitive_link( primitive_link(
f, f,
PrimitiveType::Tuple, PrimitiveType::Tuple,
format_args!("({})", generic_names.iter().map(|s| s.as_str()).join(", ")), format_args!(
"({})",
fmt::from_fn(|f| generic_names.iter().joined(", ", f))
),
cx, cx,
) )
} else { } else {
write!(f, "(")?; f.write_str("(")?;
for (i, item) in many.iter().enumerate() { many.iter().map(|item| item.print(cx)).joined(", ", f)?;
if i != 0 { f.write_str(")")
write!(f, ", ")?;
}
item.print(cx).fmt(f)?;
}
write!(f, ")")
} }
} }
}, },
@ -1407,16 +1361,17 @@ impl clean::Arguments {
cx: &'a Context<'tcx>, cx: &'a Context<'tcx>,
) -> impl Display + 'a + Captures<'tcx> { ) -> impl Display + 'a + Captures<'tcx> {
fmt::from_fn(move |f| { fmt::from_fn(move |f| {
for (i, input) in self.values.iter().enumerate() { self.values
.iter()
.map(|input| {
fmt::from_fn(|f| {
if !input.name.is_empty() { if !input.name.is_empty() {
write!(f, "{}: ", input.name)?; write!(f, "{}: ", input.name)?;
} }
input.type_.print(cx).fmt(f)?; input.type_.print(cx).fmt(f)
if i + 1 < self.values.len() { })
write!(f, ", ")?; })
} .joined(", ", f)
}
Ok(())
}) })
} }
} }
@ -1725,12 +1680,7 @@ impl clean::ImportSource {
} }
let name = self.path.last(); let name = self.path.last();
if let hir::def::Res::PrimTy(p) = self.path.res { if let hir::def::Res::PrimTy(p) = self.path.res {
primitive_link( primitive_link(f, PrimitiveType::from(p), format_args!("{name}"), cx)?;
f,
PrimitiveType::from(p),
format_args!("{}", name.as_str()),
cx,
)?;
} else { } else {
f.write_str(name.as_str())?; f.write_str(name.as_str())?;
} }

View File

@ -1,5 +1,6 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use std::fmt; use std::fmt;
use std::fmt::{Display, Write};
use itertools::Itertools; use itertools::Itertools;
use rinja::Template; use rinja::Template;
@ -36,6 +37,7 @@ use crate::html::format::{
use crate::html::markdown::{HeadingOffset, MarkdownSummaryLine}; use crate::html::markdown::{HeadingOffset, MarkdownSummaryLine};
use crate::html::render::{document_full, document_item_info}; use crate::html::render::{document_full, document_item_info};
use crate::html::url_parts_builder::UrlPartsBuilder; use crate::html::url_parts_builder::UrlPartsBuilder;
use crate::joined::Joined as _;
/// Generates a Rinja template struct for rendering items with common methods. /// Generates a Rinja template struct for rendering items with common methods.
/// ///
@ -290,7 +292,7 @@ fn should_hide_fields(n_fields: usize) -> bool {
n_fields > 12 n_fields > 12
} }
fn toggle_open(mut w: impl fmt::Write, text: impl fmt::Display) { fn toggle_open(mut w: impl fmt::Write, text: impl Display) {
write!( write!(
w, w,
"<details class=\"toggle type-contents-toggle\">\ "<details class=\"toggle type-contents-toggle\">\
@ -305,7 +307,7 @@ fn toggle_close(mut w: impl fmt::Write) {
w.write_str("</details>").unwrap(); w.write_str("</details>").unwrap();
} }
trait ItemTemplate<'a, 'cx: 'a>: rinja::Template + fmt::Display { trait ItemTemplate<'a, 'cx: 'a>: rinja::Template + Display {
fn item_and_cx(&self) -> (&'a clean::Item, &'a Context<'cx>); fn item_and_cx(&self) -> (&'a clean::Item, &'a Context<'cx>);
} }
@ -519,13 +521,9 @@ fn extra_info_tags<'a, 'tcx: 'a>(
item: &'a clean::Item, item: &'a clean::Item,
parent: &'a clean::Item, parent: &'a clean::Item,
import_def_id: Option<DefId>, import_def_id: Option<DefId>,
) -> impl fmt::Display + 'a + Captures<'tcx> { ) -> impl Display + 'a + Captures<'tcx> {
fmt::from_fn(move |f| { fmt::from_fn(move |f| {
fn tag_html<'a>( fn tag_html<'a>(class: &'a str, title: &'a str, contents: &'a str) -> impl Display + 'a {
class: &'a str,
title: &'a str,
contents: &'a str,
) -> impl fmt::Display + 'a {
fmt::from_fn(move |f| { fmt::from_fn(move |f| {
write!( write!(
f, f,
@ -1374,7 +1372,7 @@ fn item_union(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, s: &clean::Uni
); );
impl<'a, 'cx: 'a> ItemUnion<'a, 'cx> { impl<'a, 'cx: 'a> ItemUnion<'a, 'cx> {
fn render_union<'b>(&'b self) -> impl fmt::Display + Captures<'a> + 'b + Captures<'cx> { fn render_union<'b>(&'b self) -> impl Display + Captures<'a> + 'b + Captures<'cx> {
fmt::from_fn(move |f| { fmt::from_fn(move |f| {
let v = render_union(self.it, Some(&self.s.generics), &self.s.fields, self.cx); let v = render_union(self.it, Some(&self.s.generics), &self.s.fields, self.cx);
write!(f, "{v}") write!(f, "{v}")
@ -1384,7 +1382,7 @@ fn item_union(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, s: &clean::Uni
fn document_field<'b>( fn document_field<'b>(
&'b self, &'b self,
field: &'a clean::Item, field: &'a clean::Item,
) -> impl fmt::Display + Captures<'a> + 'b + Captures<'cx> { ) -> impl Display + Captures<'a> + 'b + Captures<'cx> {
fmt::from_fn(move |f| { fmt::from_fn(move |f| {
let v = document(self.cx, field, Some(self.it), HeadingOffset::H3); let v = document(self.cx, field, Some(self.it), HeadingOffset::H3);
write!(f, "{v}") write!(f, "{v}")
@ -1398,7 +1396,7 @@ fn item_union(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, s: &clean::Uni
fn print_ty<'b>( fn print_ty<'b>(
&'b self, &'b self,
ty: &'a clean::Type, ty: &'a clean::Type,
) -> impl fmt::Display + Captures<'a> + 'b + Captures<'cx> { ) -> impl Display + Captures<'a> + 'b + Captures<'cx> {
fmt::from_fn(move |f| { fmt::from_fn(move |f| {
let v = ty.print(self.cx); let v = ty.print(self.cx);
write!(f, "{v}") write!(f, "{v}")
@ -1425,7 +1423,7 @@ fn item_union(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, s: &clean::Uni
fn print_tuple_struct_fields<'a, 'cx: 'a>( fn print_tuple_struct_fields<'a, 'cx: 'a>(
cx: &'a Context<'cx>, cx: &'a Context<'cx>,
s: &'a [clean::Item], s: &'a [clean::Item],
) -> impl fmt::Display + 'a + Captures<'cx> { ) -> impl Display + 'a + Captures<'cx> {
fmt::from_fn(|f| { fmt::from_fn(|f| {
if !s.is_empty() if !s.is_empty()
&& s.iter().all(|field| { && s.iter().all(|field| {
@ -1435,17 +1433,15 @@ fn print_tuple_struct_fields<'a, 'cx: 'a>(
return f.write_str("<span class=\"comment\">/* private fields */</span>"); return f.write_str("<span class=\"comment\">/* private fields */</span>");
} }
for (i, ty) in s.iter().enumerate() { s.iter()
if i > 0 { .map(|ty| {
f.write_str(", ")?; fmt::from_fn(|f| match ty.kind {
} clean::StrippedItem(box clean::StructFieldItem(_)) => f.write_str("_"),
match ty.kind { clean::StructFieldItem(ref ty) => write!(f, "{}", ty.print(cx)),
clean::StrippedItem(box clean::StructFieldItem(_)) => f.write_str("_")?,
clean::StructFieldItem(ref ty) => write!(f, "{}", ty.print(cx))?,
_ => unreachable!(), _ => unreachable!(),
} })
} })
Ok(()) .joined(", ", f)
}) })
} }
@ -2068,12 +2064,12 @@ fn bounds(t_bounds: &[clean::GenericBound], trait_alias: bool, cx: &Context<'_>)
bounds.push_str(": "); bounds.push_str(": ");
} }
} }
for (i, p) in t_bounds.iter().enumerate() { write!(
if i > 0 { bounds,
bounds.push_str(inter_str); "{}",
} fmt::from_fn(|f| t_bounds.iter().map(|p| p.print(cx)).joined(inter_str, f))
bounds.push_str(&p.print(cx).to_string()); )
} .unwrap();
bounds bounds
} }
@ -2150,7 +2146,7 @@ fn render_union<'a, 'cx: 'a>(
g: Option<&'a clean::Generics>, g: Option<&'a clean::Generics>,
fields: &'a [clean::Item], fields: &'a [clean::Item],
cx: &'a Context<'cx>, cx: &'a Context<'cx>,
) -> impl fmt::Display + 'a + Captures<'cx> { ) -> impl Display + 'a + Captures<'cx> {
fmt::from_fn(move |mut f| { fmt::from_fn(move |mut f| {
write!(f, "{}union {}", visibility_print_with_space(it, cx), it.name.unwrap(),)?; write!(f, "{}union {}", visibility_print_with_space(it, cx), it.name.unwrap(),)?;
@ -2330,7 +2326,7 @@ fn document_non_exhaustive_header(item: &clean::Item) -> &str {
if item.is_non_exhaustive() { " (Non-exhaustive)" } else { "" } if item.is_non_exhaustive() { " (Non-exhaustive)" } else { "" }
} }
fn document_non_exhaustive(item: &clean::Item) -> impl fmt::Display + '_ { fn document_non_exhaustive(item: &clean::Item) -> impl Display + '_ {
fmt::from_fn(|f| { fmt::from_fn(|f| {
if item.is_non_exhaustive() { if item.is_non_exhaustive() {
write!( write!(

29
src/librustdoc/joined.rs Normal file
View File

@ -0,0 +1,29 @@
use std::fmt::{self, Display, Formatter};
pub(crate) trait Joined: IntoIterator {
/// Takes an iterator over elements that implement [`Display`], and format them into `f`, separated by `sep`.
///
/// This is similar to [`Itertools::format`](itertools::Itertools::format), but instead of returning an implementation of `Display`,
/// it formats directly into a [`Formatter`].
///
/// The performance of `joined` is slightly better than `format`, since it doesn't need to use a `Cell` to keep track of whether [`fmt`](Display::fmt)
/// was already called (`joined`'s API doesn't allow it be called more than once).
fn joined(self, sep: &str, f: &mut Formatter<'_>) -> fmt::Result;
}
impl<I, T> Joined for I
where
I: IntoIterator<Item = T>,
T: Display,
{
fn joined(self, sep: &str, f: &mut Formatter<'_>) -> fmt::Result {
let mut iter = self.into_iter();
let Some(first) = iter.next() else { return Ok(()) };
first.fmt(f)?;
while let Some(item) = iter.next() {
f.write_str(sep)?;
item.fmt(f)?;
}
Ok(())
}
}

View File

@ -113,6 +113,7 @@ mod fold;
mod formats; mod formats;
// used by the error-index generator, so it needs to be public // used by the error-index generator, so it needs to be public
pub mod html; pub mod html;
mod joined;
mod json; mod json;
pub(crate) mod lint; pub(crate) mod lint;
mod markdown; mod markdown;