1use 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
90fn 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 let has_content = !self.items.is_empty();
104
105 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 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 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 meta_str.contains("cfg") && meta_str.contains("test")
445 }
446
447 #[test]
448 fn test_to_syn_modscope_test_injects_cfg_test() {
449 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 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 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 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 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}