Skip to main content

rustdoc/html/render/
write_shared.rs

1//! Rustdoc writes aut two kinds of shared files:
2//!  - Static files, which are embedded in the rustdoc binary and are written with a
3//!    filename that includes a hash of their contents. These will always have a new
4//!    URL if the contents change, so they are safe to cache with the
5//!    `Cache-Control: immutable` directive. They are written under the static.files/
6//!    directory and are written when --emit-type is empty (default) or contains
7//!    "toolchain-specific". If using the --static-root-path flag, it should point
8//!    to a URL path prefix where each of these filenames can be fetched.
9//!  - Invocation specific files. These are generated based on the crate(s) being
10//!    documented. Their filenames need to be predictable without knowing their
11//!    contents, so they do not include a hash in their filename and are not safe to
12//!    cache with `Cache-Control: immutable`. They include the contents of the
13//!    --resource-suffix flag and are emitted when --emit-type is empty (default)
14//!    or contains "invocation-specific".
15
16use std::cell::RefCell;
17use std::ffi::{OsStr, OsString};
18use std::fs::File;
19use std::io::{self, Write as _};
20use std::iter::once;
21use std::marker::PhantomData;
22use std::path::{Component, Path, PathBuf};
23use std::rc::{Rc, Weak};
24use std::str::FromStr;
25use std::{fmt, fs};
26
27use indexmap::IndexMap;
28use rustc_ast::join_path_syms;
29use rustc_data_structures::flock;
30use rustc_data_structures::fx::{FxHashSet, FxIndexMap, FxIndexSet};
31use rustc_middle::ty::TyCtxt;
32use rustc_middle::ty::fast_reject::DeepRejectCtxt;
33use rustc_span::Symbol;
34use rustc_span::def_id::DefId;
35use serde::de::DeserializeOwned;
36use serde::ser::SerializeSeq;
37use serde::{Deserialize, Serialize, Serializer};
38
39use super::{Context, RenderMode, collect_paths_for_type, ensure_trailing_slash};
40use crate::clean::{Crate, Item, ItemId, ItemKind};
41use crate::config::{EmitType, PathToParts, RenderOptions, ShouldMerge};
42use crate::docfs::PathError;
43use crate::error::Error;
44use crate::formats::Impl;
45use crate::formats::item_type::ItemType;
46use crate::html::format::{print_impl, print_path};
47use crate::html::layout;
48use crate::html::render::ordered_json::{EscapedJson, OrderedJson};
49use crate::html::render::print_item::compare_names;
50use crate::html::render::search_index::{SerializedSearchIndex, build_index};
51use crate::html::render::sorted_template::{self, FileFormat, SortedTemplate};
52use crate::html::render::{AssocItemLink, ImplRenderingParameters, StylePath};
53use crate::html::static_files::{self, suffix_path};
54use crate::visit::DocVisitor;
55use crate::{try_err, try_none};
56
57pub(crate) fn write_shared(
58    cx: &mut Context<'_>,
59    krate: &Crate,
60    opt: &RenderOptions,
61    tcx: TyCtxt<'_>,
62) -> Result<(), Error> {
63    // NOTE(EtomicBomb): I don't think we need sync here because no read-after-write?
64    cx.shared.fs.set_sync_only(true);
65    let lock_file = cx.dst.join(".lock");
66    // Write shared runs within a flock; disable thread dispatching of IO temporarily.
67    let _lock = try_err!(flock::Lock::new(&lock_file, true, true, true), &lock_file);
68
69    let search_index = build_index(
70        krate,
71        &mut cx.shared.cache,
72        tcx,
73        &cx.dst,
74        &cx.shared.resource_suffix,
75        &opt.should_merge,
76    )?;
77
78    let crate_name = krate.name(cx.tcx());
79    let crate_name = crate_name.as_str(); // rand
80    let crate_name_json = OrderedJson::serialize(crate_name).unwrap(); // "rand"
81    let external_crates = hack_get_external_crate_names(&cx.dst, &cx.shared.resource_suffix)?;
82    let info = CrateInfo {
83        version: CrateInfoVersion::V2,
84        src_files_js: SourcesPart::get(cx, &crate_name_json)?,
85        search_index,
86        all_crates: AllCratesPart::get(crate_name_json.clone(), &cx.shared.resource_suffix)?,
87        crates_index: CratesIndexPart::get(crate_name, &external_crates)?,
88        trait_impl: TraitAliasPart::get(cx, &crate_name_json)?,
89        type_impl: TypeAliasPart::get(cx, krate, &crate_name_json)?,
90    };
91
92    if let Some(parts_out_dir) = &opt.parts_out_dir {
93        let mut parts_out_file = parts_out_dir.0.clone();
94        parts_out_file.push(&format!("{crate_name}.json"));
95        create_parents(&parts_out_file)?;
96        try_err!(
97            fs::write(&parts_out_file, serde_json::to_string(&info).unwrap()),
98            &parts_out_dir.0
99        );
100    }
101
102    let mut crates = CrateInfo::read_many(&opt.include_parts_dir)?;
103    crates.push(info);
104
105    if opt.should_merge.write_rendered_cci {
106        write_not_crate_specific(
107            &crates,
108            &cx.dst,
109            opt,
110            &cx.shared.style_files,
111            cx.shared.layout.css_file_extension.as_deref(),
112            &cx.shared.resource_suffix,
113            cx.info.include_sources,
114        )?;
115        match &opt.index_page {
116            Some(index_page) if opt.enable_index_page => {
117                let mut md_opts = opt.clone();
118                md_opts.output = cx.dst.clone();
119                md_opts.external_html = cx.shared.layout.external_html.clone();
120                let file = try_err!(cx.sess().source_map().load_file(&index_page), &index_page);
121                try_err!(
122                    crate::markdown::render_and_write(file, md_opts, cx.shared.edition()),
123                    &index_page
124                );
125            }
126            None if opt.enable_index_page => {
127                write_rendered_cci::<CratesIndexPart, _>(
128                    || CratesIndexPart::blank(cx),
129                    &cx.dst,
130                    &crates,
131                    &opt.should_merge,
132                )?;
133            }
134            _ => {} // they don't want an index page
135        }
136    }
137
138    cx.shared.fs.set_sync_only(false);
139    Ok(())
140}
141
142/// Writes files that are written directly to the `--out-dir`, without the prefix from the current
143/// crate. These are the rendered cross-crate files that encode info from multiple crates (e.g.
144/// search index), and the static files.
145pub(crate) fn write_not_crate_specific(
146    crates: &[CrateInfo],
147    dst: &Path,
148    opt: &RenderOptions,
149    style_files: &[StylePath],
150    css_file_extension: Option<&Path>,
151    resource_suffix: &str,
152    include_sources: bool,
153) -> Result<(), Error> {
154    write_rendered_cross_crate_info(crates, dst, opt, include_sources, resource_suffix)?;
155    write_resources(dst, opt, style_files, css_file_extension, resource_suffix)?;
156    Ok(())
157}
158
159fn write_rendered_cross_crate_info(
160    crates: &[CrateInfo],
161    dst: &Path,
162    opt: &RenderOptions,
163    include_sources: bool,
164    resource_suffix: &str,
165) -> Result<(), Error> {
166    let m = &opt.should_merge;
167    if opt.emit.contains(&EmitType::HtmlNonStaticFiles) {
168        if include_sources {
169            write_rendered_cci::<SourcesPart, _>(SourcesPart::blank, dst, crates, m)?;
170        }
171        crates
172            .iter()
173            .fold(SerializedSearchIndex::default(), |a, b| a.union(&b.search_index))
174            .sort()
175            .write_to(dst, resource_suffix)?;
176        write_rendered_cci::<AllCratesPart, _>(AllCratesPart::blank, dst, crates, m)?;
177    }
178    write_rendered_cci::<TraitAliasPart, _>(TraitAliasPart::blank, dst, crates, m)?;
179    write_rendered_cci::<TypeAliasPart, _>(TypeAliasPart::blank, dst, crates, m)?;
180    Ok(())
181}
182
183/// Writes the static files, the style files, and the css extensions.
184/// Have to be careful about these, because they write to the root out dir.
185fn write_resources(
186    dst: &Path,
187    opt: &RenderOptions,
188    style_files: &[StylePath],
189    css_file_extension: Option<&Path>,
190    resource_suffix: &str,
191) -> Result<(), Error> {
192    if opt.emit.contains(&EmitType::HtmlNonStaticFiles) {
193        // Handle added third-party themes
194        for entry in style_files {
195            let theme = entry.basename()?;
196            let extension =
197                try_none!(try_none!(entry.path.extension(), &entry.path).to_str(), &entry.path);
198
199            // Skip the official themes. They are written below as part of STATIC_FILES_LIST.
200            if matches!(theme.as_str(), "light" | "dark" | "ayu") {
201                continue;
202            }
203
204            let bytes = try_err!(fs::read(&entry.path), &entry.path);
205            let filename = format!("{theme}{resource_suffix}.{extension}");
206            let dst_filename = dst.join(filename);
207            try_err!(fs::write(&dst_filename, bytes), &dst_filename);
208        }
209
210        // When the user adds their own CSS files with --extend-css, we write that as an
211        // invocation-specific file (that is, with a resource suffix).
212        if let Some(css) = css_file_extension {
213            let buffer = try_err!(fs::read_to_string(css), css);
214            let path = static_files::suffix_path("theme.css", resource_suffix);
215            let dst_path = dst.join(path);
216            try_err!(fs::write(&dst_path, buffer), &dst_path);
217        }
218    }
219
220    if opt.emit.contains(&EmitType::HtmlStaticFiles) {
221        let static_dir = dst.join("static.files");
222        try_err!(fs::create_dir_all(&static_dir), &static_dir);
223
224        static_files::for_each(|f: &static_files::StaticFile| {
225            let filename = static_dir.join(f.output_filename());
226            let contents: &[u8] =
227                if opt.disable_minification { f.src_bytes } else { f.minified_bytes };
228            fs::write(&filename, contents).map_err(|e| PathError::new(e, &filename))
229        })?;
230    }
231
232    Ok(())
233}
234
235/// Contains pre-rendered contents to insert into the CCI template
236#[derive(Serialize, Deserialize, Clone, Debug)]
237pub(crate) struct CrateInfo {
238    version: CrateInfoVersion,
239    src_files_js: PartsAndLocations<SourcesPart>,
240    search_index: SerializedSearchIndex,
241    all_crates: PartsAndLocations<AllCratesPart>,
242    crates_index: PartsAndLocations<CratesIndexPart>,
243    trait_impl: PartsAndLocations<TraitAliasPart>,
244    type_impl: PartsAndLocations<TypeAliasPart>,
245}
246
247impl CrateInfo {
248    /// Read all of the crate info from its location on the filesystem
249    pub(crate) fn read_many(parts_paths: &[PathToParts]) -> Result<Vec<Self>, Error> {
250        parts_paths
251            .iter()
252            .fold(Ok(Vec::new()), |acc, parts_path| {
253                let mut acc = acc?;
254                let dir = &parts_path.0;
255                acc.append(&mut try_err!(std::fs::read_dir(dir), dir.as_path())
256                    .filter_map(|file| {
257                        let to_crate_info = |file: Result<std::fs::DirEntry, std::io::Error>| -> Result<Option<CrateInfo>, Error> {
258                            let file = try_err!(file, dir.as_path());
259                            if file.path().extension() != Some(OsStr::new("json")) {
260                                return Ok(None);
261                            }
262                            let parts = try_err!(fs::read(file.path()), file.path());
263                            let parts: CrateInfo = try_err!(serde_json::from_slice(&parts), file.path());
264                            Ok(Some(parts))
265                        };
266                        to_crate_info(file).transpose()
267                    })
268                    .collect::<Result<Vec<CrateInfo>, Error>>()?);
269                Ok(acc)
270            })
271    }
272}
273
274/// Version for the format of the crate-info file.
275///
276/// This enum should only ever have one variant, representing the current version.
277/// Gives pretty good error message about expecting the current version on deserialize.
278///
279/// Must be incremented (V2, V3, etc.) upon any changes to the search index or CrateInfo,
280/// to provide better diagnostics about including an invalid file.
281#[derive(Serialize, Deserialize, Clone, Debug)]
282enum CrateInfoVersion {
283    V2,
284}
285
286/// Paths (relative to the doc root) and their pre-merge contents
287#[derive(Serialize, Deserialize, Debug, Clone)]
288#[serde(transparent)]
289struct PartsAndLocations<P> {
290    parts: Vec<(PathBuf, P)>,
291}
292
293impl<P> Default for PartsAndLocations<P> {
294    fn default() -> Self {
295        Self { parts: Vec::default() }
296    }
297}
298
299impl<T, U> PartsAndLocations<Part<T, U>> {
300    fn push(&mut self, path: PathBuf, item: U) {
301        self.parts.push((path, Part { _artifact: PhantomData, item }));
302    }
303
304    /// Singleton part, one file
305    fn with(path: PathBuf, part: U) -> Self {
306        let mut ret = Self::default();
307        ret.push(path, part);
308        ret
309    }
310}
311
312/// A piece of one of the shared artifacts for documentation (search index, sources, alias list, etc.)
313///
314/// Merged at a user specified time and written to the `doc/` directory
315#[derive(Serialize, Deserialize, Debug, Clone)]
316#[serde(transparent)]
317struct Part<T, U> {
318    #[serde(skip)]
319    _artifact: PhantomData<T>,
320    item: U,
321}
322
323impl<T, U: fmt::Display> fmt::Display for Part<T, U> {
324    /// Writes serialized JSON
325    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326        write!(f, "{}", self.item)
327    }
328}
329
330/// Wrapper trait for `Part<T, U>`
331trait CciPart: Sized + fmt::Display + DeserializeOwned + 'static {
332    /// Identifies the file format of the cross-crate information
333    type FileFormat: sorted_template::FileFormat;
334    fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations<Self>;
335}
336
337#[derive(Serialize, Deserialize, Clone, Default, Debug)]
338struct AllCrates;
339type AllCratesPart = Part<AllCrates, OrderedJson>;
340impl CciPart for AllCratesPart {
341    type FileFormat = sorted_template::Js;
342    fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations<Self> {
343        &crate_info.all_crates
344    }
345}
346
347impl AllCratesPart {
348    fn blank() -> SortedTemplate<<Self as CciPart>::FileFormat> {
349        SortedTemplate::from_before_after("window.ALL_CRATES = [", "];")
350    }
351
352    fn get(
353        crate_name_json: OrderedJson,
354        resource_suffix: &str,
355    ) -> Result<PartsAndLocations<Self>, Error> {
356        // external hack_get_external_crate_names not needed here, because
357        // there's no way that we write the search index but not crates.js
358        let path = suffix_path("crates.js", resource_suffix);
359        Ok(PartsAndLocations::with(path, crate_name_json))
360    }
361}
362
363/// Reads `crates.js`, which seems like the best
364/// place to obtain the list of externally documented crates if the index
365/// page was disabled when documenting the deps.
366///
367/// This is to match the current behavior of rustdoc, which allows you to get all crates
368/// on the index page, even if --enable-index-page is only passed to the last crate.
369fn hack_get_external_crate_names(
370    doc_root: &Path,
371    resource_suffix: &str,
372) -> Result<Vec<String>, Error> {
373    let path = doc_root.join(suffix_path("crates.js", resource_suffix));
374    let Ok(content) = fs::read_to_string(&path) else {
375        // they didn't emit invocation specific, so we just say there were no crates
376        return Ok(Vec::default());
377    };
378    // this is only run once so it's fine not to cache it
379    // !dot_matches_new_line: all crates on same line. greedy: match last bracket
380    if let Some(start) = content.find('[')
381        && let Some(end) = content[start..].find(']')
382    {
383        let content: Vec<String> =
384            try_err!(serde_json::from_str(&content[start..=start + end]), &path);
385        Ok(content)
386    } else {
387        Err(Error::new("could not find crates list in crates.js", path))
388    }
389}
390
391#[derive(Serialize, Deserialize, Clone, Default, Debug)]
392struct CratesIndex;
393type CratesIndexPart = Part<CratesIndex, String>;
394impl CciPart for CratesIndexPart {
395    type FileFormat = sorted_template::Html;
396    fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations<Self> {
397        &crate_info.crates_index
398    }
399}
400
401impl CratesIndexPart {
402    fn blank(cx: &Context<'_>) -> SortedTemplate<<Self as CciPart>::FileFormat> {
403        let page = layout::Page {
404            title: "Index of crates",
405            short_title: "Crates",
406            css_class: "mod sys",
407            root_path: "./",
408            static_root_path: cx.shared.static_root_path.as_deref(),
409            description: "List of crates",
410            resource_suffix: &cx.shared.resource_suffix,
411            rust_logo: true,
412        };
413        let layout = &cx.shared.layout;
414        let style_files = &cx.shared.style_files;
415        const DELIMITER: &str = "\u{FFFC}"; // users are being naughty if they have this
416        let content = format!(
417            "<div class=\"main-heading\">\
418                <h1>List of all crates</h1>\
419                <rustdoc-toolbar></rustdoc-toolbar>\
420            </div>\
421            <ul class=\"all-items\">{DELIMITER}</ul>"
422        );
423        let template = layout::render(layout, &page, "", content, style_files);
424        SortedTemplate::from_template(&template, DELIMITER)
425            .expect("Object Replacement Character (U+FFFC) should not appear in the --index-page")
426    }
427
428    /// Might return parts that are duplicate with ones in preexisting index.html
429    fn get(crate_name: &str, external_crates: &[String]) -> Result<PartsAndLocations<Self>, Error> {
430        let mut ret = PartsAndLocations::default();
431        let path = Path::new("index.html");
432        for crate_name in external_crates.iter().map(|s| s.as_str()).chain(once(crate_name)) {
433            let part = format!(
434                "<li><a href=\"{trailing_slash}index.html\">{crate_name}</a></li>",
435                trailing_slash = ensure_trailing_slash(crate_name),
436            );
437            ret.push(path.to_path_buf(), part);
438        }
439        Ok(ret)
440    }
441}
442
443#[derive(Serialize, Deserialize, Clone, Default, Debug)]
444struct Sources;
445type SourcesPart = Part<Sources, EscapedJson>;
446impl CciPart for SourcesPart {
447    type FileFormat = sorted_template::Js;
448    fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations<Self> {
449        &crate_info.src_files_js
450    }
451}
452
453impl SourcesPart {
454    fn blank() -> SortedTemplate<<Self as CciPart>::FileFormat> {
455        // This needs to be `var`, not `const`.
456        // This variable needs declared in the current global scope so that if
457        // src-script.js loads first, it can pick it up.
458        SortedTemplate::from_before_after(r"createSrcSidebar('[", r"]');")
459    }
460
461    fn get(cx: &Context<'_>, crate_name: &OrderedJson) -> Result<PartsAndLocations<Self>, Error> {
462        let hierarchy = Rc::new(Hierarchy::default());
463        cx.shared
464            .local_sources
465            .iter()
466            .filter_map(|p| p.0.strip_prefix(&cx.shared.src_root).ok())
467            .for_each(|source| hierarchy.add_path(source));
468        let path = suffix_path("src-files.js", &cx.shared.resource_suffix);
469        let hierarchy = hierarchy.to_json_string();
470        let part = OrderedJson::array_unsorted([crate_name, &hierarchy]);
471        let part = EscapedJson::from(part);
472        Ok(PartsAndLocations::with(path, part))
473    }
474}
475
476/// Source files directory tree
477#[derive(Debug, Default)]
478struct Hierarchy {
479    parent: Weak<Self>,
480    elem: OsString,
481    children: RefCell<FxIndexMap<OsString, Rc<Self>>>,
482    elems: RefCell<FxIndexSet<OsString>>,
483}
484
485impl Hierarchy {
486    fn with_parent(elem: OsString, parent: &Rc<Self>) -> Self {
487        Self { elem, parent: Rc::downgrade(parent), ..Self::default() }
488    }
489
490    fn to_json_string(&self) -> OrderedJson {
491        let subs = self.children.borrow();
492        let files = self.elems.borrow();
493        let name = OrderedJson::serialize(self.elem.to_str().expect("invalid osstring conversion"))
494            .unwrap();
495        let mut out = Vec::from([name]);
496        if !subs.is_empty() || !files.is_empty() {
497            let subs = subs.iter().map(|(_, s)| s.to_json_string());
498            out.push(OrderedJson::array_sorted(subs));
499        }
500        if !files.is_empty() {
501            let files = files
502                .iter()
503                .map(|s| OrderedJson::serialize(s.to_str().expect("invalid osstring")).unwrap());
504            out.push(OrderedJson::array_sorted(files));
505        }
506        OrderedJson::array_unsorted(out)
507    }
508
509    fn add_path(self: &Rc<Self>, path: &Path) {
510        let mut h = Rc::clone(self);
511        let mut components = path
512            .components()
513            .filter(|component| matches!(component, Component::Normal(_) | Component::ParentDir))
514            .peekable();
515
516        assert!(components.peek().is_some(), "empty file path");
517        while let Some(component) = components.next() {
518            match component {
519                Component::Normal(s) => {
520                    if components.peek().is_none() {
521                        h.elems.borrow_mut().insert(s.to_owned());
522                        break;
523                    }
524                    h = {
525                        let mut children = h.children.borrow_mut();
526
527                        if let Some(existing) = children.get(s) {
528                            Rc::clone(existing)
529                        } else {
530                            let new_node = Rc::new(Self::with_parent(s.to_owned(), &h));
531                            children.insert(s.to_owned(), Rc::clone(&new_node));
532                            new_node
533                        }
534                    };
535                }
536                Component::ParentDir if let Some(parent) = h.parent.upgrade() => {
537                    h = parent;
538                }
539                _ => {}
540            }
541        }
542    }
543}
544
545#[derive(Serialize, Deserialize, Clone, Default, Debug)]
546struct TypeAlias;
547type TypeAliasPart = Part<TypeAlias, OrderedJson>;
548impl CciPart for TypeAliasPart {
549    type FileFormat = sorted_template::Js;
550    fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations<Self> {
551        &crate_info.type_impl
552    }
553}
554
555impl TypeAliasPart {
556    fn blank() -> SortedTemplate<<Self as CciPart>::FileFormat> {
557        SortedTemplate::from_before_after(
558            r"(function() {
559    var type_impls = Object.fromEntries([",
560            r"]);
561    if (window.register_type_impls) {
562        window.register_type_impls(type_impls);
563    } else {
564        window.pending_type_impls = type_impls;
565    }
566})()",
567        )
568    }
569
570    fn get(
571        cx: &mut Context<'_>,
572        krate: &Crate,
573        crate_name_json: &OrderedJson,
574    ) -> Result<PartsAndLocations<Self>, Error> {
575        let mut path_parts = PartsAndLocations::default();
576
577        let mut type_impl_collector = TypeImplCollector {
578            aliased_types: IndexMap::default(),
579            visited_aliases: FxHashSet::default(),
580            cx,
581        };
582        DocVisitor::visit_crate(&mut type_impl_collector, krate);
583        let cx = type_impl_collector.cx;
584        let aliased_types = type_impl_collector.aliased_types;
585        for aliased_type in aliased_types.values() {
586            let impls = aliased_type.impl_.values().filter_map(
587                |AliasedTypeImpl { impl_, type_aliases }| {
588                    let mut ret: Option<AliasSerializableImpl> = None;
589                    // render_impl will filter out "impossible-to-call" methods
590                    // to make that functionality work here, it needs to be called with
591                    // each type alias, and if it gives a different result, split the impl
592                    for &(type_alias_fqp, type_alias_item) in type_aliases {
593                        cx.id_map.borrow_mut().clear();
594                        cx.deref_id_map.borrow_mut().clear();
595                        let type_alias_fqp = join_path_syms(type_alias_fqp);
596                        if let Some(ret) = &mut ret {
597                            ret.aliases.push(type_alias_fqp);
598                        } else {
599                            let target_trait_did =
600                                impl_.inner_impl().trait_.as_ref().map(|trait_| trait_.def_id());
601                            let provided_methods;
602                            let assoc_link = if let Some(target_trait_did) = target_trait_did {
603                                provided_methods =
604                                    impl_.inner_impl().provided_trait_methods(cx.tcx());
605                                AssocItemLink::GotoSource(
606                                    ItemId::DefId(target_trait_did),
607                                    &provided_methods,
608                                )
609                            } else {
610                                AssocItemLink::Anchor(None)
611                            };
612                            let text = super::render_impl(
613                                cx,
614                                impl_,
615                                type_alias_item,
616                                assoc_link,
617                                RenderMode::Normal,
618                                None,
619                                &[],
620                                ImplRenderingParameters {
621                                    show_def_docs: true,
622                                    show_default_items: true,
623                                    show_non_assoc_items: true,
624                                    toggle_open_by_default: true,
625                                },
626                            )
627                            .to_string();
628                            // The alternate display prints it as plaintext instead of HTML.
629                            let trait_ = impl_
630                                .inner_impl()
631                                .trait_
632                                .as_ref()
633                                .map(|trait_| format!("{:#}", print_path(trait_, cx)));
634                            ret = Some(AliasSerializableImpl {
635                                text,
636                                trait_,
637                                aliases: vec![type_alias_fqp],
638                            })
639                        }
640                    }
641                    ret
642                },
643            );
644
645            let mut path = PathBuf::from("type.impl");
646            for component in &aliased_type.target_fqp[..aliased_type.target_fqp.len() - 1] {
647                path.push(component.as_str());
648            }
649            let aliased_item_type = aliased_type.target_type;
650            path.push(format!(
651                "{aliased_item_type}.{}.js",
652                aliased_type.target_fqp[aliased_type.target_fqp.len() - 1]
653            ));
654
655            let part = OrderedJson::array_sorted(
656                impls.map(|impl_| OrderedJson::serialize(impl_).unwrap()),
657            );
658            path_parts.push(path, OrderedJson::array_unsorted([crate_name_json, &part]));
659        }
660        Ok(path_parts)
661    }
662}
663
664#[derive(Serialize, Deserialize, Clone, Default, Debug)]
665struct TraitAlias;
666type TraitAliasPart = Part<TraitAlias, OrderedJson>;
667impl CciPart for TraitAliasPart {
668    type FileFormat = sorted_template::Js;
669    fn from_crate_info(crate_info: &CrateInfo) -> &PartsAndLocations<Self> {
670        &crate_info.trait_impl
671    }
672}
673
674impl TraitAliasPart {
675    fn blank() -> SortedTemplate<<Self as CciPart>::FileFormat> {
676        SortedTemplate::from_before_after(
677            r"(function() {
678    const implementors = Object.fromEntries([",
679            r"]);
680    if (window.register_implementors) {
681        window.register_implementors(implementors);
682    } else {
683        window.pending_implementors = implementors;
684    }
685})()",
686        )
687    }
688
689    fn get(
690        cx: &Context<'_>,
691        crate_name_json: &OrderedJson,
692    ) -> Result<PartsAndLocations<Self>, Error> {
693        let cache = &cx.shared.cache;
694        let mut path_parts = PartsAndLocations::default();
695        // Update the list of all implementors for traits
696        // <https://github.com/search?q=repo%3Arust-lang%2Frust+[RUSTDOCIMPL]+trait.impl&type=code>
697        for (&did, imps) in &cache.implementors {
698            // Private modules can leak through to this phase of rustdoc, which
699            // could contain implementations for otherwise private types. In some
700            // rare cases we could find an implementation for an item which wasn't
701            // indexed, so we just skip this step in that case.
702            //
703            // FIXME: this is a vague explanation for why this can't be a `get`, in
704            //        theory it should be...
705            let (remote_path, remote_item_type) = match cache.exact_paths.get(&did) {
706                Some(p) => match cache.paths.get(&did).or_else(|| cache.external_paths.get(&did)) {
707                    Some((_, t)) => (p, t),
708                    None => continue,
709                },
710                None => match cache.external_paths.get(&did) {
711                    Some((p, t)) => (p, t),
712                    None => continue,
713                },
714            };
715
716            let mut implementors = imps
717                .iter()
718                .filter_map(|imp| {
719                    // If the trait and implementation are in the same crate, then
720                    // there's no need to emit information about it (there's inlining
721                    // going on). If they're in different crates then the crate defining
722                    // the trait will be interested in our implementation.
723                    //
724                    // If the implementation is from another crate then that crate
725                    // should add it.
726                    if imp.impl_item.item_id.krate() == did.krate
727                        || !imp.impl_item.item_id.is_local()
728                    {
729                        None
730                    } else {
731                        let impl_ = imp.inner_impl();
732                        let print = print_impl(impl_, false, cx);
733                        Some(Implementor {
734                            text: format!("{}", print),
735                            cmp_text: format!("{:#}", print),
736                            synthetic: imp.inner_impl().kind.is_auto(),
737                            types: collect_paths_for_type(&imp.inner_impl().for_, cache),
738                            is_negative: impl_.is_negative_trait_impl(),
739                        })
740                    }
741                })
742                .peekable();
743
744            // Only create a js file if we have impls to add to it. If the trait is
745            // documented locally though we always create the file to avoid dead
746            // links.
747            if implementors.peek().is_none() && !cache.paths.contains_key(&did) {
748                continue;
749            }
750
751            let mut path = PathBuf::from("trait.impl");
752            for component in &remote_path[..remote_path.len() - 1] {
753                path.push(component.as_str());
754            }
755            path.push(format!("{remote_item_type}.{}.js", remote_path[remote_path.len() - 1]));
756
757            let mut implementors = implementors.collect::<Vec<_>>();
758            // Negative impls are naturally sorted first, because `impl !A` is less than `impl B`
759            // for any value of `B`, because `!` is less than any identifier-starting char.
760            implementors.sort_unstable_by(|a, b| compare_names(&a.cmp_text, &b.cmp_text));
761
762            let part = OrderedJson::array_unsorted(
763                implementors
764                    .iter()
765                    .map(OrderedJson::serialize)
766                    .collect::<Result<Vec<_>, _>>()
767                    .unwrap(),
768            );
769            path_parts.push(path, OrderedJson::array_unsorted([crate_name_json, &part]));
770        }
771        Ok(path_parts)
772    }
773}
774
775struct Implementor {
776    // HTML text used in generated output.
777    text: String,
778    // Plain text used just for sorting output. This is a performance win, because this plain text
779    // is much shorter than the HTML output and sorting is hot.
780    cmp_text: String,
781    synthetic: bool,
782    types: Vec<String>,
783    is_negative: bool,
784}
785
786impl Serialize for Implementor {
787    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
788    where
789        S: Serializer,
790    {
791        let mut seq = serializer.serialize_seq(None)?;
792        seq.serialize_element(&self.text)?;
793        seq.serialize_element(if self.is_negative { &1 } else { &0 })?;
794        if self.synthetic {
795            seq.serialize_element(&1)?;
796            seq.serialize_element(&self.types)?;
797        }
798        seq.end()
799    }
800}
801
802/// Collect the list of aliased types and their aliases.
803/// <https://github.com/search?q=repo%3Arust-lang%2Frust+[RUSTDOCIMPL]+type.impl&type=code>
804///
805/// The clean AST has type aliases that point at their types, but
806/// this visitor works to reverse that: `aliased_types` is a map
807/// from target to the aliases that reference it, and each one
808/// will generate one file.
809struct TypeImplCollector<'cx, 'cache, 'item> {
810    /// Map from DefId-of-aliased-type to its data.
811    aliased_types: IndexMap<DefId, AliasedType<'cache, 'item>>,
812    visited_aliases: FxHashSet<DefId>,
813    cx: &'cache Context<'cx>,
814}
815
816/// Data for an aliased type.
817///
818/// In the final file, the format will be roughly:
819///
820/// ```json
821/// // type.impl/CRATE/TYPENAME.js
822/// JSONP(
823/// "CRATE": [
824///   ["IMPL1 HTML", "ALIAS1", "ALIAS2", ...],
825///   ["IMPL2 HTML", "ALIAS3", "ALIAS4", ...],
826///    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ struct AliasedType
827///   ...
828/// ]
829/// )
830/// ```
831struct AliasedType<'cache, 'item> {
832    /// This is used to generate the actual filename of this aliased type.
833    target_fqp: &'cache [Symbol],
834    target_type: ItemType,
835    /// This is the data stored inside the file.
836    /// ItemId is used to deduplicate impls.
837    impl_: IndexMap<ItemId, AliasedTypeImpl<'cache, 'item>>,
838}
839
840/// The `impl_` contains data that's used to figure out if an alias will work,
841/// and to generate the HTML at the end.
842///
843/// The `type_aliases` list is built up with each type alias that matches.
844struct AliasedTypeImpl<'cache, 'item> {
845    impl_: &'cache Impl,
846    type_aliases: Vec<(&'cache [Symbol], &'item Item)>,
847}
848
849impl<'item> DocVisitor<'item> for TypeImplCollector<'_, '_, 'item> {
850    fn visit_item(&mut self, it: &'item Item) {
851        self.visit_item_recur(it);
852        let cache = &self.cx.shared.cache;
853        let ItemKind::TypeAliasItem(ref t) = it.kind else { return };
854        let Some(self_did) = it.item_id.as_def_id() else { return };
855        if !self.visited_aliases.insert(self_did) {
856            return;
857        }
858        let Some(target_did) = t.type_.def_id(cache) else { return };
859        let get_extern = { || cache.external_paths.get(&target_did) };
860        let Some(&(ref target_fqp, target_type)) = cache.paths.get(&target_did).or_else(get_extern)
861        else {
862            return;
863        };
864        let aliased_type = self.aliased_types.entry(target_did).or_insert_with(|| {
865            let impl_ = cache
866                .impls
867                .get(&target_did)
868                .into_iter()
869                .flatten()
870                .map(|impl_| {
871                    (impl_.impl_item.item_id, AliasedTypeImpl { impl_, type_aliases: Vec::new() })
872                })
873                .collect();
874            AliasedType { target_fqp: &target_fqp[..], target_type, impl_ }
875        });
876        let get_local = { || cache.paths.get(&self_did).map(|(p, _)| p) };
877        let Some(self_fqp) = cache.exact_paths.get(&self_did).or_else(get_local) else {
878            return;
879        };
880        let aliased_ty = self.cx.tcx().type_of(self_did).skip_binder();
881        // Exclude impls that are directly on this type. They're already in the HTML.
882        // Some inlining scenarios can cause there to be two versions of the same
883        // impl: one on the type alias and one on the underlying target type.
884        let mut seen_impls: FxHashSet<ItemId> =
885            cache.impls.get(&self_did).into_iter().flatten().map(|i| i.impl_item.item_id).collect();
886        for (impl_item_id, aliased_type_impl) in &mut aliased_type.impl_ {
887            // Only include this impl if it actually unifies with this alias.
888            // Synthetic impls are not included; those are also included in the HTML.
889            //
890            // FIXME(lazy_type_alias): Once the feature is complete or stable, rewrite this
891            // to use type unification.
892            // Be aware of `tests/rustdoc-html/type-alias/deeply-nested-112515.rs` which might
893            // regress.
894            let Some(impl_did) = impl_item_id.as_def_id() else { continue };
895            let for_ty = self.cx.tcx().type_of(impl_did).skip_binder();
896            let reject_cx = DeepRejectCtxt::relate_infer_infer(self.cx.tcx());
897            if !reject_cx.types_may_unify(aliased_ty, for_ty) {
898                continue;
899            }
900            // Avoid duplicates
901            if !seen_impls.insert(*impl_item_id) {
902                continue;
903            }
904            // This impl was not found in the set of rejected impls
905            aliased_type_impl.type_aliases.push((&self_fqp[..], it));
906        }
907    }
908}
909
910/// Final serialized form of the alias impl
911struct AliasSerializableImpl {
912    text: String,
913    trait_: Option<String>,
914    aliases: Vec<String>,
915}
916
917impl Serialize for AliasSerializableImpl {
918    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
919    where
920        S: Serializer,
921    {
922        let mut seq = serializer.serialize_seq(None)?;
923        seq.serialize_element(&self.text)?;
924        if let Some(trait_) = &self.trait_ {
925            seq.serialize_element(trait_)?;
926        } else {
927            seq.serialize_element(&0)?;
928        }
929        for type_ in &self.aliases {
930            seq.serialize_element(type_)?;
931        }
932        seq.end()
933    }
934}
935
936fn get_path_parts<T: CciPart>(
937    dst: &Path,
938    crates_info: &[CrateInfo],
939) -> FxIndexMap<PathBuf, Vec<String>> {
940    let mut templates: FxIndexMap<PathBuf, Vec<String>> = FxIndexMap::default();
941    crates_info.iter().flat_map(|crate_info| T::from_crate_info(crate_info).parts.iter()).for_each(
942        |(path, part)| {
943            let path = dst.join(path);
944            let part = part.to_string();
945            templates.entry(path).or_default().push(part);
946        },
947    );
948    templates
949}
950
951/// Create all parents
952fn create_parents(path: &Path) -> Result<(), Error> {
953    let parent = path.parent().expect("should not have an empty path here");
954    try_err!(fs::create_dir_all(parent), parent);
955    Ok(())
956}
957
958/// Returns a blank template unless we could find one to append to
959fn read_template_or_blank<F, T: FileFormat>(
960    mut make_blank: F,
961    path: &Path,
962    should_merge: &ShouldMerge,
963) -> Result<SortedTemplate<T>, Error>
964where
965    F: FnMut() -> SortedTemplate<T>,
966{
967    if !should_merge.read_rendered_cci {
968        return Ok(make_blank());
969    }
970    match fs::read_to_string(path) {
971        Ok(template) => Ok(try_err!(SortedTemplate::from_str(&template), &path)),
972        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(make_blank()),
973        Err(e) => Err(Error::new(e, path)),
974    }
975}
976
977/// info from this crate and the --include-info-json'd crates
978fn write_rendered_cci<T: CciPart, F>(
979    mut make_blank: F,
980    dst: &Path,
981    crates_info: &[CrateInfo],
982    should_merge: &ShouldMerge,
983) -> Result<(), Error>
984where
985    F: FnMut() -> SortedTemplate<T::FileFormat>,
986{
987    // write the merged cci to disk
988    for (path, parts) in get_path_parts::<T>(dst, crates_info) {
989        create_parents(&path)?;
990        // read previous rendered cci from storage, append to them
991        let mut template =
992            read_template_or_blank::<_, T::FileFormat>(&mut make_blank, &path, should_merge)?;
993        for part in parts {
994            template.append(part);
995        }
996        let mut file = try_err!(File::create_buffered(&path), &path);
997        try_err!(write!(file, "{template}"), &path);
998        try_err!(file.flush(), &path);
999    }
1000    Ok(())
1001}
1002
1003#[cfg(test)]
1004mod tests;