Skip to main content

oracle_lib/analyzer/
parser.rs

1//! Rust source code parser using syn
2
3use crate::analyzer::types::*;
4use crate::error::Result;
5use quote::ToTokens;
6use std::fs;
7use std::path::{Path, PathBuf};
8use syn::{
9    File, Item, ItemConst, ItemEnum, ItemFn, ItemImpl, ItemStatic, ItemStruct, ItemTrait, ItemType,
10};
11
12/// Rust source code analyzer using syn for parsing
13pub struct RustAnalyzer {
14    include_private: bool,
15}
16
17impl RustAnalyzer {
18    pub fn new() -> Self {
19        Self {
20            include_private: true,
21        }
22    }
23
24    pub fn with_private(mut self, include: bool) -> Self {
25        self.include_private = include;
26        self
27    }
28
29    /// Analyze a Rust source file
30    pub fn analyze_file(&self, path: &Path) -> Result<Vec<AnalyzedItem>> {
31        let content = fs::read_to_string(path)?;
32        self.analyze_source_with_path(&content, Some(path.to_path_buf()))
33    }
34
35    /// Analyze a Rust source file with a base module path prefix
36    pub fn analyze_file_with_module(
37        &self,
38        path: &Path,
39        module_path: Vec<String>,
40    ) -> Result<Vec<AnalyzedItem>> {
41        let content = fs::read_to_string(path)?;
42        self.analyze_source_with_module(&content, Some(path.to_path_buf()), module_path)
43    }
44
45    /// Analyze Rust source code from a string
46    pub fn analyze_source(&self, source: &str) -> Result<Vec<AnalyzedItem>> {
47        self.analyze_source_with_path(source, None)
48    }
49
50    /// Analyze Rust source code with optional file path
51    pub fn analyze_source_with_path(
52        &self,
53        source: &str,
54        path: Option<PathBuf>,
55    ) -> Result<Vec<AnalyzedItem>> {
56        // Derive module path from file path
57        let module_path = path
58            .as_ref()
59            .map(|p| Self::derive_module_path(p))
60            .unwrap_or_default();
61        self.analyze_source_with_module(source, path, module_path)
62    }
63
64    /// Analyze Rust source code with explicit module path
65    pub fn analyze_source_with_module(
66        &self,
67        source: &str,
68        path: Option<PathBuf>,
69        module_path: Vec<String>,
70    ) -> Result<Vec<AnalyzedItem>> {
71        let syntax_tree: File = syn::parse_str(source)?;
72        let mut items = Vec::new();
73
74        for item in syntax_tree.items {
75            // Inline modules: expand inner items as first-class AnalyzedItems with synthetic path
76            if let Item::Mod(md) = &item {
77                if let Some((_, ref content)) = &md.content {
78                    let child_path: Vec<String> = {
79                        let mut p = module_path.clone();
80                        p.push(md.ident.to_string());
81                        p
82                    };
83                    let inner = self.collect_inline_module_items(content, &path, child_path);
84                    items.extend(inner);
85                }
86            }
87
88            if let Some(mut analyzed) = self.analyze_item(&item, &path) {
89                Self::set_module_path(&mut analyzed, module_path.clone());
90
91                if let Some(ref file_path) = path {
92                    if let Some(span) = Self::get_item_span(&item) {
93                        let line = span.start().line;
94                        Self::set_source_location(&mut analyzed, file_path.clone(), line);
95                    }
96                }
97
98                if self.include_private || self.is_public(&analyzed) {
99                    items.push(analyzed);
100                }
101            }
102        }
103
104        Ok(items)
105    }
106
107    /// Recursively collect items from inline module content as first-class AnalyzedItems.
108    fn collect_inline_module_items(
109        &self,
110        content: &[Item],
111        path: &Option<PathBuf>,
112        module_path: Vec<String>,
113    ) -> Vec<AnalyzedItem> {
114        let mut items = Vec::new();
115        for item in content {
116            if let Item::Mod(md) = item {
117                if let Some((_, ref inner_content)) = &md.content {
118                    let child_path: Vec<String> = {
119                        let mut p = module_path.clone();
120                        p.push(md.ident.to_string());
121                        p
122                    };
123                    let inner = self.collect_inline_module_items(inner_content, path, child_path);
124                    items.extend(inner);
125                }
126            }
127            if let Some(mut analyzed) = self.analyze_item(item, path) {
128                Self::set_module_path(&mut analyzed, module_path.clone());
129                if let Some(ref file_path) = path {
130                    if let Some(span) = Self::get_item_span(item) {
131                        let line = span.start().line;
132                        Self::set_source_location(&mut analyzed, file_path.clone(), line);
133                    }
134                }
135                if self.include_private || self.is_public(&analyzed) {
136                    items.push(analyzed);
137                }
138            }
139        }
140        items
141    }
142
143    /// Derive module path from file path (e.g., src/analyzer/parser.rs -> ["analyzer", "parser"])
144    fn derive_module_path(path: &Path) -> Vec<String> {
145        let mut components: Vec<String> = path
146            .iter()
147            .filter_map(|c| c.to_str())
148            .map(|s| s.to_string())
149            .collect();
150
151        // Remove file extension from last component
152        if let Some(last) = components.last_mut() {
153            if last.ends_with(".rs") {
154                *last = last.trim_end_matches(".rs").to_string();
155            }
156        }
157
158        // Find 'src' directory and take everything after it
159        if let Some(src_pos) = components.iter().position(|c| c == "src") {
160            components = components[src_pos + 1..].to_vec();
161        }
162
163        // Remove special module names
164        components.retain(|c| c != "lib" && c != "main" && c != "mod");
165
166        components
167    }
168
169    fn set_module_path(item: &mut AnalyzedItem, path: Vec<String>) {
170        match item {
171            AnalyzedItem::Function(f) => f.module_path = path,
172            AnalyzedItem::Struct(s) => s.module_path = path,
173            AnalyzedItem::Enum(e) => e.module_path = path,
174            AnalyzedItem::Trait(t) => t.module_path = path,
175            AnalyzedItem::Impl(i) => i.module_path = path,
176            AnalyzedItem::Module(m) => m.module_path = path,
177            AnalyzedItem::TypeAlias(t) => t.module_path = path,
178            AnalyzedItem::Const(c) => c.module_path = path,
179            AnalyzedItem::Static(s) => s.module_path = path,
180        }
181    }
182
183    fn get_item_span(item: &Item) -> Option<proc_macro2::Span> {
184        match item {
185            Item::Fn(f) => Some(f.sig.ident.span()),
186            Item::Struct(s) => Some(s.ident.span()),
187            Item::Enum(e) => Some(e.ident.span()),
188            Item::Trait(t) => Some(t.ident.span()),
189            Item::Impl(i) => Some(i.impl_token.span),
190            Item::Mod(m) => Some(m.ident.span()),
191            Item::Type(t) => Some(t.ident.span()),
192            Item::Const(c) => Some(c.ident.span()),
193            Item::Static(s) => Some(s.ident.span()),
194            _ => None,
195        }
196    }
197
198    fn set_source_location(item: &mut AnalyzedItem, file: PathBuf, line: usize) {
199        let loc = SourceLocation::new(file, line);
200        match item {
201            AnalyzedItem::Function(f) => f.source_location = loc,
202            AnalyzedItem::Struct(s) => s.source_location = loc,
203            AnalyzedItem::Enum(e) => e.source_location = loc,
204            AnalyzedItem::Trait(t) => t.source_location = loc,
205            AnalyzedItem::Impl(i) => i.source_location = loc,
206            AnalyzedItem::Module(m) => m.source_location = loc,
207            AnalyzedItem::TypeAlias(t) => t.source_location = loc,
208            AnalyzedItem::Const(c) => c.source_location = loc,
209            AnalyzedItem::Static(s) => s.source_location = loc,
210        }
211    }
212
213    fn is_public(&self, item: &AnalyzedItem) -> bool {
214        matches!(item.visibility(), Some(Visibility::Public))
215    }
216
217    fn analyze_item(&self, item: &Item, _path: &Option<PathBuf>) -> Option<AnalyzedItem> {
218        match item {
219            Item::Fn(func) => Some(self.analyze_function(func)),
220            Item::Struct(st) => Some(self.analyze_struct(st)),
221            Item::Enum(en) => Some(self.analyze_enum(en)),
222            Item::Trait(tr) => Some(self.analyze_trait(tr)),
223            Item::Impl(im) => Some(self.analyze_impl(im)),
224            Item::Mod(md) => Some(self.analyze_module(md)),
225            Item::Type(ty) => Some(self.analyze_type_alias(ty)),
226            Item::Const(c) => Some(self.analyze_const(c)),
227            Item::Static(s) => Some(self.analyze_static(s)),
228            _ => None,
229        }
230    }
231
232    fn analyze_function(&self, func: &ItemFn) -> AnalyzedItem {
233        let name = func.sig.ident.to_string();
234        let signature = func.sig.to_token_stream().to_string();
235        let visibility = Self::parse_visibility(&func.vis);
236        let is_async = func.sig.asyncness.is_some();
237        let is_const = func.sig.constness.is_some();
238        let is_unsafe = func.sig.unsafety.is_some();
239
240        let generics = Self::extract_generics(&func.sig.generics);
241        let parameters = Self::extract_parameters(&func.sig.inputs);
242        let return_type = Self::extract_return_type(&func.sig.output);
243        let where_clause = Self::extract_where_clause(&func.sig.generics.where_clause);
244        let documentation = Self::extract_docs(&func.attrs);
245        let attributes = Self::extract_attributes(&func.attrs);
246
247        AnalyzedItem::Function(FunctionInfo {
248            name,
249            signature,
250            visibility,
251            is_async,
252            is_const,
253            is_unsafe,
254            generics,
255            parameters,
256            return_type,
257            documentation,
258            attributes,
259            where_clause,
260            source_location: SourceLocation::default(),
261            module_path: Vec::new(),
262        })
263    }
264
265    fn analyze_struct(&self, st: &ItemStruct) -> AnalyzedItem {
266        let name = st.ident.to_string();
267        let visibility = Self::parse_visibility(&st.vis);
268        let generics = Self::extract_generics(&st.generics);
269        let where_clause = Self::extract_where_clause(&st.generics.where_clause);
270
271        let (fields, kind) = match &st.fields {
272            syn::Fields::Named(named) => {
273                let fields = named
274                    .named
275                    .iter()
276                    .map(|f| Field {
277                        name: f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default(),
278                        ty: f.ty.to_token_stream().to_string(),
279                        visibility: Self::parse_visibility(&f.vis),
280                        documentation: Self::extract_docs(&f.attrs),
281                    })
282                    .collect();
283                (fields, StructKind::Named)
284            }
285            syn::Fields::Unnamed(unnamed) => {
286                let fields = unnamed
287                    .unnamed
288                    .iter()
289                    .enumerate()
290                    .map(|(i, f)| Field {
291                        name: i.to_string(),
292                        ty: f.ty.to_token_stream().to_string(),
293                        visibility: Self::parse_visibility(&f.vis),
294                        documentation: Self::extract_docs(&f.attrs),
295                    })
296                    .collect();
297                (fields, StructKind::Tuple)
298            }
299            syn::Fields::Unit => (vec![], StructKind::Unit),
300        };
301
302        let derives = Self::extract_derives(&st.attrs);
303        let documentation = Self::extract_docs(&st.attrs);
304        let attributes = Self::extract_attributes(&st.attrs);
305
306        AnalyzedItem::Struct(StructInfo {
307            name,
308            visibility,
309            generics,
310            fields,
311            kind,
312            documentation,
313            derives,
314            attributes,
315            where_clause,
316            source_location: SourceLocation::default(),
317            module_path: Vec::new(),
318        })
319    }
320
321    fn analyze_enum(&self, en: &ItemEnum) -> AnalyzedItem {
322        let name = en.ident.to_string();
323        let visibility = Self::parse_visibility(&en.vis);
324        let generics = Self::extract_generics(&en.generics);
325        let where_clause = Self::extract_where_clause(&en.generics.where_clause);
326
327        let variants = en
328            .variants
329            .iter()
330            .map(|v| {
331                let fields = match &v.fields {
332                    syn::Fields::Named(named) => {
333                        let fields = named
334                            .named
335                            .iter()
336                            .map(|f| Field {
337                                name: f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default(),
338                                ty: f.ty.to_token_stream().to_string(),
339                                visibility: Self::parse_visibility(&f.vis),
340                                documentation: Self::extract_docs(&f.attrs),
341                            })
342                            .collect();
343                        VariantFields::Named(fields)
344                    }
345                    syn::Fields::Unnamed(unnamed) => {
346                        let types = unnamed
347                            .unnamed
348                            .iter()
349                            .map(|f| f.ty.to_token_stream().to_string())
350                            .collect();
351                        VariantFields::Unnamed(types)
352                    }
353                    syn::Fields::Unit => VariantFields::Unit,
354                };
355
356                let discriminant = v
357                    .discriminant
358                    .as_ref()
359                    .map(|(_, expr)| expr.to_token_stream().to_string());
360
361                Variant {
362                    name: v.ident.to_string(),
363                    fields,
364                    discriminant,
365                    documentation: Self::extract_docs(&v.attrs),
366                }
367            })
368            .collect();
369
370        let derives = Self::extract_derives(&en.attrs);
371        let documentation = Self::extract_docs(&en.attrs);
372        let attributes = Self::extract_attributes(&en.attrs);
373
374        AnalyzedItem::Enum(EnumInfo {
375            name,
376            visibility,
377            generics,
378            variants,
379            documentation,
380            derives,
381            attributes,
382            where_clause,
383            source_location: SourceLocation::default(),
384            module_path: Vec::new(),
385        })
386    }
387
388    fn analyze_trait(&self, tr: &ItemTrait) -> AnalyzedItem {
389        let name = tr.ident.to_string();
390        let visibility = Self::parse_visibility(&tr.vis);
391        let is_unsafe = tr.unsafety.is_some();
392        let is_auto = tr.auto_token.is_some();
393        let generics = Self::extract_generics(&tr.generics);
394        let where_clause = Self::extract_where_clause(&tr.generics.where_clause);
395
396        let supertraits = tr
397            .supertraits
398            .iter()
399            .map(|t| t.to_token_stream().to_string())
400            .collect();
401
402        let mut methods = Vec::new();
403        let mut associated_types = Vec::new();
404        let mut associated_consts = Vec::new();
405
406        for item in &tr.items {
407            match item {
408                syn::TraitItem::Fn(method) => {
409                    methods.push(TraitMethod {
410                        name: method.sig.ident.to_string(),
411                        signature: method.sig.to_token_stream().to_string(),
412                        has_default: method.default.is_some(),
413                        is_async: method.sig.asyncness.is_some(),
414                        documentation: Self::extract_docs(&method.attrs),
415                    });
416                }
417                syn::TraitItem::Type(ty) => {
418                    associated_types.push(AssociatedType {
419                        name: ty.ident.to_string(),
420                        bounds: ty
421                            .bounds
422                            .iter()
423                            .map(|b| b.to_token_stream().to_string())
424                            .collect(),
425                        default: ty
426                            .default
427                            .as_ref()
428                            .map(|(_, t)| t.to_token_stream().to_string()),
429                    });
430                }
431                syn::TraitItem::Const(c) => {
432                    associated_consts.push(AssociatedConst {
433                        name: c.ident.to_string(),
434                        ty: c.ty.to_token_stream().to_string(),
435                        default: c
436                            .default
437                            .as_ref()
438                            .map(|(_, e)| e.to_token_stream().to_string()),
439                    });
440                }
441                _ => {}
442            }
443        }
444
445        let documentation = Self::extract_docs(&tr.attrs);
446
447        AnalyzedItem::Trait(TraitInfo {
448            name,
449            visibility,
450            generics,
451            supertraits,
452            methods,
453            associated_types,
454            associated_consts,
455            documentation,
456            is_unsafe,
457            is_auto,
458            where_clause,
459            source_location: SourceLocation::default(),
460            module_path: Vec::new(),
461        })
462    }
463
464    fn analyze_impl(&self, im: &ItemImpl) -> AnalyzedItem {
465        let self_ty = im.self_ty.to_token_stream().to_string();
466        let trait_name = im
467            .trait_
468            .as_ref()
469            .map(|(_, path, _)| path.to_token_stream().to_string());
470        let is_unsafe = im.unsafety.is_some();
471        let is_negative = im
472            .trait_
473            .as_ref()
474            .is_some_and(|(bang, _, _)| bang.is_some());
475        let generics = Self::extract_generics(&im.generics);
476        let where_clause = Self::extract_where_clause(&im.generics.where_clause);
477
478        let methods = im
479            .items
480            .iter()
481            .filter_map(|item| {
482                if let syn::ImplItem::Fn(method) = item {
483                    Some(self.extract_impl_method(method))
484                } else {
485                    None
486                }
487            })
488            .collect();
489
490        AnalyzedItem::Impl(ImplInfo {
491            self_ty,
492            trait_name,
493            generics,
494            methods,
495            is_unsafe,
496            is_negative,
497            where_clause,
498            source_location: SourceLocation::default(),
499            module_path: Vec::new(),
500        })
501    }
502
503    fn analyze_module(&self, md: &syn::ItemMod) -> AnalyzedItem {
504        let name = md.ident.to_string();
505        let visibility = Self::parse_visibility(&md.vis);
506        let documentation = Self::extract_docs(&md.attrs);
507        let is_inline = md.content.is_some();
508
509        let (items, submodules) = if let Some((_, content)) = &md.content {
510            let mut item_names = Vec::new();
511            let mut submod_names = Vec::new();
512
513            for item in content {
514                match item {
515                    Item::Mod(m) => submod_names.push(m.ident.to_string()),
516                    Item::Fn(f) => item_names.push(format!("fn {}", f.sig.ident)),
517                    Item::Struct(s) => item_names.push(format!("struct {}", s.ident)),
518                    Item::Enum(e) => item_names.push(format!("enum {}", e.ident)),
519                    Item::Trait(t) => item_names.push(format!("trait {}", t.ident)),
520                    Item::Impl(i) => {
521                        let ty = i.self_ty.to_token_stream().to_string();
522                        if let Some((_, path, _)) = &i.trait_ {
523                            item_names.push(format!("impl {} for {}", path.to_token_stream(), ty));
524                        } else {
525                            item_names.push(format!("impl {}", ty));
526                        }
527                    }
528                    Item::Type(t) => item_names.push(format!("type {}", t.ident)),
529                    Item::Const(c) => item_names.push(format!("const {}", c.ident)),
530                    Item::Static(s) => item_names.push(format!("static {}", s.ident)),
531                    _ => {}
532                }
533            }
534
535            (item_names, submod_names)
536        } else {
537            (Vec::new(), Vec::new())
538        };
539
540        AnalyzedItem::Module(ModuleInfo {
541            name,
542            path: String::new(),
543            visibility,
544            items,
545            submodules,
546            documentation,
547            is_inline,
548            source_location: SourceLocation::default(),
549            module_path: Vec::new(),
550        })
551    }
552
553    fn analyze_type_alias(&self, ty: &ItemType) -> AnalyzedItem {
554        AnalyzedItem::TypeAlias(TypeAliasInfo {
555            name: ty.ident.to_string(),
556            visibility: Self::parse_visibility(&ty.vis),
557            generics: Self::extract_generics(&ty.generics),
558            ty: ty.ty.to_token_stream().to_string(),
559            documentation: Self::extract_docs(&ty.attrs),
560            where_clause: Self::extract_where_clause(&ty.generics.where_clause),
561            source_location: SourceLocation::default(),
562            module_path: Vec::new(),
563        })
564    }
565
566    fn analyze_const(&self, c: &ItemConst) -> AnalyzedItem {
567        AnalyzedItem::Const(ConstInfo {
568            name: c.ident.to_string(),
569            visibility: Self::parse_visibility(&c.vis),
570            ty: c.ty.to_token_stream().to_string(),
571            value: Some(c.expr.to_token_stream().to_string()),
572            documentation: Self::extract_docs(&c.attrs),
573            source_location: SourceLocation::default(),
574            module_path: Vec::new(),
575        })
576    }
577
578    fn analyze_static(&self, s: &ItemStatic) -> AnalyzedItem {
579        let is_mut = matches!(s.mutability, syn::StaticMutability::Mut(_));
580        AnalyzedItem::Static(StaticInfo {
581            name: s.ident.to_string(),
582            visibility: Self::parse_visibility(&s.vis),
583            ty: s.ty.to_token_stream().to_string(),
584            is_mut,
585            documentation: Self::extract_docs(&s.attrs),
586            source_location: SourceLocation::default(),
587            module_path: Vec::new(),
588        })
589    }
590
591    fn extract_impl_method(&self, method: &syn::ImplItemFn) -> FunctionInfo {
592        FunctionInfo {
593            name: method.sig.ident.to_string(),
594            signature: method.sig.to_token_stream().to_string(),
595            visibility: Self::parse_visibility(&method.vis),
596            is_async: method.sig.asyncness.is_some(),
597            is_const: method.sig.constness.is_some(),
598            is_unsafe: method.sig.unsafety.is_some(),
599            generics: Self::extract_generics(&method.sig.generics),
600            parameters: Self::extract_parameters(&method.sig.inputs),
601            return_type: Self::extract_return_type(&method.sig.output),
602            documentation: Self::extract_docs(&method.attrs),
603            attributes: Self::extract_attributes(&method.attrs),
604            where_clause: Self::extract_where_clause(&method.sig.generics.where_clause),
605            source_location: SourceLocation::default(),
606            module_path: Vec::new(),
607        }
608    }
609
610    fn parse_visibility(vis: &syn::Visibility) -> Visibility {
611        match vis {
612            syn::Visibility::Public(_) => Visibility::Public,
613            syn::Visibility::Restricted(r) => {
614                if r.path.is_ident("crate") {
615                    Visibility::Crate
616                } else if r.path.is_ident("super") {
617                    Visibility::Super
618                } else if r.path.is_ident("self") {
619                    Visibility::SelfOnly
620                } else {
621                    Visibility::Private
622                }
623            }
624            syn::Visibility::Inherited => Visibility::Private,
625        }
626    }
627
628    fn extract_generics(generics: &syn::Generics) -> Vec<String> {
629        generics
630            .params
631            .iter()
632            .map(|p| p.to_token_stream().to_string())
633            .collect()
634    }
635
636    fn extract_parameters(
637        inputs: &syn::punctuated::Punctuated<syn::FnArg, syn::Token![,]>,
638    ) -> Vec<Parameter> {
639        inputs
640            .iter()
641            .map(|arg| match arg {
642                syn::FnArg::Receiver(recv) => Parameter {
643                    name: "self".to_string(),
644                    ty: "Self".to_string(),
645                    is_self: true,
646                    is_mut: recv.mutability.is_some(),
647                    is_ref: recv.reference.is_some(),
648                },
649                syn::FnArg::Typed(pat_type) => Parameter {
650                    name: pat_type.pat.to_token_stream().to_string(),
651                    ty: pat_type.ty.to_token_stream().to_string(),
652                    is_self: false,
653                    is_mut: false,
654                    is_ref: false,
655                },
656            })
657            .collect()
658    }
659
660    fn extract_return_type(output: &syn::ReturnType) -> Option<String> {
661        match output {
662            syn::ReturnType::Default => None,
663            syn::ReturnType::Type(_, ty) => Some(ty.to_token_stream().to_string()),
664        }
665    }
666
667    fn extract_where_clause(where_clause: &Option<syn::WhereClause>) -> Option<String> {
668        where_clause
669            .as_ref()
670            .map(|w| w.to_token_stream().to_string())
671    }
672
673    fn extract_docs(attrs: &[syn::Attribute]) -> Option<String> {
674        let docs: Vec<String> = attrs
675            .iter()
676            .filter_map(|attr| {
677                if attr.path().is_ident("doc") {
678                    attr.meta.require_name_value().ok().and_then(|nv| {
679                        if let syn::Expr::Lit(expr_lit) = &nv.value {
680                            if let syn::Lit::Str(lit_str) = &expr_lit.lit {
681                                return Some(lit_str.value().trim().to_string());
682                            }
683                        }
684                        None
685                    })
686                } else {
687                    None
688                }
689            })
690            .collect();
691
692        if docs.is_empty() {
693            None
694        } else {
695            Some(docs.join("\n"))
696        }
697    }
698
699    fn extract_derives(attrs: &[syn::Attribute]) -> Vec<String> {
700        attrs
701            .iter()
702            .filter_map(|attr| {
703                if attr.path().is_ident("derive") {
704                    attr.parse_args_with(
705                        syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
706                    )
707                    .ok()
708                    .map(|paths| {
709                        paths
710                            .iter()
711                            .map(|p| p.to_token_stream().to_string())
712                            .collect::<Vec<_>>()
713                    })
714                } else {
715                    None
716                }
717            })
718            .flatten()
719            .collect()
720    }
721
722    fn extract_attributes(attrs: &[syn::Attribute]) -> Vec<String> {
723        attrs
724            .iter()
725            .filter(|attr| !attr.path().is_ident("doc") && !attr.path().is_ident("derive"))
726            .map(|attr| attr.to_token_stream().to_string())
727            .collect()
728    }
729}
730
731impl Default for RustAnalyzer {
732    fn default() -> Self {
733        Self::new()
734    }
735}
736
737#[cfg(test)]
738mod tests {
739    use super::*;
740
741    #[test]
742    fn test_analyze_function() {
743        let source = r#"
744            /// A test function
745            pub fn hello(name: &str) -> String {
746                format!("Hello, {}!", name)
747            }
748        "#;
749
750        let analyzer = RustAnalyzer::new();
751        let items = analyzer.analyze_source(source).unwrap();
752
753        assert_eq!(items.len(), 1);
754        if let AnalyzedItem::Function(f) = &items[0] {
755            assert_eq!(f.name, "hello");
756            assert_eq!(f.visibility, Visibility::Public);
757            assert!(f.documentation.is_some());
758        } else {
759            panic!("Expected function");
760        }
761    }
762
763    #[test]
764    fn test_analyze_struct() {
765        let source = r#"
766            #[derive(Debug, Clone)]
767            pub struct Point {
768                pub x: f64,
769                pub y: f64,
770            }
771        "#;
772
773        let analyzer = RustAnalyzer::new();
774        let items = analyzer.analyze_source(source).unwrap();
775
776        assert_eq!(items.len(), 1);
777        if let AnalyzedItem::Struct(s) = &items[0] {
778            assert_eq!(s.name, "Point");
779            assert_eq!(s.fields.len(), 2);
780            assert!(s.derives.contains(&"Debug".to_string()));
781            assert!(s.derives.contains(&"Clone".to_string()));
782        } else {
783            panic!("Expected struct");
784        }
785    }
786
787    #[test]
788    fn test_analyze_enum() {
789        let source = r#"
790            pub enum Result<T, E> {
791                Ok(T),
792                Err(E),
793            }
794        "#;
795        let analyzer = RustAnalyzer::new();
796        let items = analyzer.analyze_source(source).unwrap();
797        assert_eq!(items.len(), 1);
798        if let AnalyzedItem::Enum(e) = &items[0] {
799            assert_eq!(e.name, "Result");
800            assert_eq!(e.variants.len(), 2);
801        } else {
802            panic!("Expected enum");
803        }
804    }
805
806    #[test]
807    fn test_analyze_module_path_from_path() {
808        use std::path::Path;
809        assert_eq!(
810            RustAnalyzer::derive_module_path(Path::new("src/lib.rs")),
811            Vec::<String>::new()
812        );
813        assert_eq!(
814            RustAnalyzer::derive_module_path(Path::new("src/foo/bar.rs")),
815            vec!["foo".to_string(), "bar".to_string()]
816        );
817    }
818
819    #[test]
820    fn test_analyze_source_with_module_prefix() {
821        let source = "pub fn util() {}";
822        let analyzer = RustAnalyzer::new();
823        let items = analyzer
824            .analyze_source_with_module(source, None, vec!["mymod".to_string()])
825            .unwrap();
826        assert_eq!(items.len(), 1);
827        if let AnalyzedItem::Function(f) = &items[0] {
828            assert_eq!(f.name, "util");
829            assert_eq!(f.module_path.as_slice(), &["mymod"]);
830        } else {
831            panic!("Expected function");
832        }
833    }
834}