1use crate::error::{AuditError, Result};
8use crate::parser::{ApiItemType, ApiReference};
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use syn::{
12 Attribute, Expr, Item, ItemConst, ItemEnum, ItemFn, ItemImpl, ItemStatic, ItemStruct,
13 ItemTrait, ItemType, Lit, Meta, Visibility,
14};
15use tracing::{debug, info, instrument, warn};
16use walkdir::WalkDir;
17
18#[derive(Debug, Clone)]
20pub struct CrateRegistry {
21 pub crates: HashMap<String, CrateInfo>,
23}
24
25#[derive(Debug, Clone)]
27pub struct CrateInfo {
28 pub name: String,
30 pub version: String,
32 pub path: PathBuf,
34 pub public_apis: Vec<PublicApi>,
36 pub feature_flags: Vec<String>,
38 pub dependencies: Vec<Dependency>,
40 pub rust_version: Option<String>,
42}
43
44#[derive(Debug, Clone)]
46pub struct PublicApi {
47 pub path: String,
49 pub signature: String,
51 pub item_type: ApiItemType,
53 pub documentation: Option<String>,
55 pub deprecated: bool,
57 pub source_file: PathBuf,
59 pub line_number: usize,
61}
62
63#[derive(Debug, Clone)]
65pub struct Dependency {
66 pub name: String,
68 pub version: String,
70 pub optional: bool,
72 pub features: Vec<String>,
74}
75
76#[derive(Debug, Clone)]
78pub struct ValidationResult {
79 pub success: bool,
81 pub errors: Vec<String>,
83 pub warnings: Vec<String>,
85 pub suggestions: Vec<String>,
87 pub found_api: Option<PublicApi>,
89}
90
91pub struct CodeAnalyzer {
93 workspace_path: PathBuf,
95 crate_registry: Option<CrateRegistry>,
97}
98
99impl CodeAnalyzer {
100 pub fn new(workspace_path: PathBuf) -> Self {
102 Self { workspace_path, crate_registry: None }
103 }
104
105 #[instrument(skip(self))]
107 pub async fn analyze_workspace(&mut self) -> Result<&CrateRegistry> {
108 info!("Starting workspace analysis at: {}", self.workspace_path.display());
109
110 let mut crates = HashMap::new();
111
112 let cargo_files = self.find_cargo_files()?;
114 info!("Found {} Cargo.toml files", cargo_files.len());
115
116 for cargo_path in cargo_files {
117 if let Some(crate_info) = self.analyze_crate(&cargo_path).await? {
118 debug!("Analyzed crate: {}", crate_info.name);
119 crates.insert(crate_info.name.clone(), crate_info);
120 }
121 }
122
123 let registry = CrateRegistry { crates };
124 self.crate_registry = Some(registry);
125
126 info!(
127 "Workspace analysis complete. Found {} crates",
128 self.crate_registry.as_ref().unwrap().crates.len()
129 );
130 Ok(self.crate_registry.as_ref().unwrap())
131 }
132
133 pub async fn get_registry(&mut self) -> Result<&CrateRegistry> {
135 if self.crate_registry.is_none() {
136 self.analyze_workspace().await?;
137 }
138 Ok(self.crate_registry.as_ref().unwrap())
139 }
140
141 #[instrument(skip(self))]
143 pub async fn validate_api_reference(
144 &mut self,
145 api_ref: &ApiReference,
146 ) -> Result<ValidationResult> {
147 let registry = self.get_registry().await?;
149
150 debug!("Validating API reference: {}::{}", api_ref.crate_name, api_ref.item_path);
151
152 let crate_info = match registry.crates.get(&api_ref.crate_name) {
154 Some(info) => info,
155 None => {
156 let available_crates: Vec<String> = registry.crates.keys().cloned().collect();
158 let suggestion = Self::suggest_similar_crate_names_static(
159 &api_ref.crate_name,
160 &available_crates,
161 );
162 return Ok(ValidationResult {
163 success: false,
164 errors: vec![format!("Crate '{}' not found in workspace", api_ref.crate_name)],
165 warnings: vec![],
166 suggestions: vec![suggestion],
167 found_api: None,
168 });
169 }
170 };
171
172 let matching_apis: Vec<&PublicApi> = crate_info
174 .public_apis
175 .iter()
176 .filter(|api| {
177 api.path.ends_with(&api_ref.item_path) && api.item_type == api_ref.item_type
178 })
179 .collect();
180
181 match matching_apis.len() {
182 0 => {
183 let suggestion = Self::suggest_similar_api_names_static(
184 &api_ref.item_path,
185 &crate_info.public_apis,
186 );
187 Ok(ValidationResult {
188 success: false,
189 errors: vec![format!(
190 "API '{}' of type '{:?}' not found in crate '{}'",
191 api_ref.item_path, api_ref.item_type, api_ref.crate_name
192 )],
193 warnings: vec![],
194 suggestions: vec![suggestion],
195 found_api: None,
196 })
197 }
198 1 => {
199 let found_api = matching_apis[0].clone();
200 let mut warnings = vec![];
201
202 if found_api.deprecated {
204 warnings.push(format!("API '{}' is deprecated", api_ref.item_path));
205 }
206
207 Ok(ValidationResult {
208 success: true,
209 errors: vec![],
210 warnings,
211 suggestions: vec![],
212 found_api: Some(found_api),
213 })
214 }
215 _ => Ok(ValidationResult {
216 success: false,
217 errors: vec![format!(
218 "Multiple APIs matching '{}' found in crate '{}'. Please be more specific.",
219 api_ref.item_path, api_ref.crate_name
220 )],
221 warnings: vec![],
222 suggestions: matching_apis.iter().map(|api| api.path.clone()).collect(),
223 found_api: None,
224 }),
225 }
226 }
227
228 #[instrument(skip(self, documented_apis))]
230 pub async fn find_undocumented_apis(
231 &mut self,
232 documented_apis: &[ApiReference],
233 ) -> Result<Vec<PublicApi>> {
234 let registry = self.get_registry().await?;
235 let mut undocumented = Vec::new();
236
237 let documented_paths: std::collections::HashSet<String> = documented_apis
239 .iter()
240 .map(|api| format!("{}::{}", api.crate_name, api.item_path))
241 .collect();
242
243 for crate_info in registry.crates.values() {
245 for api in &crate_info.public_apis {
246 let full_path = format!("{}::{}", crate_info.name, api.path);
247 if !documented_paths.contains(&full_path) {
248 undocumented.push(api.clone());
249 }
250 }
251 }
252
253 info!("Found {} undocumented APIs", undocumented.len());
254 Ok(undocumented)
255 }
256
257 #[instrument(skip(self))]
259 pub async fn validate_function_signature(
260 &mut self,
261 api_ref: &ApiReference,
262 expected_signature: &str,
263 ) -> Result<ValidationResult> {
264 let validation_result = self.validate_api_reference(api_ref).await?;
265
266 if !validation_result.success {
267 return Ok(validation_result);
268 }
269
270 if let Some(found_api) = &validation_result.found_api {
271 let normalized_expected = self.normalize_signature(expected_signature);
273 let normalized_found = self.normalize_signature(&found_api.signature);
274
275 if normalized_expected == normalized_found {
276 Ok(validation_result)
277 } else {
278 Ok(ValidationResult {
279 success: false,
280 errors: vec![format!(
281 "Function signature mismatch for '{}'. Expected: '{}', Found: '{}'",
282 api_ref.item_path, expected_signature, found_api.signature
283 )],
284 warnings: validation_result.warnings,
285 suggestions: vec![format!(
286 "Update documentation to use: {}",
287 found_api.signature
288 )],
289 found_api: validation_result.found_api,
290 })
291 }
292 } else {
293 Ok(validation_result)
294 }
295 }
296
297 #[instrument(skip(self))]
299 pub async fn validate_struct_fields(
300 &mut self,
301 api_ref: &ApiReference,
302 expected_fields: &[String],
303 ) -> Result<ValidationResult> {
304 let validation_result = self.validate_api_reference(api_ref).await?;
305
306 if !validation_result.success {
307 return Ok(validation_result);
308 }
309
310 if let Some(found_api) = &validation_result.found_api {
311 let actual_fields = self.extract_struct_fields(&found_api.signature);
313 let missing_fields: Vec<&String> =
314 expected_fields.iter().filter(|field| !actual_fields.contains(field)).collect();
315
316 if missing_fields.is_empty() {
317 Ok(validation_result)
318 } else {
319 Ok(ValidationResult {
320 success: false,
321 errors: vec![format!(
322 "Struct '{}' is missing fields: {}",
323 api_ref.item_path,
324 missing_fields.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", ")
325 )],
326 warnings: validation_result.warnings,
327 suggestions: vec![format!("Available fields: {}", actual_fields.join(", "))],
328 found_api: validation_result.found_api,
329 })
330 }
331 } else {
332 Ok(validation_result)
333 }
334 }
335
336 #[instrument(skip(self))]
338 pub async fn validate_import_statement(
339 &mut self,
340 import_path: &str,
341 ) -> Result<ValidationResult> {
342 let registry = self.get_registry().await?;
343
344 debug!("Validating import statement: {}", import_path);
345
346 let parts: Vec<&str> = import_path.split("::").collect();
348 if parts.is_empty() {
349 return Ok(ValidationResult {
350 success: false,
351 errors: vec!["Invalid import path format".to_string()],
352 warnings: vec![],
353 suggestions: vec![],
354 found_api: None,
355 });
356 }
357
358 let crate_name = parts[0].replace('_', "-"); let crate_info = match registry.crates.get(&crate_name) {
362 Some(info) => info,
363 None => {
364 match registry.crates.get(parts[0]) {
366 Some(info) => info,
367 None => {
368 let available_crates: Vec<String> =
369 registry.crates.keys().cloned().collect();
370 let suggestion =
371 Self::suggest_similar_crate_names_static(parts[0], &available_crates);
372 return Ok(ValidationResult {
373 success: false,
374 errors: vec![format!("Crate '{}' not found in workspace", parts[0])],
375 warnings: vec![],
376 suggestions: vec![suggestion],
377 found_api: None,
378 });
379 }
380 }
381 }
382 };
383
384 if parts.len() == 1 {
386 return Ok(ValidationResult {
387 success: true,
388 errors: vec![],
389 warnings: vec![],
390 suggestions: vec![],
391 found_api: None,
392 });
393 }
394
395 let item_path = parts[1..].join("::");
397 let matching_apis: Vec<&PublicApi> =
398 crate_info.public_apis.iter().filter(|api| api.path.ends_with(&item_path)).collect();
399
400 if matching_apis.is_empty() {
401 let suggestion =
402 Self::suggest_similar_api_names_static(&item_path, &crate_info.public_apis);
403 Ok(ValidationResult {
404 success: false,
405 errors: vec![format!("Item '{}' not found in crate '{}'", item_path, crate_name)],
406 warnings: vec![],
407 suggestions: vec![suggestion],
408 found_api: None,
409 })
410 } else {
411 Ok(ValidationResult {
412 success: true,
413 errors: vec![],
414 warnings: vec![],
415 suggestions: vec![],
416 found_api: Some(matching_apis[0].clone()),
417 })
418 }
419 }
420
421 #[instrument(skip(self))]
423 pub async fn validate_method_exists(
424 &mut self,
425 type_ref: &ApiReference,
426 method_name: &str,
427 ) -> Result<ValidationResult> {
428 debug!(
429 "Validating method '{}' exists on type '{}::{}'",
430 method_name, type_ref.crate_name, type_ref.item_path
431 );
432
433 let type_validation = self.validate_api_reference(type_ref).await?;
435 if !type_validation.success {
436 return Ok(type_validation);
437 }
438
439 let registry = self.get_registry().await?;
441
442 let crate_info = registry.crates.get(&type_ref.crate_name).unwrap();
444 let type_name = type_ref.item_path.split("::").last().unwrap_or(&type_ref.item_path);
445
446 let method_path = format!("{}::{}", type_ref.item_path, method_name);
447 let matching_methods: Vec<&PublicApi> = crate_info
448 .public_apis
449 .iter()
450 .filter(|api| api.item_type == ApiItemType::Method && api.path.ends_with(&method_path))
451 .collect();
452
453 if matching_methods.is_empty() {
454 let type_methods: Vec<&PublicApi> = crate_info
456 .public_apis
457 .iter()
458 .filter(|api| {
459 api.item_type == ApiItemType::Method
460 && api.path.contains(&format!("{}::", type_name))
461 })
462 .collect();
463
464 let suggestions: Vec<String> = type_methods
465 .iter()
466 .map(|api| api.path.split("::").last().unwrap_or(&api.path).to_string())
467 .collect();
468
469 Ok(ValidationResult {
470 success: false,
471 errors: vec![format!(
472 "Method '{}' not found on type '{}'",
473 method_name, type_ref.item_path
474 )],
475 warnings: vec![],
476 suggestions: if suggestions.is_empty() {
477 vec!["No methods found on this type".to_string()]
478 } else {
479 vec![format!("Available methods: {}", suggestions.join(", "))]
480 },
481 found_api: None,
482 })
483 } else {
484 Ok(ValidationResult {
485 success: true,
486 errors: vec![],
487 warnings: vec![],
488 suggestions: vec![],
489 found_api: Some(matching_methods[0].clone()),
490 })
491 }
492 }
493
494 fn find_cargo_files(&self) -> Result<Vec<PathBuf>> {
496 let mut cargo_files = Vec::new();
497
498 for entry in
499 WalkDir::new(&self.workspace_path).follow_links(true).into_iter().filter_map(|e| e.ok())
500 {
501 if entry.file_name() == "Cargo.toml" {
502 let path_str = entry.path().to_string_lossy();
504 if !path_str.contains("/target/") && !path_str.contains("\\target\\") {
505 cargo_files.push(entry.path().to_path_buf());
506 }
507 }
508 }
509
510 Ok(cargo_files)
511 }
512
513 #[instrument(skip(self))]
515 async fn analyze_crate(&self, cargo_path: &Path) -> Result<Option<CrateInfo>> {
516 debug!("Analyzing crate at: {}", cargo_path.display());
517
518 let cargo_content = std::fs::read_to_string(cargo_path).map_err(|e| {
520 AuditError::IoError { path: cargo_path.to_path_buf(), details: e.to_string() }
521 })?;
522
523 let cargo_toml: toml::Value = toml::from_str(&cargo_content).map_err(|e| {
524 AuditError::TomlError { file_path: cargo_path.to_path_buf(), details: e.to_string() }
525 })?;
526
527 let package = match cargo_toml.get("package") {
529 Some(pkg) => pkg,
530 None => {
531 debug!("No package section found in {}, skipping", cargo_path.display());
533 return Ok(None);
534 }
535 };
536
537 let name = package
538 .get("name")
539 .and_then(|n| n.as_str())
540 .ok_or_else(|| AuditError::TomlError {
541 file_path: cargo_path.to_path_buf(),
542 details: "Missing package name".to_string(),
543 })?
544 .to_string();
545
546 let version =
547 package.get("version").and_then(|v| v.as_str()).unwrap_or("0.0.0").to_string();
548
549 let rust_version =
550 package.get("rust-version").and_then(|v| v.as_str()).map(|s| s.to_string());
551
552 let feature_flags = self.extract_feature_flags(&cargo_toml);
554
555 let dependencies = self.extract_dependencies(&cargo_toml);
557
558 let crate_dir = cargo_path.parent().unwrap();
560 let src_dir = crate_dir.join("src");
561
562 let public_apis = if src_dir.exists() {
563 self.analyze_source_files(&src_dir, &name).await?
564 } else {
565 warn!("No src directory found for crate: {}", name);
566 Vec::new()
567 };
568
569 Ok(Some(CrateInfo {
570 name,
571 version,
572 path: crate_dir.to_path_buf(),
573 public_apis,
574 feature_flags,
575 dependencies,
576 rust_version,
577 }))
578 }
579
580 fn extract_feature_flags(&self, cargo_toml: &toml::Value) -> Vec<String> {
582 cargo_toml
583 .get("features")
584 .and_then(|f| f.as_table())
585 .map(|table| table.keys().cloned().collect())
586 .unwrap_or_default()
587 }
588
589 fn extract_dependencies(&self, cargo_toml: &toml::Value) -> Vec<Dependency> {
591 let mut dependencies = Vec::new();
592
593 if let Some(deps) = cargo_toml.get("dependencies").and_then(|d| d.as_table()) {
595 for (name, spec) in deps {
596 dependencies.push(self.parse_dependency(name, spec));
597 }
598 }
599
600 if let Some(deps) = cargo_toml.get("dev-dependencies").and_then(|d| d.as_table()) {
602 for (name, spec) in deps {
603 dependencies.push(self.parse_dependency(name, spec));
604 }
605 }
606
607 dependencies
608 }
609
610 fn parse_dependency(&self, name: &str, spec: &toml::Value) -> Dependency {
612 match spec {
613 toml::Value::String(version) => Dependency {
614 name: name.to_string(),
615 version: version.clone(),
616 optional: false,
617 features: Vec::new(),
618 },
619 toml::Value::Table(table) => {
620 let version =
621 table.get("version").and_then(|v| v.as_str()).unwrap_or("*").to_string();
622
623 let optional = table.get("optional").and_then(|o| o.as_bool()).unwrap_or(false);
624
625 let features = table
626 .get("features")
627 .and_then(|f| f.as_array())
628 .map(|arr| {
629 arr.iter().filter_map(|v| v.as_str()).map(|s| s.to_string()).collect()
630 })
631 .unwrap_or_default();
632
633 Dependency { name: name.to_string(), version, optional, features }
634 }
635 _ => Dependency {
636 name: name.to_string(),
637 version: "*".to_string(),
638 optional: false,
639 features: Vec::new(),
640 },
641 }
642 }
643
644 #[instrument(skip(self))]
646 async fn analyze_source_files(
647 &self,
648 src_dir: &Path,
649 crate_name: &str,
650 ) -> Result<Vec<PublicApi>> {
651 let mut apis = Vec::new();
652
653 for entry in WalkDir::new(src_dir).follow_links(true).into_iter().filter_map(|e| e.ok()) {
654 if entry.path().extension().and_then(|s| s.to_str()) == Some("rs") {
655 let file_apis = self.analyze_rust_file(entry.path(), crate_name).await?;
656 apis.extend(file_apis);
657 }
658 }
659
660 Ok(apis)
661 }
662
663 #[instrument(skip(self))]
665 async fn analyze_rust_file(
666 &self,
667 file_path: &Path,
668 crate_name: &str,
669 ) -> Result<Vec<PublicApi>> {
670 debug!("Analyzing Rust file: {}", file_path.display());
671
672 let content = std::fs::read_to_string(file_path).map_err(|e| AuditError::IoError {
673 path: file_path.to_path_buf(),
674 details: e.to_string(),
675 })?;
676
677 let syntax_tree = syn::parse_file(&content).map_err(|e| AuditError::SyntaxError {
678 details: format!("Failed to parse {}: {}", file_path.display(), e),
679 })?;
680
681 let mut apis = Vec::new();
682 let mut current_module_path = vec![crate_name.to_string()];
683
684 self.extract_apis_from_items(
685 &syntax_tree.items,
686 &mut current_module_path,
687 file_path,
688 &mut apis,
689 );
690
691 Ok(apis)
692 }
693
694 fn extract_apis_from_items(
696 &self,
697 items: &[Item],
698 module_path: &mut Vec<String>,
699 file_path: &Path,
700 apis: &mut Vec<PublicApi>,
701 ) {
702 for item in items {
703 match item {
704 Item::Fn(item_fn) => {
705 if self.is_public(&item_fn.vis) {
706 let api = self.create_function_api(item_fn, module_path, file_path);
707 apis.push(api);
708 }
709 }
710 Item::Struct(item_struct) => {
711 if self.is_public(&item_struct.vis) {
712 let api = self.create_struct_api(item_struct, module_path, file_path);
713 apis.push(api);
714 }
715 }
716 Item::Enum(item_enum) => {
717 if self.is_public(&item_enum.vis) {
718 let api = self.create_enum_api(item_enum, module_path, file_path);
719 apis.push(api);
720 }
721 }
722 Item::Trait(item_trait) => {
723 if self.is_public(&item_trait.vis) {
724 let api = self.create_trait_api(item_trait, module_path, file_path);
725 apis.push(api);
726 }
727 }
728 Item::Type(item_type) => {
729 if self.is_public(&item_type.vis) {
730 let api = self.create_type_api(item_type, module_path, file_path);
731 apis.push(api);
732 }
733 }
734 Item::Const(item_const) => {
735 if self.is_public(&item_const.vis) {
736 let api = self.create_const_api(item_const, module_path, file_path);
737 apis.push(api);
738 }
739 }
740 Item::Static(item_static) => {
741 if self.is_public(&item_static.vis) {
742 let api = self.create_static_api(item_static, module_path, file_path);
743 apis.push(api);
744 }
745 }
746 Item::Mod(item_mod) => {
747 if self.is_public(&item_mod.vis) {
748 module_path.push(item_mod.ident.to_string());
750 if let Some((_, items)) = &item_mod.content {
751 self.extract_apis_from_items(items, module_path, file_path, apis);
752 }
753 module_path.pop();
754 }
755 }
756 Item::Impl(item_impl) => {
757 self.extract_impl_methods(item_impl, module_path, file_path, apis);
759 }
760 _ => {
761 }
763 }
764 }
765 }
766
767 fn is_public(&self, vis: &Visibility) -> bool {
769 matches!(vis, Visibility::Public(_))
770 }
771
772 fn create_function_api(
774 &self,
775 item_fn: &ItemFn,
776 module_path: &[String],
777 file_path: &Path,
778 ) -> PublicApi {
779 let path = format!("{}::{}", module_path.join("::"), item_fn.sig.ident);
780 let signature = format!("fn {}", quote::quote!(#item_fn.sig));
781 let documentation = self.extract_doc_comments(&item_fn.attrs);
782 let deprecated = self.is_deprecated(&item_fn.attrs);
783
784 PublicApi {
785 path,
786 signature,
787 item_type: ApiItemType::Function,
788 documentation,
789 deprecated,
790 source_file: file_path.to_path_buf(),
791 line_number: 0, }
793 }
794
795 fn create_struct_api(
797 &self,
798 item_struct: &ItemStruct,
799 module_path: &[String],
800 file_path: &Path,
801 ) -> PublicApi {
802 let path = format!("{}::{}", module_path.join("::"), item_struct.ident);
803 let signature = format!("struct {}", quote::quote!(#item_struct));
804 let documentation = self.extract_doc_comments(&item_struct.attrs);
805 let deprecated = self.is_deprecated(&item_struct.attrs);
806
807 PublicApi {
808 path,
809 signature,
810 item_type: ApiItemType::Struct,
811 documentation,
812 deprecated,
813 source_file: file_path.to_path_buf(),
814 line_number: 0,
815 }
816 }
817
818 fn create_enum_api(
820 &self,
821 item_enum: &ItemEnum,
822 module_path: &[String],
823 file_path: &Path,
824 ) -> PublicApi {
825 let path = format!("{}::{}", module_path.join("::"), item_enum.ident);
826 let signature = format!("enum {}", quote::quote!(#item_enum));
827 let documentation = self.extract_doc_comments(&item_enum.attrs);
828 let deprecated = self.is_deprecated(&item_enum.attrs);
829
830 PublicApi {
831 path,
832 signature,
833 item_type: ApiItemType::Enum,
834 documentation,
835 deprecated,
836 source_file: file_path.to_path_buf(),
837 line_number: 0,
838 }
839 }
840
841 fn create_trait_api(
843 &self,
844 item_trait: &ItemTrait,
845 module_path: &[String],
846 file_path: &Path,
847 ) -> PublicApi {
848 let path = format!("{}::{}", module_path.join("::"), item_trait.ident);
849 let signature = format!("trait {}", quote::quote!(#item_trait));
850 let documentation = self.extract_doc_comments(&item_trait.attrs);
851 let deprecated = self.is_deprecated(&item_trait.attrs);
852
853 PublicApi {
854 path,
855 signature,
856 item_type: ApiItemType::Trait,
857 documentation,
858 deprecated,
859 source_file: file_path.to_path_buf(),
860 line_number: 0,
861 }
862 }
863
864 fn create_type_api(
866 &self,
867 item_type: &ItemType,
868 module_path: &[String],
869 file_path: &Path,
870 ) -> PublicApi {
871 let path = format!("{}::{}", module_path.join("::"), item_type.ident);
872 let signature = format!("type {}", quote::quote!(#item_type));
873 let documentation = self.extract_doc_comments(&item_type.attrs);
874 let deprecated = self.is_deprecated(&item_type.attrs);
875
876 PublicApi {
877 path,
878 signature,
879 item_type: ApiItemType::Struct, documentation,
881 deprecated,
882 source_file: file_path.to_path_buf(),
883 line_number: 0,
884 }
885 }
886
887 fn create_const_api(
889 &self,
890 item_const: &ItemConst,
891 module_path: &[String],
892 file_path: &Path,
893 ) -> PublicApi {
894 let path = format!("{}::{}", module_path.join("::"), item_const.ident);
895 let signature = format!("const {}", quote::quote!(#item_const));
896 let documentation = self.extract_doc_comments(&item_const.attrs);
897 let deprecated = self.is_deprecated(&item_const.attrs);
898
899 PublicApi {
900 path,
901 signature,
902 item_type: ApiItemType::Constant,
903 documentation,
904 deprecated,
905 source_file: file_path.to_path_buf(),
906 line_number: 0,
907 }
908 }
909
910 fn create_static_api(
912 &self,
913 item_static: &ItemStatic,
914 module_path: &[String],
915 file_path: &Path,
916 ) -> PublicApi {
917 let path = format!("{}::{}", module_path.join("::"), item_static.ident);
918 let signature = format!("static {}", quote::quote!(#item_static));
919 let documentation = self.extract_doc_comments(&item_static.attrs);
920 let deprecated = self.is_deprecated(&item_static.attrs);
921
922 PublicApi {
923 path,
924 signature,
925 item_type: ApiItemType::Constant, documentation,
927 deprecated,
928 source_file: file_path.to_path_buf(),
929 line_number: 0,
930 }
931 }
932
933 fn extract_impl_methods(
935 &self,
936 item_impl: &ItemImpl,
937 module_path: &[String],
938 file_path: &Path,
939 apis: &mut Vec<PublicApi>,
940 ) {
941 let type_name = match &*item_impl.self_ty {
943 syn::Type::Path(type_path) => type_path
944 .path
945 .segments
946 .last()
947 .map(|seg| seg.ident.to_string())
948 .unwrap_or_else(|| "Unknown".to_string()),
949 _ => "Unknown".to_string(),
950 };
951
952 for item in &item_impl.items {
953 if let syn::ImplItem::Fn(method) = item {
954 if self.is_public(&method.vis) {
955 let path =
956 format!("{}::{}::{}", module_path.join("::"), type_name, method.sig.ident);
957 let signature = format!("fn {}", quote::quote!(#method.sig));
958 let documentation = self.extract_doc_comments(&method.attrs);
959 let deprecated = self.is_deprecated(&method.attrs);
960
961 apis.push(PublicApi {
962 path,
963 signature,
964 item_type: ApiItemType::Method,
965 documentation,
966 deprecated,
967 source_file: file_path.to_path_buf(),
968 line_number: 0,
969 });
970 }
971 }
972 }
973 }
974
975 fn extract_doc_comments(&self, attrs: &[Attribute]) -> Option<String> {
977 let mut doc_lines = Vec::new();
978
979 for attr in attrs {
980 if attr.path().is_ident("doc") {
981 if let Meta::NameValue(meta) = &attr.meta {
982 if let Expr::Lit(expr_lit) = &meta.value {
983 if let Lit::Str(lit_str) = &expr_lit.lit {
984 doc_lines.push(lit_str.value());
985 }
986 }
987 }
988 }
989 }
990
991 if doc_lines.is_empty() { None } else { Some(doc_lines.join("\n")) }
992 }
993
994 fn is_deprecated(&self, attrs: &[Attribute]) -> bool {
996 attrs.iter().any(|attr| attr.path().is_ident("deprecated"))
997 }
998
999 #[allow(dead_code)]
1001 fn suggest_similar_crate_names(&self, target: &str, registry: &CrateRegistry) -> String {
1002 Self::suggest_similar_crate_names_static(
1003 target,
1004 ®istry.crates.keys().cloned().collect::<Vec<_>>(),
1005 )
1006 }
1007
1008 fn suggest_similar_crate_names_static(target: &str, available_crates: &[String]) -> String {
1010 let mut suggestions = Vec::new();
1011
1012 for crate_name in available_crates {
1013 if crate_name.contains(target) || target.contains(crate_name) {
1014 suggestions.push(crate_name.clone());
1015 }
1016 }
1017
1018 if suggestions.is_empty() {
1019 format!("Available crates: {}", available_crates.join(", "))
1020 } else {
1021 format!("Did you mean: {}?", suggestions.join(", "))
1022 }
1023 }
1024
1025 #[allow(dead_code)]
1027 fn suggest_similar_api_names(&self, target: &str, crate_info: &CrateInfo) -> String {
1028 Self::suggest_similar_api_names_static(target, &crate_info.public_apis)
1029 }
1030
1031 fn suggest_similar_api_names_static(target: &str, public_apis: &[PublicApi]) -> String {
1033 let mut suggestions = Vec::new();
1034
1035 for api in public_apis {
1036 let api_name = api.path.split("::").last().unwrap_or(&api.path);
1037 if api_name.contains(target) || target.contains(api_name) {
1038 suggestions.push(api.path.clone());
1039 }
1040 }
1041
1042 if suggestions.is_empty() {
1043 "No similar APIs found".to_string()
1044 } else {
1045 format!("Did you mean: {}?", suggestions.join(", "))
1046 }
1047 }
1048
1049 fn normalize_signature(&self, signature: &str) -> String {
1051 signature.chars().filter(|c| !c.is_whitespace()).collect::<String>().to_lowercase()
1052 }
1053
1054 fn extract_struct_fields(&self, signature: &str) -> Vec<String> {
1056 let mut fields = Vec::new();
1059
1060 if let Some(start) = signature.find('{') {
1062 if let Some(end) = signature.rfind('}') {
1063 let fields_section = &signature[start + 1..end];
1064 for line in fields_section.lines() {
1065 let trimmed = line.trim();
1066 if let Some(colon_pos) = trimmed.find(':') {
1067 let field_name = trimmed[..colon_pos].trim();
1068 if !field_name.is_empty()
1069 && field_name.chars().all(|c| c.is_alphanumeric() || c == '_')
1070 {
1071 fields.push(field_name.to_string());
1072 }
1073 }
1074 }
1075 }
1076 }
1077
1078 fields
1079 }
1080}
1081
1082#[cfg(test)]
1083mod tests {
1084 use super::*;
1085 use std::fs;
1086 use tempfile::TempDir;
1087
1088 #[tokio::test]
1089 async fn test_analyzer_creation() {
1090 let temp_dir = TempDir::new().unwrap();
1091 let analyzer = CodeAnalyzer::new(temp_dir.path().to_path_buf());
1092 assert_eq!(analyzer.workspace_path, temp_dir.path());
1093 }
1094
1095 #[tokio::test]
1096 async fn test_find_cargo_files() {
1097 let temp_dir = TempDir::new().unwrap();
1098
1099 let crate1_dir = temp_dir.path().join("crate1");
1101 fs::create_dir_all(&crate1_dir).unwrap();
1102 fs::write(
1103 crate1_dir.join("Cargo.toml"),
1104 r#"
1105[package]
1106name = "crate1"
1107version = "0.1.0"
1108"#,
1109 )
1110 .unwrap();
1111
1112 let crate2_dir = temp_dir.path().join("crate2");
1113 fs::create_dir_all(&crate2_dir).unwrap();
1114 fs::write(
1115 crate2_dir.join("Cargo.toml"),
1116 r#"
1117[package]
1118name = "crate2"
1119version = "0.1.0"
1120"#,
1121 )
1122 .unwrap();
1123
1124 let analyzer = CodeAnalyzer::new(temp_dir.path().to_path_buf());
1125 let cargo_files = analyzer.find_cargo_files().unwrap();
1126
1127 assert_eq!(cargo_files.len(), 2);
1128 assert!(cargo_files.iter().any(|p| p.ends_with("crate1/Cargo.toml")));
1129 assert!(cargo_files.iter().any(|p| p.ends_with("crate2/Cargo.toml")));
1130 }
1131
1132 #[test]
1133 fn test_extract_feature_flags() {
1134 let analyzer = CodeAnalyzer::new(PathBuf::from("."));
1135
1136 let cargo_toml: toml::Value = toml::from_str(
1137 r#"
1138[features]
1139default = []
1140feature1 = []
1141feature2 = ["dep1"]
1142"#,
1143 )
1144 .unwrap();
1145
1146 let flags = analyzer.extract_feature_flags(&cargo_toml);
1147 assert_eq!(flags.len(), 3);
1148 assert!(flags.contains(&"default".to_string()));
1149 assert!(flags.contains(&"feature1".to_string()));
1150 assert!(flags.contains(&"feature2".to_string()));
1151 }
1152
1153 #[test]
1154 fn test_parse_dependency() {
1155 let analyzer = CodeAnalyzer::new(PathBuf::from("."));
1156
1157 let dep1 = analyzer.parse_dependency("serde", &toml::Value::String("1.0".to_string()));
1159 assert_eq!(dep1.name, "serde");
1160 assert_eq!(dep1.version, "1.0");
1161 assert!(!dep1.optional);
1162
1163 let table: toml::Value = toml::from_str(
1165 r#"
1166version = "1.0"
1167optional = true
1168features = ["derive"]
1169"#,
1170 )
1171 .unwrap();
1172
1173 let dep2 = analyzer.parse_dependency("serde", &table);
1174 assert_eq!(dep2.name, "serde");
1175 assert_eq!(dep2.version, "1.0");
1176 assert!(dep2.optional);
1177 assert_eq!(dep2.features, vec!["derive"]);
1178 }
1179}