adk_doc_audit/
version.rs

1//! Version consistency validation for documentation audit.
2//!
3//! This module provides functionality to validate version references in documentation
4//! against actual workspace versions, ensuring consistency across all documentation files.
5
6use crate::{AuditError, FeatureMention, Result, VersionReference, VersionType};
7use regex::Regex;
8use semver::Version;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::path::Path;
12use toml::Value;
13
14/// Version validator that checks consistency between documentation and workspace.
15#[derive(Debug)]
16pub struct VersionValidator {
17    /// Current workspace version information
18    workspace_info: WorkspaceVersionInfo,
19    /// Compiled regex patterns for version parsing
20    patterns: VersionPatterns,
21}
22
23/// Comprehensive workspace version information.
24#[derive(Debug, Clone)]
25pub struct WorkspaceVersionInfo {
26    /// Main workspace version
27    pub workspace_version: String,
28    /// Required Rust version
29    pub rust_version: String,
30    /// Individual crate versions in the workspace
31    pub crate_versions: HashMap<String, String>,
32    /// Dependency versions used across the workspace
33    pub dependency_versions: HashMap<String, String>,
34    /// Feature flags defined in workspace crates
35    pub workspace_features: HashMap<String, Vec<String>>,
36}
37
38/// Compiled regex patterns for version validation.
39#[derive(Debug)]
40struct VersionPatterns {
41    /// Pattern for semantic version strings
42    semver: Regex,
43    /// Pattern for version requirements (e.g., "^1.0", ">=0.5")
44    #[allow(dead_code)]
45    version_req: Regex,
46    /// Pattern for git version references
47    #[allow(dead_code)]
48    git_version: Regex,
49    /// Pattern for path dependencies
50    #[allow(dead_code)]
51    path_dependency: Regex,
52}
53
54/// Result of version validation for a single reference.
55#[derive(Debug, Clone, PartialEq)]
56pub struct VersionValidationResult {
57    /// Whether the version reference is valid
58    pub is_valid: bool,
59    /// Expected version if different from found version
60    pub expected_version: Option<String>,
61    /// Detailed validation message
62    pub message: String,
63    /// Severity of the validation issue
64    pub severity: ValidationSeverity,
65    /// Suggested fix for the issue
66    pub suggestion: Option<String>,
67}
68
69/// Severity levels for version validation issues.
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum ValidationSeverity {
72    /// Critical issue that must be fixed
73    Critical,
74    /// Warning that should be addressed
75    Warning,
76    /// Informational notice
77    Info,
78}
79
80/// Configuration for version validation behavior.
81#[derive(Debug, Clone)]
82pub struct VersionValidationConfig {
83    /// Whether to allow pre-release versions
84    pub allow_prerelease: bool,
85    /// Whether to enforce exact version matches
86    pub strict_matching: bool,
87    /// Whether to validate git dependencies
88    pub validate_git_deps: bool,
89    /// Tolerance for version differences (e.g., patch versions)
90    pub version_tolerance: VersionTolerance,
91}
92
93/// Tolerance levels for version differences.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum VersionTolerance {
96    /// Must match exactly
97    Exact,
98    /// Allow patch version differences
99    Patch,
100    /// Allow minor version differences
101    Minor,
102    /// Allow major version differences (not recommended)
103    Major,
104}
105
106/// Cargo.toml dependency specification.
107#[derive(Debug, Clone, Deserialize, Serialize)]
108#[serde(untagged)]
109pub enum DependencySpec {
110    /// Simple version string
111    Simple(String),
112    /// Detailed dependency specification
113    Detailed {
114        version: Option<String>,
115        git: Option<String>,
116        branch: Option<String>,
117        tag: Option<String>,
118        rev: Option<String>,
119        path: Option<String>,
120        features: Option<Vec<String>>,
121        #[serde(rename = "default-features")]
122        default_features: Option<bool>,
123        optional: Option<bool>,
124    },
125}
126
127impl VersionValidator {
128    /// Creates a new version validator for the given workspace.
129    ///
130    /// # Arguments
131    ///
132    /// * `workspace_path` - Path to the workspace root directory
133    ///
134    /// # Returns
135    ///
136    /// A new `VersionValidator` instance or an error if workspace analysis fails.
137    pub async fn new(workspace_path: &Path) -> Result<Self> {
138        let workspace_info = Self::analyze_workspace(workspace_path).await?;
139        let patterns = VersionPatterns::new()?;
140
141        Ok(Self { workspace_info, patterns })
142    }
143
144    /// Creates a version validator with custom workspace information.
145    ///
146    /// This is useful for testing or when workspace information is already available.
147    pub fn with_workspace_info(workspace_info: WorkspaceVersionInfo) -> Result<Self> {
148        let patterns = VersionPatterns::new()?;
149
150        Ok(Self { workspace_info, patterns })
151    }
152
153    /// Validates a version reference against workspace information.
154    ///
155    /// # Arguments
156    ///
157    /// * `version_ref` - The version reference to validate
158    /// * `config` - Validation configuration options
159    ///
160    /// # Returns
161    ///
162    /// A `VersionValidationResult` indicating whether the reference is valid.
163    pub fn validate_version_reference(
164        &self,
165        version_ref: &VersionReference,
166        config: &VersionValidationConfig,
167    ) -> Result<VersionValidationResult> {
168        match version_ref.version_type {
169            VersionType::RustVersion => self.validate_rust_version(version_ref, config),
170            VersionType::WorkspaceVersion => self.validate_workspace_version(version_ref, config),
171            VersionType::CrateVersion => self.validate_crate_version(version_ref, config),
172            VersionType::Generic => self.validate_generic_version(version_ref, config),
173        }
174    }
175
176    /// Validates multiple version references in batch.
177    ///
178    /// # Arguments
179    ///
180    /// * `version_refs` - Collection of version references to validate
181    /// * `config` - Validation configuration options
182    ///
183    /// # Returns
184    ///
185    /// A vector of validation results corresponding to each input reference.
186    pub fn validate_version_references(
187        &self,
188        version_refs: &[VersionReference],
189        config: &VersionValidationConfig,
190    ) -> Result<Vec<VersionValidationResult>> {
191        version_refs
192            .iter()
193            .map(|version_ref| self.validate_version_reference(version_ref, config))
194            .collect()
195    }
196
197    /// Validates dependency version compatibility across the workspace.
198    ///
199    /// This method checks that all dependency versions used in documentation
200    /// are compatible with the versions actually used in the workspace.
201    pub fn validate_dependency_compatibility(
202        &self,
203        dependency_name: &str,
204        documented_version: &str,
205        config: &VersionValidationConfig,
206    ) -> Result<VersionValidationResult> {
207        if let Some(workspace_version) =
208            self.workspace_info.dependency_versions.get(dependency_name)
209        {
210            self.compare_versions(
211                documented_version,
212                workspace_version,
213                &format!("dependency '{}'", dependency_name),
214                config,
215            )
216        } else {
217            Ok(VersionValidationResult {
218                is_valid: false,
219                expected_version: None,
220                message: format!("Dependency '{}' not found in workspace", dependency_name),
221                severity: ValidationSeverity::Warning,
222                suggestion: Some(format!(
223                    "Remove reference to '{}' or add it to workspace dependencies",
224                    dependency_name
225                )),
226            })
227        }
228    }
229
230    /// Checks if a version string represents a compatible version.
231    ///
232    /// This method uses semantic versioning rules to determine compatibility.
233    pub fn is_version_compatible(
234        &self,
235        version1: &str,
236        version2: &str,
237        tolerance: &VersionTolerance,
238    ) -> Result<bool> {
239        let v1 = Version::parse(version1).map_err(|e| AuditError::ConfigurationError {
240            message: format!("Invalid version '{}': {}", version1, e),
241        })?;
242
243        let v2 = Version::parse(version2).map_err(|e| AuditError::ConfigurationError {
244            message: format!("Invalid version '{}': {}", version2, e),
245        })?;
246
247        let compatible = match tolerance {
248            VersionTolerance::Exact => v1 == v2,
249            VersionTolerance::Patch => v1.major == v2.major && v1.minor == v2.minor,
250            VersionTolerance::Minor => v1.major == v2.major,
251            VersionTolerance::Major => true, // Always compatible with major tolerance
252        };
253
254        Ok(compatible)
255    }
256
257    /// Validates that mentioned crate names exist in the workspace.
258    ///
259    /// # Arguments
260    ///
261    /// * `crate_name` - Name of the crate to validate
262    ///
263    /// # Returns
264    ///
265    /// A `VersionValidationResult` indicating whether the crate exists.
266    pub fn validate_crate_exists(&self, crate_name: &str) -> VersionValidationResult {
267        if self.workspace_info.crate_versions.contains_key(crate_name) {
268            VersionValidationResult {
269                is_valid: true,
270                expected_version: None,
271                message: format!("Crate '{}' exists in workspace", crate_name),
272                severity: ValidationSeverity::Info,
273                suggestion: None,
274            }
275        } else {
276            let suggestion = self.suggest_similar_crate_name(crate_name);
277            VersionValidationResult {
278                is_valid: false,
279                expected_version: None,
280                message: format!("Crate '{}' not found in workspace", crate_name),
281                severity: ValidationSeverity::Warning,
282                suggestion,
283            }
284        }
285    }
286
287    /// Validates that feature flags exist in the specified crate.
288    ///
289    /// # Arguments
290    ///
291    /// * `crate_name` - Name of the crate that should define the feature
292    /// * `feature_name` - Name of the feature flag to validate
293    ///
294    /// # Returns
295    ///
296    /// A `VersionValidationResult` indicating whether the feature exists.
297    pub fn validate_feature_flag(
298        &self,
299        crate_name: &str,
300        feature_name: &str,
301    ) -> VersionValidationResult {
302        // First check if the crate exists
303        if !self.workspace_info.crate_versions.contains_key(crate_name) {
304            return VersionValidationResult {
305                is_valid: false,
306                expected_version: None,
307                message: format!(
308                    "Cannot validate feature '{}': crate '{}' not found in workspace",
309                    feature_name, crate_name
310                ),
311                severity: ValidationSeverity::Warning,
312                suggestion: self.suggest_similar_crate_name(crate_name),
313            };
314        }
315
316        // Check if the feature exists in the crate
317        if let Some(features) = self.workspace_info.workspace_features.get(crate_name) {
318            if features.contains(&feature_name.to_string()) {
319                VersionValidationResult {
320                    is_valid: true,
321                    expected_version: None,
322                    message: format!("Feature '{}' exists in crate '{}'", feature_name, crate_name),
323                    severity: ValidationSeverity::Info,
324                    suggestion: None,
325                }
326            } else {
327                let suggestion = self.suggest_similar_feature_name(crate_name, feature_name);
328                VersionValidationResult {
329                    is_valid: false,
330                    expected_version: None,
331                    message: format!(
332                        "Feature '{}' not found in crate '{}'",
333                        feature_name, crate_name
334                    ),
335                    severity: ValidationSeverity::Warning,
336                    suggestion,
337                }
338            }
339        } else {
340            VersionValidationResult {
341                is_valid: false,
342                expected_version: None,
343                message: format!("Crate '{}' has no features defined", crate_name),
344                severity: ValidationSeverity::Info,
345                suggestion: Some(format!("Check if '{}' is the correct crate name", crate_name)),
346            }
347        }
348    }
349
350    /// Validates multiple crate names in batch.
351    ///
352    /// # Arguments
353    ///
354    /// * `crate_names` - Collection of crate names to validate
355    ///
356    /// # Returns
357    ///
358    /// A vector of validation results corresponding to each input crate name.
359    pub fn validate_crate_names(&self, crate_names: &[String]) -> Vec<VersionValidationResult> {
360        crate_names.iter().map(|crate_name| self.validate_crate_exists(crate_name)).collect()
361    }
362
363    /// Validates feature flag references from documentation.
364    ///
365    /// This method processes feature mentions extracted from documentation
366    /// and validates them against the workspace feature definitions.
367    pub fn validate_feature_mentions(
368        &self,
369        feature_mentions: &[FeatureMention],
370    ) -> Vec<VersionValidationResult> {
371        feature_mentions
372            .iter()
373            .map(|mention| {
374                if let Some(crate_name) = &mention.crate_name {
375                    self.validate_feature_flag(crate_name, &mention.feature_name)
376                } else {
377                    // Try to infer crate name from context or check all crates
378                    self.validate_feature_in_any_crate(&mention.feature_name)
379                }
380            })
381            .collect()
382    }
383
384    /// Validates that a feature exists in any workspace crate.
385    ///
386    /// This is used when the crate name cannot be determined from context.
387    pub fn validate_feature_in_any_crate(&self, feature_name: &str) -> VersionValidationResult {
388        for (crate_name, features) in &self.workspace_info.workspace_features {
389            if features.contains(&feature_name.to_string()) {
390                return VersionValidationResult {
391                    is_valid: true,
392                    expected_version: None,
393                    message: format!("Feature '{}' found in crate '{}'", feature_name, crate_name),
394                    severity: ValidationSeverity::Info,
395                    suggestion: None,
396                };
397            }
398        }
399
400        VersionValidationResult {
401            is_valid: false,
402            expected_version: None,
403            message: format!("Feature '{}' not found in any workspace crate", feature_name),
404            severity: ValidationSeverity::Warning,
405            suggestion: self.suggest_similar_feature_in_workspace(feature_name),
406        }
407    }
408
409    /// Gets all crate names in the workspace.
410    pub fn get_workspace_crates(&self) -> Vec<String> {
411        self.workspace_info.crate_versions.keys().cloned().collect()
412    }
413
414    /// Gets all features defined in a specific crate.
415    pub fn get_crate_features(&self, crate_name: &str) -> Option<Vec<String>> {
416        self.workspace_info.workspace_features.get(crate_name).cloned()
417    }
418
419    /// Gets all features defined across the workspace.
420    pub fn get_all_workspace_features(&self) -> HashMap<String, Vec<String>> {
421        self.workspace_info.workspace_features.clone()
422    }
423
424    /// Suggests the correct version for an invalid reference.
425    ///
426    /// This method provides intelligent suggestions based on the type of version
427    /// reference and available workspace information.
428    pub fn suggest_correct_version(&self, version_ref: &VersionReference) -> Option<String> {
429        match version_ref.version_type {
430            VersionType::RustVersion => Some(self.workspace_info.rust_version.clone()),
431            VersionType::WorkspaceVersion => Some(self.workspace_info.workspace_version.clone()),
432            VersionType::CrateVersion => {
433                // Try to extract crate name from context
434                if let Some(crate_name) = self.extract_crate_name_from_context(&version_ref.context)
435                {
436                    self.workspace_info.crate_versions.get(&crate_name).cloned()
437                } else {
438                    None
439                }
440            }
441            VersionType::Generic => {
442                // Try to match against known dependencies
443                self.find_best_version_match(&version_ref.version)
444            }
445        }
446    }
447
448    /// Suggests a similar crate name when a crate is not found.
449    fn suggest_similar_crate_name(&self, target: &str) -> Option<String> {
450        let crate_names: Vec<&String> = self.workspace_info.crate_versions.keys().collect();
451
452        // Look for exact substring matches first
453        for crate_name in &crate_names {
454            if crate_name.contains(target) || target.contains(crate_name.as_str()) {
455                return Some(format!("Did you mean '{}'?", crate_name));
456            }
457        }
458
459        // Look for similar names using edit distance
460        let mut best_match = None;
461        let mut best_distance = usize::MAX;
462
463        for crate_name in &crate_names {
464            let distance = self.edit_distance(target, crate_name);
465            if distance < best_distance && distance <= 3 {
466                best_distance = distance;
467                best_match = Some(crate_name);
468            }
469        }
470
471        if let Some(match_name) = best_match {
472            Some(format!("Did you mean '{}'?", match_name))
473        } else if !crate_names.is_empty() {
474            Some(format!(
475                "Available crates: {}",
476                crate_names.iter().take(5).map(|s| s.as_str()).collect::<Vec<_>>().join(", ")
477            ))
478        } else {
479            None
480        }
481    }
482
483    /// Suggests a similar feature name when a feature is not found.
484    fn suggest_similar_feature_name(&self, crate_name: &str, target: &str) -> Option<String> {
485        if let Some(features) = self.workspace_info.workspace_features.get(crate_name) {
486            // Look for exact substring matches first
487            for feature in features {
488                if feature.contains(target) || target.contains(feature.as_str()) {
489                    return Some(format!("Did you mean '{}'?", feature));
490                }
491            }
492
493            // Look for similar names using edit distance
494            let mut best_match = None;
495            let mut best_distance = usize::MAX;
496
497            for feature in features {
498                let distance = self.edit_distance(target, feature);
499                if distance < best_distance && distance <= 2 {
500                    best_distance = distance;
501                    best_match = Some(feature);
502                }
503            }
504
505            if let Some(match_name) = best_match {
506                Some(format!("Did you mean '{}'?", match_name))
507            } else if !features.is_empty() {
508                Some(format!(
509                    "Available features in '{}': {}",
510                    crate_name,
511                    features.iter().take(5).cloned().collect::<Vec<_>>().join(", ")
512                ))
513            } else {
514                None
515            }
516        } else {
517            None
518        }
519    }
520
521    /// Suggests a similar feature name across all workspace crates.
522    fn suggest_similar_feature_in_workspace(&self, target: &str) -> Option<String> {
523        let mut all_features = Vec::new();
524
525        for (crate_name, features) in &self.workspace_info.workspace_features {
526            for feature in features {
527                all_features.push((crate_name, feature));
528            }
529        }
530
531        // Look for exact substring matches first
532        for (crate_name, feature) in &all_features {
533            if feature.contains(target) || target.contains(feature.as_str()) {
534                return Some(format!("Did you mean '{}' in crate '{}'?", feature, crate_name));
535            }
536        }
537
538        // Look for similar names using edit distance
539        let mut best_match = None;
540        let mut best_distance = usize::MAX;
541
542        for (crate_name, feature) in &all_features {
543            let distance = self.edit_distance(target, feature);
544            if distance < best_distance && distance <= 2 {
545                best_distance = distance;
546                best_match = Some((crate_name, feature));
547            }
548        }
549
550        if let Some((crate_name, feature)) = best_match {
551            Some(format!("Did you mean '{}' in crate '{}'?", feature, crate_name))
552        } else if !all_features.is_empty() {
553            let sample_features: Vec<String> = all_features
554                .iter()
555                .take(3)
556                .map(|(crate_name, feature)| format!("'{}' in '{}'", feature, crate_name))
557                .collect();
558            Some(format!("Available features: {}", sample_features.join(", ")))
559        } else {
560            None
561        }
562    }
563
564    /// Calculates edit distance between two strings for similarity matching.
565    fn edit_distance(&self, s1: &str, s2: &str) -> usize {
566        let len1 = s1.len();
567        let len2 = s2.len();
568
569        if len1 == 0 {
570            return len2;
571        }
572        if len2 == 0 {
573            return len1;
574        }
575
576        let mut matrix = vec![vec![0; len2 + 1]; len1 + 1];
577
578        // Initialize first row and column
579        for (i, row) in matrix.iter_mut().enumerate().take(len1 + 1) {
580            row[0] = i;
581        }
582        #[allow(clippy::needless_range_loop)]
583        for j in 0..=len2 {
584            matrix[0][j] = j;
585        }
586
587        let s1_chars: Vec<char> = s1.chars().collect();
588        let s2_chars: Vec<char> = s2.chars().collect();
589
590        for i in 1..=len1 {
591            for j in 1..=len2 {
592                let cost = if s1_chars[i - 1] == s2_chars[j - 1] { 0 } else { 1 };
593
594                matrix[i][j] = std::cmp::min(
595                    std::cmp::min(
596                        matrix[i - 1][j] + 1, // deletion
597                        matrix[i][j - 1] + 1, // insertion
598                    ),
599                    matrix[i - 1][j - 1] + cost, // substitution
600                );
601            }
602        }
603
604        matrix[len1][len2]
605    }
606
607    /// Analyzes the workspace to extract version information.
608    async fn analyze_workspace(workspace_path: &Path) -> Result<WorkspaceVersionInfo> {
609        let workspace_toml_path = workspace_path.join("Cargo.toml");
610        let workspace_content =
611            tokio::fs::read_to_string(&workspace_toml_path).await.map_err(|e| {
612                AuditError::IoError { path: workspace_toml_path.clone(), details: e.to_string() }
613            })?;
614
615        let workspace_toml: Value = toml::from_str(&workspace_content).map_err(|e| {
616            AuditError::TomlError { file_path: workspace_toml_path, details: e.to_string() }
617        })?;
618
619        // Extract workspace version
620        let workspace_version = workspace_toml
621            .get("workspace")
622            .and_then(|w| w.get("package"))
623            .and_then(|p| p.get("version"))
624            .and_then(|v| v.as_str())
625            .unwrap_or("0.1.0")
626            .to_string();
627
628        // Extract Rust version requirement
629        let rust_version = workspace_toml
630            .get("workspace")
631            .and_then(|w| w.get("package"))
632            .and_then(|p| p.get("rust-version"))
633            .and_then(|v| v.as_str())
634            .unwrap_or("1.85.0")
635            .to_string();
636
637        // Analyze individual crates in the workspace
638        let mut crate_versions = HashMap::new();
639        let mut dependency_versions = HashMap::new();
640        let mut workspace_features = HashMap::new();
641
642        if let Some(members) = workspace_toml
643            .get("workspace")
644            .and_then(|w| w.get("members"))
645            .and_then(|m| m.as_array())
646        {
647            for member in members {
648                if let Some(member_path) = member.as_str() {
649                    let crate_path = workspace_path.join(member_path);
650                    if let Ok(crate_info) = Self::analyze_crate(&crate_path).await {
651                        crate_versions.insert(crate_info.name.clone(), crate_info.version);
652
653                        // Collect dependencies
654                        for dep in crate_info.dependencies {
655                            dependency_versions.insert(dep.name, dep.version);
656                        }
657
658                        // Collect features
659                        if !crate_info.features.is_empty() {
660                            workspace_features.insert(crate_info.name, crate_info.features);
661                        }
662                    }
663                }
664            }
665        }
666
667        Ok(WorkspaceVersionInfo {
668            workspace_version,
669            rust_version,
670            crate_versions,
671            dependency_versions,
672            workspace_features,
673        })
674    }
675
676    /// Analyzes a single crate to extract its version and dependency information.
677    async fn analyze_crate(crate_path: &Path) -> Result<CrateAnalysisResult> {
678        let cargo_toml_path = crate_path.join("Cargo.toml");
679        let content = tokio::fs::read_to_string(&cargo_toml_path).await.map_err(|e| {
680            AuditError::IoError { path: cargo_toml_path.clone(), details: e.to_string() }
681        })?;
682
683        let cargo_toml: Value = toml::from_str(&content).map_err(|e| AuditError::TomlError {
684            file_path: cargo_toml_path,
685            details: e.to_string(),
686        })?;
687
688        // Extract crate name and version
689        let name = cargo_toml
690            .get("package")
691            .and_then(|p| p.get("name"))
692            .and_then(|n| n.as_str())
693            .unwrap_or("unknown")
694            .to_string();
695
696        let version = cargo_toml
697            .get("package")
698            .and_then(|p| p.get("version"))
699            .and_then(|v| v.as_str())
700            .unwrap_or("0.1.0")
701            .to_string();
702
703        // Extract dependencies
704        let mut dependencies = Vec::new();
705        if let Some(deps) = cargo_toml.get("dependencies").and_then(|d| d.as_table()) {
706            for (dep_name, dep_spec) in deps {
707                if let Some(version) = Self::extract_dependency_version(dep_spec) {
708                    dependencies.push(DependencyInfo { name: dep_name.clone(), version });
709                }
710            }
711        }
712
713        // Extract features
714        let mut features = Vec::new();
715        if let Some(feature_table) = cargo_toml.get("features").and_then(|f| f.as_table()) {
716            features.extend(feature_table.keys().cloned());
717        }
718
719        Ok(CrateAnalysisResult { name, version, dependencies, features })
720    }
721
722    /// Extracts version string from a dependency specification.
723    fn extract_dependency_version(dep_spec: &Value) -> Option<String> {
724        match dep_spec {
725            Value::String(version) => Some(version.clone()),
726            Value::Table(table) => {
727                table.get("version").and_then(|v| v.as_str()).map(|s| s.to_string())
728            }
729            _ => None,
730        }
731    }
732
733    /// Validates a Rust version reference.
734    fn validate_rust_version(
735        &self,
736        version_ref: &VersionReference,
737        config: &VersionValidationConfig,
738    ) -> Result<VersionValidationResult> {
739        self.compare_versions(
740            &version_ref.version,
741            &self.workspace_info.rust_version,
742            "Rust version",
743            config,
744        )
745    }
746
747    /// Validates a workspace version reference.
748    fn validate_workspace_version(
749        &self,
750        version_ref: &VersionReference,
751        config: &VersionValidationConfig,
752    ) -> Result<VersionValidationResult> {
753        self.compare_versions(
754            &version_ref.version,
755            &self.workspace_info.workspace_version,
756            "workspace version",
757            config,
758        )
759    }
760
761    /// Validates a crate version reference.
762    fn validate_crate_version(
763        &self,
764        version_ref: &VersionReference,
765        config: &VersionValidationConfig,
766    ) -> Result<VersionValidationResult> {
767        // Try to extract crate name from context
768        if let Some(crate_name) = self.extract_crate_name_from_context(&version_ref.context) {
769            if let Some(expected_version) = self.workspace_info.crate_versions.get(&crate_name) {
770                return self.compare_versions(
771                    &version_ref.version,
772                    expected_version,
773                    &format!("crate '{}' version", crate_name),
774                    config,
775                );
776            }
777        }
778
779        // If we can't determine the specific crate, provide a generic validation
780        Ok(VersionValidationResult {
781            is_valid: true, // Assume valid if we can't verify
782            expected_version: None,
783            message: "Unable to verify crate version - crate name not found in context".to_string(),
784            severity: ValidationSeverity::Info,
785            suggestion: Some(
786                "Ensure crate name is clearly specified in the documentation".to_string(),
787            ),
788        })
789    }
790
791    /// Validates a generic version reference.
792    fn validate_generic_version(
793        &self,
794        version_ref: &VersionReference,
795        _config: &VersionValidationConfig,
796    ) -> Result<VersionValidationResult> {
797        // For generic versions, we can only do basic format validation
798        if self.patterns.semver.is_match(&version_ref.version) {
799            Ok(VersionValidationResult {
800                is_valid: true,
801                expected_version: None,
802                message: "Version format is valid".to_string(),
803                severity: ValidationSeverity::Info,
804                suggestion: None,
805            })
806        } else {
807            Ok(VersionValidationResult {
808                is_valid: false,
809                expected_version: None,
810                message: format!("Invalid version format: '{}'", version_ref.version),
811                severity: ValidationSeverity::Warning,
812                suggestion: Some("Use semantic versioning format (e.g., '1.0.0')".to_string()),
813            })
814        }
815    }
816
817    /// Compares two version strings and returns validation result.
818    fn compare_versions(
819        &self,
820        found_version: &str,
821        expected_version: &str,
822        context: &str,
823        config: &VersionValidationConfig,
824    ) -> Result<VersionValidationResult> {
825        if found_version == expected_version {
826            return Ok(VersionValidationResult {
827                is_valid: true,
828                expected_version: None,
829                message: format!("{} is correct", context),
830                severity: ValidationSeverity::Info,
831                suggestion: None,
832            });
833        }
834
835        // Check if versions are compatible based on tolerance
836        let compatible =
837            self.is_version_compatible(found_version, expected_version, &config.version_tolerance)?;
838
839        if compatible {
840            Ok(VersionValidationResult {
841                is_valid: true,
842                expected_version: Some(expected_version.to_string()),
843                message: format!(
844                    "{} '{}' is compatible with expected '{}'",
845                    context, found_version, expected_version
846                ),
847                severity: ValidationSeverity::Info,
848                suggestion: None,
849            })
850        } else {
851            let severity = if config.strict_matching {
852                ValidationSeverity::Critical
853            } else {
854                ValidationSeverity::Warning
855            };
856
857            Ok(VersionValidationResult {
858                is_valid: false,
859                expected_version: Some(expected_version.to_string()),
860                message: format!(
861                    "{} mismatch: found '{}', expected '{}'",
862                    context, found_version, expected_version
863                ),
864                severity,
865                suggestion: Some(format!("Update to version '{}'", expected_version)),
866            })
867        }
868    }
869
870    /// Extracts crate name from the context string.
871    fn extract_crate_name_from_context(&self, context: &str) -> Option<String> {
872        // Look for patterns like 'adk-core = { version = "..."' or 'adk_core::'
873        if let Some(captures) = Regex::new(r#"(adk[-_]\w+)"#).ok()?.captures(context) {
874            if let Some(crate_match) = captures.get(1) {
875                return Some(crate_match.as_str().replace('_', "-"));
876            }
877        }
878        None
879    }
880
881    /// Finds the best version match for a given version string.
882    fn find_best_version_match(&self, version: &str) -> Option<String> {
883        // Try to find a dependency with a similar version
884        for dep_version in self.workspace_info.dependency_versions.values() {
885            if let (Ok(v1), Ok(v2)) = (Version::parse(version), Version::parse(dep_version)) {
886                if v1.major == v2.major {
887                    return Some(dep_version.clone());
888                }
889            }
890        }
891        None
892    }
893}
894
895impl VersionPatterns {
896    /// Creates new compiled regex patterns for version validation.
897    fn new() -> Result<Self> {
898        Ok(Self {
899            semver: Regex::new(r"^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?$")
900                .map_err(|e| AuditError::RegexError {
901                    pattern: "semver".to_string(),
902                    details: e.to_string(),
903                })?,
904
905            version_req: Regex::new(r"^[~^>=<]*\d+(?:\.\d+)?(?:\.\d+)?").map_err(|e| {
906                AuditError::RegexError {
907                    pattern: "version_req".to_string(),
908                    details: e.to_string(),
909                }
910            })?,
911
912            git_version: Regex::new(r#"git\s*=\s*"([^"]+)""#).map_err(|e| {
913                AuditError::RegexError {
914                    pattern: "git_version".to_string(),
915                    details: e.to_string(),
916                }
917            })?,
918
919            path_dependency: Regex::new(r#"path\s*=\s*"([^"]+)""#).map_err(|e| {
920                AuditError::RegexError {
921                    pattern: "path_dependency".to_string(),
922                    details: e.to_string(),
923                }
924            })?,
925        })
926    }
927}
928
929impl Default for VersionValidationConfig {
930    fn default() -> Self {
931        Self {
932            allow_prerelease: false,
933            strict_matching: false,
934            validate_git_deps: true,
935            version_tolerance: VersionTolerance::Patch,
936        }
937    }
938}
939
940/// Result of analyzing a single crate.
941#[derive(Debug, Clone)]
942struct CrateAnalysisResult {
943    name: String,
944    version: String,
945    dependencies: Vec<DependencyInfo>,
946    features: Vec<String>,
947}
948
949/// Information about a dependency.
950#[derive(Debug, Clone)]
951struct DependencyInfo {
952    name: String,
953    version: String,
954}
955
956#[cfg(test)]
957mod tests {
958    use super::*;
959    use std::collections::HashMap;
960
961    fn create_test_workspace_info() -> WorkspaceVersionInfo {
962        let mut crate_versions = HashMap::new();
963        crate_versions.insert("adk-core".to_string(), "0.1.0".to_string());
964        crate_versions.insert("adk-model".to_string(), "0.1.0".to_string());
965
966        let mut dependency_versions = HashMap::new();
967        dependency_versions.insert("serde".to_string(), "1.0.195".to_string());
968        dependency_versions.insert("tokio".to_string(), "1.35.0".to_string());
969
970        let mut workspace_features = HashMap::new();
971        workspace_features.insert("adk-core".to_string(), vec!["async".to_string()]);
972
973        WorkspaceVersionInfo {
974            workspace_version: "0.1.0".to_string(),
975            rust_version: "1.85.0".to_string(),
976            crate_versions,
977            dependency_versions,
978            workspace_features,
979        }
980    }
981
982    fn create_test_validator() -> VersionValidator {
983        let workspace_info = create_test_workspace_info();
984        VersionValidator::with_workspace_info(workspace_info).unwrap()
985    }
986
987    #[test]
988    fn test_validator_creation() {
989        let validator = create_test_validator();
990        assert_eq!(validator.workspace_info.workspace_version, "0.1.0");
991        assert_eq!(validator.workspace_info.rust_version, "1.85.0");
992    }
993
994    #[test]
995    fn test_rust_version_validation() {
996        let validator = create_test_validator();
997        let config = VersionValidationConfig::default();
998
999        // Valid Rust version
1000        let valid_ref = VersionReference {
1001            version: "1.85.0".to_string(),
1002            version_type: VersionType::RustVersion,
1003            line_number: 1,
1004            context: "rust-version = \"1.85.0\"".to_string(),
1005        };
1006
1007        let result = validator.validate_version_reference(&valid_ref, &config).unwrap();
1008        assert!(result.is_valid);
1009        assert_eq!(result.severity, ValidationSeverity::Info);
1010
1011        // Invalid Rust version
1012        let invalid_ref = VersionReference {
1013            version: "1.80.0".to_string(),
1014            version_type: VersionType::RustVersion,
1015            line_number: 1,
1016            context: "rust-version = \"1.80.0\"".to_string(),
1017        };
1018
1019        let result = validator.validate_version_reference(&invalid_ref, &config).unwrap();
1020        assert!(!result.is_valid);
1021        assert_eq!(result.expected_version, Some("1.85.0".to_string()));
1022    }
1023
1024    #[test]
1025    fn test_workspace_version_validation() {
1026        let validator = create_test_validator();
1027        let config = VersionValidationConfig::default();
1028
1029        let version_ref = VersionReference {
1030            version: "0.1.0".to_string(),
1031            version_type: VersionType::WorkspaceVersion,
1032            line_number: 1,
1033            context: "adk-core = { version = \"0.1.0\" }".to_string(),
1034        };
1035
1036        let result = validator.validate_version_reference(&version_ref, &config).unwrap();
1037        assert!(result.is_valid);
1038    }
1039
1040    #[test]
1041    fn test_crate_version_validation() {
1042        let validator = create_test_validator();
1043        let config = VersionValidationConfig::default();
1044
1045        let version_ref = VersionReference {
1046            version: "0.1.0".to_string(),
1047            version_type: VersionType::CrateVersion,
1048            line_number: 1,
1049            context: "adk-core = { version = \"0.1.0\" }".to_string(),
1050        };
1051
1052        let result = validator.validate_version_reference(&version_ref, &config).unwrap();
1053        assert!(result.is_valid);
1054    }
1055
1056    #[test]
1057    fn test_version_compatibility() {
1058        let validator = create_test_validator();
1059
1060        // Test exact compatibility
1061        assert!(
1062            validator.is_version_compatible("1.0.0", "1.0.0", &VersionTolerance::Exact).unwrap()
1063        );
1064        assert!(
1065            !validator.is_version_compatible("1.0.0", "1.0.1", &VersionTolerance::Exact).unwrap()
1066        );
1067
1068        // Test patch compatibility
1069        assert!(
1070            validator.is_version_compatible("1.0.0", "1.0.1", &VersionTolerance::Patch).unwrap()
1071        );
1072        assert!(
1073            !validator.is_version_compatible("1.0.0", "1.1.0", &VersionTolerance::Patch).unwrap()
1074        );
1075
1076        // Test minor compatibility
1077        assert!(
1078            validator.is_version_compatible("1.0.0", "1.1.0", &VersionTolerance::Minor).unwrap()
1079        );
1080        assert!(
1081            !validator.is_version_compatible("1.0.0", "2.0.0", &VersionTolerance::Minor).unwrap()
1082        );
1083
1084        // Test major compatibility
1085        assert!(
1086            validator.is_version_compatible("1.0.0", "2.0.0", &VersionTolerance::Major).unwrap()
1087        );
1088    }
1089
1090    #[test]
1091    fn test_dependency_compatibility() {
1092        let validator = create_test_validator();
1093        let config = VersionValidationConfig::default();
1094
1095        // Valid dependency version
1096        let result =
1097            validator.validate_dependency_compatibility("serde", "1.0.195", &config).unwrap();
1098        assert!(result.is_valid);
1099
1100        // Invalid dependency version (different minor version)
1101        let result =
1102            validator.validate_dependency_compatibility("serde", "1.1.0", &config).unwrap();
1103        assert!(!result.is_valid);
1104        assert_eq!(result.expected_version, Some("1.0.195".to_string()));
1105
1106        // Unknown dependency
1107        let result =
1108            validator.validate_dependency_compatibility("unknown", "1.0.0", &config).unwrap();
1109        assert!(!result.is_valid);
1110        assert_eq!(result.severity, ValidationSeverity::Warning);
1111    }
1112
1113    #[test]
1114    fn test_version_suggestion() {
1115        let validator = create_test_validator();
1116
1117        // Rust version suggestion
1118        let rust_ref = VersionReference {
1119            version: "1.80.0".to_string(),
1120            version_type: VersionType::RustVersion,
1121            line_number: 1,
1122            context: "rust-version = \"1.80.0\"".to_string(),
1123        };
1124        assert_eq!(validator.suggest_correct_version(&rust_ref), Some("1.85.0".to_string()));
1125
1126        // Workspace version suggestion
1127        let workspace_ref = VersionReference {
1128            version: "0.0.1".to_string(),
1129            version_type: VersionType::WorkspaceVersion,
1130            line_number: 1,
1131            context: "version = \"0.0.1\"".to_string(),
1132        };
1133        assert_eq!(validator.suggest_correct_version(&workspace_ref), Some("0.1.0".to_string()));
1134    }
1135
1136    #[test]
1137    fn test_crate_name_extraction() {
1138        let validator = create_test_validator();
1139
1140        // Test various context formats
1141        assert_eq!(
1142            validator.extract_crate_name_from_context("adk-core = { version = \"0.1.0\" }"),
1143            Some("adk-core".to_string())
1144        );
1145
1146        assert_eq!(
1147            validator.extract_crate_name_from_context("use adk_core::Agent;"),
1148            Some("adk-core".to_string())
1149        );
1150
1151        assert_eq!(
1152            validator.extract_crate_name_from_context("The adk_model crate provides..."),
1153            Some("adk-model".to_string())
1154        );
1155
1156        assert_eq!(validator.extract_crate_name_from_context("No crate name here"), None);
1157    }
1158
1159    #[test]
1160    fn test_batch_validation() {
1161        let validator = create_test_validator();
1162        let config = VersionValidationConfig::default();
1163
1164        let version_refs = vec![
1165            VersionReference {
1166                version: "1.85.0".to_string(),
1167                version_type: VersionType::RustVersion,
1168                line_number: 1,
1169                context: "rust-version = \"1.85.0\"".to_string(),
1170            },
1171            VersionReference {
1172                version: "0.1.0".to_string(),
1173                version_type: VersionType::WorkspaceVersion,
1174                line_number: 2,
1175                context: "version = \"0.1.0\"".to_string(),
1176            },
1177        ];
1178
1179        let results = validator.validate_version_references(&version_refs, &config).unwrap();
1180        assert_eq!(results.len(), 2);
1181        assert!(results[0].is_valid);
1182        assert!(results[1].is_valid);
1183    }
1184
1185    #[test]
1186    fn test_crate_name_validation() {
1187        let validator = create_test_validator();
1188
1189        // Valid crate name
1190        let result = validator.validate_crate_exists("adk-core");
1191        assert!(result.is_valid);
1192        assert_eq!(result.severity, ValidationSeverity::Info);
1193
1194        // Invalid crate name
1195        let result = validator.validate_crate_exists("nonexistent-crate");
1196        assert!(!result.is_valid);
1197        assert_eq!(result.severity, ValidationSeverity::Warning);
1198        assert!(result.suggestion.is_some());
1199    }
1200
1201    #[test]
1202    fn test_feature_flag_validation() {
1203        let validator = create_test_validator();
1204
1205        // Valid feature in existing crate
1206        let result = validator.validate_feature_flag("adk-core", "async");
1207        assert!(result.is_valid);
1208        assert_eq!(result.severity, ValidationSeverity::Info);
1209
1210        // Invalid feature in existing crate
1211        let result = validator.validate_feature_flag("adk-core", "nonexistent-feature");
1212        assert!(!result.is_valid);
1213        assert_eq!(result.severity, ValidationSeverity::Warning);
1214
1215        // Feature in nonexistent crate
1216        let result = validator.validate_feature_flag("nonexistent-crate", "some-feature");
1217        assert!(!result.is_valid);
1218        assert_eq!(result.severity, ValidationSeverity::Warning);
1219    }
1220
1221    #[test]
1222    fn test_batch_crate_validation() {
1223        let validator = create_test_validator();
1224
1225        let crate_names =
1226            vec!["adk-core".to_string(), "adk-model".to_string(), "nonexistent".to_string()];
1227
1228        let results = validator.validate_crate_names(&crate_names);
1229        assert_eq!(results.len(), 3);
1230        assert!(results[0].is_valid); // adk-core exists
1231        assert!(results[1].is_valid); // adk-model exists
1232        assert!(!results[2].is_valid); // nonexistent doesn't exist
1233    }
1234
1235    #[test]
1236    fn test_feature_in_any_crate() {
1237        let validator = create_test_validator();
1238
1239        // Feature that exists in workspace
1240        let result = validator.validate_feature_in_any_crate("async");
1241        assert!(result.is_valid);
1242
1243        // Feature that doesn't exist anywhere
1244        let result = validator.validate_feature_in_any_crate("nonexistent-feature");
1245        assert!(!result.is_valid);
1246    }
1247
1248    #[test]
1249    fn test_workspace_queries() {
1250        let validator = create_test_validator();
1251
1252        // Test getting workspace crates
1253        let crates = validator.get_workspace_crates();
1254        assert!(crates.contains(&"adk-core".to_string()));
1255        assert!(crates.contains(&"adk-model".to_string()));
1256
1257        // Test getting crate features
1258        let features = validator.get_crate_features("adk-core");
1259        assert!(features.is_some());
1260        assert!(features.unwrap().contains(&"async".to_string()));
1261
1262        // Test getting all features
1263        let all_features = validator.get_all_workspace_features();
1264        assert!(all_features.contains_key("adk-core"));
1265    }
1266
1267    #[test]
1268    fn test_similar_name_suggestions() {
1269        let validator = create_test_validator();
1270
1271        // Test crate name suggestion
1272        let result = validator.validate_crate_exists("adk-cor"); // typo in adk-core
1273        assert!(!result.is_valid);
1274        assert!(result.suggestion.is_some());
1275        assert!(result.suggestion.unwrap().contains("adk-core"));
1276
1277        // Test feature name suggestion
1278        let result = validator.validate_feature_flag("adk-core", "asyn"); // typo in async
1279        assert!(!result.is_valid);
1280        assert!(result.suggestion.is_some());
1281        assert!(result.suggestion.unwrap().contains("async"));
1282    }
1283
1284    #[test]
1285    fn test_edit_distance() {
1286        let validator = create_test_validator();
1287
1288        assert_eq!(validator.edit_distance("", ""), 0);
1289        assert_eq!(validator.edit_distance("abc", "abc"), 0);
1290        assert_eq!(validator.edit_distance("abc", "ab"), 1);
1291        assert_eq!(validator.edit_distance("abc", "def"), 3);
1292        assert_eq!(validator.edit_distance("kitten", "sitting"), 3);
1293    }
1294
1295    #[test]
1296    fn test_version_tolerance_config() {
1297        let validator = create_test_validator();
1298
1299        // Strict matching config
1300        let strict_config = VersionValidationConfig {
1301            strict_matching: true,
1302            version_tolerance: VersionTolerance::Exact,
1303            ..Default::default()
1304        };
1305
1306        let version_ref = VersionReference {
1307            version: "1.84.0".to_string(),
1308            version_type: VersionType::RustVersion,
1309            line_number: 1,
1310            context: "rust-version = \"1.84.0\"".to_string(),
1311        };
1312
1313        let result = validator.validate_version_reference(&version_ref, &strict_config).unwrap();
1314        assert!(!result.is_valid);
1315        assert_eq!(result.severity, ValidationSeverity::Critical);
1316
1317        // Lenient matching config with major version difference
1318        let lenient_config = VersionValidationConfig {
1319            strict_matching: false,
1320            version_tolerance: VersionTolerance::Minor,
1321            ..Default::default()
1322        };
1323
1324        // Use a version with different major version to ensure it fails
1325        let major_diff_ref = VersionReference {
1326            version: "2.0.0".to_string(),
1327            version_type: VersionType::RustVersion,
1328            line_number: 1,
1329            context: "rust-version = \"2.0.0\"".to_string(),
1330        };
1331
1332        let result =
1333            validator.validate_version_reference(&major_diff_ref, &lenient_config).unwrap();
1334        // Invalid because major version difference, but warning instead of critical
1335        assert!(!result.is_valid);
1336        assert_eq!(result.severity, ValidationSeverity::Warning);
1337    }
1338}