1use std::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, FileNameDisplayPreference, RealFileName, sym};
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 href = RefCell::new(PathBuf::new());
78 clean_path(
79 self.src_root,
80 &p,
81 |component| {
82 href.borrow_mut().push(component);
83 },
84 || {
85 href.borrow_mut().pop();
86 },
87 );
88
89 let mut href = href.into_inner().to_string_lossy().into_owned();
90 if let Some(c) = href.as_bytes().last()
91 && *c != b'/'
92 {
93 href.push('/');
94 }
95 let mut src_fname = p.file_name().expect("source has no filename").to_os_string();
96 src_fname.push(".html");
97 href.push_str(&src_fname.to_string_lossy());
98 self.local_sources.insert(p, href);
99 }
100}
101
102impl DocVisitor<'_> for LocalSourcesCollector<'_, '_> {
103 fn visit_item(&mut self, item: &clean::Item) {
104 self.add_local_source(item);
105
106 self.visit_item_recur(item)
107 }
108}
109
110struct SourceCollector<'a, 'tcx> {
112 cx: &'a mut Context<'tcx>,
113
114 dst: PathBuf,
116 emitted_local_sources: FxHashSet<PathBuf>,
117
118 crate_name: &'a str,
119}
120
121impl DocVisitor<'_> for SourceCollector<'_, '_> {
122 fn visit_item(&mut self, item: &clean::Item) {
123 if !self.cx.info.include_sources {
124 return;
125 }
126
127 let tcx = self.cx.tcx();
128 let span = item.span(tcx);
129 let Some(span) = span else { return };
130 let sess = tcx.sess;
131
132 if let Some(filename) = filename_real_and_local(span, sess) {
136 let span = span.inner();
137 let pos = sess.source_map().lookup_source_file(span.lo());
138 let file_span = span.with_lo(pos.start_pos).with_hi(pos.end_position());
139 self.cx.info.include_sources = match self.emit_source(&filename, file_span) {
145 Ok(()) => true,
146 Err(e) => {
147 self.cx.shared.tcx.dcx().span_err(
148 span,
149 format!(
150 "failed to render source code for `{filename}`: {e}",
151 filename = filename.to_string_lossy(FileNameDisplayPreference::Local),
152 ),
153 );
154 false
155 }
156 };
157 }
158
159 self.visit_item_recur(item)
160 }
161}
162
163impl SourceCollector<'_, '_> {
164 fn emit_source(
166 &mut self,
167 file: &RealFileName,
168 file_span: rustc_span::Span,
169 ) -> Result<(), Error> {
170 let p = if let Some(local_path) = file.local_path() {
171 local_path.to_path_buf()
172 } else {
173 unreachable!("only the current crate should have sources emitted");
174 };
175 if self.emitted_local_sources.contains(&*p) {
176 return Ok(());
178 }
179
180 let contents = match fs::read_to_string(&p) {
181 Ok(contents) => contents,
182 Err(e) => {
183 return Err(Error::new(e, &p));
184 }
185 };
186
187 let contents = contents.strip_prefix('\u{feff}').unwrap_or(&contents);
189
190 let shared = &self.cx.shared;
191 let cur = RefCell::new(PathBuf::new());
193 let root_path = RefCell::new(PathBuf::new());
194
195 clean_path(
196 &shared.src_root,
197 &p,
198 |component| {
199 cur.borrow_mut().push(component);
200 root_path.borrow_mut().push("..");
201 },
202 || {
203 cur.borrow_mut().pop();
204 root_path.borrow_mut().pop();
205 },
206 );
207
208 let src_fname = p.file_name().expect("source has no filename").to_os_string();
209 let mut fname = src_fname.clone();
210
211 let root_path = PathBuf::from("../../").join(root_path.into_inner());
212 let mut root_path = root_path.to_string_lossy();
213 if let Some(c) = root_path.as_bytes().last()
214 && *c != b'/'
215 {
216 root_path += "/";
217 }
218 let mut file_path = Path::new(&self.crate_name).join(&*cur.borrow());
219 file_path.push(&fname);
220 fname.push(".html");
221 let mut cur = self.dst.join(cur.into_inner());
222 shared.ensure_dir(&cur)?;
223
224 cur.push(&fname);
225
226 let title = format!("{} - source", src_fname.to_string_lossy());
227 let desc = format!(
228 "Source of the Rust file `{}`.",
229 file.to_string_lossy(FileNameDisplayPreference::Remapped)
230 );
231 let page = layout::Page {
232 title: &title,
233 css_class: "src",
234 root_path: &root_path,
235 static_root_path: shared.static_root_path.as_deref(),
236 description: &desc,
237 resource_suffix: &shared.resource_suffix,
238 rust_logo: has_doc_flag(self.cx.tcx(), LOCAL_CRATE.as_def_id(), sym::rust_logo),
239 };
240 let source_context = SourceContext::Standalone { file_path };
241 let v = layout::render(
242 &shared.layout,
243 &page,
244 "",
245 fmt::from_fn(|f| {
246 print_src(
247 f,
248 contents,
249 file_span,
250 self.cx,
251 &root_path,
252 &highlight::DecorationInfo::default(),
253 &source_context,
254 )
255 }),
256 &shared.style_files,
257 );
258 shared.fs.write(cur, v)?;
259 self.emitted_local_sources.insert(p);
260 Ok(())
261 }
262}
263
264pub(crate) fn clean_path<F, P>(src_root: &Path, p: &Path, mut f: F, mut parent: P)
271where
272 F: FnMut(&OsStr),
273 P: FnMut(),
274{
275 let p = p.strip_prefix(src_root).unwrap_or(p);
277
278 let mut iter = p.components().peekable();
279
280 while let Some(c) = iter.next() {
281 if iter.peek().is_none() {
282 break;
283 }
284
285 match c {
286 Component::ParentDir => parent(),
287 Component::Normal(c) => f(c),
288 _ => continue,
289 }
290 }
291}
292
293pub(crate) struct ScrapedInfo<'a> {
294 pub(crate) offset: usize,
295 pub(crate) name: &'a str,
296 pub(crate) url: &'a str,
297 pub(crate) title: &'a str,
298 pub(crate) locations: String,
299 pub(crate) needs_expansion: bool,
300}
301
302#[derive(Template)]
303#[template(path = "scraped_source.html")]
304struct ScrapedSource<'a, Code: std::fmt::Display> {
305 info: &'a ScrapedInfo<'a>,
306 code_html: Code,
307 max_nb_digits: u32,
308}
309
310#[derive(Template)]
311#[template(path = "source.html")]
312struct Source<Code: std::fmt::Display> {
313 code_html: Code,
314 file_path: Option<(String, String)>,
315 max_nb_digits: u32,
316}
317
318pub(crate) enum SourceContext<'a> {
319 Standalone { file_path: PathBuf },
320 Embedded(ScrapedInfo<'a>),
321}
322
323pub(crate) fn print_src(
326 mut writer: impl fmt::Write,
327 s: &str,
328 file_span: rustc_span::Span,
329 context: &Context<'_>,
330 root_path: &str,
331 decoration_info: &highlight::DecorationInfo,
332 source_context: &SourceContext<'_>,
333) -> fmt::Result {
334 let mut lines = s.lines().count();
335 let line_info = if let SourceContext::Embedded(info) = source_context {
336 highlight::LineInfo::new_scraped(lines as u32, info.offset as u32)
337 } else {
338 highlight::LineInfo::new(lines as u32)
339 };
340 if line_info.is_scraped_example {
341 lines += line_info.start_line as usize;
342 }
343 let code = fmt::from_fn(move |fmt| {
344 let current_href = context
345 .href_from_span(clean::Span::new(file_span), false)
346 .expect("only local crates should have sources emitted");
347 highlight::write_code(
348 fmt,
349 s,
350 Some(highlight::HrefContext { context, file_span, root_path, current_href }),
351 Some(decoration_info),
352 Some(line_info),
353 );
354 Ok(())
355 });
356 let max_nb_digits = if lines > 0 { lines.ilog(10) + 1 } else { 1 };
357 match source_context {
358 SourceContext::Standalone { file_path } => Source {
359 code_html: code,
360 file_path: if let Some(file_name) = file_path.file_name()
361 && let Some(file_path) = file_path.parent()
362 {
363 Some((file_path.display().to_string(), file_name.display().to_string()))
364 } else {
365 None
366 },
367 max_nb_digits,
368 }
369 .render_into(&mut writer),
370 SourceContext::Embedded(info) => {
371 ScrapedSource { info, code_html: code, max_nb_digits }.render_into(&mut writer)
372 }
373 }?;
374 Ok(())
375}