Skip to main content

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