Skip to main content

ryo_source/pure/to_syn/
other.rs

1//! ToSyn implementations for const, static, type alias, mod, trait, and macro items.
2
3use syn::token;
4
5use super::helpers::{ident, make_abi, try_parse_path};
6use super::{ToSyn, ToSynError};
7use crate::pure::ast::{
8    MacroDelimiter, ModScope, PureAttrMeta, PureAttribute, PureConst, PureMacro, PureMod,
9    PureStatic, PureTrait, PureTraitItem, PureTypeAlias,
10};
11
12impl ToSyn for PureConst {
13    type Output = syn::ItemConst;
14
15    fn to_syn(&self) -> Result<syn::ItemConst, ToSynError> {
16        Ok(syn::ItemConst {
17            attrs: self
18                .attrs
19                .iter()
20                .map(|a| a.to_syn())
21                .collect::<Result<Vec<_>, _>>()?,
22            vis: self.vis.to_syn()?,
23            const_token: token::Const::default(),
24            ident: ident(&self.name),
25            generics: syn::Generics::default(),
26            colon_token: token::Colon::default(),
27            ty: Box::new(self.ty.to_syn()?),
28            eq_token: token::Eq::default(),
29            expr: Box::new(
30                self.value
31                    .as_ref()
32                    .ok_or_else(|| ToSynError::MissingValue {
33                        context: format!("ItemConst '{}' must have a value", self.name),
34                    })?
35                    .to_syn()?,
36            ),
37            semi_token: token::Semi::default(),
38        })
39    }
40}
41
42impl ToSyn for PureStatic {
43    type Output = syn::ItemStatic;
44
45    fn to_syn(&self) -> Result<syn::ItemStatic, ToSynError> {
46        Ok(syn::ItemStatic {
47            attrs: self
48                .attrs
49                .iter()
50                .map(|a| a.to_syn())
51                .collect::<Result<Vec<_>, _>>()?,
52            vis: self.vis.to_syn()?,
53            static_token: token::Static::default(),
54            mutability: if self.is_mut {
55                syn::StaticMutability::Mut(token::Mut::default())
56            } else {
57                syn::StaticMutability::None
58            },
59            ident: ident(&self.name),
60            colon_token: token::Colon::default(),
61            ty: Box::new(self.ty.to_syn()?),
62            eq_token: token::Eq::default(),
63            expr: Box::new(self.value.to_syn()?),
64            semi_token: token::Semi::default(),
65        })
66    }
67}
68
69impl ToSyn for PureTypeAlias {
70    type Output = syn::ItemType;
71
72    fn to_syn(&self) -> Result<syn::ItemType, ToSynError> {
73        Ok(syn::ItemType {
74            attrs: self
75                .attrs
76                .iter()
77                .map(|a| a.to_syn())
78                .collect::<Result<Vec<_>, _>>()?,
79            vis: self.vis.to_syn()?,
80            type_token: token::Type::default(),
81            ident: ident(&self.name),
82            generics: self.generics.to_syn()?,
83            eq_token: token::Eq::default(),
84            ty: Box::new(self.ty.to_syn()?),
85            semi_token: token::Semi::default(),
86        })
87    }
88}
89
90/// Check whether a `PureAttribute` is an exact outer `#[cfg(test)]`.
91///
92/// Used by `ToSyn for PureMod` to avoid duplicate injection when the attribute
93/// is already present in `attrs` (e.g. from `make_inline_test_module`).
94fn is_cfg_test_attr(attr: &PureAttribute) -> bool {
95    attr.path == "cfg" && attr.meta == PureAttrMeta::List("test".to_string()) && !attr.is_inner
96}
97
98impl ToSyn for PureMod {
99    type Output = syn::ItemMod;
100
101    fn to_syn(&self) -> Result<syn::ItemMod, ToSynError> {
102        // Determine if this is a file module (mod foo;) or inline module (mod foo { ... })
103        let has_content = !self.items.is_empty();
104
105        // Build content from items
106        let content = if has_content {
107            let all_items: Vec<syn::Item> = self
108                .items
109                .iter()
110                .map(|i| i.to_syn())
111                .collect::<Result<Vec<_>, _>>()?;
112            Some((token::Brace::default(), all_items))
113        } else {
114            None
115        };
116
117        // For ModScope::Test, inject #[cfg(test)] at the front of attrs unless
118        // it is already present (duplicate-prevention check).
119        let mut attrs_to_emit: Vec<PureAttribute> = self.attrs.clone();
120        if self.scope == ModScope::Test && !attrs_to_emit.iter().any(is_cfg_test_attr) {
121            attrs_to_emit.insert(
122                0,
123                PureAttribute {
124                    path: "cfg".to_string(),
125                    meta: PureAttrMeta::List("test".to_string()),
126                    is_inner: false,
127                },
128            );
129        }
130
131        Ok(syn::ItemMod {
132            attrs: attrs_to_emit
133                .iter()
134                .map(|a| a.to_syn())
135                .collect::<Result<Vec<_>, _>>()?,
136            vis: self.vis.to_syn()?,
137            unsafety: None,
138            mod_token: token::Mod::default(),
139            ident: ident(&self.name),
140            content,
141            semi: if has_content {
142                None
143            } else {
144                Some(token::Semi::default())
145            },
146        })
147    }
148}
149
150impl ToSyn for PureTrait {
151    type Output = syn::ItemTrait;
152
153    fn to_syn(&self) -> Result<syn::ItemTrait, ToSynError> {
154        Ok(syn::ItemTrait {
155            attrs: self
156                .attrs
157                .iter()
158                .map(|a| a.to_syn())
159                .collect::<Result<Vec<_>, _>>()?,
160            vis: self.vis.to_syn()?,
161            unsafety: if self.is_unsafe {
162                Some(token::Unsafe::default())
163            } else {
164                None
165            },
166            auto_token: if self.is_auto {
167                Some(token::Auto::default())
168            } else {
169                None
170            },
171            restriction: None,
172            trait_token: token::Trait::default(),
173            ident: ident(&self.name),
174            generics: self.generics.to_syn()?,
175            colon_token: if self.supertraits.is_empty() {
176                None
177            } else {
178                Some(token::Colon::default())
179            },
180            supertraits: self
181                .supertraits
182                .iter()
183                .filter_map(|s| syn::parse_str::<syn::TypeParamBound>(s).ok())
184                .collect(),
185            brace_token: token::Brace::default(),
186            items: self
187                .items
188                .iter()
189                .map(|i| i.to_syn())
190                .collect::<Result<Vec<_>, _>>()?,
191        })
192    }
193}
194
195impl ToSyn for PureTraitItem {
196    type Output = syn::TraitItem;
197
198    fn to_syn(&self) -> Result<syn::TraitItem, ToSynError> {
199        Ok(match self {
200            PureTraitItem::Fn(f) => syn::TraitItem::Fn(syn::TraitItemFn {
201                attrs: f
202                    .attrs
203                    .iter()
204                    .map(|a| a.to_syn())
205                    .collect::<Result<Vec<_>, _>>()?,
206                sig: syn::Signature {
207                    constness: if f.is_const {
208                        Some(token::Const::default())
209                    } else {
210                        None
211                    },
212                    asyncness: if f.is_async {
213                        Some(token::Async::default())
214                    } else {
215                        None
216                    },
217                    unsafety: if f.is_unsafe {
218                        Some(token::Unsafe::default())
219                    } else {
220                        None
221                    },
222                    abi: f.abi.as_ref().map(|a| make_abi(a)),
223                    fn_token: token::Fn::default(),
224                    ident: ident(&f.name),
225                    generics: f.generics.to_syn()?,
226                    paren_token: token::Paren::default(),
227                    inputs: f
228                        .params
229                        .iter()
230                        .map(|p| p.to_syn())
231                        .collect::<Result<_, _>>()?,
232                    variadic: None,
233                    output: match &f.ret {
234                        Some(ty) => {
235                            syn::ReturnType::Type(token::RArrow::default(), Box::new(ty.to_syn()?))
236                        }
237                        None => syn::ReturnType::Default,
238                    },
239                },
240                default: if f.body.stmts.is_empty() {
241                    None
242                } else {
243                    Some(f.body.to_syn()?)
244                },
245                semi_token: if f.body.stmts.is_empty() {
246                    Some(token::Semi::default())
247                } else {
248                    None
249                },
250            }),
251            PureTraitItem::Const(c) => syn::TraitItem::Const(syn::TraitItemConst {
252                attrs: c
253                    .attrs
254                    .iter()
255                    .map(|a| a.to_syn())
256                    .collect::<Result<Vec<_>, _>>()?,
257                const_token: token::Const::default(),
258                ident: ident(&c.name),
259                generics: syn::Generics::default(),
260                colon_token: token::Colon::default(),
261                ty: c.ty.to_syn()?,
262                default: c
263                    .value
264                    .as_ref()
265                    .map(|v| Ok((token::Eq::default(), v.to_syn()?)))
266                    .transpose()?,
267                semi_token: token::Semi::default(),
268            }),
269            PureTraitItem::Type {
270                name,
271                bounds,
272                default,
273            } => syn::TraitItem::Type(syn::TraitItemType {
274                attrs: vec![],
275                type_token: token::Type::default(),
276                ident: ident(name),
277                generics: syn::Generics::default(),
278                colon_token: if bounds.is_empty() {
279                    None
280                } else {
281                    Some(token::Colon::default())
282                },
283                bounds: bounds
284                    .iter()
285                    .filter_map(|b| syn::parse_str::<syn::TypeParamBound>(b).ok())
286                    .collect(),
287                default: default
288                    .as_ref()
289                    .map(|ty| Ok((token::Eq::default(), ty.to_syn()?)))
290                    .transpose()?,
291                semi_token: token::Semi::default(),
292            }),
293            PureTraitItem::Other(s) => syn::parse_str(s).map_err(|e| ToSynError::Other {
294                message: format!("Failed to parse trait item '{}': {}", s, e),
295            })?,
296        })
297    }
298}
299
300impl ToSyn for PureMacro {
301    type Output = syn::ItemMacro;
302
303    fn to_syn(&self) -> Result<syn::ItemMacro, ToSynError> {
304        let tokens: proc_macro2::TokenStream =
305            self.tokens
306                .parse()
307                .map_err(|e: proc_macro2::LexError| ToSynError::Other {
308                    message: format!("Failed to parse macro tokens '{}': {}", self.tokens, e),
309                })?;
310        let ident = self
311            .name
312            .as_ref()
313            .map(|n| syn::parse_str::<syn::Ident>(n))
314            .transpose()
315            .map_err(|e| ToSynError::Other {
316                message: format!(
317                    "Failed to parse macro_rules ident '{}': {}",
318                    self.name.as_deref().unwrap_or(""),
319                    e
320                ),
321            })?;
322        Ok(syn::ItemMacro {
323            attrs: vec![],
324            ident,
325            mac: syn::Macro {
326                path: try_parse_path(&self.path)?,
327                bang_token: token::Not::default(),
328                delimiter: match self.delimiter {
329                    MacroDelimiter::Paren => syn::MacroDelimiter::Paren(token::Paren::default()),
330                    MacroDelimiter::Brace => syn::MacroDelimiter::Brace(token::Brace::default()),
331                    MacroDelimiter::Bracket => {
332                        syn::MacroDelimiter::Bracket(token::Bracket::default())
333                    }
334                },
335                tokens,
336            },
337            semi_token: Some(token::Semi::default()),
338        })
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345    use crate::pure::ast::{PureExpr, PureGenerics, PureType, PureVis};
346    use crate::pure::convert::ToPure;
347    use quote::ToTokens;
348
349    #[test]
350    fn test_pure_const() {
351        let c = PureConst {
352            attrs: vec![],
353            vis: PureVis::Public,
354            name: "MAX".to_string(),
355            ty: PureType::Path("i32".to_string()),
356            value: Some(PureExpr::Lit("100".to_string())),
357        };
358        let syn_const = c.to_syn().unwrap();
359        let output = syn_const.to_token_stream().to_string();
360        assert!(output.contains("const"), "Output: {}", output);
361        assert!(output.contains("MAX"), "Output: {}", output);
362        assert!(output.contains("100"), "Output: {}", output);
363    }
364
365    #[test]
366    fn test_pure_static() {
367        let s = PureStatic {
368            attrs: vec![],
369            vis: PureVis::Private,
370            is_mut: true,
371            name: "COUNTER".to_string(),
372            ty: PureType::Path("i32".to_string()),
373            value: PureExpr::Lit("0".to_string()),
374        };
375        let syn_static = s.to_syn().unwrap();
376        let output = syn_static.to_token_stream().to_string();
377        assert!(output.contains("static"), "Output: {}", output);
378        assert!(output.contains("mut"), "Output: {}", output);
379        assert!(output.contains("COUNTER"), "Output: {}", output);
380    }
381
382    #[test]
383    fn test_pure_type_alias() {
384        let t = PureTypeAlias {
385            attrs: vec![],
386            vis: PureVis::Public,
387            name: "Result".to_string(),
388            generics: PureGenerics::default(),
389            ty: PureType::Path("std::result::Result<T, Error>".to_string()),
390        };
391        let syn_type = t.to_syn().unwrap();
392        let output = syn_type.to_token_stream().to_string();
393        assert!(output.contains("type"), "Output: {}", output);
394        assert!(output.contains("Result"), "Output: {}", output);
395    }
396
397    #[test]
398    fn test_pure_mod_declaration() {
399        let m = PureMod {
400            attrs: vec![],
401            vis: PureVis::Public,
402            name: "utils".to_string(),
403            items: vec![],
404            scope: Default::default(),
405        };
406        let syn_mod = m.to_syn().unwrap();
407        let output = syn_mod.to_token_stream().to_string();
408        assert!(output.contains("mod"), "Output: {}", output);
409        assert!(output.contains("utils"), "Output: {}", output);
410    }
411
412    #[test]
413    fn test_pure_trait_simple() {
414        let t = PureTrait {
415            attrs: vec![],
416            vis: PureVis::Public,
417            is_unsafe: false,
418            is_auto: false,
419            name: "Drawable".to_string(),
420            generics: PureGenerics::default(),
421            supertraits: vec![],
422            items: vec![],
423        };
424        let syn_trait = t.to_syn().unwrap();
425        let output = syn_trait.to_token_stream().to_string();
426        assert!(output.contains("trait"), "Output: {}", output);
427        assert!(output.contains("Drawable"), "Output: {}", output);
428    }
429
430    // --- ModScope injection tests ---
431
432    /// Check that a syn::Attribute is an outer #[cfg(test)].
433    fn is_syn_cfg_test(attr: &syn::Attribute) -> bool {
434        use quote::ToTokens;
435        let path = attr.path().to_token_stream().to_string();
436        if path != "cfg" {
437            return false;
438        }
439        if !matches!(attr.style, syn::AttrStyle::Outer) {
440            return false;
441        }
442        let meta_str = attr.to_token_stream().to_string();
443        // token stream: "# [cfg (test)]"  — check for cfg + test presence
444        meta_str.contains("cfg") && meta_str.contains("test")
445    }
446
447    #[test]
448    fn test_to_syn_modscope_test_injects_cfg_test() {
449        // ModScope::Test + empty attrs → emitted syn::ItemMod.attrs has #[cfg(test)]
450        let m = PureMod {
451            attrs: vec![],
452            vis: PureVis::Private,
453            name: "tests".to_string(),
454            items: vec![],
455            scope: ModScope::Test,
456        };
457        let syn_mod = m.to_syn().unwrap();
458        assert!(
459            syn_mod.attrs.iter().any(is_syn_cfg_test),
460            "expected #[cfg(test)] attr in syn_mod.attrs, got {} attrs",
461            syn_mod.attrs.len()
462        );
463    }
464
465    #[test]
466    fn test_to_syn_modscope_test_no_duplicate_cfg() {
467        // ModScope::Test + attrs already has #[cfg(test)] → syn output has exactly one cfg(test)
468        let m = PureMod {
469            attrs: vec![PureAttribute {
470                path: "cfg".to_string(),
471                meta: PureAttrMeta::List("test".to_string()),
472                is_inner: false,
473            }],
474            vis: PureVis::Private,
475            name: "tests".to_string(),
476            items: vec![],
477            scope: ModScope::Test,
478        };
479        let syn_mod = m.to_syn().unwrap();
480        let cfg_test_count = syn_mod.attrs.iter().filter(|a| is_syn_cfg_test(a)).count();
481        assert_eq!(
482            cfg_test_count, 1,
483            "#[cfg(test)] should appear exactly once in attrs, got {} times",
484            cfg_test_count
485        );
486    }
487
488    #[test]
489    fn test_to_syn_modscope_prod_no_cfg_injection() {
490        // ModScope::Prod → no cfg(test) injected into attrs
491        let m = PureMod {
492            attrs: vec![],
493            vis: PureVis::Public,
494            name: "utils".to_string(),
495            items: vec![],
496            scope: ModScope::Prod,
497        };
498        let syn_mod = m.to_syn().unwrap();
499        assert!(
500            !syn_mod.attrs.iter().any(is_syn_cfg_test),
501            "Prod scope should not inject #[cfg(test)]"
502        );
503    }
504
505    #[test]
506    fn test_to_syn_modscope_test_roundtrip_via_parse() {
507        // Full round-trip via parse: #[cfg(test)] mod tests {} → to_pure → to_syn
508        let item_mod = syn::parse_str::<syn::ItemMod>("#[cfg(test)] mod tests {}").unwrap();
509        let pure_mod = item_mod.to_pure();
510        assert_eq!(pure_mod.scope, ModScope::Test);
511        assert!(
512            pure_mod.attrs.is_empty(),
513            "cfg(test) should be removed from attrs at parse"
514        );
515
516        let syn_out = pure_mod.to_syn().unwrap();
517        // Exactly one #[cfg(test)] attribute after round-trip
518        let cfg_test_count = syn_out.attrs.iter().filter(|a| is_syn_cfg_test(a)).count();
519        assert_eq!(
520            cfg_test_count, 1,
521            "#[cfg(test)] should appear exactly once after round-trip, got {} times",
522            cfg_test_count
523        );
524    }
525}