user_doc_doc_proc_macro/
lib.rs

1//! Procedural macros
2//!
3
4use doc_data::*;
5use once_cell::sync::Lazy;
6use proc_macro::TokenStream;
7use proc_macro2::{Span, TokenTree};
8use quote::quote;
9use std::sync::atomic::{AtomicUsize, Ordering};
10use strum::IntoEnumIterator;
11use syn::{
12  parse::{Parse, ParseStream},
13  parse_macro_input, parse_str, Attribute, AttributeArgs, Error, Field, Fields, FieldsNamed,
14  FieldsUnnamed, Ident, Item, ItemEnum, ItemStruct, LitStr, Path, Result, Variant, Visibility,
15};
16
17/// Number of invocations of all procedural macros defined herein
18static INVOCATIONS: AtomicUsize = AtomicUsize::new(0);
19/// Count an invocation of any procedural macro
20fn count_invocation() -> usize {
21  INVOCATIONS.fetch_add(1, Ordering::SeqCst)
22}
23
24/// Are two [Path]s likely pointing to roughly the same thing
25fn paths_eq(
26  p_0: &Path,
27  p_1: &Path,
28) -> bool {
29  format!("{}", quote! {#p_0}) == format!("{}", quote! {#p_1})
30}
31
32/// Parsing the outer attributes of any Rust item produces ParsedOuterAttrs.
33/// The item is optional.
34#[derive(Clone, Debug)]
35#[allow(dead_code)]
36struct ParsedOuterAttrs {
37  /// Optional visibility modifier
38  pub vis_opt: Option<Visibility>,
39  /// Optional outer attributes
40  pub outer_attrs: Vec<Attribute>,
41  /// The subsequent code item being declared
42  pub item_opt: Option<Item>,
43}
44impl ParsedOuterAttrs {
45  /// A default path for docs
46  #[allow(clippy::declare_interior_mutable_const)]
47  pub const DOC_PATH: Lazy<Path> =
48    Lazy::new(|| parse_str::<Path>("doc").expect("must parse doc path"));
49  /// The start of a [syn]-parsed attribute line's string literal segment
50  #[allow(dead_code)]
51  pub const DOC_ATTR_LINE_START: &'static str = "= \"";
52  /// The end of a [syn]-parsed attribute line's string literal segment
53  pub const DOC_ATTR_LINE_END: &'static str = "\"";
54  /// When joining individual doc lines, this will go between adjacent lines
55  pub const LINE_JOINER: &'static str = "\n";
56
57  /// Extract doc data into the appropriate global record
58  pub fn extract_doc_data(
59    &self,
60    _count: usize,
61  ) -> anyhow::Result<()> {
62    type AttrsCollectionType = Vec<(Option<Ident>, Vec<(HelperAttr, Attribute)>, Vec<Attribute>)>;
63    // let known_helpers = known_helpers_string_vec!();
64    let extract_attrs_and_ident_into_map_fold_fn = |attrs: &Vec<Attribute>,
65                                                    ident: &Option<Ident>,
66                                                    m: &mut AttrsCollectionType|
67     -> anyhow::Result<()> {
68      let mut helper_attrs_and_attributes: Vec<(HelperAttr, Attribute)> =
69        Vec::with_capacity(attrs.len());
70      let mut other_attrs: Vec<Attribute> = Vec::with_capacity(attrs.len());
71      for attr in attrs.iter() {
72        if let Some(_helper_attr) =
73          HelperAttr::iter().find(|helper_attr| paths_eq(&attr.path, &helper_attr.into()))
74        {
75          helper_attrs_and_attributes.push((HelperAttr::from_attribute(attr)?, attr.clone()));
76        } else {
77          other_attrs.push(attr.clone());
78        }
79      }
80      if !helper_attrs_and_attributes.is_empty() {
81        m.push((ident.clone(), helper_attrs_and_attributes, other_attrs));
82      }
83      Ok(())
84    };
85
86    let record_doc_from_attrs_collection =
87      |attrs_collection: AttrsCollectionType| -> anyhow::Result<()> {
88        let _attrs_collection_len = attrs_collection.len();
89        // std::println!("\nattrs_collection {:?}", attrs_collection );
90        for (_i, (_ident_opt, helper_attrs_and_attributes, other_attributes)) in
91          attrs_collection.iter().enumerate()
92        {
93          // std::println!("\n\nhelper_attrs_and_attributes {:?}", helper_attrs_and_attributes );
94          let helper_attrs = helper_attrs_and_attributes
95            .iter()
96            .map(|(h, _)| h.clone())
97            .collect();
98          // std::println!("\n\nhelper_attrs {:?}", helper_attrs );
99          let content: String = Self::get_outer_doc_comments_string(other_attributes);
100
101          record_doc_from_helper_attributes_and_str(
102            true, //count == INVOCATIONS.load(Ordering::SeqCst) - 1 && i == attrs_collection_len-1,  // throttle saving
103            &content,
104            &helper_attrs,
105          )?;
106        }
107        Ok(())
108      };
109
110    // Extract helper attrs
111    if let Some(ref item) = self.item_opt {
112      match item {
113        Item::Enum(ItemEnum { ref variants, .. }) => {
114          // enum can have helper attrs on its variants, so collect them for each
115          let mut attrs_collection: AttrsCollectionType = Vec::with_capacity(variants.len());
116          for Variant {
117            ref attrs,
118            ref ident,
119            ..
120          } in variants.iter()
121          {
122            extract_attrs_and_ident_into_map_fold_fn(
123              attrs,
124              &Some(ident.clone()),
125              &mut attrs_collection,
126            )?
127          }
128          // std::println!("\n\nattrs {:?}", attrs );
129
130          record_doc_from_attrs_collection(attrs_collection)
131        }
132        Item::Struct(ItemStruct {
133          /*ref attrs, ref ident,*/ ref fields,
134          ..
135        }) => {
136          // struct can have helper attrs on its fields, so collect them for each
137          match fields {
138            Fields::Named(FieldsNamed { ref named, .. }) => {
139              let mut attrs_collection: AttrsCollectionType = Vec::with_capacity(named.len());
140              for Field {
141                ref attrs,
142                ref ident,
143                ..
144              } in named.iter()
145              {
146                extract_attrs_and_ident_into_map_fold_fn(attrs, ident, &mut attrs_collection)?
147              }
148              record_doc_from_attrs_collection(attrs_collection)
149            }
150            Fields::Unnamed(FieldsUnnamed { ref unnamed, .. }) => {
151              let mut attrs_collection: AttrsCollectionType = Vec::with_capacity(unnamed.len());
152              for Field {
153                ref attrs,
154                ref ident,
155                ..
156              } in unnamed.iter()
157              {
158                extract_attrs_and_ident_into_map_fold_fn(attrs, ident, &mut attrs_collection)?
159              }
160              record_doc_from_attrs_collection(attrs_collection)
161            }
162            _ => Ok(()),
163          }
164        }
165        _ => Ok(()),
166      }
167    } else {
168      Ok(())
169    }
170  }
171
172  #[allow(dead_code)]
173  /// Get the quoted text from a doc comment string
174  pub fn extract_comment_text(s: &str) -> String {
175    if s.starts_with(Self::DOC_ATTR_LINE_START) && s.ends_with(Self::DOC_ATTR_LINE_END) {
176      s[Self::DOC_ATTR_LINE_START.len()..(s.len() - Self::DOC_ATTR_LINE_END.len())].to_string()
177    } else {
178      String::new()
179    }
180  }
181
182  #[allow(clippy::borrow_interior_mutable_const)]
183  /// Get a collection of the lines of doc comments
184  pub fn get_doc_comments_lines(outer_attrs: &[Attribute]) -> Vec<String> {
185    outer_attrs
186      .iter()
187      .filter_map(
188        |Attribute {
189           path, ref tokens, ..
190         }| {
191          if paths_eq(path, &Self::DOC_PATH) {
192            match tokens.clone().into_iter().nth(1) {
193              Some(TokenTree::Literal(literal)) => Some(
194                literal
195                  .to_string()
196                  .trim_end_matches(Self::DOC_ATTR_LINE_END)
197                  .trim_start_matches(Self::DOC_ATTR_LINE_END)
198                  .to_string(),
199              ),
200              _ => None,
201            }
202          } else {
203            None
204          }
205        },
206      )
207      .collect()
208  }
209
210  /// Get a catenated string with all the doc comments
211  pub fn get_outer_doc_comments_string(attrs: &[Attribute]) -> String {
212    Self::get_doc_comments_lines(attrs).join(ParsedOuterAttrs::LINE_JOINER)
213  }
214}
215impl Parse for ParsedOuterAttrs {
216  fn parse(input: ParseStream) -> Result<Self> {
217    // parse the outer attributes
218    Ok(Self {
219      outer_attrs: input.call(Attribute::parse_outer)?,
220      vis_opt: input.parse().ok(),
221      item_opt: input.parse().ok(),
222    })
223  }
224}
225
226#[allow(clippy::ptr_arg)]
227/// Record a Documentable specified by the given [HelperAttr]s and [str] to the global store.
228fn record_doc_from_helper_attributes_and_str(
229  do_save: bool,
230  doc_comment_string: &str,
231  helper_attributes: &Vec<HelperAttr>,
232) -> Result<()> {
233  // std::println!("helper_attributes {:#?}", helper_attributes );
234  // Decide where to insert the generated doc
235  let mut chapter_blurb_opt = None;
236  let mut name_opt = None;
237  let mut number_opt = None;
238  let mut name_path_opt = None;
239  let mut number_path_opt = None;
240  for helper_attribute in helper_attributes.iter() {
241    match helper_attribute {
242      HelperAttr::ChapterName(ref chapter_name) => {
243        name_opt = Some(chapter_name.to_string());
244      }
245      HelperAttr::ChapterBlurb(ref chapter_blurb) => {
246        chapter_blurb_opt = Some(chapter_blurb.to_string());
247      }
248      HelperAttr::ChapterNameSlug(ref chapter_names) => {
249        name_path_opt = Some(chapter_names.to_vec());
250      }
251      HelperAttr::ChapterNum(ref chapter_number) => {
252        number_opt = Some(*chapter_number);
253      }
254      HelperAttr::ChapterNumSlug(ref chapter_numbers) => {
255        number_path_opt = Some(chapter_numbers.to_vec());
256      }
257    }
258  }
259  // std::println!("name_opt {:?}, \n\tnumber_opt {:?}, \n\tname_path_opt {:?}, \n\tnumber_path_opt {:?}", name_opt, number_opt, name_path_opt, number_path_opt );
260  // Generate the new doc record
261  let generate_documentable = |name_opt: &Option<String>| -> Documentable {
262    let documentable = Documentable::Doc(
263      name_opt.as_ref().cloned().unwrap_or_default(),
264      doc_comment_string.to_string(),
265    );
266    // std::println!("generated documentable {}", documentable );
267    documentable
268  };
269  // Prepare and write to the insertion location in the global store
270  let docs = &*doc_data::DOCS;
271  let mut docs_write_lock = docs.write().expect("Must get write lock on global docs");
272  let write_res = match (number_path_opt, name_path_opt) {
273    (Some(ref path_numbers), Some(ref path_names)) => {
274      // Update name path from last slug segments
275      if name_opt.is_none() {
276        name_opt = path_names.get(path_numbers.len() - 1).cloned();
277      }
278      // Create the documentable to insert
279      let documentable = generate_documentable(&name_opt);
280      // std::println!("writing {}  {:?}", documentable, std::time::Instant::now(), );
281      // insert at given numberpath and namepath combo
282      docs_write_lock.add_path(
283        &chapter_blurb_opt,
284        &name_opt,
285        Some(documentable),
286        Some(true),
287        path_names,
288        path_numbers,
289      )
290    }
291    (Some(ref path_numbers), None) => {
292      let documentable = generate_documentable(&name_opt);
293      // std::println!("writing {}  {:?}", documentable, std::time::Instant::now(), );
294      // insert at given numberpath with default empty names
295      docs_write_lock.add_path(
296        &chapter_blurb_opt,
297        &name_opt,
298        Some(documentable),
299        Some(true),
300        &[],
301        path_numbers,
302      )
303    }
304    (None, Some(ref path_names)) => {
305      // update name path from last slug segment
306      if name_opt.is_none() {
307        name_opt = path_names.last().cloned();
308      }
309      let documentable = generate_documentable(&name_opt);
310      // std::println!("writing {}  {:?}", documentable, std::time::Instant::now(), );
311      // insert at given namepath with default autogenerated numbers
312      docs_write_lock.add_path(
313        &chapter_blurb_opt,
314        &name_opt,
315        Some(documentable),
316        Some(true),
317        path_names,
318        &[],
319      )
320    }
321    (None, None) => {
322      // insert at autogenerated number path with default empty name
323      let documentable = generate_documentable(&name_opt);
324      // std::println!("writing {}  {:?}", documentable, std::time::Instant::now(), );
325      docs_write_lock.add_entry(documentable, name_opt, number_opt, Some(true))
326    }
327  };
328  // std::println!("docs_write_lock {:?} {:?}", std::time::Instant::now(), *docs_write_lock );
329  drop(docs_write_lock);
330  match write_res {
331    Ok(()) => {
332      if do_save {
333        // std::println!("saving to default path and file", );
334        match doc_data::persist_docs() {
335          Ok(()) => Ok(()),
336          Err(doc_save_error) => Err(Error::new(
337            Span::mixed_site(),
338            format!("{:#?}", doc_save_error),
339          )),
340        }
341      } else {
342        Ok(())
343      }
344    }
345    Err(error) => Err(Error::new(Span::mixed_site(), format!("{:#?}", error))),
346  }
347}
348
349#[proc_macro_derive(
350  user_doc_item,
351  attributes(
352    chapter_blurb,
353    chapter_name,
354    chapter_name_slug,
355    chapter_num,
356    chapter_num_slug,
357  )
358)]
359/// Use this attribute macro to define user-facing documentation on a non-function item.  
360/// # Supported derive helper attributes:
361/// - `chapter_blurb`: [ChapterBlurb](doc_data::HelperAttr::ChapterBlurb) - string literal
362/// - `chapter_name`: [ChapterName](doc_data::HelperAttr::ChapterName) - string literal
363/// - `chapter_name_slug`: [ChapterNameSlug](doc_data::HelperAttr::ChapterNameSlug) - comma-separated list of string literals
364/// - `chapter_num`: [ChapterNum](doc_data::HelperAttr::ChapterNum) - integer literal
365/// - `chapter_num_slug`: [ChapterNumSlug](doc_data::HelperAttr::ChapterNumSlug) - comma-separated list of integer literals
366pub fn user_doc_item(item: TokenStream) -> TokenStream {
367  let count = count_invocation();
368  // let span: Span = item.span();
369  let parsed_outer_attrs = parse_macro_input!(item as ParsedOuterAttrs);
370  // std::println!("parsed item outer attrs: {:?}", parsed_outer_attrs );
371  // std::println!("parsed user_doc_item derive");
372  match parsed_outer_attrs.extract_doc_data(count) {
373    Ok(()) => TokenStream::new(),
374    Err(extraction_error) => Error::new(
375      Span::call_site(),
376      format!(
377        "Could not extract doc data during derive macro invocation:\n{:#?}",
378        extraction_error
379      ),
380    )
381    .into_compile_error()
382    .into(),
383  }
384}
385
386#[proc_macro_attribute]
387/// Use this attribute macro to define user-facing documentation on a function item.
388/// # Supported Attribute Arguments
389/// - `chapter_blurb`: [ChapterBlurb](doc_data::HelperAttr::ChapterBlurb) - string literal
390/// - `chapter_name`: [ChapterName](doc_data::HelperAttr::ChapterName) - string literal
391/// - `chapter_name_slug`: [ChapterNameSlug](doc_data::HelperAttr::ChapterNameSlug) - comma-separated list of string literals
392/// - `chapter_num`: [ChapterNum](doc_data::HelperAttr::ChapterNum) - integer literal
393/// - `chapter_num_slug`: [ChapterNumSlug](doc_data::HelperAttr::ChapterNumSlug) - comma-separated list of integer literals
394pub fn user_doc_fn(
395  own_attr: TokenStream,
396  item: TokenStream,
397) -> TokenStream {
398  let _count: usize = count_invocation();
399  // Copy item for output
400  let it = item.clone();
401  // Capture own helper attributes
402  let own_attribute_args = parse_macro_input!(own_attr as AttributeArgs);
403  let helper_attributes_res: Result<Vec<HelperAttr>> =
404    HelperAttr::from_attribute_args(&own_attribute_args);
405  let parsed_outer_attrs = parse_macro_input!(item as ParsedOuterAttrs);
406  // std::println!("parsed user_doc_fn attribute");
407  // A fn can only generate a single Documentable::Doc item
408  // std::println!("parsed item outer attrs: {:?}", parsed_outer_attrs.get_doc_comments_lines() );
409  match helper_attributes_res {
410    Ok(helper_attributes) => {
411      match record_doc_from_helper_attributes_and_str(
412        true, // count == INVOCATIONS.load(Ordering::SeqCst) - 1, // Throttle saving to most recent invocation
413        &ParsedOuterAttrs::get_outer_doc_comments_string(&parsed_outer_attrs.outer_attrs),
414        &helper_attributes,
415      ) {
416        Ok(()) => it,
417        Err(err) => err.into_compile_error().into(),
418      }
419    }
420    Err(err) => err.into_compile_error().into(),
421  }
422}
423
424#[proc_macro]
425/// Define a file name for compile-to-runtime persistence.  
426///
427/// - If the input is not a (non-empty) string literal, it will be ignored.  
428/// - If this macro has been invoked once during the compile, all subsequent invocations
429/// will be ignored.  
430/// - An empty TokenStream is retured
431///  
432/// Note: This entire library works by persisting data captured at compile time
433/// to a file that is then loaded at runtime at the caller's discretion. In a workspace
434/// where multiple sub-packages are capturing data, this macro ought be invoked in
435/// each sub-package to set a unique file name for said sub-package's capture data.
436pub fn set_persistence_file_name(input: TokenStream) -> TokenStream {
437  let lit_str = parse_macro_input!(input as LitStr);
438  let value = lit_str.value();
439  if !value.is_empty() {
440    if let Ok(mut custom_output_file_name_write) = CUSTOM_OUTPUT_FILE_NAME.try_write() {
441      let _ = custom_output_file_name_write.get_or_insert(value);
442      // println!("custom value set to {:#?}", custom_output_file_name_write)
443    }
444  }
445
446  TokenStream::new()
447}