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::{
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/// Registry of all crates in the workspace with their public APIs.
19#[derive(Debug, Clone)]
20pub struct CrateRegistry {
21    /// Map of crate name to crate information.
22    pub crates: HashMap<String, CrateInfo>,
23}
24
25/// Information about a single crate in the workspace.
26#[derive(Debug, Clone)]
27pub struct CrateInfo {
28    /// Name of the crate.
29    pub name: String,
30    /// Version of the crate.
31    pub version: String,
32    /// Path to the crate directory.
33    pub path: PathBuf,
34    /// All public APIs exposed by this crate.
35    pub public_apis: Vec<PublicApi>,
36    /// Feature flags defined in Cargo.toml.
37    pub feature_flags: Vec<String>,
38    /// Dependencies listed in Cargo.toml.
39    pub dependencies: Vec<Dependency>,
40    /// Rust version requirement.
41    pub rust_version: Option<String>,
42}
43
44/// Information about a public API item.
45#[derive(Debug, Clone)]
46pub struct PublicApi {
47    /// Full path to the API item (e.g., "my_crate::module::function").
48    pub path: String,
49    /// String representation of the signature.
50    pub signature: String,
51    /// Type of API item.
52    pub item_type: ApiItemType,
53    /// Documentation comment if present.
54    pub documentation: Option<String>,
55    /// Whether the API is marked as deprecated.
56    pub deprecated: bool,
57    /// Source file where this API is defined.
58    pub source_file: PathBuf,
59    /// Line number in the source file.
60    pub line_number: usize,
61}
62
63/// Information about a dependency.
64#[derive(Debug, Clone)]
65pub struct Dependency {
66    /// Name of the dependency.
67    pub name: String,
68    /// Version requirement.
69    pub version: String,
70    /// Whether it's optional.
71    pub optional: bool,
72    /// Features enabled for this dependency.
73    pub features: Vec<String>,
74}
75
76/// Result of validating an API reference.
77#[derive(Debug, Clone)]
78pub struct ValidationResult {
79    /// Whether the validation was successful.
80    pub success: bool,
81    /// Error messages if validation failed.
82    pub errors: Vec<String>,
83    /// Warning messages.
84    pub warnings: Vec<String>,
85    /// Suggestions for fixing issues.
86    pub suggestions: Vec<String>,
87    /// The actual API that was found (if any).
88    pub found_api: Option<PublicApi>,
89}
90
91/// Code analyzer for extracting API information from Rust workspace.
92pub struct CodeAnalyzer {
93    /// Path to the workspace root.
94    workspace_path: PathBuf,
95    /// Cached crate registry.
96    crate_registry: Option<CrateRegistry>,
97}
98
99impl CodeAnalyzer {
100    /// Create a new code analyzer for the given workspace.
101    pub fn new(workspace_path: PathBuf) -> Self {
102        Self { workspace_path, crate_registry: None }
103    }
104
105    /// Analyze the entire workspace and build a registry of all crates and their APIs.
106    #[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        // Find all Cargo.toml files in the workspace
113        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    /// Get the cached crate registry, analyzing the workspace if not already done.
134    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    /// Validate an API reference against the analyzed crates.
142    #[instrument(skip(self))]
143    pub async fn validate_api_reference(
144        &mut self,
145        api_ref: &ApiReference,
146    ) -> Result<ValidationResult> {
147        // Get registry first
148        let registry = self.get_registry().await?;
149
150        debug!("Validating API reference: {}::{}", api_ref.crate_name, api_ref.item_path);
151
152        // Check if the crate exists
153        let crate_info = match registry.crates.get(&api_ref.crate_name) {
154            Some(info) => info,
155            None => {
156                // Create suggestion without borrowing self
157                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        // Look for the specific API in the crate
173        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                // Check if the API is deprecated
203                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    /// Find APIs that exist in the codebase but are not documented.
229    #[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        // Create a set of documented API paths for quick lookup
238        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        // Check each API in each crate
244        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    /// Validate that a function signature matches the documented signature.
258    #[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            // Compare signatures (simplified comparison)
272            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    /// Validate that struct fields mentioned in documentation exist.
298    #[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            // Extract field names from the struct signature
312            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    /// Validate that import statements are valid for the current crate structure.
337    #[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        // Parse the import path (e.g., "adk_core::Llm" or "crate::module::Type")
347        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('_', "-"); // Convert snake_case to kebab-case for crate names
359
360        // Check if the crate exists
361        let crate_info = match registry.crates.get(&crate_name) {
362            Some(info) => info,
363            None => {
364                // Try with the original name in case it's already kebab-case
365                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 it's just a crate import, that's valid
385        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        // Check if the specific item exists in the crate
396        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    /// Validate that method names mentioned in documentation exist on the specified type.
422    #[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        // First validate that the type exists
434        let type_validation = self.validate_api_reference(type_ref).await?;
435        if !type_validation.success {
436            return Ok(type_validation);
437        }
438
439        // Get registry again to avoid borrow checker issues
440        let registry = self.get_registry().await?;
441
442        // Look for methods on this type
443        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            // Look for similar method names on this type
455            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    /// Find all Cargo.toml files in the workspace.
495    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                // Skip target directories and other build artifacts
503                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    /// Analyze a single crate from its Cargo.toml file.
514    #[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        // Parse Cargo.toml
519        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        // Extract package information
528        let package = match cargo_toml.get("package") {
529            Some(pkg) => pkg,
530            None => {
531                // This might be a workspace root Cargo.toml without a package
532                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        // Extract feature flags
553        let feature_flags = self.extract_feature_flags(&cargo_toml);
554
555        // Extract dependencies
556        let dependencies = self.extract_dependencies(&cargo_toml);
557
558        // Find and analyze source files
559        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    /// Extract feature flags from Cargo.toml.
581    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    /// Extract dependencies from Cargo.toml.
590    fn extract_dependencies(&self, cargo_toml: &toml::Value) -> Vec<Dependency> {
591        let mut dependencies = Vec::new();
592
593        // Process regular dependencies
594        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        // Process dev-dependencies
601        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    /// Parse a single dependency specification.
611    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    /// Analyze all Rust source files in a directory.
645    #[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    /// Analyze a single Rust source file.
664    #[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    /// Extract public APIs from a list of syntax tree items.
695    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                        // Recursively analyze module contents
749                        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                    // Extract methods from impl blocks
758                    self.extract_impl_methods(item_impl, module_path, file_path, apis);
759                }
760                _ => {
761                    // Other items (use, extern crate, etc.) are not public APIs
762                }
763            }
764        }
765    }
766
767    /// Check if a visibility modifier indicates a public item.
768    fn is_public(&self, vis: &Visibility) -> bool {
769        matches!(vis, Visibility::Public(_))
770    }
771
772    /// Create a PublicApi for a function.
773    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, // TODO: Extract actual line number
792        }
793    }
794
795    /// Create a PublicApi for a struct.
796    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    /// Create a PublicApi for an enum.
819    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    /// Create a PublicApi for a trait.
842    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    /// Create a PublicApi for a type alias.
865    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, // Type aliases are categorized as structs for simplicity
880            documentation,
881            deprecated,
882            source_file: file_path.to_path_buf(),
883            line_number: 0,
884        }
885    }
886
887    /// Create a PublicApi for a constant.
888    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    /// Create a PublicApi for a static.
911    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, // Statics are categorized as constants for simplicity
926            documentation,
927            deprecated,
928            source_file: file_path.to_path_buf(),
929            line_number: 0,
930        }
931    }
932
933    /// Extract methods from impl blocks.
934    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        // Get the type being implemented
942        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    /// Extract documentation comments from attributes.
976    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    /// Check if an item is marked as deprecated.
995    fn is_deprecated(&self, attrs: &[Attribute]) -> bool {
996        attrs.iter().any(|attr| attr.path().is_ident("deprecated"))
997    }
998
999    /// Suggest similar crate names when a crate is not found.
1000    #[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            &registry.crates.keys().cloned().collect::<Vec<_>>(),
1005        )
1006    }
1007
1008    /// Static version of suggest_similar_crate_names to avoid borrow checker issues.
1009    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    /// Suggest similar API names when an API is not found.
1026    #[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    /// Static version of suggest_similar_api_names to avoid borrow checker issues.
1032    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    /// Normalize a function signature for comparison by removing whitespace and formatting.
1050    fn normalize_signature(&self, signature: &str) -> String {
1051        signature.chars().filter(|c| !c.is_whitespace()).collect::<String>().to_lowercase()
1052    }
1053
1054    /// Extract field names from a struct signature.
1055    fn extract_struct_fields(&self, signature: &str) -> Vec<String> {
1056        // This is a simplified implementation
1057        // In a real implementation, you'd want to parse the struct more carefully
1058        let mut fields = Vec::new();
1059
1060        // Look for patterns like "field_name: Type" in the signature
1061        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        // Create a test workspace structure
1100        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        // Test string version
1158        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        // Test table version
1164        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}