Skip to main content

adk_doc_audit/
analyzer.rs

1//! Code analyzer for extracting API information from Rust codebase.
2//!
3//! This module provides functionality to analyze Rust workspace crates and extract
4//! public API signatures, documentation, and metadata for validation against
5//! documentation files.
6
7use 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/// Registry of all crates in the workspace with their public APIs.
20#[derive(Debug, Clone)]
21pub struct CrateRegistry {
22    /// Map of crate name to crate information.
23    pub crates: HashMap<String, CrateInfo>,
24}
25
26/// Information about a single crate in the workspace.
27#[derive(Debug, Clone)]
28pub struct CrateInfo {
29    /// Name of the crate.
30    pub name: String,
31    /// Version of the crate.
32    pub version: String,
33    /// Path to the crate directory.
34    pub path: PathBuf,
35    /// All public APIs exposed by this crate.
36    pub public_apis: Vec<PublicApi>,
37    /// Feature flags defined in Cargo.toml.
38    pub feature_flags: Vec<String>,
39    /// Dependencies listed in Cargo.toml.
40    pub dependencies: Vec<Dependency>,
41    /// Rust version requirement.
42    pub rust_version: Option<String>,
43}
44
45/// Information about a public API item.
46#[derive(Debug, Clone)]
47pub struct PublicApi {
48    /// Full path to the API item (e.g., "my_crate::module::function").
49    pub path: String,
50    /// String representation of the signature.
51    pub signature: String,
52    /// Type of API item.
53    pub item_type: ApiItemType,
54    /// Documentation comment if present.
55    pub documentation: Option<String>,
56    /// Whether the API is marked as deprecated.
57    pub deprecated: bool,
58    /// Source file where this API is defined.
59    pub source_file: PathBuf,
60    /// Line number in the source file.
61    pub line_number: usize,
62}
63
64/// Information about a dependency.
65#[derive(Debug, Clone)]
66pub struct Dependency {
67    /// Name of the dependency.
68    pub name: String,
69    /// Version requirement.
70    pub version: String,
71    /// Whether it's optional.
72    pub optional: bool,
73    /// Features enabled for this dependency.
74    pub features: Vec<String>,
75}
76
77/// Result of validating an API reference.
78#[derive(Debug, Clone)]
79pub struct ValidationResult {
80    /// Whether the validation was successful.
81    pub success: bool,
82    /// Error messages if validation failed.
83    pub errors: Vec<String>,
84    /// Warning messages.
85    pub warnings: Vec<String>,
86    /// Suggestions for fixing issues.
87    pub suggestions: Vec<String>,
88    /// The actual API that was found (if any).
89    pub found_api: Option<PublicApi>,
90}
91
92/// Code analyzer for extracting API information from Rust workspace.
93pub struct CodeAnalyzer {
94    /// Path to the workspace root.
95    workspace_path: PathBuf,
96    /// Cached crate registry.
97    crate_registry: Option<CrateRegistry>,
98}
99
100impl CodeAnalyzer {
101    /// Create a new code analyzer for the given workspace.
102    pub fn new(workspace_path: PathBuf) -> Self {
103        Self { workspace_path, crate_registry: None }
104    }
105
106    /// Analyze the entire workspace and build a registry of all crates and their APIs.
107    #[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        // Find all Cargo.toml files in the workspace
114        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    /// Get the cached crate registry, analyzing the workspace if not already done.
135    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    /// Validate an API reference against the analyzed crates.
143    #[instrument(skip(self))]
144    pub async fn validate_api_reference(
145        &mut self,
146        api_ref: &ApiReference,
147    ) -> Result<ValidationResult> {
148        // Get registry first
149        let registry = self.get_registry().await?;
150
151        debug!("Validating API reference: {}::{}", api_ref.crate_name, api_ref.item_path);
152
153        // Check if the crate exists
154        let crate_info = match registry.crates.get(&api_ref.crate_name) {
155            Some(info) => info,
156            None => {
157                // Create suggestion using helper method
158                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        // Look for the specific API in the crate
170        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                // Check if the API is deprecated
197                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    /// Find APIs that exist in the codebase but are not documented.
223    #[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        // Create a set of documented API paths for quick lookup
232        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        // Check each API in each crate
238        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    /// Validate that a function signature matches the documented signature.
252    #[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            // Compare signatures (simplified comparison)
266            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    /// Validate that struct fields mentioned in documentation exist.
292    #[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            // Extract field names from the struct signature
306            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    /// Validate that import statements are valid for the current crate structure.
331    #[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        // Parse the import path (e.g., "adk_core::Llm" or "crate::module::Type")
341        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('_', "-"); // Convert snake_case to kebab-case for crate names
353
354        // Check if the crate exists
355        let crate_info = match registry.crates.get(&crate_name) {
356            Some(info) => info,
357            None => {
358                // Try with the original name in case it's already kebab-case
359                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 it's just a crate import, that's valid
376        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        // Check if the specific item exists in the crate
387        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    /// Validate that method names mentioned in documentation exist on the specified type.
412    #[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        // First validate that the type exists
424        let type_validation = self.validate_api_reference(type_ref).await?;
425        if !type_validation.success {
426            return Ok(type_validation);
427        }
428
429        // Get registry again to avoid borrow checker issues
430        let registry = self.get_registry().await?;
431
432        // Look for methods on this type
433        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            // Look for similar method names on this type
445            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    /// Find all Cargo.toml files in the workspace.
485    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                // Skip target directories and other build artifacts
493                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    /// Analyze a single crate from its Cargo.toml file.
504    #[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        // Parse Cargo.toml
509        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        // Extract package information
518        let package = match cargo_toml.get("package") {
519            Some(pkg) => pkg,
520            None => {
521                // This might be a workspace root Cargo.toml without a package
522                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        // Extract feature flags
543        let feature_flags = self.extract_feature_flags(&cargo_toml);
544
545        // Extract dependencies
546        let dependencies = self.extract_dependencies(&cargo_toml);
547
548        // Find and analyze source files
549        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    /// Extract feature flags from Cargo.toml.
571    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    /// Extract dependencies from Cargo.toml.
580    fn extract_dependencies(&self, cargo_toml: &toml::Value) -> Vec<Dependency> {
581        let mut dependencies = Vec::new();
582
583        // Process regular dependencies
584        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        // Process dev-dependencies
591        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    /// Parse a single dependency specification.
601    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    /// Analyze all Rust source files in a directory.
635    #[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    /// Analyze a single Rust source file.
654    #[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    /// Extract public APIs from a list of syntax tree items.
685    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                        // Recursively analyze module contents
739                        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                    // Extract methods from impl blocks
748                    self.extract_impl_methods(item_impl, module_path, file_path, apis);
749                }
750                _ => {
751                    // Other items (use, extern crate, etc.) are not public APIs
752                }
753            }
754        }
755    }
756
757    /// Check if a visibility modifier indicates a public item.
758    fn is_public(&self, vis: &Visibility) -> bool {
759        matches!(vis, Visibility::Public(_))
760    }
761
762    /// Create a PublicApi for a function.
763    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    /// Create a PublicApi for a struct.
786    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    /// Create a PublicApi for an enum.
809    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    /// Create a PublicApi for a trait.
832    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    /// Create a PublicApi for a type alias.
855    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, // Type aliases are categorized as structs for simplicity
870            documentation,
871            deprecated,
872            source_file: file_path.to_path_buf(),
873            line_number: item_type.span().start().line,
874        }
875    }
876
877    /// Create a PublicApi for a constant.
878    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    /// Create a PublicApi for a static.
901    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, // Statics are categorized as constants for simplicity
916            documentation,
917            deprecated,
918            source_file: file_path.to_path_buf(),
919            line_number: item_static.span().start().line,
920        }
921    }
922
923    /// Extract methods from impl blocks.
924    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        // Get the type being implemented
932        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    /// Extract documentation comments from attributes.
966    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    /// Check if an item is marked as deprecated.
985    fn is_deprecated(&self, attrs: &[Attribute]) -> bool {
986        attrs.iter().any(|attr| attr.path().is_ident("deprecated"))
987    }
988
989    /// Suggest similar crate names when a crate is not found.
990    fn suggest_similar_crate_names(target: &str, registry: &CrateRegistry) -> String {
991        Self::suggest_similar_crate_names_static(
992            target,
993            &registry.crates.keys().cloned().collect::<Vec<_>>(),
994        )
995    }
996
997    /// Static version of suggest_similar_crate_names to avoid borrow checker issues.
998    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    /// Suggest similar API names when an API is not found.
1015    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    /// Static version of suggest_similar_api_names to avoid borrow checker issues.
1020    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    /// Normalize a function signature for comparison by removing whitespace and formatting.
1038    fn normalize_signature(&self, signature: &str) -> String {
1039        signature.chars().filter(|c| !c.is_whitespace()).collect::<String>().to_lowercase()
1040    }
1041
1042    /// Extract field names from a struct signature.
1043    fn extract_struct_fields(&self, signature: &str) -> Vec<String> {
1044        // This is a simplified implementation
1045        // In a real implementation, you'd want to parse the struct more carefully
1046        let mut fields = Vec::new();
1047
1048        // Look for patterns like "field_name: Type" in the signature
1049        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        // Create a test workspace structure
1088        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        // Test string version
1146        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        // Test table version
1152        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}