ferrous_forge/edition/
analyzer.rs

1//! Edition compatibility analyzer
2
3use crate::{Error, Result};
4use std::path::Path;
5use std::process::Command;
6use walkdir::WalkDir;
7
8use super::Edition;
9
10/// Edition analyzer for checking compatibility
11pub struct EditionAnalyzer {
12    project_path: std::path::PathBuf,
13}
14
15impl EditionAnalyzer {
16    /// Create a new edition analyzer
17    pub fn new(project_path: impl AsRef<Path>) -> Self {
18        Self {
19            project_path: project_path.as_ref().to_path_buf(),
20        }
21    }
22    
23    /// Analyze the project for edition compatibility issues
24    pub async fn analyze(&self, target_edition: Edition) -> Result<AnalysisReport> {
25        let mut report = AnalysisReport {
26            target_edition,
27            total_files: 0,
28            issues: Vec::new(),
29            warnings: Vec::new(),
30            suggestions: Vec::new(),
31        };
32        
33        // Count Rust source files
34        for entry in WalkDir::new(&self.project_path)
35            .into_iter()
36            .filter_map(|e| e.ok())
37        {
38            if entry.path().extension().and_then(|s| s.to_str()) == Some("rs") {
39                report.total_files += 1;
40            }
41        }
42        
43        // Run cargo check with edition lints
44        self.check_edition_lints(target_edition, &mut report).await?;
45        
46        // Check for common patterns that need updating
47        self.check_common_patterns(target_edition, &mut report).await?;
48        
49        // Add general suggestions
50        self.add_suggestions(target_edition, &mut report);
51        
52        Ok(report)
53    }
54    
55    /// Run cargo check with edition-specific lints
56    async fn check_edition_lints(&self, _edition: Edition, report: &mut AnalysisReport) -> Result<()> {
57        let cargo_path = which::which("cargo")
58            .map_err(|_| Error::rust_not_found("cargo not found"))?;
59        
60        // Get migration lints for current edition
61        let current_edition = super::detect_edition(&self.project_path.join("Cargo.toml")).await?;
62        let lints = current_edition.migration_lints();
63        
64        if lints.is_empty() {
65            return Ok(());
66        }
67        
68        // Run cargo clippy with edition lints
69        for lint in lints {
70            let output = Command::new(&cargo_path)
71                .current_dir(&self.project_path)
72                .args(&["clippy", "--", "-W", &lint])
73                .output()?;
74            
75            let stderr = String::from_utf8_lossy(&output.stderr);
76            
77            // Parse lint output for issues
78            for line in stderr.lines() {
79                if line.contains("warning:") || line.contains("error:") {
80                    report.issues.push(EditionIssue {
81                        file: self.extract_file_from_lint(line),
82                        line: self.extract_line_from_lint(line),
83                        message: line.to_string(),
84                        severity: if line.contains("error:") {
85                            Severity::Error
86                        } else {
87                            Severity::Warning
88                        },
89                    });
90                }
91            }
92        }
93        
94        Ok(())
95    }
96    
97    /// Check for common patterns that need updating
98    async fn check_common_patterns(&self, target_edition: Edition, report: &mut AnalysisReport) -> Result<()> {
99        match target_edition {
100            Edition::Edition2018 => {
101                // Check for extern crate declarations (not needed in 2018+)
102                report.warnings.push(
103                    "Consider removing `extern crate` declarations (except for macros)".to_string()
104                );
105            }
106            Edition::Edition2021 => {
107                // Check for disjoint captures in closures
108                report.warnings.push(
109                    "Closures now capture individual fields instead of entire structs".to_string()
110                );
111                // Check for or patterns
112                report.warnings.push(
113                    "Or patterns in matches are now available".to_string()
114                );
115            }
116            Edition::Edition2024 => {
117                // Check for new edition 2024 features
118                report.warnings.push(
119                    "Edition 2024 includes improved async support and pattern matching".to_string()
120                );
121            }
122            _ => {}
123        }
124        
125        Ok(())
126    }
127    
128    /// Add migration suggestions
129    fn add_suggestions(&self, target_edition: Edition, report: &mut AnalysisReport) {
130        report.suggestions.push(format!(
131            "Run `cargo fix --edition` to automatically fix most edition issues"
132        ));
133        
134        report.suggestions.push(format!(
135            "After migration, update Cargo.toml to edition = \"{}\"",
136            target_edition.as_str()
137        ));
138        
139        report.suggestions.push(
140            "Review and test your code after migration".to_string()
141        );
142        
143        if target_edition >= Edition::Edition2018 {
144            report.suggestions.push(
145                "Consider using `cargo fmt` to update code style".to_string()
146            );
147        }
148    }
149    
150    fn extract_file_from_lint(&self, line: &str) -> Option<String> {
151        // Simple extraction - this would be more sophisticated in production
152        line.split(':').next().map(|s| s.trim().to_string())
153    }
154    
155    fn extract_line_from_lint(&self, line: &str) -> Option<u32> {
156        // Simple extraction - this would be more sophisticated in production
157        line.split(':')
158            .nth(1)
159            .and_then(|s| s.trim().parse().ok())
160    }
161}
162
163/// Analysis report for edition compatibility
164#[derive(Debug, Clone)]
165pub struct AnalysisReport {
166    /// Target edition
167    pub target_edition: Edition,
168    /// Total number of Rust files
169    pub total_files: usize,
170    /// Issues found
171    pub issues: Vec<EditionIssue>,
172    /// Warnings
173    pub warnings: Vec<String>,
174    /// Suggestions for migration
175    pub suggestions: Vec<String>,
176}
177
178impl AnalysisReport {
179    /// Check if the project is ready for migration
180    pub fn is_ready_for_migration(&self) -> bool {
181        self.issues.iter().all(|i| i.severity != Severity::Error)
182    }
183    
184    /// Get a summary of the analysis
185    pub fn summary(&self) -> String {
186        format!(
187            "Analysis complete: {} files, {} issues, {} warnings",
188            self.total_files,
189            self.issues.len(),
190            self.warnings.len()
191        )
192    }
193}
194
195/// Edition compatibility issue
196#[derive(Debug, Clone)]
197pub struct EditionIssue {
198    /// File path
199    pub file: Option<String>,
200    /// Line number
201    pub line: Option<u32>,
202    /// Issue message
203    pub message: String,
204    /// Severity
205    pub severity: Severity,
206}
207
208/// Issue severity
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
210pub enum Severity {
211    /// Error - must be fixed
212    Error,
213    /// Warning - should be reviewed
214    Warning,
215    /// Info - informational
216    Info,
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use tempfile::TempDir;
223    
224    #[tokio::test]
225    async fn test_analyzer_creation() {
226        let temp_dir = TempDir::new().unwrap();
227        let analyzer = EditionAnalyzer::new(temp_dir.path());
228        
229        // Create a simple Cargo.toml
230        let manifest_content = r#"
231[package]
232name = "test"
233version = "0.1.0"
234edition = "2021"
235"#;
236        
237        tokio::fs::write(temp_dir.path().join("Cargo.toml"), manifest_content)
238            .await
239            .unwrap();
240        
241        // This would normally analyze the project
242        // In tests, we just verify it doesn't panic
243        let _report = analyzer.analyze(Edition::Edition2024).await;
244    }
245}