1mod config;
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{
6 parse_macro_input, parse_quote, spanned::Spanned, Attribute, Data, DataEnum, DataStruct,
7 DataUnion, DeriveInput, Error, Expr, ExprLit, Fields, Ident, Lit, Meta, Path,
8};
9
10#[cfg(feature = "customise")]
11use crate::config::get_config_customisations;
12use crate::config::Config;
13
14fn crate_module_path() -> Path {
15 parse_quote!(::documented)
16}
17
18#[cfg_attr(not(feature = "customise"), proc_macro_derive(Documented))]
67#[cfg_attr(
68 feature = "customise",
69 proc_macro_derive(Documented, attributes(documented))
70)]
71pub fn documented(input: TokenStream) -> TokenStream {
72 let input = parse_macro_input!(input as DeriveInput);
73
74 let ident = &input.ident;
75 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
76
77 #[cfg(not(feature = "customise"))]
78 let config = Config::default();
79 #[cfg(feature = "customise")]
80 let config = match get_config_customisations(&input.attrs, "documented") {
81 Ok(Some(customisations)) => Config::default().with_customisations(customisations),
82 Ok(None) => Config::default(),
83 Err(err) => return err.into_compile_error().into(),
84 };
85
86 let docs = match get_docs(&input.attrs, &config) {
87 Ok(Some(doc)) => doc,
88 Ok(None) => {
89 return Error::new(input.ident.span(), "Missing doc comments")
90 .into_compile_error()
91 .into()
92 }
93 Err(e) => return e.into_compile_error().into(),
94 };
95
96 quote! {
97 #[automatically_derived]
98 impl #impl_generics documented::Documented for #ident #ty_generics #where_clause {
99 const DOCS: &'static str = #docs;
100 }
101 }
102 .into()
103}
104
105#[cfg_attr(not(feature = "customise"), proc_macro_derive(DocumentedFields))]
174#[cfg_attr(
175 feature = "customise",
176 proc_macro_derive(DocumentedFields, attributes(documented_fields))
177)]
178pub fn documented_fields(input: TokenStream) -> TokenStream {
179 let input = parse_macro_input!(input as DeriveInput);
180
181 let ident = &input.ident;
182 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
183
184 #[cfg(not(feature = "customise"))]
186 let base_config = Config::default();
187 #[cfg(feature = "customise")]
188 let base_config = match get_config_customisations(&input.attrs, "documented_fields") {
189 Ok(Some(customisations)) => Config::default().with_customisations(customisations),
190 Ok(None) => Config::default(),
191 Err(err) => return err.into_compile_error().into(),
192 };
193
194 let (field_idents, field_docs): (Vec<_>, Vec<_>) = {
195 let fields_attrs: Vec<(Option<Ident>, Vec<Attribute>)> = match input.data.clone() {
196 Data::Enum(DataEnum { variants, .. }) => variants
197 .into_iter()
198 .map(|v| (Some(v.ident), v.attrs))
199 .collect(),
200 Data::Struct(DataStruct { fields, .. }) => {
201 fields.into_iter().map(|f| (f.ident, f.attrs)).collect()
202 }
203 Data::Union(DataUnion { fields, .. }) => fields
204 .named
205 .into_iter()
206 .map(|f| (f.ident, f.attrs))
207 .collect(),
208 };
209
210 match fields_attrs
211 .into_iter()
212 .map(|(i, attrs)| {
213 #[cfg(not(feature = "customise"))]
214 let config = base_config;
215 #[cfg(feature = "customise")]
216 let config =
217 if let Some(c) = get_config_customisations(&attrs, "documented_fields")? {
218 base_config.with_customisations(c)
219 } else {
220 base_config
221 };
222 get_docs(&attrs, &config).map(|d| (i, d))
223 })
224 .collect::<syn::Result<Vec<_>>>()
225 {
226 Ok(t) => t.into_iter().unzip(),
227 Err(e) => return e.into_compile_error().into(),
228 }
229 };
230
231 let field_docs_tokenised: Vec<_> = field_docs
234 .into_iter()
235 .map(|opt| match opt {
236 Some(c) => quote! { Some(#c) },
237 None => quote! { None },
238 })
239 .collect();
240
241 let phf_match_arms: Vec<_> = field_idents
242 .into_iter()
243 .enumerate()
244 .filter_map(|(i, o)| o.map(|ident| (i, ident.to_string())))
245 .map(|(i, ident)| quote! { #ident => #i, })
246 .collect();
247
248 let documented_module_path = crate_module_path();
249
250 quote! {
251 #[automatically_derived]
252 impl #impl_generics documented::DocumentedFields for #ident #ty_generics #where_clause {
253 const FIELD_DOCS: &'static [Option<&'static str>] = &[#(#field_docs_tokenised),*];
254
255 fn __documented_get_index<__Documented_T: AsRef<str>>(field_name: __Documented_T) -> Option<usize> {
256 use #documented_module_path::_private_phf_reexport_for_macro as phf;
257
258 static PHF: phf::Map<&'static str, usize> = phf::phf_map! {
259 #(#phf_match_arms)*
260 };
261 PHF.get(field_name.as_ref()).copied()
262 }
263 }
264 }
265 .into()
266}
267
268#[cfg_attr(not(feature = "customise"), proc_macro_derive(DocumentedVariants))]
323#[cfg_attr(
324 feature = "customise",
325 proc_macro_derive(DocumentedVariants, attributes(documented_variants))
326)]
327pub fn documented_variants(input: TokenStream) -> TokenStream {
328 let input = parse_macro_input!(input as DeriveInput);
329
330 let ident = &input.ident;
331 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
332
333 #[cfg(not(feature = "customise"))]
335 let base_config = Config::default();
336 #[cfg(feature = "customise")]
337 let base_config = match get_config_customisations(&input.attrs, "documented_variants") {
338 Ok(Some(customisations)) => Config::default().with_customisations(customisations),
339 Ok(None) => Config::default(),
340 Err(err) => return err.into_compile_error().into(),
341 };
342
343 let variants_docs = {
344 let Data::Enum(DataEnum { variants, .. }) = input.data else {
345 return Error::new(
346 input.span(), "DocumentedVariants can only be used on enums.\n\
348 For structs and unions, use DocumentedFields instead.",
349 )
350 .into_compile_error()
351 .into();
352 };
353 match variants
354 .into_iter()
355 .map(|v| (v.ident, v.fields, v.attrs))
356 .map(|(i, f, attrs)| {
357 #[cfg(not(feature = "customise"))]
358 let config = base_config;
359 #[cfg(feature = "customise")]
360 let config =
361 if let Some(c) = get_config_customisations(&attrs, "documented_variants")? {
362 base_config.with_customisations(c)
363 } else {
364 base_config
365 };
366 get_docs(&attrs, &config).map(|d| (i, f, d))
367 })
368 .collect::<syn::Result<Vec<_>>>()
369 {
370 Ok(t) => t,
371 Err(e) => return e.into_compile_error().into(),
372 }
373 };
374
375 let match_arms: Vec<_> = variants_docs
376 .into_iter()
377 .map(|(ident, fields, docs)| {
378 let pat = match fields {
379 Fields::Unit => quote! { Self::#ident },
380 Fields::Unnamed(_) => quote! { Self::#ident(..) },
381 Fields::Named(_) => quote! { Self::#ident{..} },
382 };
383 match docs {
384 Some(docs_str) => quote! { #pat => Ok(#docs_str), },
385 None => {
386 let ident_str = ident.to_string();
387 quote! { #pat => Err(documented::Error::NoDocComments(#ident_str.into())), }
388 }
389 }
390 })
391 .collect();
392
393 quote! {
398 #[automatically_derived]
399 impl #impl_generics documented::DocumentedVariants for #ident #ty_generics #where_clause {
400 fn get_variant_docs(&self) -> Result<&'static str, documented::Error> {
401 match self {
402 #(#match_arms)*
403 }
404 }
405 }
406 }
407 .into()
408}
409
410fn get_docs(attrs: &[Attribute], config: &Config) -> syn::Result<Option<String>> {
411 let string_literals = attrs
412 .iter()
413 .filter_map(|attr| match attr.meta {
414 Meta::NameValue(ref name_value) if name_value.path.is_ident("doc") => {
415 Some(&name_value.value)
416 }
417 _ => None,
418 })
419 .map(|expr| match expr {
420 Expr::Lit(ExprLit { lit: Lit::Str(s), .. }) => Ok(s.value()),
421 other => Err(Error::new(
422 other.span(),
423 "Doc comment is not a string literal",
424 )),
425 })
426 .collect::<Result<Vec<_>, _>>()?;
427
428 if string_literals.is_empty() {
429 return Ok(None);
430 }
431
432 let docs = if config.trim {
433 string_literals
434 .iter()
435 .flat_map(|lit| lit.split('\n').collect::<Vec<_>>())
436 .map(|line| line.trim().to_string())
437 .collect::<Vec<_>>()
438 .join("\n")
439 } else {
440 string_literals.join("\n")
441 };
442
443 Ok(Some(docs))
444}