Skip to main content

rustdoc/html/
sources.rs

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