Skip to main content

cargo_doc2readme/
lib.rs

1//! **THIS IS NOT A LIBRARY. NONE OF THE APIS ARE PUBLIC. THEY DON'T
2//! ADHERE TO SEMVER. DON'T EVEN USE AT YOUR OWN RISK. DON'T USE IT
3//! AT ALL.**
4
5#[doc(hidden)]
6pub mod config;
7mod depinfo;
8#[doc(hidden)]
9pub mod diagnostic;
10mod input;
11mod links;
12mod monostate;
13mod output;
14mod preproc;
15mod verify;
16
17// this shall explicitly be documented
18#[doc(inline)]
19pub use self::{input::TargetType, output::TemplateContext};
20
21use self::{
22	config::{Config, ConfigOutput},
23	diagnostic::DiagnosticPrinter,
24	input::{CrateCode, InputFile}
25};
26use camino::Utf8Path;
27use cargo_metadata::{CargoOpt, Metadata, MetadataCommand, Target};
28use either::Either;
29use itertools::Itertools as _;
30use miette::{miette, Context as _, IntoDiagnostic as _, MietteHandlerOpts, Severity};
31use std::{
32	borrow::Cow,
33	env, fmt,
34	fs::{self, File},
35	io, iter,
36	sync::{Arc, Mutex}
37};
38
39#[doc(hidden)]
40pub fn install_miette_hook(wrap_lines: bool) {
41	// Custom miette hook to make sure that relevant text like "WARNING" isn't hidden
42	// behind a simple yellow `!` that means nothing to a user
43	miette::set_hook(Box::new(move |_| {
44		let mut styles = miette::ThemeStyles::ansi();
45		styles.highlights.truncate(1);
46		Box::new(
47			MietteHandlerOpts::new()
48				.unicode(false)
49				.graphical_theme(miette::GraphicalTheme {
50					characters: miette::ThemeCharacters {
51						error: "\nERROR:".into(),
52						warning: "\nWARNING:".into(),
53						advice: "\nINFO:".into(),
54						..miette::ThemeCharacters::unicode()
55					},
56					styles
57				})
58				.tab_width(4)
59				.wrap_lines(wrap_lines)
60				.build()
61		)
62	}))
63	.expect("Failed to initialise error report hook");
64}
65
66#[doc(hidden)]
67pub struct App<W> {
68	cfg: Config,
69
70	/// Writer that diagnostics can be printed to. Usually stderr.
71	pub stderr: W,
72
73	/// Output of cargo metadata.
74	metadata: Metadata
75}
76
77#[doc(hidden)]
78pub struct Instance<'a, W> {
79	/// App-wide configuration.
80	cfg: &'a Config,
81	/// App-wide output of cargo metadata.
82	metadata: &'a Metadata,
83
84	/// Instance-specific cargo package name.
85	pkg: Option<&'a str>,
86	/// Instance-specific output file.
87	out: Cow<'a, Utf8Path>,
88	/// Instance-specific template file.
89	template_path: Option<Cow<'a, Utf8Path>>,
90
91	/// Writer that diagnostics can be printed to.
92	stderr: W,
93	/// The code of the crate of this instance.
94	code: CrateCode,
95
96	input_file: InputFile,
97	template_filename: Cow<'static, str>,
98	template: Cow<'static, str>,
99	builtin_template: bool
100}
101
102impl<W: io::Write> App<W> {
103	pub fn with_config(cfg: Config, stderr: W) -> miette::Result<Self> {
104		// call cargo metadata
105		let mut cmd = MetadataCommand::new();
106		cmd.features(CargoOpt::AllFeatures);
107		if let Some(path) = &cfg.manifest_path {
108			cmd.manifest_path(path);
109		}
110		let metadata = cmd
111			.exec()
112			.map_err(|err| diagnostic::ExecError::new(err, &cmd.cargo_command()))?;
113
114		Ok(Self {
115			cfg,
116			stderr,
117			metadata
118		})
119	}
120
121	pub fn check_cfg(&mut self) {
122		let code = CrateCode::new_unknown();
123		let mut printer = DiagnosticPrinter::new(&code, &mut self.stderr);
124
125		if self.cfg.package.is_some() && self.cfg.packages.is_some() {
126			printer.print(diagnostic::PackagesIgnored);
127		}
128
129		if !self.cfg.expand_macros {
130			if self.cfg.features.is_some() {
131				printer.print(diagnostic::NoOpWithoutExpandMacros::flag("--features"));
132			}
133			if !self.cfg.default_features {
134				printer.print(diagnostic::NoOpWithoutExpandMacros::flag(
135					"--no-default-features"
136				));
137			}
138			if self.cfg.all_features {
139				printer
140					.print(diagnostic::NoOpWithoutExpandMacros::flag("--all-features"));
141			}
142		}
143	}
144
145	/// Return the instance if the configuration results in exactly one readme instance.
146	///
147	/// # Panics
148	///
149	/// If the configuration results in more than one instance.
150	#[track_caller]
151	pub fn instance(&mut self) -> Instance<'_, &mut W> {
152		// --workspace, unless -p is specified, could give multiple instances
153		// also, it would potentially invalidate the package of the dumb instance
154		if self.cfg.workspace && self.cfg.package.is_none() {
155			panic!("There might be multiple instances");
156		}
157		// when `out` is not a single file, even if it is a one-element array, the
158		// template path might be overwritten, so the dumb instance would be wrong
159		let out = match &self.cfg.out {
160			ConfigOutput::SingleFile(out) => out,
161			ConfigOutput::MultiFile(_) => panic!("There might be multiple instances")
162		};
163
164		Instance {
165			cfg: &self.cfg,
166			metadata: &self.metadata,
167			pkg: self.cfg.package.as_deref(),
168			out: out.as_path().into(),
169			template_path: self.cfg.template.as_deref().map(Into::into),
170			stderr: &mut self.stderr,
171			code: CrateCode::new_unknown(),
172			input_file: InputFile::dummy(),
173			template_filename: "<builtin-template>".into(),
174			template: include_str!("README.j2").into(),
175			builtin_template: true
176		}
177	}
178
179	pub fn instances(
180		&mut self
181	) -> impl Iterator<Item = Instance<'_, impl io::Write + '_>> + '_ {
182		let pkg_iter = if self.cfg.workspace && self.cfg.package.is_none() {
183			let iter = self
184				.metadata
185				.workspace_packages()
186				.into_iter()
187				.map(|pkg| pkg.name.as_str());
188			Either::Left(
189				match &self.cfg.packages {
190					Some(pkgs) => Either::Left(iter.filter(|pkg| pkgs.contains(*pkg))),
191					None => Either::Right(iter)
192				}
193				.map(Some)
194			)
195		} else {
196			Either::Right(iter::once(self.cfg.package.as_deref()))
197		};
198
199		let out_iter = match &self.cfg.out {
200			ConfigOutput::SingleFile(out) => {
201				Either::Left(iter::once((out, self.cfg.template.as_ref())))
202			},
203			ConfigOutput::MultiFile(files) if files.is_empty() => {
204				let code = CrateCode::new_unknown();
205				let mut printer = DiagnosticPrinter::new(&code, &mut self.stderr);
206				printer.print(diagnostic::NoOutputs);
207				return Either::Left(iter::empty());
208			},
209			ConfigOutput::MultiFile(files) => Either::Right(files.iter().map(|file| {
210				(
211					&file.out,
212					file.template.as_ref().or(self.cfg.template.as_ref())
213				)
214			}))
215		};
216
217		let cfg = &self.cfg;
218		let metadata = &self.metadata;
219		let stderr = SharedWrite::new(&mut self.stderr);
220
221		Either::Right(pkg_iter.cartesian_product(out_iter).map(
222			move |(pkg, (out, template_path))| Instance {
223				cfg,
224				metadata,
225				pkg,
226				out: out.as_path().into(),
227				template_path: template_path.map(|path| path.as_path().into()),
228				stderr: stderr.clone(),
229				code: CrateCode::new_unknown(),
230				input_file: InputFile::dummy(),
231				template_filename: "<builtin-template>".into(),
232				template: include_str!("README.j2").into(),
233				builtin_template: true
234			}
235		))
236	}
237}
238
239struct SharedWrite<'a, W>(Arc<Mutex<&'a mut W>>);
240
241impl<'a, W> SharedWrite<'a, W> {
242	pub fn new(inner: &'a mut W) -> Self {
243		Self(Arc::new(Mutex::new(inner)))
244	}
245}
246
247impl<W> Clone for SharedWrite<'_, W> {
248	fn clone(&self) -> Self {
249		Self(Arc::clone(&self.0))
250	}
251}
252
253impl<W: io::Write> io::Write for SharedWrite<'_, W> {
254	fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
255		self.0.lock().unwrap().write(buf)
256	}
257
258	fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result<usize> {
259		self.0.lock().unwrap().write_vectored(bufs)
260	}
261
262	fn flush(&mut self) -> io::Result<()> {
263		self.0.lock().unwrap().flush()
264	}
265
266	fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
267		self.0.lock().unwrap().write_all(buf)
268	}
269
270	fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> io::Result<()> {
271		self.0.lock().unwrap().write_fmt(args)
272	}
273}
274
275impl<W: io::Write> Instance<'_, W> {
276	pub fn read_input(&mut self) -> miette::Result<()> {
277		// (re)assemble features string
278		let features = match &self.cfg.features {
279			None => None,
280			Some(Either::Left(features)) => Some(features.clone()),
281			Some(Either::Right(features)) => Some(features.join(","))
282		};
283
284		// find the correct package within the metadata
285		let pkg = match self.pkg {
286			Some(package) => {
287				let pkg = self
288					.metadata
289					.packages
290					.iter()
291					.find(|pkg| pkg.name == package)
292					.ok_or_else(|| {
293						diagnostic::PackageNotFoundError::new(
294							package,
295							self.metadata.workspace_packages()
296						)
297					})?;
298				if !self.metadata.workspace_members.contains(&pkg.id) {
299					let code = CrateCode::new_unknown();
300					let mut printer = DiagnosticPrinter::new(&code, &mut self.stderr);
301
302					printer.print(diagnostic::PackageNotInWorkspace::new(
303						package,
304						self.metadata.workspace_packages()
305					));
306				}
307				// TODO issue warning if package not in workspace
308				pkg
309			},
310			None => self.metadata.root_package().ok_or_else(|| {
311				diagnostic::NoPackageError::new(self.metadata.workspace_packages())
312			})?
313		};
314
315		// in workspace mode, use the pkg's manifest path to make relative path's absolute
316		if self.cfg.workspace {
317			let parent = match pkg.manifest_path.parent() {
318				Some(parent) => parent,
319				None => {
320					return Err(miette!(
321						"Unable to get parent directory of {}",
322						pkg.manifest_path
323					))
324				},
325			};
326			if self.out.is_relative() {
327				self.out = parent.join(&self.out).into();
328			}
329			if let Some(template_path) = self.template_path.as_deref() {
330				if template_path.is_relative() {
331					self.template_path = Some(parent.join(template_path).into());
332				}
333			}
334		}
335
336		// find the target whose rustdoc comment we'll use.
337		// this uses a library target if exists, otherwise a binary target with the same name as the
338		// package, or otherwise the first binary target
339		let is_lib = |target: &&Target| target.is_lib() || target.is_proc_macro();
340		let is_default_bin =
341			|target: &&Target| target.is_bin() && target.name == pkg.name.as_str();
342		let target_and_type = if self.cfg.preferred_target == config::Target::Bin {
343			pkg.targets
344				.iter()
345				.find(is_default_bin)
346				.map(|target| (target, TargetType::Bin))
347				.or_else(|| {
348					pkg.targets
349						.iter()
350						.find(is_lib)
351						.map(|target| (target, TargetType::Lib))
352				})
353		} else {
354			pkg.targets
355				.iter()
356				.find(is_lib)
357				.map(|target| (target, TargetType::Lib))
358				.or_else(|| {
359					pkg.targets
360						.iter()
361						.find(is_default_bin)
362						.map(|target| (target, TargetType::Bin))
363				})
364		};
365		let (target, target_type) = target_and_type
366			.or_else(|| {
367				pkg.targets
368					.iter()
369					.find(|target| target.is_bin())
370					.map(|target| (target, TargetType::Bin))
371			})
372			.ok_or(diagnostic::NoTargetError)?;
373
374		// resolve the template
375		let default_template: &Utf8Path = "README.j2".as_ref();
376		let (template_filename, template, builtin_template) = match &self.template_path {
377			Some(template_path) => {
378				let template = fs::read_to_string(template_path.as_std_path())
379					.into_diagnostic()
380					.with_context(|| {
381						format!("Failed to read template from `{template_path}'",)
382					})?;
383				let template_path_stripped =
384					template_path.strip_prefix(env::current_dir().unwrap());
385				(
386					template_path_stripped
387						.unwrap_or(template_path)
388						.to_string()
389						.into(),
390					template.into(),
391					false
392				)
393			},
394			None if default_template.exists() => {
395				let template = fs::read_to_string(default_template)
396					.into_diagnostic()
397					.with_context(|| {
398						format!("Failed to read template from `{default_template}'",)
399					})?;
400				(default_template.as_str().into(), template.into(), false)
401			},
402			None => (
403				"<builtin-template>".into(),
404				include_str!("README.j2").into(),
405				true
406			)
407		};
408		self.template_filename = template_filename;
409		self.template = template;
410		self.builtin_template = builtin_template;
411
412		// read crate code
413		let file = &target.src_path;
414		self.code = if self.cfg.expand_macros {
415			CrateCode::read_expansion(
416				self.cfg.manifest_path.as_ref(),
417				self.pkg,
418				target,
419				features.as_deref(),
420				!self.cfg.default_features,
421				self.cfg.all_features
422			)?
423		} else {
424			CrateCode::read_from_disk(file)
425				.into_diagnostic()
426				.with_context(|| format!("Failed to read source from `{file}'"))?
427		};
428
429		// process the target
430		self.input_file = input::read_code(
431			self.metadata,
432			pkg,
433			&self.code,
434			target_type,
435			&mut self.stderr
436		)?;
437
438		Ok(())
439	}
440
441	/// Create an `io::Read` for the output.
442	///
443	/// This is used when checking the existing readme file.
444	fn out_reader(&self) -> io::Result<impl io::Read> {
445		Ok(if self.out.as_str() == "-" {
446			Either::Left(io::stdin())
447		} else {
448			Either::Right(File::open(self.out.as_std_path())?)
449		})
450	}
451
452	/// Create an `io::Write` for the output.
453	fn out_writer(&self) -> io::Result<impl io::Write> {
454		Ok(if self.out.as_str() == "-" {
455			Either::Left(io::stdout())
456		} else {
457			Either::Right(File::create(self.out.as_std_path())?)
458		})
459	}
460
461	/// Check whether this README instance is up to date. Returns `true` if it is, and
462	/// `false` if needs updating.
463	pub fn check_up2date(self) -> bool {
464		let reader = self.out_reader().into_diagnostic().with_context(|| {
465			let out_str = if self.out.as_str() == "-" {
466				"<stdin>"
467			} else {
468				self.out.as_str()
469			};
470			format!("Failed to open {out_str}")
471		});
472		let reader = match reader {
473			Ok(reader) => reader,
474			Err(err) => {
475				let mut printer = DiagnosticPrinter::new(&self.code, self.stderr);
476				printer.print(err);
477				return false;
478			}
479		};
480
481		let res = verify::check_up2date(
482			self.input_file,
483			&self.template_filename,
484			&self.template,
485			self.builtin_template,
486			reader
487		);
488		if let Err(err) = res {
489			let is_error = err.severity().is_none_or(|s| s == Severity::Error);
490			let mut printer = DiagnosticPrinter::new(&self.code, self.stderr);
491			printer.print(err);
492			return !is_error;
493		}
494
495		true
496	}
497
498	pub fn emit(self) -> miette::Result<()> {
499		let writer = self.out_writer().into_diagnostic().with_context(|| {
500			let out_str = if self.out.as_str() == "-" {
501				"<stdout>"
502			} else {
503				self.out.as_str()
504			};
505			format!("Failed to open {out_str}")
506		})?;
507		self.emit_to(writer)
508	}
509
510	pub fn emit_to(self, writer: impl io::Write) -> miette::Result<()> {
511		output::emit(
512			&self.input_file,
513			&self.template_filename,
514			&self.template,
515			self.builtin_template,
516			writer
517		)
518	}
519}