1#![forbid(unsafe_code)]
18#![cfg_attr(docsrs, feature(doc_cfg))]
20
21use proc_macro::TokenStream;
22use proc_macro2::Span;
23use quote::quote;
24use std::fs;
25use std::path::{Path, PathBuf};
26use syn::{
27 LitStr, Meta, Token, parse::Parse, parse::ParseStream, parse_macro_input,
28};
29
30struct ModuleInput {
32 paths: Vec<LitStr>,
34}
35
36impl Parse for ModuleInput {
37 fn parse(input: ParseStream) -> syn::Result<Self> {
38 let mut paths = Vec::new();
39 while !input.is_empty() {
40 paths.push(input.parse()?);
41 if !input.is_empty() {
42 input.parse::<Token![,]>()?;
43 }
44 }
45 Ok(Self { paths })
46 }
47}
48
49#[proc_macro]
122pub fn module(input: TokenStream) -> TokenStream {
123 fn inner(input: &ModuleInput) -> syn::Result<TokenStream> {
124 let base_dir = get_source_dir()?;
125
126 let docs = input
127 .paths
128 .iter()
129 .filter_map(|path_lit| {
130 let path = Path::new(&base_dir).join(path_lit.value());
131 fs::read_to_string(&path)
132 .map_err(|error| error.to_string())
133 .and_then(|content| extract_inner_docs(&content))
134 .map(|docs| if docs.is_empty() { None } else { Some(docs) })
135 .map_err(|error| {
136 syn::Error::new(
137 path_lit.span(),
138 format!("Failed to read {path:?}: {error}"),
139 )
140 })
141 .transpose()
142 })
143 .collect::<syn::Result<Vec<String>>>()?
144 .join("\n\n"); let lit = LitStr::new(&docs, Span::call_site());
147 Ok(quote! { #lit }.into())
148 }
149
150 match inner(&parse_macro_input!(input as ModuleInput)) {
151 Ok(stream) => stream,
152 Err(error) => error.to_compile_error().into(),
153 }
154}
155
156fn extract_inner_docs(content: &str) -> Result<String, String> {
162 Ok(syn::parse_file(content)
163 .map_err(|error| error.to_string())?
164 .attrs
165 .into_iter()
166 .filter_map(|attr| {
167 if attr.path().is_ident("doc")
168 && let Meta::NameValue(meta) = &attr.meta
169 && let syn::Expr::Lit(expr_lit) = &meta.value
170 && let syn::Lit::Str(lit_str) = &expr_lit.lit
171 {
172 Some(lit_str.value())
173 } else {
174 None
176 }
177 })
178 .collect::<Vec<_>>()
179 .join("\n"))
180}
181
182fn get_source_dir() -> Result<PathBuf, syn::Error> {
189 match Span::call_site()
190 .local_file()
191 .and_then(|path| path.parent().map(Path::to_path_buf))
192 {
193 Some(path) => Ok(path),
194 None => Err(syn::Error::new(
195 Span::call_site(),
196 "Could not get path to source file",
197 )),
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use assert2::assert;
205
206 #[test]
207 fn line_doc_comments() {
208 assert!(
209 extract_inner_docs(
210 r"
211//! First line
212//! Second line
213
214fn foo() {}
215"
216 )
217 .unwrap()
218 == " First line\n Second line"
219 );
220 }
221
222 #[test]
223 fn mixed_attrs() {
224 assert!(
225 extract_inner_docs(
226 r"
227//! First line
228#![forbid(unsafe_code)]
229//! Second line
230
231fn foo() {}
232"
233 )
234 .unwrap()
235 == " First line\n Second line"
236 );
237 }
238
239 #[test]
240 fn block_doc_comments() {
241 assert!(
242 extract_inner_docs(
243 r"
244/*! Block doc comment
245with multiple lines
246*/
247
248fn foo() {}
249"
250 )
251 .unwrap()
252 == " Block doc comment\nwith multiple lines\n"
253 );
254 }
255
256 #[test]
257 fn doc_attributes() {
258 assert!(
259 extract_inner_docs(
260 r#"
261#![doc = "First line"]
262#![doc = "Second line"]
263
264fn foo() {}
265"#
266 )
267 .unwrap()
268 == "First line\nSecond line"
269 );
270 }
271
272 #[test]
273 fn mixed_doc_styles() {
274 assert!(
275 extract_inner_docs(
276 r#"
277//! Line comment
278#![doc = "Attribute doc"]
279
280fn foo() {}
281"#
282 )
283 .unwrap()
284 == " Line comment\nAttribute doc"
285 );
286 }
287
288 #[test]
289 fn no_docs() {
290 assert!(extract_inner_docs("fn foo() {}\n").unwrap() == "");
291 }
292
293 #[test]
294 fn only_outer_docs_ignored() {
295 assert!(
296 extract_inner_docs(
297 r"
298/// This is an outer doc comment
299fn foo() {}
300"
301 )
302 .unwrap()
303 == ""
304 );
305 }
306
307 #[test]
308 fn realistic_module() {
309 assert!(
310 extract_inner_docs(
311 r"
312//! # Module Title
313//!
314//! This module does things.
315
316use std::io;
317
318/// Function doc
319pub fn do_thing() {}
320"
321 )
322 .unwrap()
323 == " # Module Title\n\n This module does things."
324 );
325 }
326
327 #[test]
328 fn empty_doc_lines() {
329 assert!(
330 extract_inner_docs(
331 r"
332//! First
333//!
334//! Third
335
336fn foo() {}
337"
338 )
339 .unwrap()
340 == " First\n\n Third"
341 );
342 }
343}