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