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