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 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 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
105struct SourceCollector<'a, 'tcx> {
107 cx: &'a mut Context<'tcx>,
108
109 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 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 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 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 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 let contents = contents.trim_prefix('\u{feff}');
187
188 let shared = &self.cx.shared;
189 let cur = RefCell::new(PathBuf::new());
191 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
260pub(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 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
319pub(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 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}