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