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