documented_derive/
lib.rs

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/// Derive proc-macro for `Documented` trait.
19///
20/// # Example
21///
22/// ```rust
23/// use documented::Documented;
24///
25/// /// Nice.
26/// /// Multiple single-line doc comments are supported.
27/// ///
28/// /** Multi-line doc comments are supported too.
29///     Each line of the multi-line block is individually trimmed by default.
30///     Note the lack of spaces in front of this line.
31/// */
32/// #[doc = "Attribute-style documentation is supported too."]
33/// #[derive(Documented)]
34/// struct BornIn69;
35///
36/// let doc_str = "Nice.
37/// Multiple single-line doc comments are supported.
38///
39/// Multi-line doc comments are supported too.
40/// Each line of the multi-line block is individually trimmed by default.
41/// Note the lack of spaces in front of this line.
42///
43/// Attribute-style documentation is supported too.";
44/// assert_eq!(BornIn69::DOCS, doc_str);
45/// ```
46///
47/// # Configuration
48///
49/// With the `customise` feature enabled, you can customise this macro's
50/// behaviour using the `#[documented(...)]` attribute.
51///
52/// Currently, you can disable line-trimming like so:
53///
54/// ```rust
55/// # use documented::Documented;
56/// ///     Terrible.
57/// #[derive(Documented)]
58/// #[documented(trim = false)]
59/// struct Frankly;
60///
61/// assert_eq!(Frankly::DOCS, "     Terrible.");
62/// ```
63///
64/// If there are other configuration options you wish to have, please submit an
65/// issue or a PR.
66#[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/// Derive proc-macro for `DocumentedFields` trait.
106///
107/// # Example
108///
109/// ```rust
110/// use documented::DocumentedFields;
111///
112/// #[derive(DocumentedFields)]
113/// struct BornIn69 {
114///     /// Cry like a grandmaster.
115///     rawr: String,
116///     explosive: usize,
117/// };
118///
119/// assert_eq!(BornIn69::FIELD_DOCS, [Some("Cry like a grandmaster."), None]);
120/// ```
121///
122/// You can also use [`get_field_docs`](Self::get_field_docs) to access the
123/// fields' documentation using their names.
124///
125/// ```rust
126/// # use documented::{DocumentedFields, Error};
127/// #
128/// # #[derive(DocumentedFields)]
129/// # struct BornIn69 {
130/// #     /// Cry like a grandmaster.
131/// #     rawr: String,
132/// #     explosive: usize,
133/// # };
134/// #
135/// assert_eq!(BornIn69::get_field_docs("rawr"), Ok("Cry like a grandmaster."));
136/// assert_eq!(
137///     BornIn69::get_field_docs("explosive"),
138///     Err(Error::NoDocComments("explosive".to_string()))
139/// );
140/// assert_eq!(
141///     BornIn69::get_field_docs("gotcha"),
142///     Err(Error::NoSuchField("gotcha".to_string()))
143/// );
144/// ```
145///
146/// # Configuration
147///
148/// With the `customise` feature enabled, you can customise this macro's
149/// behaviour using the `#[documented_fields(...)]` attribute. Note that this
150/// attribute works on both the container and each individual field, with the
151/// per-field configurations overriding container configurations, which
152/// override the default.
153///
154/// Currently, you can (selectively) disable line-trimming like so:
155///
156/// ```rust
157/// # use documented::DocumentedFields;
158/// #[derive(DocumentedFields)]
159/// #[documented_fields(trim = false)]
160/// struct Frankly {
161///     ///     Delicious.
162///     perrier: usize,
163///     ///     I'm vegan.
164///     #[documented_fields(trim = true)]
165///     fried_liver: bool,
166/// }
167///
168/// assert_eq!(Frankly::FIELD_DOCS, [Some("     Delicious."), Some("I'm vegan.")]);
169/// ```
170///
171/// If there are other configuration options you wish to have, please
172/// submit an issue or a PR.
173#[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    // `#[documented_fields(...)]` on container type
185    #[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    // quote macro needs some help with `Option`s
232    // see: https://github.com/dtolnay/quote/issues/213
233    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/// Derive proc-macro for `DocumentedVariants` trait.
269///
270/// # Example
271///
272/// ```rust
273/// use documented::{DocumentedVariants, Error};
274///
275/// #[derive(DocumentedVariants)]
276/// enum NeverPlay {
277///     F3,
278///     /// I fell out of my chair.
279///     F6,
280/// }
281///
282/// assert_eq!(
283///     NeverPlay::F3.get_variant_docs(),
284///     Err(Error::NoDocComments("F3".into()))
285/// );
286/// assert_eq!(
287///     NeverPlay::F6.get_variant_docs(),
288///     Ok("I fell out of my chair.")
289/// );
290/// ```
291///
292/// # Configuration
293///
294/// With the `customise` feature enabled, you can customise this macro's
295/// behaviour using the `#[documented_variants(...)]` attribute. Note that this
296/// attribute works on both the container and each individual variant, with the
297/// per-variant configurations overriding container configurations, which
298/// override the default.
299///
300/// Currently, you can (selectively) disable line-trimming like so:
301///
302/// ```rust
303/// # use documented::DocumentedVariants;
304/// #[derive(DocumentedVariants)]
305/// #[documented_variants(trim = false)]
306/// enum Always {
307///     ///     Or the quality.
308///     SacTheExchange,
309///     ///     Like a Frenchman.
310///     #[documented_variants(trim = true)]
311///     Retreat,
312/// }
313/// assert_eq!(
314///     Always::SacTheExchange.get_variant_docs(),
315///     Ok("     Or the quality.")
316/// );
317/// assert_eq!(Always::Retreat.get_variant_docs(), Ok("Like a Frenchman."));
318/// ```
319///
320/// If there are other configuration options you wish to have, please
321/// submit an issue or a PR.
322#[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    // `#[documented_variants(...)]` on container type
334    #[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(), // this targets the `struct`/`union` keyword
347                "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    // IDEA: I'd like to use phf here, but it doesn't seem to be possible at the moment,
394    // because there isn't a way to get an enum's discriminant at compile time
395    // if this becomes possible in the future, or alternatively you have a good workaround,
396    // improvement suggestions are more than welcomed
397    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}