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 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 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
111struct SourceCollector<'a, 'tcx> {
113 cx: &'a mut Context<'tcx>,
114
115 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 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 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 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 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 let contents = contents.strip_prefix('\u{feff}').unwrap_or(&contents);
190
191 let shared = &self.cx.shared;
192 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
265pub(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 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
324pub(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}