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 line_replacements: HashMap<String, Vec<TokenStream2>>,
28
29 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 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 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 struct Foo {
347 }
348 }
349 .into(),
350 );
351
352 assert_eq!(
353 result.to_string(),
354 quote! {
355 #[doc = crate::before_snippet!()]
369 #[doc = crate::after_snippet!()]
371 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 #[doc = crate::before_snippet!()]
419 #[doc = crate::after_snippet!()]
421 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 #[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 #[doc = crate::after_snippet!()]
480 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 struct Foo {
516 }
517 }
518 .into(),
519 );
520
521 assert_eq!(
522 result.to_string(),
523 quote! {
524 #[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 #[doc = crate::after_snippet!()]
540 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 #[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 #[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 #[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 #[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 #[doc = crate::before_snippet!()]
680 macro_rules! foo {
681 () => {
682 };
683 }
684 }
685 .to_string()
686 );
687 }
688
689 #[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}