1use 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
17static INVOCATIONS: AtomicUsize = AtomicUsize::new(0);
19fn count_invocation() -> usize {
21 INVOCATIONS.fetch_add(1, Ordering::SeqCst)
22}
23
24fn paths_eq(
26 p_0: &Path,
27 p_1: &Path,
28) -> bool {
29 format!("{}", quote! {#p_0}) == format!("{}", quote! {#p_1})
30}
31
32#[derive(Clone, Debug)]
35#[allow(dead_code)]
36struct ParsedOuterAttrs {
37 pub vis_opt: Option<Visibility>,
39 pub outer_attrs: Vec<Attribute>,
41 pub item_opt: Option<Item>,
43}
44impl ParsedOuterAttrs {
45 #[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 #[allow(dead_code)]
51 pub const DOC_ATTR_LINE_START: &'static str = "= \"";
52 pub const DOC_ATTR_LINE_END: &'static str = "\"";
54 pub const LINE_JOINER: &'static str = "\n";
56
57 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 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 for (_i, (_ident_opt, helper_attrs_and_attributes, other_attributes)) in
91 attrs_collection.iter().enumerate()
92 {
93 let helper_attrs = helper_attrs_and_attributes
95 .iter()
96 .map(|(h, _)| h.clone())
97 .collect();
98 let content: String = Self::get_outer_doc_comments_string(other_attributes);
100
101 record_doc_from_helper_attributes_and_str(
102 true, &content,
104 &helper_attrs,
105 )?;
106 }
107 Ok(())
108 };
109
110 if let Some(ref item) = self.item_opt {
112 match item {
113 Item::Enum(ItemEnum { ref variants, .. }) => {
114 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 record_doc_from_attrs_collection(attrs_collection)
131 }
132 Item::Struct(ItemStruct {
133 ref fields,
134 ..
135 }) => {
136 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 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 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 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 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)]
227fn record_doc_from_helper_attributes_and_str(
229 do_save: bool,
230 doc_comment_string: &str,
231 helper_attributes: &Vec<HelperAttr>,
232) -> Result<()> {
233 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 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 documentable
268 };
269 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 if name_opt.is_none() {
276 name_opt = path_names.get(path_numbers.len() - 1).cloned();
277 }
278 let documentable = generate_documentable(&name_opt);
280 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 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 if name_opt.is_none() {
307 name_opt = path_names.last().cloned();
308 }
309 let documentable = generate_documentable(&name_opt);
310 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 let documentable = generate_documentable(&name_opt);
324 docs_write_lock.add_entry(documentable, name_opt, number_opt, Some(true))
326 }
327 };
328 drop(docs_write_lock);
330 match write_res {
331 Ok(()) => {
332 if do_save {
333 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)]
359pub fn user_doc_item(item: TokenStream) -> TokenStream {
367 let count = count_invocation();
368 let parsed_outer_attrs = parse_macro_input!(item as ParsedOuterAttrs);
370 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]
387pub fn user_doc_fn(
395 own_attr: TokenStream,
396 item: TokenStream,
397) -> TokenStream {
398 let _count: usize = count_invocation();
399 let it = item.clone();
401 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 match helper_attributes_res {
410 Ok(helper_attributes) => {
411 match record_doc_from_helper_attributes_and_str(
412 true, &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]
425pub 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 }
444 }
445
446 TokenStream::new()
447}