Skip to main content

ryo_source/pure/
convert.rs

1//! Conversion between syn types and Pure types.
2
3use quote::ToTokens;
4
5use super::ast::*;
6use crate::SourceResult;
7
8/// Trait for converting syn types to pure types.
9pub trait ToPure {
10    /// Pure AST output type produced from `self`.
11    type Output;
12    /// Convert `self` into its pure-AST representation.
13    fn to_pure(&self) -> Self::Output;
14}
15
16/// Convert syn::Abi to string (e.g., "C", "Rust", "system").
17fn abi_to_string(abi: &syn::Abi) -> String {
18    abi.name
19        .as_ref()
20        .map(|lit| lit.value())
21        .unwrap_or_else(|| "C".to_string()) // `extern fn` without explicit ABI defaults to "C"
22}
23
24/// Convert syn::Label to string (e.g., "'outer").
25fn label_to_string(label: &syn::Label) -> String {
26    label.name.ident.to_string()
27}
28
29/// Infer if a return type represents an async function from `#[async_trait]` or similar macros.
30///
31/// Detects patterns like:
32/// - `Pin<Box<dyn Future<Output = T> + Send>>`
33/// - `Pin<Box<dyn Future<Output = T>>>`
34///
35/// This is needed because `#[async_trait]` transforms `async fn` into regular `fn`
36/// with a `Pin<Box<dyn Future<...>>>` return type at the syn parsing stage.
37fn infer_async_from_return_type(ret: &syn::ReturnType) -> bool {
38    let ty = match ret {
39        syn::ReturnType::Default => return false,
40        syn::ReturnType::Type(_, ty) => ty,
41    };
42
43    // Convert to string for pattern matching
44    let ty_str = ty.to_token_stream().to_string();
45
46    // Check for Pin<Box<dyn Future<...>>> pattern
47    // The string representation includes spaces, e.g.:
48    // "Pin < Box < dyn Future < Output = Result < ... > > + Send > >"
49    is_pinned_boxed_future(&ty_str)
50}
51
52/// Check if a type string represents `Pin<Box<dyn Future<...>>>`.
53fn is_pinned_boxed_future(ty_str: &str) -> bool {
54    // Remove all whitespace for easier matching
55    let normalized: String = ty_str.chars().filter(|c| !c.is_whitespace()).collect();
56
57    // Check for the pattern: Pin<Box<dyn Future<...>>>
58    // Variants:
59    // - Pin<Box<dyn Future<Output=T>>>
60    // - Pin<Box<dyn Future<Output=T>+Send>>
61    // - Pin<Box<dyn Future<Output=T>+Send+'static>>
62    normalized.starts_with("Pin<Box<dynFuture<")
63        || normalized.starts_with("::core::pin::Pin<Box<dynFuture<")
64        || normalized.starts_with("core::pin::Pin<Box<dynFuture<")
65        || normalized.starts_with("std::pin::Pin<Box<dynFuture<")
66        || normalized.starts_with("::std::pin::Pin<Box<dynFuture<")
67}
68
69impl PureFile {
70    /// Parse source code directly into PureFile.
71    pub fn from_source(source: &str) -> SourceResult<Self> {
72        let file = syn::parse_file(source)?;
73        Ok(file.to_pure())
74    }
75}
76
77impl ToPure for syn::File {
78    type Output = PureFile;
79
80    fn to_pure(&self) -> PureFile {
81        PureFile {
82            attrs: self.attrs.iter().map(|a| a.to_pure()).collect(),
83            items: self.items.iter().map(|i| i.to_pure()).collect(),
84        }
85    }
86}
87
88impl ToPure for syn::Attribute {
89    type Output = PureAttribute;
90
91    fn to_pure(&self) -> PureAttribute {
92        let meta = match &self.meta {
93            syn::Meta::Path(_) => PureAttrMeta::Path,
94            syn::Meta::List(list) => {
95                // Extract just the tokens inside the parentheses
96                PureAttrMeta::List(list.tokens.to_string())
97            }
98            syn::Meta::NameValue(nv) => {
99                // Extract the value part
100                PureAttrMeta::NameValue(nv.value.to_token_stream().to_string())
101            }
102        };
103
104        PureAttribute {
105            path: path_to_string(self.path()),
106            meta,
107            is_inner: matches!(self.style, syn::AttrStyle::Inner(_)),
108        }
109    }
110}
111
112impl ToPure for syn::Item {
113    type Output = PureItem;
114
115    fn to_pure(&self) -> PureItem {
116        match self {
117            syn::Item::Use(u) => PureItem::Use(u.to_pure()),
118            syn::Item::Fn(f) => PureItem::Fn(f.to_pure()),
119            syn::Item::Struct(s) => PureItem::Struct(s.to_pure()),
120            syn::Item::Enum(e) => PureItem::Enum(e.to_pure()),
121            syn::Item::Impl(i) => PureItem::Impl(i.to_pure()),
122            syn::Item::Const(c) => PureItem::Const(c.to_pure()),
123            syn::Item::Static(s) => PureItem::Static(s.to_pure()),
124            syn::Item::Type(t) => PureItem::Type(t.to_pure()),
125            syn::Item::Mod(m) => PureItem::Mod(m.to_pure()),
126            syn::Item::Trait(t) => PureItem::Trait(t.to_pure()),
127            syn::Item::Macro(m) => PureItem::Macro(m.to_pure()),
128            other => PureItem::Other(other.to_token_stream().to_string()),
129        }
130    }
131}
132
133impl ToPure for syn::ItemUse {
134    type Output = PureUse;
135
136    fn to_pure(&self) -> PureUse {
137        PureUse {
138            vis: self.vis.to_pure(),
139            tree: self.tree.to_pure(),
140        }
141    }
142}
143
144impl ToPure for syn::UseTree {
145    type Output = PureUseTree;
146
147    fn to_pure(&self) -> PureUseTree {
148        match self {
149            syn::UseTree::Path(p) => PureUseTree::Path {
150                path: p.ident.to_string(),
151                tree: Box::new(p.tree.to_pure()),
152            },
153            syn::UseTree::Name(n) => PureUseTree::Name(n.ident.to_string()),
154            syn::UseTree::Rename(r) => PureUseTree::Rename {
155                name: r.ident.to_string(),
156                rename: r.rename.to_string(),
157            },
158            syn::UseTree::Glob(_) => PureUseTree::Glob,
159            syn::UseTree::Group(g) => {
160                PureUseTree::Group(g.items.iter().map(|t| t.to_pure()).collect())
161            }
162        }
163    }
164}
165
166impl ToPure for syn::Visibility {
167    type Output = PureVis;
168
169    fn to_pure(&self) -> PureVis {
170        match self {
171            syn::Visibility::Public(_) => PureVis::Public,
172            syn::Visibility::Restricted(r) => {
173                let path = path_to_string(&r.path);
174                match path.as_str() {
175                    "crate" => PureVis::Crate,
176                    "super" => PureVis::Super,
177                    _ => PureVis::In(path),
178                }
179            }
180            syn::Visibility::Inherited => PureVis::Private,
181        }
182    }
183}
184
185impl ToPure for syn::ItemFn {
186    type Output = PureFn;
187
188    fn to_pure(&self) -> PureFn {
189        let is_async = self.sig.asyncness.is_some();
190        let is_async_inferred = !is_async && infer_async_from_return_type(&self.sig.output);
191
192        PureFn {
193            attrs: self.attrs.iter().map(|a| a.to_pure()).collect(),
194            vis: self.vis.to_pure(),
195            is_async,
196            is_async_inferred,
197            is_const: self.sig.constness.is_some(),
198            is_unsafe: self.sig.unsafety.is_some(),
199            abi: self.sig.abi.as_ref().map(abi_to_string),
200            name: self.sig.ident.to_string(),
201            generics: self.sig.generics.to_pure(),
202            params: self.sig.inputs.iter().map(|p| p.to_pure()).collect(),
203            ret: match &self.sig.output {
204                syn::ReturnType::Default => None,
205                syn::ReturnType::Type(_, ty) => Some(ty.to_pure()),
206            },
207            body: self.block.to_pure(),
208        }
209    }
210}
211
212impl ToPure for syn::FnArg {
213    type Output = PureParam;
214
215    fn to_pure(&self) -> PureParam {
216        match self {
217            syn::FnArg::Receiver(r) => PureParam::SelfValue {
218                is_ref: r.reference.is_some(),
219                is_mut: r.mutability.is_some(),
220            },
221            syn::FnArg::Typed(t) => {
222                // Preserve the original pattern when it's not a bare
223                // identifier; `pat_to_name` collapses these to lossy
224                // names like `"SpecGroup"` which would otherwise produce
225                // an uppercase-leading binding at re-emission.
226                let pat = match &*t.pat {
227                    syn::Pat::Ident(_) => None,
228                    other => Some(other.to_pure()),
229                };
230                PureParam::Typed {
231                    name: pat_to_name(&t.pat),
232                    ty: t.ty.to_pure(),
233                    is_mut: matches!(
234                        &*t.pat,
235                        syn::Pat::Ident(syn::PatIdent {
236                            mutability: Some(_),
237                            ..
238                        })
239                    ),
240                    pat,
241                }
242            }
243        }
244    }
245}
246
247impl ToPure for syn::Generics {
248    type Output = PureGenerics;
249
250    fn to_pure(&self) -> PureGenerics {
251        PureGenerics {
252            params: self.params.iter().map(|p| p.to_pure()).collect(),
253            where_clause: self
254                .where_clause
255                .as_ref()
256                .map(|w| {
257                    w.predicates
258                        .iter()
259                        .map(|p| p.to_token_stream().to_string())
260                        .collect()
261                })
262                .unwrap_or_default(),
263        }
264    }
265}
266
267impl ToPure for syn::GenericParam {
268    type Output = PureGenericParam;
269
270    fn to_pure(&self) -> PureGenericParam {
271        match self {
272            syn::GenericParam::Type(t) => PureGenericParam::Type {
273                name: t.ident.to_string(),
274                bounds: t
275                    .bounds
276                    .iter()
277                    .map(|b| b.to_token_stream().to_string())
278                    .collect(),
279            },
280            syn::GenericParam::Lifetime(l) => PureGenericParam::Lifetime {
281                name: l.lifetime.to_string(),
282                bounds: l.bounds.iter().map(|b| b.to_string()).collect(),
283            },
284            syn::GenericParam::Const(c) => PureGenericParam::Const {
285                name: c.ident.to_string(),
286                ty: c.ty.to_token_stream().to_string(),
287            },
288        }
289    }
290}
291
292impl ToPure for syn::Block {
293    type Output = PureBlock;
294
295    fn to_pure(&self) -> PureBlock {
296        PureBlock {
297            stmts: self.stmts.iter().map(|s| s.to_pure()).collect(),
298        }
299    }
300}
301
302impl ToPure for syn::Stmt {
303    type Output = PureStmt;
304
305    fn to_pure(&self) -> PureStmt {
306        match self {
307            syn::Stmt::Local(l) => {
308                // Extract type annotation from Pat::Type if present
309                let (pattern, ty) = match &l.pat {
310                    syn::Pat::Type(pt) => (pt.pat.to_pure(), Some(pt.ty.to_pure())),
311                    other => (other.to_pure(), None),
312                };
313                PureStmt::Local {
314                    pattern,
315                    ty,
316                    init: l.init.as_ref().map(|i| i.expr.to_pure()),
317                    else_branch: l
318                        .init
319                        .as_ref()
320                        .and_then(|i| i.diverge.as_ref())
321                        .map(|(_else_kw, expr)| Box::new(expr.to_pure())),
322                }
323            }
324            syn::Stmt::Item(i) => PureStmt::Item(Box::new(i.to_pure())),
325            syn::Stmt::Expr(e, semi) => {
326                if semi.is_some() {
327                    PureStmt::Semi(e.to_pure())
328                } else {
329                    PureStmt::Expr(e.to_pure())
330                }
331            }
332            syn::Stmt::Macro(m) => PureStmt::Semi(PureExpr::Macro {
333                name: path_to_string(&m.mac.path),
334                delimiter: match m.mac.delimiter {
335                    syn::MacroDelimiter::Paren(_) => MacroDelimiter::Paren,
336                    syn::MacroDelimiter::Brace(_) => MacroDelimiter::Brace,
337                    syn::MacroDelimiter::Bracket(_) => MacroDelimiter::Bracket,
338                },
339                tokens: m.mac.tokens.to_string(),
340            }),
341        }
342    }
343}
344
345impl ToPure for syn::Pat {
346    type Output = PurePattern;
347
348    fn to_pure(&self) -> PurePattern {
349        match self {
350            syn::Pat::Ident(i) => PurePattern::Ident {
351                name: i.ident.to_string(),
352                is_mut: i.mutability.is_some(),
353                by_ref: i.by_ref.is_some(),
354            },
355            syn::Pat::Wild(_) => PurePattern::Wild,
356            syn::Pat::Tuple(t) => PurePattern::Tuple(t.elems.iter().map(|p| p.to_pure()).collect()),
357            syn::Pat::TupleStruct(ts) => PurePattern::Struct {
358                path: path_to_string(&ts.path),
359                fields: ts
360                    .elems
361                    .iter()
362                    .enumerate()
363                    .map(|(i, p)| (i.to_string(), p.to_pure()))
364                    .collect(),
365                rest: false,
366            },
367            syn::Pat::Struct(s) => PurePattern::Struct {
368                path: path_to_string(&s.path),
369                fields: s
370                    .fields
371                    .iter()
372                    .map(|f| {
373                        let name = f.member.to_token_stream().to_string();
374                        (name, f.pat.to_pure())
375                    })
376                    .collect(),
377                rest: s.rest.is_some(),
378            },
379            syn::Pat::Reference(r) => PurePattern::Ref {
380                is_mut: r.mutability.is_some(),
381                pattern: Box::new(r.pat.to_pure()),
382            },
383            syn::Pat::Lit(l) => PurePattern::Lit(l.lit.to_token_stream().to_string()),
384            syn::Pat::Or(o) => PurePattern::Or(o.cases.iter().map(|p| p.to_pure()).collect()),
385            syn::Pat::Type(t) => t.pat.to_pure(),
386            syn::Pat::Path(p) => PurePattern::Path(path_to_string(&p.path)),
387            syn::Pat::Range(r) => PurePattern::Range {
388                start: r.start.as_ref().map(|e| e.to_token_stream().to_string()),
389                end: r.end.as_ref().map(|e| e.to_token_stream().to_string()),
390                inclusive: matches!(r.limits, syn::RangeLimits::Closed(_)),
391            },
392            syn::Pat::Slice(s) => PurePattern::Slice(s.elems.iter().map(|p| p.to_pure()).collect()),
393            syn::Pat::Rest(_) => PurePattern::Rest,
394            syn::Pat::Paren(p) => p.pat.to_pure(),
395            other => PurePattern::Other(other.to_token_stream().to_string()),
396        }
397    }
398}
399
400impl ToPure for syn::Expr {
401    type Output = PureExpr;
402
403    fn to_pure(&self) -> PureExpr {
404        match self {
405            syn::Expr::Lit(l) => PureExpr::Lit(l.lit.to_token_stream().to_string()),
406            syn::Expr::Path(p) => PureExpr::Path(path_to_string(&p.path)),
407            syn::Expr::Binary(b) => PureExpr::Binary {
408                op: b.op.to_token_stream().to_string(),
409                left: Box::new(b.left.to_pure()),
410                right: Box::new(b.right.to_pure()),
411            },
412            syn::Expr::Unary(u) => PureExpr::Unary {
413                op: u.op.to_token_stream().to_string(),
414                expr: Box::new(u.expr.to_pure()),
415            },
416            syn::Expr::Call(c) => PureExpr::Call {
417                func: Box::new(c.func.to_pure()),
418                args: c.args.iter().map(|a| a.to_pure()).collect(),
419            },
420            syn::Expr::MethodCall(m) => PureExpr::MethodCall {
421                receiver: Box::new(m.receiver.to_pure()),
422                method: m.method.to_string(),
423                turbofish: m
424                    .turbofish
425                    .as_ref()
426                    .map(|t| t.args.to_token_stream().to_string()),
427                args: m.args.iter().map(|a| a.to_pure()).collect(),
428            },
429            syn::Expr::Field(f) => PureExpr::Field {
430                expr: Box::new(f.base.to_pure()),
431                field: f.member.to_token_stream().to_string(),
432            },
433            syn::Expr::Index(i) => PureExpr::Index {
434                expr: Box::new(i.expr.to_pure()),
435                index: Box::new(i.index.to_pure()),
436            },
437            syn::Expr::Block(b) => PureExpr::Block {
438                label: b.label.as_ref().map(label_to_string),
439                block: b.block.to_pure(),
440            },
441            syn::Expr::If(i) => PureExpr::If {
442                cond: Box::new(i.cond.to_pure()),
443                then_branch: i.then_branch.to_pure(),
444                else_branch: i.else_branch.as_ref().map(|(_, e)| Box::new(e.to_pure())),
445            },
446            syn::Expr::Match(m) => PureExpr::Match {
447                expr: Box::new(m.expr.to_pure()),
448                arms: m.arms.iter().map(|a| a.to_pure()).collect(),
449            },
450            syn::Expr::Loop(l) => PureExpr::Loop {
451                label: l.label.as_ref().map(label_to_string),
452                body: l.body.to_pure(),
453            },
454            syn::Expr::While(w) => PureExpr::While {
455                label: w.label.as_ref().map(label_to_string),
456                cond: Box::new(w.cond.to_pure()),
457                body: w.body.to_pure(),
458            },
459            syn::Expr::ForLoop(f) => PureExpr::For {
460                label: f.label.as_ref().map(label_to_string),
461                pat: f.pat.to_pure(),
462                expr: Box::new(f.expr.to_pure()),
463                body: f.body.to_pure(),
464            },
465            syn::Expr::Return(r) => {
466                PureExpr::Return(r.expr.as_ref().map(|e| Box::new(e.to_pure())))
467            }
468            syn::Expr::Break(b) => PureExpr::Break {
469                label: b.label.as_ref().map(|l| l.ident.to_string()),
470                expr: b.expr.as_ref().map(|e| Box::new(e.to_pure())),
471            },
472            syn::Expr::Continue(c) => PureExpr::Continue {
473                label: c.label.as_ref().map(|l| l.ident.to_string()),
474            },
475            syn::Expr::Closure(c) => PureExpr::Closure {
476                is_async: c.asyncness.is_some(),
477                is_move: c.capture.is_some(),
478                params: c
479                    .inputs
480                    .iter()
481                    .map(|p| match p {
482                        syn::Pat::Type(pt) => {
483                            PureClosureParam::typed(pt.pat.to_pure(), pt.ty.to_pure())
484                        }
485                        other => PureClosureParam::untyped(other.to_pure()),
486                    })
487                    .collect(),
488                ret: match &c.output {
489                    syn::ReturnType::Default => None,
490                    syn::ReturnType::Type(_, ty) => Some(ty.to_pure()),
491                },
492                body: Box::new(c.body.to_pure()),
493            },
494            syn::Expr::Struct(s) => PureExpr::Struct {
495                path: path_to_string(&s.path),
496                fields: s
497                    .fields
498                    .iter()
499                    .map(|f| {
500                        let name = f.member.to_token_stream().to_string();
501                        (name, f.expr.to_pure())
502                    })
503                    .collect(),
504                rest: s.rest.as_ref().map(|e| Box::new(e.to_pure())),
505            },
506            syn::Expr::Tuple(t) => PureExpr::Tuple(t.elems.iter().map(|e| e.to_pure()).collect()),
507            syn::Expr::Array(a) => PureExpr::Array(a.elems.iter().map(|e| e.to_pure()).collect()),
508            syn::Expr::Reference(r) => PureExpr::Ref {
509                is_mut: r.mutability.is_some(),
510                expr: Box::new(r.expr.to_pure()),
511            },
512            syn::Expr::Macro(m) => PureExpr::Macro {
513                name: path_to_string(&m.mac.path),
514                delimiter: match m.mac.delimiter {
515                    syn::MacroDelimiter::Paren(_) => MacroDelimiter::Paren,
516                    syn::MacroDelimiter::Brace(_) => MacroDelimiter::Brace,
517                    syn::MacroDelimiter::Bracket(_) => MacroDelimiter::Bracket,
518                },
519                tokens: m.mac.tokens.to_string(),
520            },
521            syn::Expr::Await(a) => PureExpr::Await(Box::new(a.base.to_pure())),
522            syn::Expr::Try(t) => PureExpr::Try(Box::new(t.expr.to_pure())),
523            syn::Expr::Paren(p) => p.expr.to_pure(),
524            // Assignment: `a = b` - treated as Binary with "="
525            syn::Expr::Assign(a) => PureExpr::Binary {
526                op: "=".to_string(),
527                left: Box::new(a.left.to_pure()),
528                right: Box::new(a.right.to_pure()),
529            },
530            // Range: `a..b`, `..b`, `a..`, `..`, `a..=b`
531            syn::Expr::Range(r) => PureExpr::Range {
532                start: r.start.as_ref().map(|e| Box::new(e.to_pure())),
533                end: r.end.as_ref().map(|e| Box::new(e.to_pure())),
534                inclusive: matches!(r.limits, syn::RangeLimits::Closed(_)),
535            },
536            // Cast: `x as T`
537            syn::Expr::Cast(c) => PureExpr::Cast {
538                expr: Box::new(c.expr.to_pure()),
539                ty: c.ty.to_pure(),
540            },
541            // Let expression (in conditions): `let Some(x) = y`
542            syn::Expr::Let(l) => PureExpr::Let {
543                pattern: l.pat.to_pure(),
544                expr: Box::new(l.expr.to_pure()),
545            },
546            // Async block: `async { ... }` or `async move { ... }`
547            syn::Expr::Async(a) => PureExpr::Async {
548                is_move: a.capture.is_some(),
549                body: a.block.to_pure(),
550            },
551            // Unsafe block: `unsafe { ... }`
552            syn::Expr::Unsafe(u) => PureExpr::Unsafe(u.block.to_pure()),
553            // Array repeat: `[expr; N]`
554            syn::Expr::Repeat(r) => PureExpr::Repeat {
555                expr: Box::new(r.expr.to_pure()),
556                len: Box::new(r.len.to_pure()),
557            },
558            other => PureExpr::Other(other.to_token_stream().to_string()),
559        }
560    }
561}
562
563impl ToPure for syn::Arm {
564    type Output = PureMatchArm;
565
566    fn to_pure(&self) -> PureMatchArm {
567        PureMatchArm {
568            pattern: self.pat.to_pure(),
569            guard: self.guard.as_ref().map(|(_, e)| e.to_pure()),
570            body: self.body.to_pure(),
571        }
572    }
573}
574
575impl ToPure for syn::Type {
576    type Output = PureType;
577
578    fn to_pure(&self) -> PureType {
579        match self {
580            syn::Type::Path(p) => PureType::Path(path_to_string(&p.path)),
581            syn::Type::Reference(r) => PureType::Ref {
582                lifetime: r.lifetime.as_ref().map(|l| l.to_string()),
583                is_mut: r.mutability.is_some(),
584                ty: Box::new(r.elem.to_pure()),
585            },
586            syn::Type::Tuple(t) => PureType::Tuple(t.elems.iter().map(|t| t.to_pure()).collect()),
587            syn::Type::Array(a) => PureType::Array {
588                ty: Box::new(a.elem.to_pure()),
589                len: a.len.to_token_stream().to_string(),
590            },
591            syn::Type::Slice(s) => PureType::Slice(Box::new(s.elem.to_pure())),
592            syn::Type::BareFn(f) => PureType::Fn {
593                params: f.inputs.iter().map(|i| i.ty.to_pure()).collect(),
594                ret: match &f.output {
595                    syn::ReturnType::Default => None,
596                    syn::ReturnType::Type(_, ty) => Some(Box::new(ty.to_pure())),
597                },
598            },
599            syn::Type::ImplTrait(i) => PureType::ImplTrait(
600                i.bounds
601                    .iter()
602                    .map(|b| b.to_token_stream().to_string())
603                    .collect(),
604            ),
605            syn::Type::TraitObject(t) => PureType::TraitObject(
606                t.bounds
607                    .iter()
608                    .map(|b| b.to_token_stream().to_string())
609                    .collect(),
610            ),
611            syn::Type::Infer(_) => PureType::Infer,
612            syn::Type::Never(_) => PureType::Never,
613            syn::Type::Paren(p) => p.elem.to_pure(),
614            other => PureType::Other(other.to_token_stream().to_string()),
615        }
616    }
617}
618
619impl ToPure for syn::ItemStruct {
620    type Output = PureStruct;
621
622    fn to_pure(&self) -> PureStruct {
623        PureStruct {
624            attrs: self.attrs.iter().map(|a| a.to_pure()).collect(),
625            vis: self.vis.to_pure(),
626            name: self.ident.to_string(),
627            generics: self.generics.to_pure(),
628            fields: self.fields.to_pure(),
629        }
630    }
631}
632
633impl ToPure for syn::Fields {
634    type Output = PureFields;
635
636    fn to_pure(&self) -> PureFields {
637        match self {
638            syn::Fields::Named(n) => {
639                PureFields::Named(n.named.iter().map(|f| f.to_pure()).collect())
640            }
641            syn::Fields::Unnamed(u) => PureFields::Tuple(
642                u.unnamed
643                    .iter()
644                    .map(|f| PureTupleField {
645                        attrs: f.attrs.iter().map(|a| a.to_pure()).collect(),
646                        vis: f.vis.to_pure(),
647                        ty: f.ty.to_pure(),
648                    })
649                    .collect(),
650            ),
651            syn::Fields::Unit => PureFields::Unit,
652        }
653    }
654}
655
656impl ToPure for syn::Field {
657    type Output = PureField;
658
659    fn to_pure(&self) -> PureField {
660        PureField {
661            attrs: self.attrs.iter().map(|a| a.to_pure()).collect(),
662            vis: self.vis.to_pure(),
663            name: self
664                .ident
665                .as_ref()
666                .map(|i| i.to_string())
667                .unwrap_or_default(),
668            ty: self.ty.to_pure(),
669        }
670    }
671}
672
673impl ToPure for syn::ItemEnum {
674    type Output = PureEnum;
675
676    fn to_pure(&self) -> PureEnum {
677        PureEnum {
678            attrs: self.attrs.iter().map(|a| a.to_pure()).collect(),
679            vis: self.vis.to_pure(),
680            name: self.ident.to_string(),
681            generics: self.generics.to_pure(),
682            variants: self.variants.iter().map(|v| v.to_pure()).collect(),
683        }
684    }
685}
686
687impl ToPure for syn::Variant {
688    type Output = PureVariant;
689
690    fn to_pure(&self) -> PureVariant {
691        PureVariant {
692            attrs: self.attrs.iter().map(|a| a.to_pure()).collect(),
693            name: self.ident.to_string(),
694            fields: self.fields.to_pure(),
695            discriminant: self
696                .discriminant
697                .as_ref()
698                .map(|(_, e)| e.to_token_stream().to_string()),
699        }
700    }
701}
702
703impl ToPure for syn::ItemImpl {
704    type Output = PureImpl;
705
706    fn to_pure(&self) -> PureImpl {
707        PureImpl {
708            attrs: self.attrs.iter().map(|a| a.to_pure()).collect(),
709            generics: self.generics.to_pure(),
710            is_unsafe: self.unsafety.is_some(),
711            trait_: self.trait_.as_ref().map(|(_, p, _)| path_to_string(p)),
712            self_ty: self.self_ty.to_token_stream().to_string(),
713            items: self.items.iter().map(|i| i.to_pure()).collect(),
714        }
715    }
716}
717
718impl ToPure for syn::ImplItem {
719    type Output = PureImplItem;
720
721    fn to_pure(&self) -> PureImplItem {
722        match self {
723            syn::ImplItem::Fn(f) => PureImplItem::Fn(f.to_pure()),
724            syn::ImplItem::Const(c) => PureImplItem::Const(c.to_pure()),
725            syn::ImplItem::Type(t) => PureImplItem::Type(t.to_pure()),
726            other => PureImplItem::Other(other.to_token_stream().to_string()),
727        }
728    }
729}
730
731impl ToPure for syn::ImplItemFn {
732    type Output = PureFn;
733
734    fn to_pure(&self) -> PureFn {
735        let is_async = self.sig.asyncness.is_some();
736        let is_async_inferred = !is_async && infer_async_from_return_type(&self.sig.output);
737
738        PureFn {
739            attrs: self.attrs.iter().map(|a| a.to_pure()).collect(),
740            vis: self.vis.to_pure(),
741            is_async,
742            is_async_inferred,
743            is_const: self.sig.constness.is_some(),
744            is_unsafe: self.sig.unsafety.is_some(),
745            abi: self.sig.abi.as_ref().map(abi_to_string),
746            name: self.sig.ident.to_string(),
747            generics: self.sig.generics.to_pure(),
748            params: self.sig.inputs.iter().map(|p| p.to_pure()).collect(),
749            ret: match &self.sig.output {
750                syn::ReturnType::Default => None,
751                syn::ReturnType::Type(_, ty) => Some(ty.to_pure()),
752            },
753            body: self.block.to_pure(),
754        }
755    }
756}
757
758impl ToPure for syn::ImplItemConst {
759    type Output = PureConst;
760
761    fn to_pure(&self) -> PureConst {
762        PureConst {
763            attrs: self.attrs.iter().map(|a| a.to_pure()).collect(),
764            vis: self.vis.to_pure(),
765            name: self.ident.to_string(),
766            ty: self.ty.to_pure(),
767            value: Some(self.expr.to_pure()),
768        }
769    }
770}
771
772impl ToPure for syn::ImplItemType {
773    type Output = PureTypeAlias;
774
775    fn to_pure(&self) -> PureTypeAlias {
776        PureTypeAlias {
777            attrs: self.attrs.iter().map(|a| a.to_pure()).collect(),
778            vis: self.vis.to_pure(),
779            name: self.ident.to_string(),
780            generics: self.generics.to_pure(),
781            ty: self.ty.to_pure(),
782        }
783    }
784}
785
786impl ToPure for syn::ItemConst {
787    type Output = PureConst;
788
789    fn to_pure(&self) -> PureConst {
790        PureConst {
791            attrs: self.attrs.iter().map(|a| a.to_pure()).collect(),
792            vis: self.vis.to_pure(),
793            name: self.ident.to_string(),
794            ty: self.ty.to_pure(),
795            value: Some(self.expr.to_pure()),
796        }
797    }
798}
799
800impl ToPure for syn::ItemStatic {
801    type Output = PureStatic;
802
803    fn to_pure(&self) -> PureStatic {
804        PureStatic {
805            attrs: self.attrs.iter().map(|a| a.to_pure()).collect(),
806            vis: self.vis.to_pure(),
807            is_mut: matches!(self.mutability, syn::StaticMutability::Mut(_)),
808            name: self.ident.to_string(),
809            ty: self.ty.to_pure(),
810            value: self.expr.to_pure(),
811        }
812    }
813}
814
815impl ToPure for syn::ItemType {
816    type Output = PureTypeAlias;
817
818    fn to_pure(&self) -> PureTypeAlias {
819        PureTypeAlias {
820            attrs: self.attrs.iter().map(|a| a.to_pure()).collect(),
821            vis: self.vis.to_pure(),
822            name: self.ident.to_string(),
823            generics: self.generics.to_pure(),
824            ty: self.ty.to_pure(),
825        }
826    }
827}
828
829/// Check whether a `PureAttribute` is an exact outer `#[cfg(test)]`.
830///
831/// Returns `true` only when:
832/// - `path == "cfg"`
833/// - `meta == PureAttrMeta::List("test")`
834/// - `is_inner == false` (outer attribute, not `#![cfg(test)]`)
835///
836/// Partial matches such as `cfg(not(test))` or `cfg(feature = "...")` return
837/// `false` so that they are preserved in `attrs` as `ModScope::Prod`.
838fn is_cfg_test_attr(attr: &PureAttribute) -> bool {
839    attr.path == "cfg" && attr.meta == PureAttrMeta::List("test".to_string()) && !attr.is_inner
840}
841
842/// Scan `attrs`, remove the first exact `#[cfg(test)]` if present, and return
843/// `ModScope::Test`; otherwise return `ModScope::Prod`.
844///
845/// All other cfg variants (`cfg(not(test))`, `cfg(feature="...")`, etc.) are
846/// left in `attrs` with `ModScope::Prod` — they must not be silently dropped.
847fn extract_cfg_test_scope(attrs: &mut Vec<PureAttribute>) -> ModScope {
848    if let Some(pos) = attrs.iter().position(is_cfg_test_attr) {
849        attrs.remove(pos);
850        ModScope::Test
851    } else {
852        ModScope::Prod
853    }
854}
855
856impl ToPure for syn::ItemMod {
857    type Output = PureMod;
858
859    fn to_pure(&self) -> PureMod {
860        // Convert all items (inline module) or empty (file module)
861        let items = if let Some((_, items)) = &self.content {
862            items.iter().map(|item| item.to_pure()).collect()
863        } else {
864            // File module (mod foo;) - no inline content
865            Vec::new()
866        };
867
868        let mut attrs: Vec<PureAttribute> = self.attrs.iter().map(|a| a.to_pure()).collect();
869        let scope = extract_cfg_test_scope(&mut attrs);
870
871        PureMod {
872            attrs,
873            vis: self.vis.to_pure(),
874            name: self.ident.to_string(),
875            items,
876            scope,
877        }
878    }
879}
880
881impl ToPure for syn::ItemTrait {
882    type Output = PureTrait;
883
884    fn to_pure(&self) -> PureTrait {
885        PureTrait {
886            attrs: self.attrs.iter().map(|a| a.to_pure()).collect(),
887            vis: self.vis.to_pure(),
888            is_unsafe: self.unsafety.is_some(),
889            is_auto: self.auto_token.is_some(),
890            name: self.ident.to_string(),
891            generics: self.generics.to_pure(),
892            supertraits: self
893                .supertraits
894                .iter()
895                .map(|b| b.to_token_stream().to_string())
896                .collect(),
897            items: self.items.iter().map(|i| i.to_pure()).collect(),
898        }
899    }
900}
901
902impl ToPure for syn::TraitItem {
903    type Output = PureTraitItem;
904
905    fn to_pure(&self) -> PureTraitItem {
906        match self {
907            syn::TraitItem::Fn(f) => PureTraitItem::Fn(f.to_pure()),
908            syn::TraitItem::Const(c) => PureTraitItem::Const(c.to_pure()),
909            syn::TraitItem::Type(t) => PureTraitItem::Type {
910                name: t.ident.to_string(),
911                bounds: t
912                    .bounds
913                    .iter()
914                    .map(|b| b.to_token_stream().to_string())
915                    .collect(),
916                default: t.default.as_ref().map(|(_, ty)| ty.to_pure()),
917            },
918            other => PureTraitItem::Other(other.to_token_stream().to_string()),
919        }
920    }
921}
922
923impl ToPure for syn::TraitItemFn {
924    type Output = PureFn;
925
926    fn to_pure(&self) -> PureFn {
927        let is_async = self.sig.asyncness.is_some();
928        let is_async_inferred = !is_async && infer_async_from_return_type(&self.sig.output);
929
930        PureFn {
931            attrs: self.attrs.iter().map(|a| a.to_pure()).collect(),
932            vis: PureVis::Private, // Trait methods don't have visibility
933            is_async,
934            is_async_inferred,
935            is_const: self.sig.constness.is_some(),
936            is_unsafe: self.sig.unsafety.is_some(),
937            abi: self.sig.abi.as_ref().map(abi_to_string),
938            name: self.sig.ident.to_string(),
939            generics: self.sig.generics.to_pure(),
940            params: self.sig.inputs.iter().map(|p| p.to_pure()).collect(),
941            ret: match &self.sig.output {
942                syn::ReturnType::Default => None,
943                syn::ReturnType::Type(_, ty) => Some(ty.to_pure()),
944            },
945            body: self
946                .default
947                .as_ref()
948                .map(|b| b.to_pure())
949                .unwrap_or_default(),
950        }
951    }
952}
953
954impl ToPure for syn::TraitItemConst {
955    type Output = PureConst;
956
957    fn to_pure(&self) -> PureConst {
958        PureConst {
959            attrs: self.attrs.iter().map(|a| a.to_pure()).collect(),
960            vis: PureVis::Private,
961            name: self.ident.to_string(),
962            ty: self.ty.to_pure(),
963            value: self.default.as_ref().map(|(_, e)| e.to_pure()),
964        }
965    }
966}
967
968impl ToPure for syn::ItemMacro {
969    type Output = PureMacro;
970
971    fn to_pure(&self) -> PureMacro {
972        PureMacro {
973            path: path_to_string(&self.mac.path),
974            name: self.ident.as_ref().map(|i| i.to_string()),
975            delimiter: match self.mac.delimiter {
976                syn::MacroDelimiter::Paren(_) => MacroDelimiter::Paren,
977                syn::MacroDelimiter::Brace(_) => MacroDelimiter::Brace,
978                syn::MacroDelimiter::Bracket(_) => MacroDelimiter::Bracket,
979            },
980            tokens: self.mac.tokens.to_string(),
981        }
982    }
983}
984
985// Helper functions
986
987fn path_to_string(path: &syn::Path) -> String {
988    path.segments
989        .iter()
990        .map(|s| {
991            let ident = s.ident.to_string();
992            match &s.arguments {
993                syn::PathArguments::None => ident,
994                syn::PathArguments::AngleBracketed(args) => {
995                    format!("{}{}", ident, args.to_token_stream())
996                }
997                syn::PathArguments::Parenthesized(args) => {
998                    format!("{}{}", ident, args.to_token_stream())
999                }
1000            }
1001        })
1002        .collect::<Vec<_>>()
1003        .join("::")
1004}
1005
1006fn pat_to_name(pat: &syn::Pat) -> String {
1007    match pat {
1008        syn::Pat::Ident(i) => i.ident.to_string(),
1009        syn::Pat::Type(t) => pat_to_name(&t.pat),
1010        syn::Pat::Tuple(t) => {
1011            // Tuple patterns like (a, b) - use placeholder name
1012            format!("_tuple{}", t.elems.len())
1013        }
1014        syn::Pat::TupleStruct(ts) => {
1015            // TupleStruct patterns like Some(x) - use struct name
1016            ts.path
1017                .segments
1018                .last()
1019                .map(|s| s.ident.to_string())
1020                .unwrap_or_else(|| "_pattern".to_string())
1021        }
1022        syn::Pat::Struct(s) => {
1023            // Struct patterns like Point { x, y } - use struct name
1024            s.path
1025                .segments
1026                .last()
1027                .map(|seg| seg.ident.to_string())
1028                .unwrap_or_else(|| "_pattern".to_string())
1029        }
1030        syn::Pat::Wild(_) => "_".to_string(),
1031        syn::Pat::Rest(_) => "_rest".to_string(),
1032        syn::Pat::Slice(_) => "_slice".to_string(),
1033        syn::Pat::Reference(r) => pat_to_name(&r.pat),
1034        syn::Pat::Or(o) => {
1035            // Or patterns like A | B - use first pattern's name
1036            o.cases
1037                .first()
1038                .map(pat_to_name)
1039                .unwrap_or_else(|| "_or".to_string())
1040        }
1041        _other => "_param".to_string(),
1042    }
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047    use super::*;
1048    use crate::pure::to_syn::ToSyn;
1049    use quote::ToTokens;
1050
1051    #[test]
1052    fn test_parse_simple_fn() {
1053        let pure = PureFile::from_source("fn main() {}").unwrap();
1054        assert_eq!(pure.functions().len(), 1);
1055        assert_eq!(pure.functions()[0].name, "main");
1056    }
1057
1058    #[test]
1059    fn test_parse_struct() {
1060        let pure = PureFile::from_source(
1061            r#"
1062            struct Point {
1063                x: i32,
1064                y: i32,
1065            }
1066            "#,
1067        )
1068        .unwrap();
1069
1070        assert_eq!(pure.structs().len(), 1);
1071        let s = &pure.structs()[0];
1072        assert_eq!(s.name, "Point");
1073
1074        if let PureFields::Named(fields) = &s.fields {
1075            assert_eq!(fields.len(), 2);
1076            assert_eq!(fields[0].name, "x");
1077            assert_eq!(fields[1].name, "y");
1078        } else {
1079            panic!("Expected named fields");
1080        }
1081    }
1082
1083    #[test]
1084    fn test_parse_use() {
1085        let pure = PureFile::from_source("use std::io;").unwrap();
1086        assert_eq!(pure.uses().len(), 1);
1087    }
1088
1089    #[test]
1090    fn test_thread_safe() {
1091        use std::sync::Arc;
1092        use std::thread;
1093
1094        let pure = PureFile::from_source(
1095            r#"
1096            fn foo() {}
1097            fn bar() {}
1098            "#,
1099        )
1100        .unwrap();
1101
1102        let shared = Arc::new(pure);
1103
1104        let s1 = Arc::clone(&shared);
1105        let s2 = Arc::clone(&shared);
1106
1107        let h1 = thread::spawn(move || s1.functions().len());
1108        let h2 = thread::spawn(move || s2.find_fn("foo").map(|f| f.name.clone()));
1109
1110        assert_eq!(h1.join().unwrap(), 2);
1111        assert_eq!(h2.join().unwrap(), Some("foo".to_string()));
1112    }
1113
1114    // ========== Expression Tests ==========
1115
1116    #[test]
1117    fn test_expr_binary() {
1118        let pure = PureFile::from_source("fn f() { let _ = 1 + 2; }").unwrap();
1119        let f = &pure.functions()[0];
1120        if let PureStmt::Local {
1121            init: Some(expr), ..
1122        } = &f.body.stmts[0]
1123        {
1124            if let PureExpr::Binary { op, .. } = expr {
1125                assert_eq!(op, "+");
1126            } else {
1127                panic!("Expected Binary expr");
1128            }
1129        } else {
1130            panic!("Expected Local stmt");
1131        }
1132    }
1133
1134    #[test]
1135    fn test_expr_unary() {
1136        let pure = PureFile::from_source("fn f() { let _ = -x; }").unwrap();
1137        let f = &pure.functions()[0];
1138        if let PureStmt::Local {
1139            init: Some(expr), ..
1140        } = &f.body.stmts[0]
1141        {
1142            if let PureExpr::Unary { op, .. } = expr {
1143                assert_eq!(op, "-");
1144            } else {
1145                panic!("Expected Unary expr");
1146            }
1147        } else {
1148            panic!("Expected Local stmt");
1149        }
1150    }
1151
1152    #[test]
1153    fn test_expr_method_call() {
1154        let pure = PureFile::from_source("fn f() { x.foo(1, 2); }").unwrap();
1155        let f = &pure.functions()[0];
1156        if let PureStmt::Semi(PureExpr::MethodCall { method, args, .. }) = &f.body.stmts[0] {
1157            assert_eq!(method, "foo");
1158            assert_eq!(args.len(), 2);
1159        } else {
1160            panic!("Expected MethodCall expr");
1161        }
1162    }
1163
1164    #[test]
1165    fn test_expr_method_call_with_turbofish() {
1166        let pure = PureFile::from_source("fn f() { x.collect::<Vec<_>>(); }").unwrap();
1167        let f = &pure.functions()[0];
1168        if let PureStmt::Semi(PureExpr::MethodCall {
1169            method, turbofish, ..
1170        }) = &f.body.stmts[0]
1171        {
1172            assert_eq!(method, "collect");
1173            assert!(turbofish.is_some());
1174            assert!(turbofish.as_ref().unwrap().contains("Vec"));
1175        } else {
1176            panic!("Expected MethodCall expr with turbofish");
1177        }
1178    }
1179
1180    #[test]
1181    fn test_expr_closure() {
1182        let pure = PureFile::from_source("fn f() { let _ = |x| x + 1; }").unwrap();
1183        let f = &pure.functions()[0];
1184        if let PureStmt::Local {
1185            init: Some(expr), ..
1186        } = &f.body.stmts[0]
1187        {
1188            if let PureExpr::Closure {
1189                params, is_move, ..
1190            } = expr
1191            {
1192                assert!(!is_move);
1193                assert_eq!(params.len(), 1);
1194            } else {
1195                panic!("Expected Closure expr");
1196            }
1197        } else {
1198            panic!("Expected Local stmt");
1199        }
1200    }
1201
1202    #[test]
1203    fn test_expr_closure_move() {
1204        let pure = PureFile::from_source("fn f() { let _ = move |x| x; }").unwrap();
1205        let f = &pure.functions()[0];
1206        if let PureStmt::Local {
1207            init: Some(PureExpr::Closure { is_move, .. }),
1208            ..
1209        } = &f.body.stmts[0]
1210        {
1211            assert!(is_move);
1212        } else {
1213            panic!("Expected move Closure");
1214        }
1215    }
1216
1217    #[test]
1218    fn test_expr_async_closure() {
1219        let pure = PureFile::from_source("fn f() { let _ = async || {}; }").unwrap();
1220        let f = &pure.functions()[0];
1221        if let PureStmt::Local {
1222            init: Some(PureExpr::Closure { is_async, .. }),
1223            ..
1224        } = &f.body.stmts[0]
1225        {
1226            assert!(is_async);
1227        } else {
1228            panic!("Expected async Closure");
1229        }
1230    }
1231
1232    #[test]
1233    fn test_expr_closure_typed_params() {
1234        let pure =
1235            PureFile::from_source("fn f() { let _ = |x: i32, y: String| -> bool { true }; }")
1236                .unwrap();
1237        let f = &pure.functions()[0];
1238        if let PureStmt::Local {
1239            init: Some(expr), ..
1240        } = &f.body.stmts[0]
1241        {
1242            if let PureExpr::Closure { params, ret, .. } = expr {
1243                assert_eq!(params.len(), 2);
1244                // First param: x: i32
1245                assert!(params[0].ty.is_some());
1246                assert_eq!(
1247                    params[0].ty.as_ref().unwrap(),
1248                    &PureType::Path("i32".to_string())
1249                );
1250                // Second param: y: String
1251                assert!(params[1].ty.is_some());
1252                assert_eq!(
1253                    params[1].ty.as_ref().unwrap(),
1254                    &PureType::Path("String".to_string())
1255                );
1256                // Return type: -> bool
1257                assert!(ret.is_some());
1258                assert_eq!(ret.as_ref().unwrap(), &PureType::Path("bool".to_string()));
1259            } else {
1260                panic!("Expected Closure expr");
1261            }
1262        } else {
1263            panic!("Expected Local stmt");
1264        }
1265    }
1266
1267    #[test]
1268    fn test_expr_closure_untyped_params_no_ret() {
1269        let pure = PureFile::from_source("fn f() { let _ = |x| x + 1; }").unwrap();
1270        let f = &pure.functions()[0];
1271        if let PureStmt::Local {
1272            init: Some(PureExpr::Closure { params, ret, .. }),
1273            ..
1274        } = &f.body.stmts[0]
1275        {
1276            assert_eq!(params.len(), 1);
1277            assert!(params[0].ty.is_none());
1278            assert!(ret.is_none());
1279        } else {
1280            panic!("Expected Closure");
1281        }
1282    }
1283
1284    #[test]
1285    fn test_expr_if() {
1286        let pure = PureFile::from_source("fn f() { if true { 1 } else { 2 } }").unwrap();
1287        let f = &pure.functions()[0];
1288        if let PureStmt::Expr(PureExpr::If { else_branch, .. }) = &f.body.stmts[0] {
1289            assert!(else_branch.is_some());
1290        } else {
1291            panic!("Expected If expr");
1292        }
1293    }
1294
1295    #[test]
1296    fn test_expr_match() {
1297        let pure =
1298            PureFile::from_source(r#"fn f() { match x { 1 => "one", _ => "other" } }"#).unwrap();
1299        let f = &pure.functions()[0];
1300        if let PureStmt::Expr(PureExpr::Match { arms, .. }) = &f.body.stmts[0] {
1301            assert_eq!(arms.len(), 2);
1302        } else {
1303            panic!("Expected Match expr");
1304        }
1305    }
1306
1307    #[test]
1308    fn test_expr_for_loop() {
1309        let pure = PureFile::from_source("fn f() { for i in 0..10 {} }").unwrap();
1310        let f = &pure.functions()[0];
1311        // For loop without semicolon is Expr, with semicolon is Semi
1312        match &f.body.stmts[0] {
1313            PureStmt::Expr(PureExpr::For { pat, .. })
1314            | PureStmt::Semi(PureExpr::For { pat, .. }) => {
1315                if let PurePattern::Ident { name, .. } = pat {
1316                    assert_eq!(name, "i");
1317                } else {
1318                    panic!("Expected Ident pattern");
1319                }
1320            }
1321            _ => panic!("Expected For expr"),
1322        }
1323    }
1324
1325    #[test]
1326    fn test_expr_while() {
1327        let pure = PureFile::from_source("fn f() { while x > 0 { x -= 1; } }").unwrap();
1328        let f = &pure.functions()[0];
1329        match &f.body.stmts[0] {
1330            PureStmt::Expr(PureExpr::While { .. }) | PureStmt::Semi(PureExpr::While { .. }) => {}
1331            _ => panic!("Expected While expr"),
1332        }
1333    }
1334
1335    #[test]
1336    fn test_expr_loop() {
1337        let pure = PureFile::from_source("fn f() { loop { break; } }").unwrap();
1338        let f = &pure.functions()[0];
1339        match &f.body.stmts[0] {
1340            PureStmt::Expr(PureExpr::Loop { .. }) | PureStmt::Semi(PureExpr::Loop { .. }) => {}
1341            _ => panic!("Expected Loop expr"),
1342        }
1343    }
1344
1345    #[test]
1346    fn test_expr_return() {
1347        let pure = PureFile::from_source("fn f() -> i32 { return 42; }").unwrap();
1348        let f = &pure.functions()[0];
1349        if let PureStmt::Semi(PureExpr::Return(Some(_))) = &f.body.stmts[0] {
1350            // ok
1351        } else {
1352            panic!("Expected Return expr");
1353        }
1354    }
1355
1356    #[test]
1357    fn test_expr_struct() {
1358        let pure = PureFile::from_source("fn f() { Point { x: 1, y: 2 } }").unwrap();
1359        let f = &pure.functions()[0];
1360        if let PureStmt::Expr(PureExpr::Struct { path, fields, .. }) = &f.body.stmts[0] {
1361            assert_eq!(path, "Point");
1362            assert_eq!(fields.len(), 2);
1363        } else {
1364            panic!("Expected Struct expr");
1365        }
1366    }
1367
1368    #[test]
1369    fn test_expr_tuple() {
1370        let pure = PureFile::from_source("fn f() { (1, 2, 3) }").unwrap();
1371        let f = &pure.functions()[0];
1372        if let PureStmt::Expr(PureExpr::Tuple(elems)) = &f.body.stmts[0] {
1373            assert_eq!(elems.len(), 3);
1374        } else {
1375            panic!("Expected Tuple expr");
1376        }
1377    }
1378
1379    #[test]
1380    fn test_expr_array() {
1381        let pure = PureFile::from_source("fn f() { [1, 2, 3] }").unwrap();
1382        let f = &pure.functions()[0];
1383        if let PureStmt::Expr(PureExpr::Array(elems)) = &f.body.stmts[0] {
1384            assert_eq!(elems.len(), 3);
1385        } else {
1386            panic!("Expected Array expr");
1387        }
1388    }
1389
1390    #[test]
1391    fn test_expr_repeat() {
1392        let pure = PureFile::from_source("fn f() { [0; 10] }").unwrap();
1393        let f = &pure.functions()[0];
1394        if let PureStmt::Expr(PureExpr::Repeat { .. }) = &f.body.stmts[0] {
1395            // ok
1396        } else {
1397            panic!("Expected Repeat expr");
1398        }
1399    }
1400
1401    #[test]
1402    fn test_expr_reference() {
1403        let pure = PureFile::from_source("fn f() { let _ = &x; }").unwrap();
1404        let f = &pure.functions()[0];
1405        if let PureStmt::Local {
1406            init: Some(PureExpr::Ref { is_mut, .. }),
1407            ..
1408        } = &f.body.stmts[0]
1409        {
1410            assert!(!is_mut);
1411        } else {
1412            panic!("Expected Ref expr");
1413        }
1414    }
1415
1416    #[test]
1417    fn test_expr_reference_mut() {
1418        let pure = PureFile::from_source("fn f() { let _ = &mut x; }").unwrap();
1419        let f = &pure.functions()[0];
1420        if let PureStmt::Local {
1421            init: Some(PureExpr::Ref { is_mut, .. }),
1422            ..
1423        } = &f.body.stmts[0]
1424        {
1425            assert!(is_mut);
1426        } else {
1427            panic!("Expected mut Ref expr");
1428        }
1429    }
1430
1431    #[test]
1432    fn test_expr_await() {
1433        let pure = PureFile::from_source("async fn f() { x.await }").unwrap();
1434        let f = &pure.functions()[0];
1435        if let PureStmt::Expr(PureExpr::Await(_)) = &f.body.stmts[0] {
1436            // ok
1437        } else {
1438            panic!("Expected Await expr");
1439        }
1440    }
1441
1442    #[test]
1443    fn test_expr_try() {
1444        let pure = PureFile::from_source("fn f() -> Result<(), ()> { x?; Ok(()) }").unwrap();
1445        let f = &pure.functions()[0];
1446        if let PureStmt::Semi(PureExpr::Try(_)) = &f.body.stmts[0] {
1447            // ok
1448        } else {
1449            panic!("Expected Try expr");
1450        }
1451    }
1452
1453    #[test]
1454    fn test_expr_range() {
1455        let pure = PureFile::from_source("fn f() { let _ = 0..10; }").unwrap();
1456        let f = &pure.functions()[0];
1457        if let PureStmt::Local {
1458            init:
1459                Some(PureExpr::Range {
1460                    start,
1461                    end,
1462                    inclusive,
1463                }),
1464            ..
1465        } = &f.body.stmts[0]
1466        {
1467            assert!(start.is_some());
1468            assert!(end.is_some());
1469            assert!(!inclusive);
1470        } else {
1471            panic!("Expected Range expr");
1472        }
1473    }
1474
1475    #[test]
1476    fn test_expr_range_inclusive() {
1477        let pure = PureFile::from_source("fn f() { let _ = 0..=10; }").unwrap();
1478        let f = &pure.functions()[0];
1479        if let PureStmt::Local {
1480            init: Some(PureExpr::Range { inclusive, .. }),
1481            ..
1482        } = &f.body.stmts[0]
1483        {
1484            assert!(inclusive);
1485        } else {
1486            panic!("Expected inclusive Range expr");
1487        }
1488    }
1489
1490    #[test]
1491    fn test_expr_cast() {
1492        let pure = PureFile::from_source("fn f() { let _ = x as u32; }").unwrap();
1493        let f = &pure.functions()[0];
1494        if let PureStmt::Local {
1495            init: Some(PureExpr::Cast { ty, .. }),
1496            ..
1497        } = &f.body.stmts[0]
1498        {
1499            if let PureType::Path(p) = ty {
1500                assert_eq!(p, "u32");
1501            } else {
1502                panic!("Expected Path type");
1503            }
1504        } else {
1505            panic!("Expected Cast expr");
1506        }
1507    }
1508
1509    #[test]
1510    fn test_expr_let() {
1511        let pure = PureFile::from_source("fn f() { if let Some(x) = y { } }").unwrap();
1512        let f = &pure.functions()[0];
1513        let if_expr = match &f.body.stmts[0] {
1514            PureStmt::Expr(e) | PureStmt::Semi(e) => e,
1515            _ => panic!("Expected expression statement"),
1516        };
1517        if let PureExpr::If { cond, .. } = if_expr {
1518            if let PureExpr::Let { pattern, .. } = cond.as_ref() {
1519                if let PurePattern::Struct { path, .. } = pattern {
1520                    assert_eq!(path, "Some");
1521                } else {
1522                    panic!("Expected Struct pattern");
1523                }
1524            } else {
1525                panic!("Expected Let expr in condition");
1526            }
1527        } else {
1528            panic!("Expected If expr");
1529        }
1530    }
1531
1532    #[test]
1533    fn test_expr_async_block() {
1534        let pure = PureFile::from_source("fn f() { async { 42 } }").unwrap();
1535        let f = &pure.functions()[0];
1536        if let PureStmt::Expr(PureExpr::Async { is_move, .. }) = &f.body.stmts[0] {
1537            assert!(!is_move);
1538        } else {
1539            panic!("Expected Async block");
1540        }
1541    }
1542
1543    #[test]
1544    fn test_expr_unsafe_block() {
1545        let pure = PureFile::from_source("fn f() { unsafe { dangerous() } }").unwrap();
1546        let f = &pure.functions()[0];
1547        if let PureStmt::Expr(PureExpr::Unsafe(_)) = &f.body.stmts[0] {
1548            // ok
1549        } else {
1550            panic!("Expected Unsafe block");
1551        }
1552    }
1553
1554    #[test]
1555    fn test_expr_macro() {
1556        let pure = PureFile::from_source("fn f() { println!(\"hello\"); }").unwrap();
1557        let f = &pure.functions()[0];
1558        if let PureStmt::Semi(PureExpr::Macro { name, .. }) = &f.body.stmts[0] {
1559            assert_eq!(name, "println");
1560        } else {
1561            panic!("Expected Macro expr");
1562        }
1563    }
1564
1565    // ========== Pattern Tests ==========
1566
1567    #[test]
1568    fn test_pattern_ident() {
1569        let pure = PureFile::from_source("fn f() { let x = 1; }").unwrap();
1570        let f = &pure.functions()[0];
1571        if let PureStmt::Local {
1572            pattern: PurePattern::Ident { name, is_mut, .. },
1573            ..
1574        } = &f.body.stmts[0]
1575        {
1576            assert_eq!(name, "x");
1577            assert!(!is_mut);
1578        } else {
1579            panic!("Expected Ident pattern");
1580        }
1581    }
1582
1583    #[test]
1584    fn test_pattern_ident_mut() {
1585        let pure = PureFile::from_source("fn f() { let mut x = 1; }").unwrap();
1586        let f = &pure.functions()[0];
1587        if let PureStmt::Local {
1588            pattern: PurePattern::Ident { is_mut, .. },
1589            ..
1590        } = &f.body.stmts[0]
1591        {
1592            assert!(is_mut);
1593        } else {
1594            panic!("Expected mut Ident pattern");
1595        }
1596    }
1597
1598    #[test]
1599    fn test_pattern_wild() {
1600        let pure = PureFile::from_source("fn f() { let _ = 1; }").unwrap();
1601        let f = &pure.functions()[0];
1602        if let PureStmt::Local {
1603            pattern: PurePattern::Wild,
1604            ..
1605        } = &f.body.stmts[0]
1606        {
1607            // ok
1608        } else {
1609            panic!("Expected Wild pattern");
1610        }
1611    }
1612
1613    #[test]
1614    fn test_pattern_tuple() {
1615        let pure = PureFile::from_source("fn f() { let (a, b) = (1, 2); }").unwrap();
1616        let f = &pure.functions()[0];
1617        if let PureStmt::Local {
1618            pattern: PurePattern::Tuple(elems),
1619            ..
1620        } = &f.body.stmts[0]
1621        {
1622            assert_eq!(elems.len(), 2);
1623        } else {
1624            panic!("Expected Tuple pattern");
1625        }
1626    }
1627
1628    #[test]
1629    fn test_pattern_struct() {
1630        let pure = PureFile::from_source("fn f() { let Point { x, y } = p; }").unwrap();
1631        let f = &pure.functions()[0];
1632        if let PureStmt::Local {
1633            pattern: PurePattern::Struct { path, fields, rest },
1634            ..
1635        } = &f.body.stmts[0]
1636        {
1637            assert_eq!(path, "Point");
1638            assert_eq!(fields.len(), 2);
1639            assert!(!rest);
1640        } else {
1641            panic!("Expected Struct pattern");
1642        }
1643    }
1644
1645    #[test]
1646    fn test_pattern_struct_with_rest() {
1647        let pure = PureFile::from_source("fn f() { let Point { x, .. } = p; }").unwrap();
1648        let f = &pure.functions()[0];
1649        if let PureStmt::Local {
1650            pattern: PurePattern::Struct { rest, .. },
1651            ..
1652        } = &f.body.stmts[0]
1653        {
1654            assert!(rest);
1655        } else {
1656            panic!("Expected Struct pattern with rest");
1657        }
1658    }
1659
1660    #[test]
1661    fn test_pattern_tuple_struct() {
1662        let pure = PureFile::from_source("fn f() { let Some(x) = opt; }").unwrap();
1663        let f = &pure.functions()[0];
1664        if let PureStmt::Local {
1665            pattern: PurePattern::Struct { path, .. },
1666            ..
1667        } = &f.body.stmts[0]
1668        {
1669            assert_eq!(path, "Some");
1670        } else {
1671            panic!("Expected TupleStruct pattern");
1672        }
1673    }
1674
1675    #[test]
1676    fn test_pattern_reference() {
1677        let pure = PureFile::from_source("fn f() { let &x = r; }").unwrap();
1678        let f = &pure.functions()[0];
1679        if let PureStmt::Local {
1680            pattern: PurePattern::Ref { is_mut, .. },
1681            ..
1682        } = &f.body.stmts[0]
1683        {
1684            assert!(!is_mut);
1685        } else {
1686            panic!("Expected Ref pattern");
1687        }
1688    }
1689
1690    #[test]
1691    fn test_pattern_or() {
1692        // Or patterns work in match arms
1693        let pure =
1694            PureFile::from_source("fn f(x: i32) { match x { 1 | 2 | 3 => {} _ => {} } }").unwrap();
1695        let f = &pure.functions()[0];
1696        if let PureStmt::Expr(PureExpr::Match { arms, .. }) = &f.body.stmts[0] {
1697            if let PurePattern::Or(cases) = &arms[0].pattern {
1698                assert_eq!(cases.len(), 3);
1699            } else {
1700                panic!("Expected Or pattern");
1701            }
1702        } else {
1703            panic!("Expected Match expr");
1704        }
1705    }
1706
1707    #[test]
1708    fn test_pattern_range() {
1709        let pure =
1710            PureFile::from_source("fn f(x: i32) { match x { 0..=10 => {} _ => {} } }").unwrap();
1711        let f = &pure.functions()[0];
1712        if let PureStmt::Expr(PureExpr::Match { arms, .. }) = &f.body.stmts[0] {
1713            if let PurePattern::Range { inclusive, .. } = &arms[0].pattern {
1714                assert!(inclusive);
1715            } else {
1716                panic!("Expected Range pattern");
1717            }
1718        } else {
1719            panic!("Expected Match expr");
1720        }
1721    }
1722
1723    #[test]
1724    fn test_pattern_slice() {
1725        let pure = PureFile::from_source("fn f() { let [a, b, c] = arr; }").unwrap();
1726        let f = &pure.functions()[0];
1727        if let PureStmt::Local {
1728            pattern: PurePattern::Slice(elems),
1729            ..
1730        } = &f.body.stmts[0]
1731        {
1732            assert_eq!(elems.len(), 3);
1733        } else {
1734            panic!("Expected Slice pattern");
1735        }
1736    }
1737
1738    #[test]
1739    fn test_pattern_path() {
1740        // Test path patterns like std::cmp::Ordering::Less
1741        let pure = PureFile::from_source(
1742            "fn f(x: std::cmp::Ordering) { match x { std::cmp::Ordering::Less => {} _ => {} } }",
1743        )
1744        .unwrap();
1745        let f = &pure.functions()[0];
1746        if let PureStmt::Expr(PureExpr::Match { arms, .. }) = &f.body.stmts[0] {
1747            if let PurePattern::Path(path) = &arms[0].pattern {
1748                assert!(path.contains("Ordering"));
1749                assert!(path.contains("Less"));
1750            } else {
1751                panic!("Expected Path pattern, got {:?}", arms[0].pattern);
1752            }
1753        } else {
1754            panic!("Expected Match expr");
1755        }
1756    }
1757
1758    #[test]
1759    fn test_pattern_ident_by_ref() {
1760        // `ref x` binding should preserve by_ref=true
1761        let pure = PureFile::from_source(
1762            "fn f(opt: Option<i32>) { match opt { Some(ref x) => {} _ => {} } }",
1763        )
1764        .unwrap();
1765        let f = &pure.functions()[0];
1766        if let PureStmt::Expr(PureExpr::Match { arms, .. }) = &f.body.stmts[0] {
1767            if let PurePattern::Struct { fields, .. } = &arms[0].pattern {
1768                if let PurePattern::Ident { by_ref, is_mut, .. } = &fields[0].1 {
1769                    assert!(*by_ref, "Expected by_ref=true for `ref x`");
1770                    assert!(!is_mut, "Expected is_mut=false for `ref x`");
1771                } else {
1772                    panic!("Expected Ident sub-pattern");
1773                }
1774            } else {
1775                panic!("Expected Struct pattern for Some(ref x)");
1776            }
1777        } else {
1778            panic!("Expected Match expr");
1779        }
1780    }
1781
1782    #[test]
1783    fn test_pattern_ident_ref_mut() {
1784        // `ref mut x` binding should preserve by_ref=true, is_mut=true
1785        let pure = PureFile::from_source(
1786            "fn f(opt: Option<i32>) { match opt { Some(ref mut x) => {} _ => {} } }",
1787        )
1788        .unwrap();
1789        let f = &pure.functions()[0];
1790        if let PureStmt::Expr(PureExpr::Match { arms, .. }) = &f.body.stmts[0] {
1791            if let PurePattern::Struct { fields, .. } = &arms[0].pattern {
1792                if let PurePattern::Ident { by_ref, is_mut, .. } = &fields[0].1 {
1793                    assert!(*by_ref, "Expected by_ref=true for `ref mut x`");
1794                    assert!(*is_mut, "Expected is_mut=true for `ref mut x`");
1795                } else {
1796                    panic!("Expected Ident sub-pattern");
1797                }
1798            } else {
1799                panic!("Expected Struct pattern for Some(ref mut x)");
1800            }
1801        } else {
1802            panic!("Expected Match expr");
1803        }
1804    }
1805
1806    #[test]
1807    fn test_pattern_ident_ref_round_trip() {
1808        use syn::token;
1809        // Round-trip: syn::Pat → PurePattern → syn::Pat for ref/ref mut/mut/plain
1810        // (by_ref, is_mut, name)
1811        let cases: &[(bool, bool, &str)] = &[
1812            (true, false, "x"),
1813            (true, true, "x"),
1814            (false, true, "x"),
1815            (false, false, "x"),
1816        ];
1817        for &(expected_by_ref, expected_is_mut, name) in cases {
1818            let syn_pat = syn::Pat::Ident(syn::PatIdent {
1819                attrs: vec![],
1820                by_ref: if expected_by_ref {
1821                    Some(token::Ref::default())
1822                } else {
1823                    None
1824                },
1825                mutability: if expected_is_mut {
1826                    Some(token::Mut::default())
1827                } else {
1828                    None
1829                },
1830                ident: syn::Ident::new(name, proc_macro2::Span::call_site()),
1831                subpat: None,
1832            });
1833            let pure = syn_pat.to_pure();
1834            if let PurePattern::Ident { by_ref, is_mut, .. } = &pure {
1835                assert_eq!(
1836                    *by_ref, expected_by_ref,
1837                    "by_ref mismatch (by_ref={}, is_mut={})",
1838                    expected_by_ref, expected_is_mut
1839                );
1840                assert_eq!(
1841                    *is_mut, expected_is_mut,
1842                    "is_mut mismatch (by_ref={}, is_mut={})",
1843                    expected_by_ref, expected_is_mut
1844                );
1845            } else {
1846                panic!(
1847                    "Expected PurePattern::Ident (by_ref={}, is_mut={})",
1848                    expected_by_ref, expected_is_mut
1849                );
1850            }
1851            // to_syn round-trip
1852            let restored = pure.to_syn().unwrap();
1853            let restored_tokens = restored.to_token_stream().to_string();
1854            let original_tokens = syn_pat.to_token_stream().to_string();
1855            assert_eq!(
1856                restored_tokens, original_tokens,
1857                "Round-trip token mismatch (by_ref={}, is_mut={})",
1858                expected_by_ref, expected_is_mut
1859            );
1860        }
1861    }
1862
1863    // ========== Type Tests ==========
1864
1865    #[test]
1866    fn test_type_path() {
1867        let pure = PureFile::from_source("fn f(x: i32) {}").unwrap();
1868        let f = &pure.functions()[0];
1869        if let PureParam::Typed {
1870            ty: PureType::Path(p),
1871            ..
1872        } = &f.params[0]
1873        {
1874            assert_eq!(p, "i32");
1875        } else {
1876            panic!("Expected Path type");
1877        }
1878    }
1879
1880    #[test]
1881    fn test_type_reference() {
1882        let pure = PureFile::from_source("fn f(x: &str) {}").unwrap();
1883        let f = &pure.functions()[0];
1884        if let PureParam::Typed {
1885            ty: PureType::Ref { is_mut, .. },
1886            ..
1887        } = &f.params[0]
1888        {
1889            assert!(!is_mut);
1890        } else {
1891            panic!("Expected Ref type");
1892        }
1893    }
1894
1895    #[test]
1896    fn test_type_reference_mut() {
1897        let pure = PureFile::from_source("fn f(x: &mut String) {}").unwrap();
1898        let f = &pure.functions()[0];
1899        if let PureParam::Typed {
1900            ty: PureType::Ref { is_mut, .. },
1901            ..
1902        } = &f.params[0]
1903        {
1904            assert!(is_mut);
1905        } else {
1906            panic!("Expected mut Ref type");
1907        }
1908    }
1909
1910    #[test]
1911    fn test_type_reference_with_lifetime() {
1912        let pure = PureFile::from_source("fn f<'a>(x: &'a str) {}").unwrap();
1913        let f = &pure.functions()[0];
1914        if let PureParam::Typed {
1915            ty: PureType::Ref { lifetime, .. },
1916            ..
1917        } = &f.params[0]
1918        {
1919            assert!(lifetime.is_some());
1920            assert!(lifetime.as_ref().unwrap().contains("'a"));
1921        } else {
1922            panic!("Expected Ref type with lifetime");
1923        }
1924    }
1925
1926    #[test]
1927    fn test_type_tuple() {
1928        let pure = PureFile::from_source("fn f(x: (i32, i32)) {}").unwrap();
1929        let f = &pure.functions()[0];
1930        if let PureParam::Typed {
1931            ty: PureType::Tuple(elems),
1932            ..
1933        } = &f.params[0]
1934        {
1935            assert_eq!(elems.len(), 2);
1936        } else {
1937            panic!("Expected Tuple type");
1938        }
1939    }
1940
1941    #[test]
1942    fn test_type_array() {
1943        let pure = PureFile::from_source("fn f(x: [i32; 10]) {}").unwrap();
1944        let f = &pure.functions()[0];
1945        if let PureParam::Typed {
1946            ty: PureType::Array { len, .. },
1947            ..
1948        } = &f.params[0]
1949        {
1950            assert_eq!(len, "10");
1951        } else {
1952            panic!("Expected Array type");
1953        }
1954    }
1955
1956    #[test]
1957    fn test_type_slice() {
1958        let pure = PureFile::from_source("fn f(x: &[i32]) {}").unwrap();
1959        let f = &pure.functions()[0];
1960        if let PureParam::Typed {
1961            ty: PureType::Ref { ty, .. },
1962            ..
1963        } = &f.params[0]
1964        {
1965            if let PureType::Slice(_) = ty.as_ref() {
1966                // ok
1967            } else {
1968                panic!("Expected Slice type");
1969            }
1970        } else {
1971            panic!("Expected Ref to Slice type");
1972        }
1973    }
1974
1975    #[test]
1976    fn test_type_fn() {
1977        let pure = PureFile::from_source("fn f(x: fn(i32) -> i32) {}").unwrap();
1978        let f = &pure.functions()[0];
1979        if let PureParam::Typed {
1980            ty: PureType::Fn { params, ret },
1981            ..
1982        } = &f.params[0]
1983        {
1984            assert_eq!(params.len(), 1);
1985            assert!(ret.is_some());
1986        } else {
1987            panic!("Expected Fn type");
1988        }
1989    }
1990
1991    #[test]
1992    fn test_type_impl_trait() {
1993        let pure =
1994            PureFile::from_source("fn f() -> impl Iterator<Item = i32> { todo!() }").unwrap();
1995        let f = &pure.functions()[0];
1996        if let Some(PureType::ImplTrait(bounds)) = &f.ret {
1997            assert!(!bounds.is_empty());
1998        } else {
1999            panic!("Expected ImplTrait type");
2000        }
2001    }
2002
2003    #[test]
2004    fn test_type_never() {
2005        let pure = PureFile::from_source("fn f() -> ! { panic!() }").unwrap();
2006        let f = &pure.functions()[0];
2007        if let Some(PureType::Never) = &f.ret {
2008            // ok
2009        } else {
2010            panic!("Expected Never type");
2011        }
2012    }
2013
2014    // ========== Generics Tests ==========
2015
2016    #[test]
2017    fn test_generics_type_param() {
2018        let pure = PureFile::from_source("fn f<T>(x: T) {}").unwrap();
2019        let f = &pure.functions()[0];
2020        assert_eq!(f.generics.params.len(), 1);
2021        if let PureGenericParam::Type { name, .. } = &f.generics.params[0] {
2022            assert_eq!(name, "T");
2023        } else {
2024            panic!("Expected Type param");
2025        }
2026    }
2027
2028    #[test]
2029    fn test_generics_type_param_with_bound() {
2030        let pure = PureFile::from_source("fn f<T: Clone>(x: T) {}").unwrap();
2031        let f = &pure.functions()[0];
2032        if let PureGenericParam::Type { bounds, .. } = &f.generics.params[0] {
2033            assert!(!bounds.is_empty());
2034            assert!(bounds[0].contains("Clone"));
2035        } else {
2036            panic!("Expected Type param with bound");
2037        }
2038    }
2039
2040    #[test]
2041    fn test_generics_lifetime_param() {
2042        let pure = PureFile::from_source("fn f<'a>(x: &'a str) {}").unwrap();
2043        let f = &pure.functions()[0];
2044        if let PureGenericParam::Lifetime { name, .. } = &f.generics.params[0] {
2045            assert!(name.contains("'a"));
2046        } else {
2047            panic!("Expected Lifetime param");
2048        }
2049    }
2050
2051    #[test]
2052    fn test_generics_const_param() {
2053        let pure = PureFile::from_source("fn f<const N: usize>() {}").unwrap();
2054        let f = &pure.functions()[0];
2055        if let PureGenericParam::Const { name, ty } = &f.generics.params[0] {
2056            assert_eq!(name, "N");
2057            assert!(ty.contains("usize"));
2058        } else {
2059            panic!("Expected Const param");
2060        }
2061    }
2062
2063    #[test]
2064    fn test_generics_where_clause() {
2065        let pure = PureFile::from_source("fn f<T>(x: T) where T: Clone {}").unwrap();
2066        let f = &pure.functions()[0];
2067        assert!(!f.generics.where_clause.is_empty());
2068        assert!(f.generics.where_clause[0].contains("Clone"));
2069    }
2070
2071    // ========== Function Tests ==========
2072
2073    #[test]
2074    fn test_fn_async() {
2075        let pure = PureFile::from_source("async fn f() {}").unwrap();
2076        assert!(pure.functions()[0].is_async);
2077    }
2078
2079    #[test]
2080    fn test_fn_const() {
2081        let pure = PureFile::from_source("const fn f() {}").unwrap();
2082        assert!(pure.functions()[0].is_const);
2083    }
2084
2085    #[test]
2086    fn test_fn_unsafe() {
2087        let pure = PureFile::from_source("unsafe fn f() {}").unwrap();
2088        assert!(pure.functions()[0].is_unsafe);
2089    }
2090
2091    #[test]
2092    fn test_fn_self_param() {
2093        let pure = PureFile::from_source("impl Foo { fn f(&self) {} }").unwrap();
2094        let impls = pure.impls();
2095        if let PureImplItem::Fn(f) = &impls[0].items[0] {
2096            if let PureParam::SelfValue { is_ref, is_mut } = &f.params[0] {
2097                assert!(is_ref);
2098                assert!(!is_mut);
2099            } else {
2100                panic!("Expected SelfValue param");
2101            }
2102        } else {
2103            panic!("Expected Fn item");
2104        }
2105    }
2106
2107    #[test]
2108    fn test_fn_self_mut_param() {
2109        let pure = PureFile::from_source("impl Foo { fn f(&mut self) {} }").unwrap();
2110        let impls = pure.impls();
2111        if let PureImplItem::Fn(f) = &impls[0].items[0] {
2112            if let PureParam::SelfValue { is_ref, is_mut } = &f.params[0] {
2113                assert!(is_ref);
2114                assert!(is_mut);
2115            } else {
2116                panic!("Expected mut SelfValue param");
2117            }
2118        } else {
2119            panic!("Expected Fn item");
2120        }
2121    }
2122
2123    // ========== Impl Tests ==========
2124
2125    #[test]
2126    fn test_impl_inherent() {
2127        let pure = PureFile::from_source("impl Foo { fn new() -> Self { Foo } }").unwrap();
2128        let impls = pure.impls();
2129        assert_eq!(impls.len(), 1);
2130        assert!(impls[0].trait_.is_none());
2131        assert_eq!(impls[0].self_ty, "Foo");
2132    }
2133
2134    #[test]
2135    fn test_impl_trait() {
2136        let pure = PureFile::from_source("impl Clone for Foo { fn clone(&self) -> Self { Foo } }")
2137            .unwrap();
2138        let impls = pure.impls();
2139        assert_eq!(impls[0].trait_.as_ref().unwrap(), "Clone");
2140    }
2141
2142    #[test]
2143    fn test_impl_unsafe() {
2144        let pure = PureFile::from_source("unsafe impl Send for Foo {}").unwrap();
2145        let impls = pure.impls();
2146        assert!(impls[0].is_unsafe);
2147    }
2148
2149    #[test]
2150    fn test_impl_const() {
2151        let pure = PureFile::from_source("impl Foo { const X: i32 = 42; }").unwrap();
2152        let impls = pure.impls();
2153        if let PureImplItem::Const(c) = &impls[0].items[0] {
2154            assert_eq!(c.name, "X");
2155        } else {
2156            panic!("Expected Const item");
2157        }
2158    }
2159
2160    #[test]
2161    fn test_impl_type() {
2162        let pure = PureFile::from_source(
2163            "impl Iterator for Foo { type Item = i32; fn next(&mut self) -> Option<i32> { None } }",
2164        )
2165        .unwrap();
2166        let impls = pure.impls();
2167        if let PureImplItem::Type(t) = &impls[0].items[0] {
2168            assert_eq!(t.name, "Item");
2169        } else {
2170            panic!("Expected Type item");
2171        }
2172    }
2173
2174    // ========== Trait Tests ==========
2175
2176    #[test]
2177    fn test_trait_simple() {
2178        let pure = PureFile::from_source("trait Foo {}").unwrap();
2179        let traits = pure.traits();
2180        assert_eq!(traits.len(), 1);
2181        assert_eq!(traits[0].name, "Foo");
2182    }
2183
2184    #[test]
2185    fn test_trait_unsafe() {
2186        let pure = PureFile::from_source("unsafe trait Foo {}").unwrap();
2187        assert!(pure.traits()[0].is_unsafe);
2188    }
2189
2190    #[test]
2191    fn test_trait_with_supertraits() {
2192        let pure = PureFile::from_source("trait Foo: Clone + Send {}").unwrap();
2193        let traits = pure.traits();
2194        assert_eq!(traits[0].supertraits.len(), 2);
2195    }
2196
2197    #[test]
2198    fn test_trait_with_method() {
2199        let pure = PureFile::from_source("trait Foo { fn bar(&self); }").unwrap();
2200        let traits = pure.traits();
2201        if let PureTraitItem::Fn(f) = &traits[0].items[0] {
2202            assert_eq!(f.name, "bar");
2203        } else {
2204            panic!("Expected Fn item");
2205        }
2206    }
2207
2208    #[test]
2209    fn test_trait_with_default_method() {
2210        let pure = PureFile::from_source("trait Foo { fn bar(&self) { } }").unwrap();
2211        let traits = pure.traits();
2212        if let PureTraitItem::Fn(f) = &traits[0].items[0] {
2213            assert!(!f.body.stmts.is_empty() || f.body.stmts.is_empty()); // has body
2214        } else {
2215            panic!("Expected Fn item");
2216        }
2217    }
2218
2219    #[test]
2220    fn test_trait_with_associated_type() {
2221        let pure = PureFile::from_source("trait Foo { type Item; }").unwrap();
2222        let traits = pure.traits();
2223        if let PureTraitItem::Type { name, .. } = &traits[0].items[0] {
2224            assert_eq!(name, "Item");
2225        } else {
2226            panic!("Expected Type item");
2227        }
2228    }
2229
2230    #[test]
2231    fn test_trait_with_associated_const() {
2232        let pure = PureFile::from_source("trait Foo { const X: i32; }").unwrap();
2233        let traits = pure.traits();
2234        if let PureTraitItem::Const(c) = &traits[0].items[0] {
2235            assert_eq!(c.name, "X");
2236        } else {
2237            panic!("Expected Const item");
2238        }
2239    }
2240
2241    // ========== Enum Tests ==========
2242
2243    #[test]
2244    fn test_enum_simple() {
2245        let pure = PureFile::from_source("enum Color { Red, Green, Blue }").unwrap();
2246        let enums: Vec<_> = pure
2247            .items
2248            .iter()
2249            .filter_map(|i| {
2250                if let PureItem::Enum(e) = i {
2251                    Some(e)
2252                } else {
2253                    None
2254                }
2255            })
2256            .collect();
2257        assert_eq!(enums.len(), 1);
2258        assert_eq!(enums[0].variants.len(), 3);
2259    }
2260
2261    #[test]
2262    fn test_enum_with_discriminant() {
2263        let pure = PureFile::from_source("enum Num { One = 1, Two = 2 }").unwrap();
2264        if let PureItem::Enum(e) = &pure.items[0] {
2265            assert!(e.variants[0].discriminant.is_some());
2266        } else {
2267            panic!("Expected Enum item");
2268        }
2269    }
2270
2271    #[test]
2272    fn test_enum_tuple_variant() {
2273        let pure = PureFile::from_source("enum Opt { Some(i32), None }").unwrap();
2274        if let PureItem::Enum(e) = &pure.items[0] {
2275            if let PureFields::Tuple(types) = &e.variants[0].fields {
2276                assert_eq!(types.len(), 1);
2277            } else {
2278                panic!("Expected Tuple fields");
2279            }
2280        } else {
2281            panic!("Expected Enum item");
2282        }
2283    }
2284
2285    #[test]
2286    fn test_enum_struct_variant() {
2287        let pure = PureFile::from_source("enum Msg { Move { x: i32, y: i32 } }").unwrap();
2288        if let PureItem::Enum(e) = &pure.items[0] {
2289            if let PureFields::Named(fields) = &e.variants[0].fields {
2290                assert_eq!(fields.len(), 2);
2291            } else {
2292                panic!("Expected Named fields");
2293            }
2294        } else {
2295            panic!("Expected Enum item");
2296        }
2297    }
2298
2299    // ========== Struct Tests ==========
2300
2301    #[test]
2302    fn test_struct_unit() {
2303        let pure = PureFile::from_source("struct Unit;").unwrap();
2304        let structs = pure.structs();
2305        assert!(matches!(structs[0].fields, PureFields::Unit));
2306    }
2307
2308    #[test]
2309    fn test_struct_tuple() {
2310        let pure = PureFile::from_source("struct Point(i32, i32);").unwrap();
2311        let structs = pure.structs();
2312        if let PureFields::Tuple(types) = &structs[0].fields {
2313            assert_eq!(types.len(), 2);
2314        } else {
2315            panic!("Expected Tuple fields");
2316        }
2317    }
2318
2319    // ========== Visibility Tests ==========
2320
2321    #[test]
2322    fn test_vis_public() {
2323        let pure = PureFile::from_source("pub fn f() {}").unwrap();
2324        assert!(matches!(pure.functions()[0].vis, PureVis::Public));
2325    }
2326
2327    #[test]
2328    fn test_vis_private() {
2329        let pure = PureFile::from_source("fn f() {}").unwrap();
2330        assert!(matches!(pure.functions()[0].vis, PureVis::Private));
2331    }
2332
2333    #[test]
2334    fn test_vis_crate() {
2335        let pure = PureFile::from_source("pub(crate) fn f() {}").unwrap();
2336        assert!(matches!(pure.functions()[0].vis, PureVis::Crate));
2337    }
2338
2339    #[test]
2340    fn test_vis_super() {
2341        let pure = PureFile::from_source("pub(super) fn f() {}").unwrap();
2342        assert!(matches!(pure.functions()[0].vis, PureVis::Super));
2343    }
2344
2345    #[test]
2346    fn test_vis_in_path() {
2347        let pure = PureFile::from_source("pub(in crate::foo) fn f() {}").unwrap();
2348        if let PureVis::In(path) = &pure.functions()[0].vis {
2349            assert!(path.contains("crate"));
2350        } else {
2351            panic!("Expected In visibility");
2352        }
2353    }
2354
2355    // ========== Attribute Tests ==========
2356
2357    #[test]
2358    fn test_attr_derive() {
2359        let pure = PureFile::from_source("#[derive(Clone, Debug)] struct Foo;").unwrap();
2360        let attrs = &pure.structs()[0].attrs;
2361        assert_eq!(attrs.len(), 1);
2362        assert_eq!(attrs[0].path, "derive");
2363    }
2364
2365    #[test]
2366    fn test_attr_simple() {
2367        let pure = PureFile::from_source("#[test] fn f() {}").unwrap();
2368        let attrs = &pure.functions()[0].attrs;
2369        assert_eq!(attrs.len(), 1);
2370        assert_eq!(attrs[0].path, "test");
2371    }
2372
2373    #[test]
2374    fn test_attr_inner() {
2375        let pure = PureFile::from_source("#![allow(unused)]").unwrap();
2376        assert!(pure.attrs[0].is_inner);
2377    }
2378
2379    #[test]
2380    fn test_attr_outer() {
2381        let pure = PureFile::from_source("#[allow(unused)] fn f() {}").unwrap();
2382        assert!(!pure.functions()[0].attrs[0].is_inner);
2383    }
2384
2385    // ========== Use Tests ==========
2386
2387    #[test]
2388    fn test_use_path() {
2389        let pure = PureFile::from_source("use std::io::Read;").unwrap();
2390        let uses = pure.uses();
2391        // Navigate the tree: std -> io -> Read
2392        if let PureUseTree::Path { path, tree } = &uses[0].tree {
2393            assert_eq!(path, "std");
2394            if let PureUseTree::Path { path, tree } = tree.as_ref() {
2395                assert_eq!(path, "io");
2396                if let PureUseTree::Name(name) = tree.as_ref() {
2397                    assert_eq!(name, "Read");
2398                } else {
2399                    panic!("Expected Name");
2400                }
2401            } else {
2402                panic!("Expected Path");
2403            }
2404        } else {
2405            panic!("Expected Path");
2406        }
2407    }
2408
2409    #[test]
2410    fn test_use_glob() {
2411        let pure = PureFile::from_source("use std::io::*;").unwrap();
2412        let uses = pure.uses();
2413        if let PureUseTree::Path { tree, .. } = &uses[0].tree {
2414            if let PureUseTree::Path { tree, .. } = tree.as_ref() {
2415                assert!(matches!(tree.as_ref(), PureUseTree::Glob));
2416            } else {
2417                panic!("Expected Path");
2418            }
2419        } else {
2420            panic!("Expected Path");
2421        }
2422    }
2423
2424    #[test]
2425    fn test_use_group() {
2426        let pure = PureFile::from_source("use std::{io, fs};").unwrap();
2427        let uses = pure.uses();
2428        if let PureUseTree::Path { tree, .. } = &uses[0].tree {
2429            if let PureUseTree::Group(items) = tree.as_ref() {
2430                assert_eq!(items.len(), 2);
2431            } else {
2432                panic!("Expected Group");
2433            }
2434        } else {
2435            panic!("Expected Path");
2436        }
2437    }
2438
2439    #[test]
2440    fn test_use_rename() {
2441        let pure = PureFile::from_source("use std::io::Result as IoResult;").unwrap();
2442        let uses = pure.uses();
2443        // Navigate to the rename
2444        if let PureUseTree::Path { tree, .. } = &uses[0].tree {
2445            if let PureUseTree::Path { tree, .. } = tree.as_ref() {
2446                if let PureUseTree::Rename { name, rename } = tree.as_ref() {
2447                    assert_eq!(name, "Result");
2448                    assert_eq!(rename, "IoResult");
2449                } else {
2450                    panic!("Expected Rename");
2451                }
2452            } else {
2453                panic!("Expected Path");
2454            }
2455        } else {
2456            panic!("Expected Path");
2457        }
2458    }
2459
2460    // ========== Const/Static Tests ==========
2461
2462    #[test]
2463    fn test_const_item() {
2464        let pure = PureFile::from_source("const MAX: i32 = 100;").unwrap();
2465        if let PureItem::Const(c) = &pure.items[0] {
2466            assert_eq!(c.name, "MAX");
2467        } else {
2468            panic!("Expected Const item");
2469        }
2470    }
2471
2472    #[test]
2473    fn test_static_item() {
2474        let pure = PureFile::from_source("static mut COUNTER: i32 = 0;").unwrap();
2475        if let PureItem::Static(s) = &pure.items[0] {
2476            assert!(s.is_mut);
2477        } else {
2478            panic!("Expected Static item");
2479        }
2480    }
2481
2482    // ========== Type Alias Tests ==========
2483
2484    #[test]
2485    fn test_type_alias() {
2486        let pure =
2487            PureFile::from_source("type Result<T> = std::result::Result<T, Error>;").unwrap();
2488        if let PureItem::Type(t) = &pure.items[0] {
2489            assert_eq!(t.name, "Result");
2490        } else {
2491            panic!("Expected Type item");
2492        }
2493    }
2494
2495    // ========== Module Tests ==========
2496
2497    #[test]
2498    fn test_mod_declaration() {
2499        let pure = PureFile::from_source("mod foo;").unwrap();
2500        if let PureItem::Mod(m) = &pure.items[0] {
2501            assert!(m.items.is_empty());
2502        } else {
2503            panic!("Expected Mod item");
2504        }
2505    }
2506
2507    #[test]
2508    fn test_mod_inline() {
2509        let pure = PureFile::from_source("mod foo { fn bar() {} }").unwrap();
2510        if let PureItem::Mod(m) = &pure.items[0] {
2511            assert!(!m.items.is_empty());
2512            assert_eq!(m.items.len(), 1);
2513        } else {
2514            panic!("Expected Mod item");
2515        }
2516    }
2517
2518    // ========== Macro Tests ==========
2519
2520    #[test]
2521    fn test_macro_invocation() {
2522        let pure = PureFile::from_source("include!(\"foo.rs\");").unwrap();
2523        if let PureItem::Macro(m) = &pure.items[0] {
2524            assert_eq!(m.path, "include");
2525        } else {
2526            panic!("Expected Macro item");
2527        }
2528    }
2529
2530    // ========== Match Arm Tests ==========
2531
2532    #[test]
2533    fn test_match_arm_guard() {
2534        let pure =
2535            PureFile::from_source("fn f(x: i32) { match x { n if n > 0 => {} _ => {} } }").unwrap();
2536        let f = &pure.functions()[0];
2537        if let PureStmt::Expr(PureExpr::Match { arms, .. }) = &f.body.stmts[0] {
2538            assert!(arms[0].guard.is_some());
2539        } else {
2540            panic!("Expected Match expr");
2541        }
2542    }
2543
2544    // ========== Helper Function Tests ==========
2545
2546    #[test]
2547    fn test_path_to_string_simple() {
2548        assert_eq!(
2549            path_to_string(&syn::parse_str::<syn::Path>("std").unwrap()),
2550            "std"
2551        );
2552    }
2553
2554    #[test]
2555    fn test_path_to_string_nested() {
2556        assert_eq!(
2557            path_to_string(&syn::parse_str::<syn::Path>("std::io::Read").unwrap()),
2558            "std::io::Read"
2559        );
2560    }
2561
2562    #[test]
2563    fn test_path_to_string_with_generics() {
2564        let path = syn::parse_str::<syn::Path>("Vec<i32>").unwrap();
2565        let result = path_to_string(&path);
2566        assert!(result.contains("Vec"));
2567        assert!(result.contains("i32"));
2568    }
2569
2570    // ========== Async Inference Tests ==========
2571
2572    #[test]
2573    fn test_is_pinned_boxed_future_basic() {
2574        assert!(is_pinned_boxed_future("Pin<Box<dyn Future<Output = ()>>>"));
2575        assert!(is_pinned_boxed_future(
2576            "Pin<Box<dyn Future<Output = Result<T, E>>>>"
2577        ));
2578        assert!(is_pinned_boxed_future(
2579            "Pin<Box<dyn Future<Output = ()> + Send>>"
2580        ));
2581        assert!(is_pinned_boxed_future(
2582            "Pin<Box<dyn Future<Output = ()> + Send + 'static>>"
2583        ));
2584    }
2585
2586    #[test]
2587    fn test_is_pinned_boxed_future_with_spaces() {
2588        // Token stream adds spaces between tokens
2589        assert!(is_pinned_boxed_future(
2590            "Pin < Box < dyn Future < Output = () > > >"
2591        ));
2592        assert!(is_pinned_boxed_future(
2593            "Pin < Box < dyn Future < Output = Result < T , E > > + Send > >"
2594        ));
2595    }
2596
2597    #[test]
2598    fn test_is_pinned_boxed_future_with_path_prefix() {
2599        assert!(is_pinned_boxed_future(
2600            "::core::pin::Pin<Box<dyn Future<Output = ()>>>"
2601        ));
2602        assert!(is_pinned_boxed_future(
2603            "core::pin::Pin<Box<dyn Future<Output = ()>>>"
2604        ));
2605        assert!(is_pinned_boxed_future(
2606            "std::pin::Pin<Box<dyn Future<Output = ()>>>"
2607        ));
2608        assert!(is_pinned_boxed_future(
2609            "::std::pin::Pin<Box<dyn Future<Output = ()>>>"
2610        ));
2611    }
2612
2613    #[test]
2614    fn test_is_pinned_boxed_future_negative() {
2615        assert!(!is_pinned_boxed_future("Result<T, E>"));
2616        assert!(!is_pinned_boxed_future("Option<T>"));
2617        assert!(!is_pinned_boxed_future("Box<dyn Future<Output = ()>>"));
2618        assert!(!is_pinned_boxed_future("Pin<Box<T>>"));
2619        assert!(!is_pinned_boxed_future("Future<Output = ()>"));
2620    }
2621
2622    #[test]
2623    fn test_async_fn_is_async() {
2624        let pure = PureFile::from_source("async fn foo() {}").unwrap();
2625        let f = &pure.functions()[0];
2626        assert!(f.is_async);
2627        assert!(!f.is_async_inferred);
2628        assert!(f.is_effectively_async());
2629    }
2630
2631    #[test]
2632    fn test_sync_fn_not_async() {
2633        let pure = PureFile::from_source("fn foo() {}").unwrap();
2634        let f = &pure.functions()[0];
2635        assert!(!f.is_async);
2636        assert!(!f.is_async_inferred);
2637        assert!(!f.is_effectively_async());
2638    }
2639
2640    #[test]
2641    fn test_async_trait_style_fn_inferred() {
2642        // Simulates what #[async_trait] produces after macro expansion
2643        let code = r#"
2644            fn foo(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
2645                Box::pin(async { })
2646            }
2647        "#;
2648        let pure = PureFile::from_source(code).unwrap();
2649        let f = &pure.functions()[0];
2650        assert!(!f.is_async, "Should not be explicitly async");
2651        assert!(
2652            f.is_async_inferred,
2653            "Should be inferred as async from return type"
2654        );
2655        assert!(f.is_effectively_async());
2656    }
2657
2658    #[test]
2659    fn test_async_trait_in_impl() {
2660        let code = r#"
2661            struct Foo;
2662            impl Foo {
2663                fn bar(&self) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send>> {
2664                    Box::pin(async { Ok(()) })
2665                }
2666            }
2667        "#;
2668        let pure = PureFile::from_source(code).unwrap();
2669        let impls = pure.impls();
2670        assert_eq!(impls.len(), 1);
2671
2672        if let PureImplItem::Fn(method) = &impls[0].items[0] {
2673            assert!(!method.is_async);
2674            assert!(method.is_async_inferred);
2675            assert!(method.is_effectively_async());
2676        } else {
2677            panic!("Expected Fn in impl");
2678        }
2679    }
2680
2681    // --- ModScope / cfg(test) round-trip tests ---
2682
2683    #[test]
2684    fn test_to_pure_cfg_test_promotes_to_modscope_test() {
2685        // #[cfg(test)] mod tests; → scope == Test, cfg(test) removed from attrs
2686        let item_mod = syn::parse_str::<syn::ItemMod>("#[cfg(test)] mod tests;").unwrap();
2687        let pure_mod = item_mod.to_pure();
2688        assert_eq!(pure_mod.scope, ModScope::Test, "scope should be Test");
2689        assert!(
2690            pure_mod.attrs.is_empty(),
2691            "cfg(test) should be removed from attrs, got: {:?}",
2692            pure_mod.attrs
2693        );
2694        assert_eq!(pure_mod.name, "tests");
2695    }
2696
2697    #[test]
2698    fn test_to_pure_cfg_not_test_stays_in_attrs() {
2699        // #[cfg(not(test))] mod foo; → scope == Prod, cfg(not(test)) stays in attrs
2700        let item_mod = syn::parse_str::<syn::ItemMod>("#[cfg(not(test))] mod foo;").unwrap();
2701        let pure_mod = item_mod.to_pure();
2702        assert_eq!(pure_mod.scope, ModScope::Prod, "scope should be Prod");
2703        assert_eq!(
2704            pure_mod.attrs.len(),
2705            1,
2706            "cfg(not(test)) should remain in attrs"
2707        );
2708        assert_eq!(pure_mod.attrs[0].path, "cfg");
2709        assert_eq!(
2710            pure_mod.attrs[0].meta,
2711            PureAttrMeta::List("not (test)".to_string())
2712        );
2713    }
2714
2715    #[test]
2716    fn test_to_pure_cfg_feature_stays_in_attrs() {
2717        // #[cfg(feature = "bar")] mod foo; → scope == Prod, attr stays
2718        let item_mod =
2719            syn::parse_str::<syn::ItemMod>("#[cfg(feature = \"bar\")] mod foo;").unwrap();
2720        let pure_mod = item_mod.to_pure();
2721        assert_eq!(pure_mod.scope, ModScope::Prod, "scope should be Prod");
2722        assert_eq!(
2723            pure_mod.attrs.len(),
2724            1,
2725            "cfg(feature=...) should remain in attrs"
2726        );
2727        assert_eq!(pure_mod.attrs[0].path, "cfg");
2728    }
2729
2730    #[test]
2731    fn test_to_pure_cfg_test_inline_mod_promotes() {
2732        // Inline module with #[cfg(test)]: scope == Test, cfg(test) removed from attrs
2733        let item_mod =
2734            syn::parse_str::<syn::ItemMod>("#[cfg(test)] mod tests { fn helper() {} }").unwrap();
2735        let pure_mod = item_mod.to_pure();
2736        assert_eq!(pure_mod.scope, ModScope::Test, "scope should be Test");
2737        assert!(
2738            pure_mod.attrs.is_empty(),
2739            "cfg(test) should be removed from attrs"
2740        );
2741        assert_eq!(pure_mod.items.len(), 1);
2742    }
2743
2744    fn is_syn_cfg_test_attr(attr: &syn::Attribute) -> bool {
2745        use quote::ToTokens;
2746        if !matches!(attr.style, syn::AttrStyle::Outer) {
2747            return false;
2748        }
2749        let path_str = attr.path().to_token_stream().to_string();
2750        if path_str != "cfg" {
2751            return false;
2752        }
2753        let meta_str = attr.to_token_stream().to_string();
2754        meta_str.contains("cfg") && meta_str.contains("test")
2755    }
2756
2757    #[test]
2758    fn test_to_pure_cfg_test_roundtrip() {
2759        // parse → to_pure → to_syn round-trip: syn output must have exactly one #[cfg(test)] attr
2760        let item_mod = syn::parse_str::<syn::ItemMod>("#[cfg(test)] mod tests {}").unwrap();
2761        let pure_mod = item_mod.to_pure();
2762        assert_eq!(pure_mod.scope, ModScope::Test);
2763        assert!(
2764            pure_mod.attrs.is_empty(),
2765            "cfg(test) should be removed from attrs at parse"
2766        );
2767
2768        let syn_out = pure_mod.to_syn().unwrap();
2769        let cfg_test_count = syn_out
2770            .attrs
2771            .iter()
2772            .filter(|a| is_syn_cfg_test_attr(a))
2773            .count();
2774        assert_eq!(
2775            cfg_test_count, 1,
2776            "#[cfg(test)] should appear exactly once after round-trip, got {} times",
2777            cfg_test_count
2778        );
2779    }
2780}