1use 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
12pub 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 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 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 pub fn analyze_source(&self, source: &str) -> Result<Vec<AnalyzedItem>> {
47 self.analyze_source_with_path(source, None)
48 }
49
50 pub fn analyze_source_with_path(
52 &self,
53 source: &str,
54 path: Option<PathBuf>,
55 ) -> Result<Vec<AnalyzedItem>> {
56 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 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 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 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 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 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 if let Some(src_pos) = components.iter().position(|c| c == "src") {
160 components = components[src_pos + 1..].to_vec();
161 }
162
163 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}