Skip to main content

esp_hal_procmacros/
doc_replace.rs

1use std::{collections::HashMap, str::FromStr};
2
3use proc_macro2::{TokenStream, TokenStream as TokenStream2};
4use quote::quote;
5use syn::{
6    AttrStyle,
7    Attribute,
8    Expr,
9    ExprLit,
10    Item,
11    Lit,
12    LitStr,
13    Meta,
14    MetaNameValue,
15    Token,
16    braced,
17    parse::Parse,
18    punctuated::Punctuated,
19    spanned::Spanned,
20    token,
21};
22
23struct Replacements {
24    // Placeholder => [attribute contents]
25    //
26    // Replaces `# {tag}` placeholders with the attribute contents. Replaces the entire line.
27    line_replacements: HashMap<String, Vec<TokenStream2>>,
28
29    // Placeholder => [(condition, string contents)], may be unconditional.
30    //
31    // Replaces `__tag__` placeholders with the string contents. Replaces only the placeholder
32    // inside the line, and applies the condition to the entire line if present.
33    inline_replacements: HashMap<String, Vec<(Option<TokenStream>, String)>>,
34}
35
36impl Replacements {
37    fn get(&self, lines: &str, outer: bool, span: proc_macro2::Span) -> Vec<Attribute> {
38        let mut attributes = vec![];
39        for line in lines.split('\n') {
40            let mut attrs = vec![];
41            let trimmed = line.trim();
42            if let Some(lines) = self.line_replacements.get(trimmed) {
43                for line in lines {
44                    if outer {
45                        attrs.push(syn::parse_quote_spanned! { span => #[ #line ] });
46                    } else {
47                        attrs.push(syn::parse_quote_spanned! { span => #![ #line ] });
48                    }
49                }
50            } else if let Some((placeholder, replacements)) = self
51                .inline_replacements
52                .iter()
53                .find(|(k, _v)| trimmed.contains(k.as_str()))
54            {
55                for (cfg, replacement) in replacements.iter() {
56                    let line = line.replace(placeholder, replacement);
57                    let line = create_raw_string(&line);
58                    let attr_inner = if let Some(condition) = cfg {
59                        quote! { cfg_attr(#condition, doc = #line) }
60                    } else {
61                        quote! { doc = #line }
62                    };
63                    if outer {
64                        attrs.push(syn::parse_quote_spanned! { span => #[ #attr_inner ] });
65                    } else {
66                        attrs.push(syn::parse_quote_spanned! { span => #![ #attr_inner ] });
67                    }
68                }
69            } else {
70                // Just append the line, in the expected format (`doc = r" Foobar"`)
71                let line = create_raw_string(line);
72                if outer {
73                    attrs.push(syn::parse_quote_spanned! { span => #[doc = #line] });
74                } else {
75                    attrs.push(syn::parse_quote_spanned! { span => #![doc = #line] });
76                }
77            }
78            attributes.extend(attrs);
79        }
80
81        attributes
82    }
83}
84
85fn create_raw_string(line: &str) -> TokenStream2 {
86    let hash = if line.contains("#\"") {
87        "##"
88    } else if line.contains('"') {
89        "#"
90    } else {
91        ""
92    };
93
94    TokenStream2::from_str(&format!("r{hash}\"{line}\"{hash}")).unwrap()
95}
96
97impl Parse for Replacements {
98    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
99        let mut line_replacements = HashMap::new();
100        let mut inline_replacements = HashMap::new();
101
102        let mut add_line_replacement = |placeholder: &str, replacement: Vec<TokenStream2>| {
103            line_replacements.insert(format!("# {{{placeholder}}}"), replacement);
104        };
105        let mut add_inline_replacement =
106            |placeholder: &str, replacement: Vec<(Option<TokenStream2>, String)>| {
107                // The placeholder must be a valid Rust identifier to keep rustfmt happy
108                inline_replacements.insert(format!("__{placeholder}__"), replacement);
109            };
110
111        if !input.is_empty() {
112            let args = Punctuated::<Replacement, Token![,]>::parse_terminated(input)?;
113            for arg in args {
114                match arg.replacement {
115                    ReplacementKind::Literal(expr) => {
116                        if let Expr::Lit(ExprLit {
117                            lit: Lit::Str(ref lit_str),
118                            ..
119                        }) = expr
120                        {
121                            add_inline_replacement(&arg.placeholder, vec![(None, lit_str.value())]);
122                        }
123
124                        add_line_replacement(
125                            &arg.placeholder,
126                            vec![quote! {
127                                doc = #expr
128                            }],
129                        );
130                    }
131                    ReplacementKind::Choice(items) => {
132                        let mut conditions = vec![];
133                        let mut bodies = vec![];
134                        let mut lit_strs = vec![];
135                        let mut cfgs = vec![];
136
137                        for branch in items {
138                            let body = branch.body;
139
140                            if let Expr::Lit(ExprLit {
141                                lit: Lit::Str(ref lit_str),
142                                ..
143                            }) = body
144                            {
145                                lit_strs.push(lit_str.value());
146                            }
147
148                            match branch.condition {
149                                Some(Meta::List(cfg)) if cfg.path.is_ident("cfg") => {
150                                    let condition = cfg.tokens;
151
152                                    cfgs.push(condition.clone());
153                                    conditions.push(condition);
154                                    bodies.push(body);
155                                }
156                                None => {
157                                    conditions.push(quote! { not(any( #(#cfgs),*) ) });
158                                    bodies.push(body);
159                                }
160                                _ => {
161                                    return Err(syn::Error::new(
162                                        branch.condition.span(),
163                                        "Expected a cfg condition or catch-all condition using `_`",
164                                    ));
165                                }
166                            }
167                        }
168
169                        let branches = conditions
170                            .iter()
171                            .zip(bodies.iter())
172                            .map(|(condition, body)| {
173                                quote! {
174                                    cfg_attr(#condition, doc = #body)
175                                }
176                            })
177                            .collect::<Vec<_>>();
178                        add_line_replacement(&arg.placeholder, branches);
179
180                        if lit_strs.len() == bodies.len() {
181                            let branches = conditions
182                                .into_iter()
183                                .map(Some)
184                                .zip(lit_strs)
185                                .collect::<Vec<_>>();
186                            add_inline_replacement(&arg.placeholder, branches);
187                        }
188                    }
189                }
190            }
191        }
192
193        Ok(Self {
194            line_replacements,
195            inline_replacements,
196        })
197    }
198}
199
200struct Replacement {
201    placeholder: String,
202    replacement: ReplacementKind,
203}
204
205impl Parse for Replacement {
206    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
207        let placeholder: LitStr = input.parse()?;
208        let _arrow: Token![=>] = input.parse()?;
209        let replacement: ReplacementKind = input.parse()?;
210
211        Ok(Self {
212            placeholder: placeholder.value(),
213            replacement,
214        })
215    }
216}
217
218enum ReplacementKind {
219    Literal(Expr),
220    Choice(Punctuated<Branch, Token![,]>),
221}
222
223impl Parse for ReplacementKind {
224    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
225        if !input.peek(token::Brace) {
226            return input.parse().map(Self::Literal);
227        }
228
229        let choices;
230        braced!(choices in input);
231
232        let branches = Punctuated::<Branch, Token![,]>::parse_terminated(&choices)?;
233
234        Ok(Self::Choice(branches))
235    }
236}
237
238struct Branch {
239    condition: Option<Meta>,
240    body: Expr,
241}
242
243impl Parse for Branch {
244    fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
245        let condition: Option<Meta> = if input.parse::<Token![_]>().is_ok() {
246            None
247        } else {
248            Some(input.parse()?)
249        };
250
251        input.parse::<syn::Token![=>]>()?;
252
253        let body: Expr = input.parse()?;
254
255        Ok(Branch { condition, body })
256    }
257}
258
259pub(crate) fn replace(attr: TokenStream, input: TokenStream) -> TokenStream {
260    let mut replacements: Replacements = match syn::parse2(attr) {
261        Ok(replacements) => replacements,
262        Err(e) => return e.into_compile_error(),
263    };
264
265    replacements.line_replacements.insert(
266        "# {before_snippet}".to_string(),
267        vec![quote! { doc = crate::before_snippet!() }],
268    );
269    replacements.line_replacements.insert(
270        "# {after_snippet}".to_string(),
271        vec![quote! { doc = crate::after_snippet!() }],
272    );
273
274    let mut item: Item = crate::unwrap_or_compile_error!(syn::parse2(input));
275
276    let mut replacement_attrs = Vec::new();
277
278    let attrs = item.attrs_mut();
279    for attr in attrs {
280        if let Meta::NameValue(MetaNameValue { path, value, .. }) = &attr.meta
281            && let Some(ident) = path.get_ident()
282            && ident == "doc"
283            && let Expr::Lit(lit) = value
284            && let Lit::Str(doc) = &lit.lit
285        {
286            let replacement = replacements.get(
287                doc.value().as_str(),
288                attr.style == AttrStyle::Outer,
289                attr.span(),
290            );
291            replacement_attrs.extend(replacement);
292        } else {
293            replacement_attrs.push(attr.clone());
294        }
295    }
296
297    *item.attrs_mut() = replacement_attrs;
298
299    quote! { #item }
300}
301
302trait ItemLike {
303    fn attrs_mut(&mut self) -> &mut Vec<syn::Attribute>;
304}
305
306impl ItemLike for Item {
307    fn attrs_mut(&mut self) -> &mut Vec<syn::Attribute> {
308        match self {
309            Item::Fn(item_fn) => &mut item_fn.attrs,
310            Item::Struct(item_struct) => &mut item_struct.attrs,
311            Item::Enum(item_enum) => &mut item_enum.attrs,
312            Item::Trait(item_trait) => &mut item_trait.attrs,
313            Item::Mod(module) => &mut module.attrs,
314            Item::Macro(item_macro) => &mut item_macro.attrs,
315            _ => panic!("Unsupported item type for switch macro"),
316        }
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn test_basic() {
326        let result = replace(
327            quote! {}.into(),
328            quote! {
329                /// # Configuration
330                ///
331                /// ## Overview
332                ///
333                /// This module contains the initial configuration for the system.
334                /// ## Configuration
335                /// In the [`esp_hal::init()`][crate::init] method, we can configure different
336                /// parameters for the system:
337                /// - CPU clock configuration.
338                /// - Watchdog configuration.
339                /// ## Examples
340                /// ### Default initialization
341                /// ```rust, no_run
342                /// # {before_snippet}
343                /// let peripherals = esp_hal::init(esp_hal::Config::default());
344                /// # {after_snippet}
345                /// ```
346                struct Foo {
347                }
348            }
349            .into(),
350        );
351
352        assert_eq!(
353            result.to_string(),
354            quote! {
355                /// # Configuration
356                ///
357                /// ## Overview
358                ///
359                /// This module contains the initial configuration for the system.
360                /// ## Configuration
361                /// In the [`esp_hal::init()`][crate::init] method, we can configure different
362                /// parameters for the system:
363                /// - CPU clock configuration.
364                /// - Watchdog configuration.
365                /// ## Examples
366                /// ### Default initialization
367                /// ```rust, no_run
368                #[doc = crate::before_snippet!()]
369                /// let peripherals = esp_hal::init(esp_hal::Config::default());
370                #[doc = crate::after_snippet!()]
371                /// ```
372                struct Foo {}
373            }
374            .to_string()
375        );
376    }
377
378    #[test]
379    fn test_one_doc_attr() {
380        let result = replace(
381            quote! {}.into(),
382            quote! {
383                #[doc = r#" # Configuration
384 ## Overview
385 This module contains the initial configuration for the system.
386 ## Configuration
387 In the [`esp_hal::init()`][crate::init] method, we can configure different
388 parameters for the system:
389 - CPU clock configuration.
390 - Watchdog configuration.
391 ## Examples
392 ### Default initialization
393 ```rust, no_run
394 # {before_snippet}
395 let peripherals = esp_hal::init(esp_hal::Config::default());
396 # {after_snippet}
397 ```"#]
398                struct Foo {
399                }
400            }
401            .into(),
402        );
403
404        assert_eq!(
405            result.to_string(),
406            quote! {
407                /// # Configuration
408                /// ## Overview
409                /// This module contains the initial configuration for the system.
410                /// ## Configuration
411                /// In the [`esp_hal::init()`][crate::init] method, we can configure different
412                /// parameters for the system:
413                /// - CPU clock configuration.
414                /// - Watchdog configuration.
415                /// ## Examples
416                /// ### Default initialization
417                /// ```rust, no_run
418                #[doc = crate::before_snippet!()]
419                /// let peripherals = esp_hal::init(esp_hal::Config::default());
420                #[doc = crate::after_snippet!()]
421                /// ```
422                struct Foo {}
423            }
424            .to_string()
425        );
426    }
427
428    #[test]
429    fn test_custom_replacements() {
430        let result = replace(
431            quote! {
432                "freq" => {
433                    cfg(esp32h2) => "let freq = Rate::from_mhz(32);",
434                    _ => "let freq = Rate::from_mhz(80);"
435                },
436                "other" => "replacement"
437            }.into(),
438            quote! {
439                #[doc = " # Configuration"]
440                #[doc = " ## Overview"]
441                #[doc = " This module contains the initial configuration for the system."]
442                #[doc = " ## Configuration"]
443                #[doc = " In the [`esp_hal::init()`][crate::init] method, we can configure different"]
444                #[doc = " parameters for the system:"]
445                #[doc = " - CPU clock configuration."]
446                #[doc = " - Watchdog configuration."]
447                #[doc = " ## Examples"]
448                #[doc = " ### Default initialization"]
449                #[doc = " ```rust, no_run"]
450                #[doc = " # {freq}"]
451                #[doc = " # {before_snippet}"]
452                #[doc = " let peripherals = esp_hal::init(esp_hal::Config::default());"]
453                #[doc = " # {after_snippet}"]
454                #[doc = " ```"]
455                struct Foo {
456                }
457            }
458            .into(),
459        );
460
461        assert_eq!(
462            result.to_string(),
463            quote! {
464                /// # Configuration
465                /// ## Overview
466                /// This module contains the initial configuration for the system.
467                /// ## Configuration
468                /// In the [`esp_hal::init()`][crate::init] method, we can configure different
469                /// parameters for the system:
470                /// - CPU clock configuration.
471                /// - Watchdog configuration.
472                /// ## Examples
473                /// ### Default initialization
474                /// ```rust, no_run
475                #[cfg_attr (esp32h2 , doc = "let freq = Rate::from_mhz(32);")]
476                #[cfg_attr (not (any (esp32h2)) , doc = "let freq = Rate::from_mhz(80);")]
477                #[doc = crate::before_snippet!()]
478                /// let peripherals = esp_hal::init(esp_hal::Config::default());
479                #[doc = crate::after_snippet!()]
480                /// ```
481                struct Foo {}
482            }
483            .to_string()
484        );
485    }
486
487    #[test]
488    fn test_custom_inline_replacements() {
489        let result = replace(
490            quote! {
491                "freq" => {
492                    cfg(esp32h2) => "32",
493                    _ => "80"
494                },
495                "other" => "Replacement"
496            }
497            .into(),
498            quote! {
499                /// # Configuration
500                /// ## Overview
501                /// This module contains the initial configuration for the system.
502                /// ## Configuration
503                /// In the [`esp_hal::init()`][crate::init] method, we can configure different
504                /// parameters for the system:
505                /// - CPU clock configuration.
506                /// - Watchdog configuration.
507                /// ## __other__ Examples
508                /// ### Default initialization
509                /// ```rust, no_run
510                /// let freq = Rate::from_mhz(__freq__);
511                /// # {before_snippet}
512                /// let peripherals = esp_hal::init(esp_hal::Config::default());
513                /// # {after_snippet}
514                /// ```
515                struct Foo {
516                }
517            }
518            .into(),
519        );
520
521        assert_eq!(
522            result.to_string(),
523            quote! {
524                /// # Configuration
525                /// ## Overview
526                /// This module contains the initial configuration for the system.
527                /// ## Configuration
528                /// In the [`esp_hal::init()`][crate::init] method, we can configure different
529                /// parameters for the system:
530                /// - CPU clock configuration.
531                /// - Watchdog configuration.
532                /// ## Replacement Examples
533                /// ### Default initialization
534                /// ```rust, no_run
535                #[cfg_attr (esp32h2 , doc = r" let freq = Rate::from_mhz(32);")]
536                #[cfg_attr (not (any (esp32h2)) , doc = r" let freq = Rate::from_mhz(80);")]
537                #[doc = crate::before_snippet!()]
538                /// let peripherals = esp_hal::init(esp_hal::Config::default());
539                #[doc = crate::after_snippet!()]
540                /// ```
541                struct Foo {}
542            }
543            .to_string()
544        );
545    }
546
547    #[test]
548    fn test_custom_fail() {
549        let result = replace(
550            quote! {
551                "freq" => {
552                    abc(esp32h2) => "let freq = Rate::from_mhz(32);",
553                },
554            }
555            .into(),
556            quote! {}.into(),
557        );
558
559        assert_eq!(result.to_string(), quote! {
560            ::core::compile_error!{ "Expected a cfg condition or catch-all condition using `_`" }
561        }.to_string());
562    }
563
564    #[test]
565    fn test_basic_fn() {
566        let result = replace(
567            quote! {}.into(),
568            quote! {
569                #[doc = " docs"]
570                #[doc = " # {before_snippet}"]
571                fn foo() {
572                }
573            }
574            .into(),
575        );
576
577        assert_eq!(
578            result.to_string(),
579            quote! {
580                /// docs
581                #[doc = crate::before_snippet!()]
582                fn foo () { }
583            }
584            .to_string()
585        );
586    }
587
588    #[test]
589    fn test_basic_enum() {
590        let result = replace(
591            quote! {}.into(),
592            quote! {
593                #[doc = " docs"]
594                #[doc = " # {before_snippet}"]
595                enum Foo {
596                }
597            }
598            .into(),
599        );
600
601        assert_eq!(
602            result.to_string(),
603            quote! {
604                /// docs
605                #[doc = crate::before_snippet!()]
606                enum Foo { }
607            }
608            .to_string()
609        );
610    }
611
612    #[test]
613    fn test_basic_trait() {
614        let result = replace(
615            quote! {}.into(),
616            quote! {
617                #[doc = " docs"]
618                #[doc = " # {before_snippet}"]
619                trait Foo {
620                }
621            }
622            .into(),
623        );
624
625        assert_eq!(
626            result.to_string(),
627            quote! {
628                /// docs
629                #[doc = crate::before_snippet!()]
630                trait Foo { }
631            }
632            .to_string()
633        );
634    }
635
636    #[test]
637    fn test_basic_mod() {
638        let result = replace(
639            quote! {}.into(),
640            quote! {
641                #[doc = " docs"]
642                #[doc = " # {before_snippet}"]
643                mod foo {
644                }
645            }
646            .into(),
647        );
648
649        assert_eq!(
650            result.to_string(),
651            quote! {
652                /// docs
653                #[doc = crate::before_snippet!()]
654                mod foo { }
655            }
656            .to_string()
657        );
658    }
659
660    #[test]
661    fn test_basic_macro() {
662        let result = replace(
663            quote! {}.into(),
664            quote! {
665                #[doc = " docs"]
666                #[doc = " # {before_snippet}"]
667                macro_rules! foo {
668                    () => {
669                    };
670                }
671            }
672            .into(),
673        );
674
675        assert_eq!(
676            result.to_string(),
677            quote! {
678                /// docs
679                #[doc = crate::before_snippet!()]
680                macro_rules! foo {
681                    () => {
682                    };
683                }
684            }
685            .to_string()
686        );
687    }
688
689    // TODO panicking is not the nicest way to handle this
690    #[test]
691    #[should_panic]
692    fn test_basic_fail_wrong_item() {
693        replace(
694            quote! {}.into(),
695            quote! {
696                #[doc = " docs"]
697                #[doc = " # {before_snippet}"]
698                static FOO: u32 = 0u32;
699            }
700            .into(),
701        );
702    }
703}