1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3extern crate proc_macro;
4
5mod logic;
6mod tokenmanip;
7mod imports {
8 pub(crate) use proc_macro::{
9 Delimiter, Group, Ident as I, Literal, Punct, Spacing, Span, TokenStream as TS,
10 TokenTree as TT,
11 };
12}
13
14use std::{
15 borrow::Cow,
16 fs::File,
17 io::{BufRead, BufReader},
18 path::PathBuf,
19};
20use {imports::*, logic::*, tokenmanip::*};
21
22type MResult<T = TS> = Result<T, Error>;
23
24struct Error {
25 msg: Cow<'static, str>,
26 span: Span,
27}
28impl Error {
29 fn new_static(msg: &'static str, span: Span) -> Self {
30 Self {
31 msg: Cow::Borrowed(msg),
32 span,
33 }
34 }
35 fn new_owned(msg: String, span: Span) -> Self {
36 Self {
37 msg: Cow::Owned(msg),
38 span,
39 }
40 }
41}
42
43#[proc_macro]
48pub fn include_doctest(input: TS) -> TS {
49 macro_main(input).unwrap_or_else(compile_error)
50}
51
52struct Input {
53 filename: PathBuf,
54 filename_span: Span,
55}
56
57fn parse_input(input: TS) -> MResult<Input> {
58 let mut input = input.into_iter();
59 let Some(literal) = input.next() else {
60 return Err(Error::new_static(
61 "expected filename, found empty parameter list",
62 Span::call_site(),
63 ));
64 };
65 let lspan = literal.span();
66 let TT::Literal(literal) = literal else {
67 return Err(Error::new_owned(
68 format!("expected literal, found \"{literal}\""),
69 lspan,
70 ));
71 };
72
73 Ok(Input {
74 filename: PathBuf::from(parse_literal(literal)?),
75 filename_span: lspan,
76 })
77}
78
79fn macro_main(input: TS) -> MResult {
80 let input = parse_input(input)?;
81 let mut path = if input.filename.is_relative() {
83 std::env::var_os("CARGO_MANIFEST_DIR")
84 .map(PathBuf::from)
85 .ok_or_else(|| {
86 Error::new_static(
87 "the CARGO_MANIFEST_DIR environment variable is not set",
88 Span::call_site(),
89 )
90 })?
91 } else {
92 PathBuf::new()
93 };
94 path.push(&input.filename);
95
96 let fln = input.filename.display();
97 let ioe = |m, e| {
98 Error::new_owned(
99 format!("I/O error (file {fln}) {m}: {e}"),
100 input.filename_span,
101 )
102 };
103 let file = File::open(path).map_err(|e| ioe("could not open", e))?;
104
105 let lines = BufReader::new(file)
106 .lines()
107 .map(|rslt| rslt.map_err(|e| ioe("read failed", e)));
108
109 let mut pass1 = Pass1::new(lines);
110 let mut lines_pass2 = Vec::with_capacity(256);
111 for rslt in &mut pass1 {
112 let t = rslt?;
113 lines_pass2.push(t);
114 }
115
116 let mut docstring = String::with_capacity(pass1.total_length());
117 let dedent = pass1.min_indent();
118 for (line, visible) in lines_pass2 {
119 if visible {
120 docstring.push(' ');
123 let indent = indent_of(&line);
124 for _ in 0..indent.saturating_sub(dedent) {
125 docstring.push(' ');
126 }
127 docstring.push_str(line.trim_start());
128 } else {
129 docstring.push_str("# ");
130 docstring.push_str(&line);
131 }
132 docstring.push('\n');
133 }
134
135 while docstring.ends_with('\n') {
136 docstring.pop();
137 }
138
139 Ok(TT::Literal(Literal::string(&docstring)).into())
140}