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