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