//! HTML formatting module //! //! This module contains a large number of `Display` implementations for //! various types in `rustdoc::clean`. //! //! These implementations all emit HTML. As an internal implementation detail, //! some of them support an alternate format that emits text, but that should //! not be used external to this module. use std::borrow::Cow; use std::cmp::Ordering; use std::fmt::{self, Display, Write}; use std::iter::{self, once}; use itertools::Either; use rustc_abi::ExternAbi; use rustc_attr_parsing::{ConstStability, StabilityLevel, StableSince}; use rustc_data_structures::captures::Captures; use rustc_data_structures::fx::FxHashSet; use rustc_hir as hir; use rustc_hir::def::DefKind; use rustc_hir::def_id::{DefId, LOCAL_CRATE}; use rustc_metadata::creader::{CStore, LoadedMacro}; use rustc_middle::ty::{self, TyCtxt, TypingMode}; use rustc_span::symbol::kw; use rustc_span::{Symbol, sym}; use tracing::{debug, trace}; 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::passes::collect_intra_doc_links::UrlFragment; pub(crate) fn write_str(s: &mut String, f: fmt::Arguments<'_>) { s.write_fmt(f).unwrap(); } pub(crate) fn print_generic_bounds<'a, 'tcx: 'a>( bounds: &'a [clean::GenericBound], cx: &'a Context<'tcx>, ) -> impl Display + 'a + Captures<'tcx> { fmt::from_fn(move |f| { let mut bounds_dup = FxHashSet::default(); bounds .iter() .filter(move |b| bounds_dup.insert(*b)) .map(|bound| bound.print(cx)) .joined(" + ", f) }) } impl clean::GenericParamDef { pub(crate) fn print<'a, 'tcx: 'a>( &'a self, cx: &'a Context<'tcx>, ) -> impl Display + 'a + Captures<'tcx> { fmt::from_fn(move |f| match &self.kind { clean::GenericParamDefKind::Lifetime { outlives } => { write!(f, "{}", self.name)?; if !outlives.is_empty() { f.write_str(": ")?; outlives.iter().map(|lt| lt.print()).joined(" + ", f)?; } Ok(()) } clean::GenericParamDefKind::Type { bounds, default, .. } => { f.write_str(self.name.as_str())?; if !bounds.is_empty() { f.write_str(": ")?; print_generic_bounds(bounds, cx).fmt(f)?; } if let Some(ref ty) = default { f.write_str(" = ")?; ty.print(cx).fmt(f)?; } Ok(()) } clean::GenericParamDefKind::Const { ty, default, .. } => { write!(f, "const {}: ", self.name)?; ty.print(cx).fmt(f)?; if let Some(default) = default { f.write_str(" = ")?; if f.alternate() { write!(f, "{default}")?; } else { write!(f, "{}", Escape(default))?; } } Ok(()) } }) } } impl clean::Generics { pub(crate) fn print<'a, 'tcx: 'a>( &'a self, cx: &'a Context<'tcx>, ) -> impl Display + 'a + Captures<'tcx> { fmt::from_fn(move |f| { let mut real_params = self.params.iter().filter(|p| !p.is_synthetic_param()).peekable(); if real_params.peek().is_none() { return Ok(()); } let real_params = fmt::from_fn(|f| real_params.clone().map(|g| g.print(cx)).joined(", ", f)); if f.alternate() { write!(f, "<{:#}>", real_params) } else { write!(f, "<{}>", real_params) } }) } } #[derive(Clone, Copy, PartialEq, Eq)] pub(crate) enum Ending { Newline, NoNewline, } fn print_where_predicate<'a, 'tcx: 'a>( predicate: &'a clean::WherePredicate, cx: &'a Context<'tcx>, ) -> impl Display + 'a + Captures<'tcx> { fmt::from_fn(move |f| { match predicate { clean::WherePredicate::BoundPredicate { ty, bounds, bound_params } => { print_higher_ranked_params_with_space(bound_params, cx, "for").fmt(f)?; ty.print(cx).fmt(f)?; f.write_str(":")?; if !bounds.is_empty() { f.write_str(" ")?; print_generic_bounds(bounds, cx).fmt(f)?; } Ok(()) } clean::WherePredicate::RegionPredicate { lifetime, bounds } => { // We don't need to check `alternate` since we can be certain that neither // the lifetime nor the bounds contain any characters which need escaping. write!(f, "{}:", lifetime.print())?; if !bounds.is_empty() { write!(f, " {}", print_generic_bounds(bounds, cx))?; } Ok(()) } clean::WherePredicate::EqPredicate { lhs, rhs } => { if f.alternate() { write!(f, "{:#} == {:#}", lhs.print(cx), rhs.print(cx)) } else { write!(f, "{} == {}", lhs.print(cx), rhs.print(cx)) } } } }) } /// * 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(()); } 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() { if ending == Ending::Newline { format!(" where{where_preds},") } else { format!(" where{where_preds}") } } else { let mut br_with_padding = String::with_capacity(6 * indent + 28); br_with_padding.push('\n'); let where_indent = 3; let padding_amount = if ending == Ending::Newline { indent + 4 } else if indent == 0 { 4 } else { indent + where_indent + "where ".len() }; for _ in 0..padding_amount { br_with_padding.push(' '); } let where_preds = where_preds.to_string().replace('\n', &br_with_padding); if ending == Ending::Newline { let mut clause = " ".repeat(indent.saturating_sub(1)); write!(clause, "
`/`code-header` block, so indentation and newlines
/// are preserved.
/// * `indent`: The number of spaces to indent each successive line with, if line-wrapping is
/// necessary.
pub(crate) fn full_print<'a, 'tcx: 'a>(
&'a self,
header_len: usize,
indent: usize,
cx: &'a Context<'tcx>,
) -> impl Display + 'a + Captures<'tcx> {
fmt::from_fn(move |f| {
// First, generate the text form of the declaration, with no line wrapping, and count the bytes.
let mut counter = WriteCounter(0);
write!(&mut counter, "{:#}", fmt::from_fn(|f| { self.inner_full_print(None, f, cx) }))
.unwrap();
// If the text form was over 80 characters wide, we will line-wrap our output.
let line_wrapping_indent =
if header_len + counter.0 > 80 { Some(indent) } else { None };
// Generate the final output. This happens to accept `{:#}` formatting to get textual
// output but in practice it is only formatted with `{}` to get HTML output.
self.inner_full_print(line_wrapping_indent, f, cx)
})
}
fn inner_full_print(
&self,
// For None, the declaration will not be line-wrapped. For Some(n),
// the declaration will be line-wrapped, with an indent of n spaces.
line_wrapping_indent: Option,
f: &mut fmt::Formatter<'_>,
cx: &Context<'_>,
) -> fmt::Result {
let amp = if f.alternate() { "&" } else { "&" };
write!(f, "(")?;
if let Some(n) = line_wrapping_indent
&& !self.inputs.values.is_empty()
{
write!(f, "\n{}", Indent(n + 4))?;
}
let last_input_index = self.inputs.values.len().checked_sub(1);
for (i, input) in self.inputs.values.iter().enumerate() {
if let Some(selfty) = input.to_receiver() {
match selfty {
clean::SelfTy => {
write!(f, "self")?;
}
clean::BorrowedRef { lifetime, mutability, type_: box clean::SelfTy } => {
write!(f, "{amp}")?;
if let Some(lt) = lifetime {
write!(f, "{lt} ", lt = lt.print())?;
}
write!(f, "{mutability}self", mutability = mutability.print_with_space())?;
}
_ => {
write!(f, "self: ")?;
selfty.print(cx).fmt(f)?;
}
}
} else {
if input.is_const {
write!(f, "const ")?;
}
write!(f, "{}: ", input.name)?;
input.type_.print(cx).fmt(f)?;
}
match (line_wrapping_indent, last_input_index) {
(_, None) => (),
(None, Some(last_i)) if i != last_i => write!(f, ", ")?,
(None, Some(_)) => (),
(Some(n), Some(last_i)) if i != last_i => write!(f, ",\n{}", Indent(n + 4))?,
(Some(_), Some(_)) => writeln!(f, ",")?,
}
}
if self.c_variadic {
match line_wrapping_indent {
None => write!(f, ", ...")?,
Some(n) => writeln!(f, "{}...", Indent(n + 4))?,
};
}
match line_wrapping_indent {
None => write!(f, ")")?,
Some(n) => write!(f, "{})", Indent(n))?,
};
self.print_output(cx).fmt(f)
}
fn print_output<'a, 'tcx: 'a>(
&'a self,
cx: &'a Context<'tcx>,
) -> impl Display + 'a + Captures<'tcx> {
fmt::from_fn(move |f| match &self.output {
clean::Tuple(tys) if tys.is_empty() => Ok(()),
ty if f.alternate() => {
write!(f, " -> {:#}", ty.print(cx))
}
ty => write!(f, " -> {}", ty.print(cx)),
})
}
}
pub(crate) fn visibility_print_with_space<'a, 'tcx: 'a>(
item: &clean::Item,
cx: &'a Context<'tcx>,
) -> impl Display + 'a + Captures<'tcx> {
use std::fmt::Write as _;
let vis: Cow<'static, str> = match item.visibility(cx.tcx()) {
None => "".into(),
Some(ty::Visibility::Public) => "pub ".into(),
Some(ty::Visibility::Restricted(vis_did)) => {
// FIXME(camelid): This may not work correctly if `item_did` is a module.
// However, rustdoc currently never displays a module's
// visibility, so it shouldn't matter.
let parent_module = find_nearest_parent_module(cx.tcx(), item.item_id.expect_def_id());
if vis_did.is_crate_root() {
"pub(crate) ".into()
} else if parent_module == Some(vis_did) {
// `pub(in foo)` where `foo` is the parent module
// is the same as no visibility modifier
"".into()
} else if parent_module.and_then(|parent| find_nearest_parent_module(cx.tcx(), parent))
== Some(vis_did)
{
"pub(super) ".into()
} else {
let path = cx.tcx().def_path(vis_did);
debug!("path={path:?}");
// modified from `resolved_path()` to work with `DefPathData`
let last_name = path.data.last().unwrap().data.get_opt_name().unwrap();
let anchor = anchor(vis_did, last_name, cx);
let mut s = "pub(in ".to_owned();
for seg in &path.data[..path.data.len() - 1] {
let _ = write!(s, "{}::", seg.data.get_opt_name().unwrap());
}
let _ = write!(s, "{anchor}) ");
s.into()
}
}
};
let is_doc_hidden = item.is_doc_hidden();
fmt::from_fn(move |f| {
if is_doc_hidden {
f.write_str("#[doc(hidden)] ")?;
}
f.write_str(&vis)
})
}
pub(crate) trait PrintWithSpace {
fn print_with_space(&self) -> &str;
}
impl PrintWithSpace for hir::Safety {
fn print_with_space(&self) -> &str {
self.prefix_str()
}
}
impl PrintWithSpace for hir::HeaderSafety {
fn print_with_space(&self) -> &str {
match self {
hir::HeaderSafety::SafeTargetFeatures => "",
hir::HeaderSafety::Normal(safety) => safety.print_with_space(),
}
}
}
impl PrintWithSpace for hir::IsAsync {
fn print_with_space(&self) -> &str {
match self {
hir::IsAsync::Async(_) => "async ",
hir::IsAsync::NotAsync => "",
}
}
}
impl PrintWithSpace for hir::Mutability {
fn print_with_space(&self) -> &str {
match self {
hir::Mutability::Not => "",
hir::Mutability::Mut => "mut ",
}
}
}
pub(crate) fn print_constness_with_space(
c: &hir::Constness,
overall_stab: Option,
const_stab: Option,
) -> &'static str {
match c {
hir::Constness::Const => match (overall_stab, const_stab) {
// const stable...
(_, Some(ConstStability { level: StabilityLevel::Stable { .. }, .. }))
// ...or when feature(staged_api) is not set...
| (_, None)
// ...or when const unstable, but overall unstable too
| (None, Some(ConstStability { level: StabilityLevel::Unstable { .. }, .. })) => {
"const "
}
// const unstable (and overall stable)
(Some(_), Some(ConstStability { level: StabilityLevel::Unstable { .. }, .. })) => "",
},
// not const
hir::Constness::NotConst => "",
}
}
impl clean::Import {
pub(crate) fn print<'a, 'tcx: 'a>(
&'a self,
cx: &'a Context<'tcx>,
) -> impl Display + 'a + Captures<'tcx> {
fmt::from_fn(move |f| match self.kind {
clean::ImportKind::Simple(name) => {
if name == self.source.path.last() {
write!(f, "use {};", self.source.print(cx))
} else {
write!(f, "use {source} as {name};", source = self.source.print(cx))
}
}
clean::ImportKind::Glob => {
if self.source.path.segments.is_empty() {
write!(f, "use *;")
} else {
write!(f, "use {}::*;", self.source.print(cx))
}
}
})
}
}
impl clean::ImportSource {
pub(crate) fn print<'a, 'tcx: 'a>(
&'a self,
cx: &'a Context<'tcx>,
) -> impl Display + 'a + Captures<'tcx> {
fmt::from_fn(move |f| match self.did {
Some(did) => resolved_path(f, did, &self.path, true, false, cx),
_ => {
for seg in &self.path.segments[..self.path.segments.len() - 1] {
write!(f, "{}::", seg.name)?;
}
let name = self.path.last();
if let hir::def::Res::PrimTy(p) = self.path.res {
primitive_link(f, PrimitiveType::from(p), format_args!("{name}"), cx)?;
} else {
f.write_str(name.as_str())?;
}
Ok(())
}
})
}
}
impl clean::AssocItemConstraint {
pub(crate) fn print<'a, 'tcx: 'a>(
&'a self,
cx: &'a Context<'tcx>,
) -> impl Display + 'a + Captures<'tcx> {
fmt::from_fn(move |f| {
f.write_str(self.assoc.name.as_str())?;
self.assoc.args.print(cx).fmt(f)?;
match self.kind {
clean::AssocItemConstraintKind::Equality { ref term } => {
f.write_str(" = ")?;
term.print(cx).fmt(f)?;
}
clean::AssocItemConstraintKind::Bound { ref bounds } => {
if !bounds.is_empty() {
f.write_str(": ")?;
print_generic_bounds(bounds, cx).fmt(f)?;
}
}
}
Ok(())
})
}
}
pub(crate) fn print_abi_with_space(abi: ExternAbi) -> impl Display {
fmt::from_fn(move |f| {
let quot = if f.alternate() { "\"" } else { """ };
match abi {
ExternAbi::Rust => Ok(()),
abi => write!(f, "extern {0}{1}{0} ", quot, abi.name()),
}
})
}
pub(crate) fn print_default_space<'a>(v: bool) -> &'a str {
if v { "default " } else { "" }
}
impl clean::GenericArg {
pub(crate) fn print<'a, 'tcx: 'a>(
&'a self,
cx: &'a Context<'tcx>,
) -> impl Display + 'a + Captures<'tcx> {
fmt::from_fn(move |f| match self {
clean::GenericArg::Lifetime(lt) => lt.print().fmt(f),
clean::GenericArg::Type(ty) => ty.print(cx).fmt(f),
clean::GenericArg::Const(ct) => ct.print(cx.tcx()).fmt(f),
clean::GenericArg::Infer => Display::fmt("_", f),
})
}
}
impl clean::Term {
pub(crate) fn print<'a, 'tcx: 'a>(
&'a self,
cx: &'a Context<'tcx>,
) -> impl Display + 'a + Captures<'tcx> {
fmt::from_fn(move |f| match self {
clean::Term::Type(ty) => ty.print(cx).fmt(f),
clean::Term::Constant(ct) => ct.print(cx.tcx()).fmt(f),
})
}
}