rustdoc/html/
sources.rs

1use std::cell::RefCell;
2use std::ffi::OsStr;
3use std::path::{Component, Path, PathBuf};
4use std::{fmt, fs};
5
6use rinja::Template;
7use rustc_data_structures::fx::{FxHashSet, FxIndexMap};
8use rustc_hir::def_id::LOCAL_CRATE;
9use rustc_middle::ty::TyCtxt;
10use rustc_session::Session;
11use rustc_span::{FileName, FileNameDisplayPreference, RealFileName, sym};
12use tracing::info;
13
14use super::highlight;
15use super::layout::{self, BufDisplay};
16use super::render::Context;
17use crate::clean;
18use crate::clean::utils::has_doc_flag;
19use crate::docfs::PathError;
20use crate::error::Error;
21use crate::visit::DocVisitor;
22
23pub(crate) fn render(cx: &mut Context<'_>, krate: &clean::Crate) -> Result<(), Error> {
24    info!("emitting source files");
25
26    let dst = cx.dst.join("src").join(krate.name(cx.tcx()).as_str());
27    cx.shared.ensure_dir(&dst)?;
28    let crate_name = krate.name(cx.tcx());
29    let crate_name = crate_name.as_str();
30
31    let mut collector =
32        SourceCollector { dst, cx, emitted_local_sources: FxHashSet::default(), crate_name };
33    collector.visit_crate(krate);
34    Ok(())
35}
36
37pub(crate) fn collect_local_sources(
38    tcx: TyCtxt<'_>,
39    src_root: &Path,
40    krate: &clean::Crate,
41) -> FxIndexMap<PathBuf, String> {
42    let mut lsc = LocalSourcesCollector { tcx, local_sources: FxIndexMap::default(), src_root };
43    lsc.visit_crate(krate);
44    lsc.local_sources
45}
46
47struct LocalSourcesCollector<'a, 'tcx> {
48    tcx: TyCtxt<'tcx>,
49    local_sources: FxIndexMap<PathBuf, String>,
50    src_root: &'a Path,
51}
52
53fn filename_real_and_local(span: clean::Span, sess: &Session) -> Option<RealFileName> {
54    if span.cnum(sess) == LOCAL_CRATE
55        && let FileName::Real(file) = span.filename(sess)
56    {
57        Some(file)
58    } else {
59        None
60    }
61}
62
63impl LocalSourcesCollector<'_, '_> {
64    fn add_local_source(&mut self, item: &clean::Item) {
65        let sess = self.tcx.sess;
66        let span = item.span(self.tcx);
67        let Some(span) = span else { return };
68        // skip all synthetic "files"
69        let Some(p) = filename_real_and_local(span, sess).and_then(|file| file.into_local_path())
70        else {
71            return;
72        };
73        if self.local_sources.contains_key(&*p) {
74            // We've already emitted this source
75            return;
76        }
77
78        let href = RefCell::new(PathBuf::new());
79        clean_path(
80            self.src_root,
81            &p,
82            |component| {
83                href.borrow_mut().push(component);
84            },
85            || {
86                href.borrow_mut().pop();
87            },
88        );
89
90        let mut href = href.into_inner().to_string_lossy().into_owned();
91        if let Some(c) = href.as_bytes().last()
92            && *c != b'/'
93        {
94            href.push('/');
95        }
96        let mut src_fname = p.file_name().expect("source has no filename").to_os_string();
97        src_fname.push(".html");
98        href.push_str(&src_fname.to_string_lossy());
99        self.local_sources.insert(p, href);
100    }
101}
102
103impl DocVisitor<'_> for LocalSourcesCollector<'_, '_> {
104    fn visit_item(&mut self, item: &clean::Item) {
105        self.add_local_source(item);
106
107        self.visit_item_recur(item)
108    }
109}
110
111/// Helper struct to render all source code to HTML pages
112struct SourceCollector<'a, 'tcx> {
113    cx: &'a mut Context<'tcx>,
114
115    /// Root destination to place all HTML output into
116    dst: PathBuf,
117    emitted_local_sources: FxHashSet<PathBuf>,
118
119    crate_name: &'a str,
120}
121
122impl DocVisitor<'_> for SourceCollector<'_, '_> {
123    fn visit_item(&mut self, item: &clean::Item) {
124        if !self.cx.info.include_sources {
125            return;
126        }
127
128        let tcx = self.cx.tcx();
129        let span = item.span(tcx);
130        let Some(span) = span else { return };
131        let sess = tcx.sess;
132
133        // If we're not rendering sources, there's nothing to do.
134        // If we're including source files, and we haven't seen this file yet,
135        // then we need to render it out to the filesystem.
136        if let Some(filename) = filename_real_and_local(span, sess) {
137            let span = span.inner();
138            let pos = sess.source_map().lookup_source_file(span.lo());
139            let file_span = span.with_lo(pos.start_pos).with_hi(pos.end_position());
140            // If it turns out that we couldn't read this file, then we probably
141            // can't read any of the files (generating html output from json or
142            // something like that), so just don't include sources for the
143            // entire crate. The other option is maintaining this mapping on a
144            // per-file basis, but that's probably not worth it...
145            self.cx.info.include_sources = match self.emit_source(&filename, file_span) {
146                Ok(()) => true,
147                Err(e) => {
148                    self.cx.shared.tcx.dcx().span_err(
149                        span,
150                        format!(
151                            "failed to render source code for `{filename}`: {e}",
152                            filename = filename.to_string_lossy(FileNameDisplayPreference::Local),
153                        ),
154                    );
155                    false
156                }
157            };
158        }
159
160        self.visit_item_recur(item)
161    }
162}
163
164impl SourceCollector<'_, '_> {
165    /// Renders the given filename into its corresponding HTML source file.
166    fn emit_source(
167        &mut self,
168        file: &RealFileName,
169        file_span: rustc_span::Span,
170    ) -> Result<(), Error> {
171        let p = if let Some(local_path) = file.local_path() {
172            local_path.to_path_buf()
173        } else {
174            unreachable!("only the current crate should have sources emitted");
175        };
176        if self.emitted_local_sources.contains(&*p) {
177            // We've already emitted this source
178            return Ok(());
179        }
180
181        let contents = match fs::read_to_string(&p) {
182            Ok(contents) => contents,
183            Err(e) => {
184                return Err(Error::new(e, &p));
185            }
186        };
187
188        // Remove the utf-8 BOM if any
189        let contents = contents.strip_prefix('\u{feff}').unwrap_or(&contents);
190
191        let shared = &self.cx.shared;
192        // Create the intermediate directories
193        let cur = RefCell::new(PathBuf::new());
194        let root_path = RefCell::new(PathBuf::new());
195
196        clean_path(
197            &shared.src_root,
198            &p,
199            |component| {
200                cur.borrow_mut().push(component);
201                root_path.borrow_mut().push("..");
202            },
203            || {
204                cur.borrow_mut().pop();
205                root_path.borrow_mut().pop();
206            },
207        );
208
209        let src_fname = p.file_name().expect("source has no filename").to_os_string();
210        let mut fname = src_fname.clone();
211
212        let root_path = PathBuf::from("../../").join(root_path.into_inner());
213        let mut root_path = root_path.to_string_lossy();
214        if let Some(c) = root_path.as_bytes().last()
215            && *c != b'/'
216        {
217            root_path += "/";
218        }
219        let mut file_path = Path::new(&self.crate_name).join(&*cur.borrow());
220        file_path.push(&fname);
221        fname.push(".html");
222        let mut cur = self.dst.join(cur.into_inner());
223        shared.ensure_dir(&cur)?;
224
225        cur.push(&fname);
226
227        let title = format!("{} - source", src_fname.to_string_lossy());
228        let desc = format!(
229            "Source of the Rust file `{}`.",
230            file.to_string_lossy(FileNameDisplayPreference::Remapped)
231        );
232        let page = layout::Page {
233            title: &title,
234            css_class: "src",
235            root_path: &root_path,
236            static_root_path: shared.static_root_path.as_deref(),
237            description: &desc,
238            resource_suffix: &shared.resource_suffix,
239            rust_logo: has_doc_flag(self.cx.tcx(), LOCAL_CRATE.as_def_id(), sym::rust_logo),
240        };
241        let source_context = SourceContext::Standalone { file_path };
242        let v = layout::render(
243            &shared.layout,
244            &page,
245            "",
246            BufDisplay(|buf: &mut String| {
247                print_src(
248                    buf,
249                    contents,
250                    file_span,
251                    self.cx,
252                    &root_path,
253                    &highlight::DecorationInfo::default(),
254                    &source_context,
255                );
256            }),
257            &shared.style_files,
258        );
259        shared.fs.write(cur, v)?;
260        self.emitted_local_sources.insert(p);
261        Ok(())
262    }
263}
264
265/// Takes a path to a source file and cleans the path to it. This canonicalizes
266/// things like ".." to components which preserve the "top down" hierarchy of a
267/// static HTML tree. Each component in the cleaned path will be passed as an
268/// argument to `f`. The very last component of the path (ie the file name) is ignored.
269/// If a `..` is encountered, the `parent` closure will be called to allow the callee to
270/// handle it.
271pub(crate) fn clean_path<F, P>(src_root: &Path, p: &Path, mut f: F, mut parent: P)
272where
273    F: FnMut(&OsStr),
274    P: FnMut(),
275{
276    // make it relative, if possible
277    let p = p.strip_prefix(src_root).unwrap_or(p);
278
279    let mut iter = p.components().peekable();
280
281    while let Some(c) = iter.next() {
282        if iter.peek().is_none() {
283            break;
284        }
285
286        match c {
287            Component::ParentDir => parent(),
288            Component::Normal(c) => f(c),
289            _ => continue,
290        }
291    }
292}
293
294pub(crate) struct ScrapedInfo<'a> {
295    pub(crate) offset: usize,
296    pub(crate) name: &'a str,
297    pub(crate) url: &'a str,
298    pub(crate) title: &'a str,
299    pub(crate) locations: String,
300    pub(crate) needs_expansion: bool,
301}
302
303#[derive(Template)]
304#[template(path = "scraped_source.html")]
305struct ScrapedSource<'a, Code: std::fmt::Display> {
306    info: &'a ScrapedInfo<'a>,
307    code_html: Code,
308    max_nb_digits: u32,
309}
310
311#[derive(Template)]
312#[template(path = "source.html")]
313struct Source<Code: std::fmt::Display> {
314    code_html: Code,
315    file_path: Option<(String, String)>,
316    max_nb_digits: u32,
317}
318
319pub(crate) enum SourceContext<'a> {
320    Standalone { file_path: PathBuf },
321    Embedded(ScrapedInfo<'a>),
322}
323
324/// Wrapper struct to render the source code of a file. This will do things like
325/// adding line numbers to the left-hand side.
326pub(crate) fn print_src(
327    mut writer: impl fmt::Write,
328    s: &str,
329    file_span: rustc_span::Span,
330    context: &Context<'_>,
331    root_path: &str,
332    decoration_info: &highlight::DecorationInfo,
333    source_context: &SourceContext<'_>,
334) {
335    let mut lines = s.lines().count();
336    let line_info = if let SourceContext::Embedded(ref info) = source_context {
337        highlight::LineInfo::new_scraped(lines as u32, info.offset as u32)
338    } else {
339        highlight::LineInfo::new(lines as u32)
340    };
341    if line_info.is_scraped_example {
342        lines += line_info.start_line as usize;
343    }
344    let code = fmt::from_fn(move |fmt| {
345        let current_href = context
346            .href_from_span(clean::Span::new(file_span), false)
347            .expect("only local crates should have sources emitted");
348        highlight::write_code(
349            fmt,
350            s,
351            Some(highlight::HrefContext { context, file_span, root_path, current_href }),
352            Some(decoration_info),
353            Some(line_info),
354        );
355        Ok(())
356    });
357    let max_nb_digits = if lines > 0 { lines.ilog(10) + 1 } else { 1 };
358    match source_context {
359        SourceContext::Standalone { file_path } => Source {
360            code_html: code,
361            file_path: if let Some(file_name) = file_path.file_name()
362                && let Some(file_path) = file_path.parent()
363            {
364                Some((file_path.display().to_string(), file_name.display().to_string()))
365            } else {
366                None
367            },
368            max_nb_digits,
369        }
370        .render_into(&mut writer)
371        .unwrap(),
372        SourceContext::Embedded(info) => {
373            ScrapedSource { info, code_html: code, max_nb_digits }
374                .render_into(&mut writer)
375                .unwrap();
376        }
377    };
378}