use syn::token;
use super::helpers::{ident, make_abi, try_parse_path};
use super::{ToSyn, ToSynError};
use crate::pure::ast::{
MacroDelimiter, ModScope, PureAttrMeta, PureAttribute, PureConst, PureMacro, PureMod,
PureStatic, PureTrait, PureTraitItem, PureTypeAlias,
};
impl ToSyn for PureConst {
type Output = syn::ItemConst;
fn to_syn(&self) -> Result<syn::ItemConst, ToSynError> {
Ok(syn::ItemConst {
attrs: self
.attrs
.iter()
.map(|a| a.to_syn())
.collect::<Result<Vec<_>, _>>()?,
vis: self.vis.to_syn()?,
const_token: token::Const::default(),
ident: ident(&self.name),
generics: syn::Generics::default(),
colon_token: token::Colon::default(),
ty: Box::new(self.ty.to_syn()?),
eq_token: token::Eq::default(),
expr: Box::new(
self.value
.as_ref()
.ok_or_else(|| ToSynError::MissingValue {
context: format!("ItemConst '{}' must have a value", self.name),
})?
.to_syn()?,
),
semi_token: token::Semi::default(),
})
}
}
impl ToSyn for PureStatic {
type Output = syn::ItemStatic;
fn to_syn(&self) -> Result<syn::ItemStatic, ToSynError> {
Ok(syn::ItemStatic {
attrs: self
.attrs
.iter()
.map(|a| a.to_syn())
.collect::<Result<Vec<_>, _>>()?,
vis: self.vis.to_syn()?,
static_token: token::Static::default(),
mutability: if self.is_mut {
syn::StaticMutability::Mut(token::Mut::default())
} else {
syn::StaticMutability::None
},
ident: ident(&self.name),
colon_token: token::Colon::default(),
ty: Box::new(self.ty.to_syn()?),
eq_token: token::Eq::default(),
expr: Box::new(self.value.to_syn()?),
semi_token: token::Semi::default(),
})
}
}
impl ToSyn for PureTypeAlias {
type Output = syn::ItemType;
fn to_syn(&self) -> Result<syn::ItemType, ToSynError> {
Ok(syn::ItemType {
attrs: self
.attrs
.iter()
.map(|a| a.to_syn())
.collect::<Result<Vec<_>, _>>()?,
vis: self.vis.to_syn()?,
type_token: token::Type::default(),
ident: ident(&self.name),
generics: self.generics.to_syn()?,
eq_token: token::Eq::default(),
ty: Box::new(self.ty.to_syn()?),
semi_token: token::Semi::default(),
})
}
}
fn is_cfg_test_attr(attr: &PureAttribute) -> bool {
attr.path == "cfg" && attr.meta == PureAttrMeta::List("test".to_string()) && !attr.is_inner
}
impl ToSyn for PureMod {
type Output = syn::ItemMod;
fn to_syn(&self) -> Result<syn::ItemMod, ToSynError> {
let has_content = !self.items.is_empty();
let content = if has_content {
let all_items: Vec<syn::Item> = self
.items
.iter()
.map(|i| i.to_syn())
.collect::<Result<Vec<_>, _>>()?;
Some((token::Brace::default(), all_items))
} else {
None
};
let mut attrs_to_emit: Vec<PureAttribute> = self.attrs.clone();
if self.scope == ModScope::Test && !attrs_to_emit.iter().any(is_cfg_test_attr) {
attrs_to_emit.insert(
0,
PureAttribute {
path: "cfg".to_string(),
meta: PureAttrMeta::List("test".to_string()),
is_inner: false,
},
);
}
Ok(syn::ItemMod {
attrs: attrs_to_emit
.iter()
.map(|a| a.to_syn())
.collect::<Result<Vec<_>, _>>()?,
vis: self.vis.to_syn()?,
unsafety: None,
mod_token: token::Mod::default(),
ident: ident(&self.name),
content,
semi: if has_content {
None
} else {
Some(token::Semi::default())
},
})
}
}
impl ToSyn for PureTrait {
type Output = syn::ItemTrait;
fn to_syn(&self) -> Result<syn::ItemTrait, ToSynError> {
Ok(syn::ItemTrait {
attrs: self
.attrs
.iter()
.map(|a| a.to_syn())
.collect::<Result<Vec<_>, _>>()?,
vis: self.vis.to_syn()?,
unsafety: if self.is_unsafe {
Some(token::Unsafe::default())
} else {
None
},
auto_token: if self.is_auto {
Some(token::Auto::default())
} else {
None
},
restriction: None,
trait_token: token::Trait::default(),
ident: ident(&self.name),
generics: self.generics.to_syn()?,
colon_token: if self.supertraits.is_empty() {
None
} else {
Some(token::Colon::default())
},
supertraits: self
.supertraits
.iter()
.filter_map(|s| syn::parse_str::<syn::TypeParamBound>(s).ok())
.collect(),
brace_token: token::Brace::default(),
items: self
.items
.iter()
.map(|i| i.to_syn())
.collect::<Result<Vec<_>, _>>()?,
})
}
}
impl ToSyn for PureTraitItem {
type Output = syn::TraitItem;
fn to_syn(&self) -> Result<syn::TraitItem, ToSynError> {
Ok(match self {
PureTraitItem::Fn(f) => syn::TraitItem::Fn(syn::TraitItemFn {
attrs: f
.attrs
.iter()
.map(|a| a.to_syn())
.collect::<Result<Vec<_>, _>>()?,
sig: syn::Signature {
constness: if f.is_const {
Some(token::Const::default())
} else {
None
},
asyncness: if f.is_async {
Some(token::Async::default())
} else {
None
},
unsafety: if f.is_unsafe {
Some(token::Unsafe::default())
} else {
None
},
abi: f.abi.as_ref().map(|a| make_abi(a)),
fn_token: token::Fn::default(),
ident: ident(&f.name),
generics: f.generics.to_syn()?,
paren_token: token::Paren::default(),
inputs: f
.params
.iter()
.map(|p| p.to_syn())
.collect::<Result<_, _>>()?,
variadic: None,
output: match &f.ret {
Some(ty) => {
syn::ReturnType::Type(token::RArrow::default(), Box::new(ty.to_syn()?))
}
None => syn::ReturnType::Default,
},
},
default: if f.body.stmts.is_empty() {
None
} else {
Some(f.body.to_syn()?)
},
semi_token: if f.body.stmts.is_empty() {
Some(token::Semi::default())
} else {
None
},
}),
PureTraitItem::Const(c) => syn::TraitItem::Const(syn::TraitItemConst {
attrs: c
.attrs
.iter()
.map(|a| a.to_syn())
.collect::<Result<Vec<_>, _>>()?,
const_token: token::Const::default(),
ident: ident(&c.name),
generics: syn::Generics::default(),
colon_token: token::Colon::default(),
ty: c.ty.to_syn()?,
default: c
.value
.as_ref()
.map(|v| Ok((token::Eq::default(), v.to_syn()?)))
.transpose()?,
semi_token: token::Semi::default(),
}),
PureTraitItem::Type {
name,
bounds,
default,
} => syn::TraitItem::Type(syn::TraitItemType {
attrs: vec![],
type_token: token::Type::default(),
ident: ident(name),
generics: syn::Generics::default(),
colon_token: if bounds.is_empty() {
None
} else {
Some(token::Colon::default())
},
bounds: bounds
.iter()
.filter_map(|b| syn::parse_str::<syn::TypeParamBound>(b).ok())
.collect(),
default: default
.as_ref()
.map(|ty| Ok((token::Eq::default(), ty.to_syn()?)))
.transpose()?,
semi_token: token::Semi::default(),
}),
PureTraitItem::Other(s) => syn::parse_str(s).map_err(|e| ToSynError::Other {
message: format!("Failed to parse trait item '{}': {}", s, e),
})?,
})
}
}
impl ToSyn for PureMacro {
type Output = syn::ItemMacro;
fn to_syn(&self) -> Result<syn::ItemMacro, ToSynError> {
let tokens: proc_macro2::TokenStream =
self.tokens
.parse()
.map_err(|e: proc_macro2::LexError| ToSynError::Other {
message: format!("Failed to parse macro tokens '{}': {}", self.tokens, e),
})?;
let ident = self
.name
.as_ref()
.map(|n| syn::parse_str::<syn::Ident>(n))
.transpose()
.map_err(|e| ToSynError::Other {
message: format!(
"Failed to parse macro_rules ident '{}': {}",
self.name.as_deref().unwrap_or(""),
e
),
})?;
Ok(syn::ItemMacro {
attrs: vec![],
ident,
mac: syn::Macro {
path: try_parse_path(&self.path)?,
bang_token: token::Not::default(),
delimiter: match self.delimiter {
MacroDelimiter::Paren => syn::MacroDelimiter::Paren(token::Paren::default()),
MacroDelimiter::Brace => syn::MacroDelimiter::Brace(token::Brace::default()),
MacroDelimiter::Bracket => {
syn::MacroDelimiter::Bracket(token::Bracket::default())
}
},
tokens,
},
semi_token: Some(token::Semi::default()),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pure::ast::{PureExpr, PureGenerics, PureType, PureVis};
use crate::pure::convert::ToPure;
use quote::ToTokens;
#[test]
fn test_pure_const() {
let c = PureConst {
attrs: vec![],
vis: PureVis::Public,
name: "MAX".to_string(),
ty: PureType::Path("i32".to_string()),
value: Some(PureExpr::Lit("100".to_string())),
};
let syn_const = c.to_syn().unwrap();
let output = syn_const.to_token_stream().to_string();
assert!(output.contains("const"), "Output: {}", output);
assert!(output.contains("MAX"), "Output: {}", output);
assert!(output.contains("100"), "Output: {}", output);
}
#[test]
fn test_pure_static() {
let s = PureStatic {
attrs: vec![],
vis: PureVis::Private,
is_mut: true,
name: "COUNTER".to_string(),
ty: PureType::Path("i32".to_string()),
value: PureExpr::Lit("0".to_string()),
};
let syn_static = s.to_syn().unwrap();
let output = syn_static.to_token_stream().to_string();
assert!(output.contains("static"), "Output: {}", output);
assert!(output.contains("mut"), "Output: {}", output);
assert!(output.contains("COUNTER"), "Output: {}", output);
}
#[test]
fn test_pure_type_alias() {
let t = PureTypeAlias {
attrs: vec![],
vis: PureVis::Public,
name: "Result".to_string(),
generics: PureGenerics::default(),
ty: PureType::Path("std::result::Result<T, Error>".to_string()),
};
let syn_type = t.to_syn().unwrap();
let output = syn_type.to_token_stream().to_string();
assert!(output.contains("type"), "Output: {}", output);
assert!(output.contains("Result"), "Output: {}", output);
}
#[test]
fn test_pure_mod_declaration() {
let m = PureMod {
attrs: vec![],
vis: PureVis::Public,
name: "utils".to_string(),
items: vec![],
scope: Default::default(),
};
let syn_mod = m.to_syn().unwrap();
let output = syn_mod.to_token_stream().to_string();
assert!(output.contains("mod"), "Output: {}", output);
assert!(output.contains("utils"), "Output: {}", output);
}
#[test]
fn test_pure_trait_simple() {
let t = PureTrait {
attrs: vec![],
vis: PureVis::Public,
is_unsafe: false,
is_auto: false,
name: "Drawable".to_string(),
generics: PureGenerics::default(),
supertraits: vec![],
items: vec![],
};
let syn_trait = t.to_syn().unwrap();
let output = syn_trait.to_token_stream().to_string();
assert!(output.contains("trait"), "Output: {}", output);
assert!(output.contains("Drawable"), "Output: {}", output);
}
fn is_syn_cfg_test(attr: &syn::Attribute) -> bool {
use quote::ToTokens;
let path = attr.path().to_token_stream().to_string();
if path != "cfg" {
return false;
}
if !matches!(attr.style, syn::AttrStyle::Outer) {
return false;
}
let meta_str = attr.to_token_stream().to_string();
meta_str.contains("cfg") && meta_str.contains("test")
}
#[test]
fn test_to_syn_modscope_test_injects_cfg_test() {
let m = PureMod {
attrs: vec![],
vis: PureVis::Private,
name: "tests".to_string(),
items: vec![],
scope: ModScope::Test,
};
let syn_mod = m.to_syn().unwrap();
assert!(
syn_mod.attrs.iter().any(is_syn_cfg_test),
"expected #[cfg(test)] attr in syn_mod.attrs, got {} attrs",
syn_mod.attrs.len()
);
}
#[test]
fn test_to_syn_modscope_test_no_duplicate_cfg() {
let m = PureMod {
attrs: vec![PureAttribute {
path: "cfg".to_string(),
meta: PureAttrMeta::List("test".to_string()),
is_inner: false,
}],
vis: PureVis::Private,
name: "tests".to_string(),
items: vec![],
scope: ModScope::Test,
};
let syn_mod = m.to_syn().unwrap();
let cfg_test_count = syn_mod.attrs.iter().filter(|a| is_syn_cfg_test(a)).count();
assert_eq!(
cfg_test_count, 1,
"#[cfg(test)] should appear exactly once in attrs, got {} times",
cfg_test_count
);
}
#[test]
fn test_to_syn_modscope_prod_no_cfg_injection() {
let m = PureMod {
attrs: vec![],
vis: PureVis::Public,
name: "utils".to_string(),
items: vec![],
scope: ModScope::Prod,
};
let syn_mod = m.to_syn().unwrap();
assert!(
!syn_mod.attrs.iter().any(is_syn_cfg_test),
"Prod scope should not inject #[cfg(test)]"
);
}
#[test]
fn test_to_syn_modscope_test_roundtrip_via_parse() {
let item_mod = syn::parse_str::<syn::ItemMod>("#[cfg(test)] mod tests {}").unwrap();
let pure_mod = item_mod.to_pure();
assert_eq!(pure_mod.scope, ModScope::Test);
assert!(
pure_mod.attrs.is_empty(),
"cfg(test) should be removed from attrs at parse"
);
let syn_out = pure_mod.to_syn().unwrap();
let cfg_test_count = syn_out.attrs.iter().filter(|a| is_syn_cfg_test(a)).count();
assert_eq!(
cfg_test_count, 1,
"#[cfg(test)] should appear exactly once after round-trip, got {} times",
cfg_test_count
);
}
}