ferrous_forge/edition/
mod.rs

1//! Rust edition detection and migration assistance
2//!
3//! This module provides functionality to detect the current edition used in
4//! a project, check for available migrations, and assist with the migration process.
5
6use crate::{Error, Result};
7use serde::{Deserialize, Serialize};
8use std::path::{Path, PathBuf};
9use tokio::fs;
10
11pub mod analyzer;
12pub mod migrator;
13
14pub use analyzer::EditionAnalyzer;
15pub use migrator::EditionMigrator;
16
17/// Rust edition
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
19pub enum Edition {
20    /// Rust 2015 edition
21    Edition2015,
22    /// Rust 2018 edition
23    Edition2018,
24    /// Rust 2021 edition
25    Edition2021,
26    /// Rust 2024 edition
27    Edition2024,
28}
29
30impl Edition {
31    /// Parse edition from string
32    pub fn from_str(s: &str) -> Result<Self> {
33        match s {
34            "2015" => Ok(Self::Edition2015),
35            "2018" => Ok(Self::Edition2018),
36            "2021" => Ok(Self::Edition2021),
37            "2024" => Ok(Self::Edition2024),
38            _ => Err(Error::parse(format!("Unknown edition: {}", s))),
39        }
40    }
41    
42    /// Get the edition as a string
43    pub fn as_str(&self) -> &'static str {
44        match self {
45            Self::Edition2015 => "2015",
46            Self::Edition2018 => "2018",
47            Self::Edition2021 => "2021",
48            Self::Edition2024 => "2024",
49        }
50    }
51    
52    /// Get the latest stable edition
53    pub fn latest() -> Self {
54        Self::Edition2024
55    }
56    
57    /// Check if this edition is the latest
58    pub fn is_latest(&self) -> bool {
59        *self == Self::latest()
60    }
61    
62    /// Get the next edition after this one
63    pub fn next(&self) -> Option<Self> {
64        match self {
65            Self::Edition2015 => Some(Self::Edition2018),
66            Self::Edition2018 => Some(Self::Edition2021),
67            Self::Edition2021 => Some(Self::Edition2024),
68            Self::Edition2024 => None,
69        }
70    }
71    
72    /// Get edition-specific lints for migration
73    pub fn migration_lints(&self) -> Vec<String> {
74        match self {
75            Self::Edition2015 => vec!["rust_2018_compatibility".to_string()],
76            Self::Edition2018 => vec!["rust_2021_compatibility".to_string()],
77            Self::Edition2021 => vec!["rust_2024_compatibility".to_string()],
78            Self::Edition2024 => vec![],
79        }
80    }
81}
82
83impl std::fmt::Display for Edition {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        write!(f, "Edition {}", self.as_str())
86    }
87}
88
89/// Edition compliance status
90#[derive(Debug, Clone)]
91pub struct EditionStatus {
92    /// Current edition in use
93    pub current: Edition,
94    /// Latest available edition
95    pub latest: Edition,
96    /// Is the project using the latest edition?
97    pub is_latest: bool,
98    /// Path to Cargo.toml
99    pub manifest_path: PathBuf,
100    /// Recommended migration path
101    pub migration_path: Vec<Edition>,
102}
103
104impl EditionStatus {
105    /// Create a new edition status
106    pub fn new(current: Edition, manifest_path: PathBuf) -> Self {
107        let latest = Edition::latest();
108        let is_latest = current == latest;
109        
110        // Build migration path
111        let mut migration_path = Vec::new();
112        let mut current_edition = current;
113        
114        while let Some(next) = current_edition.next() {
115            if next <= latest {
116                migration_path.push(next);
117                current_edition = next;
118            } else {
119                break;
120            }
121        }
122        
123        Self {
124            current,
125            latest,
126            is_latest,
127            manifest_path,
128            migration_path,
129        }
130    }
131}
132
133/// Detect edition from Cargo.toml
134pub async fn detect_edition(manifest_path: &Path) -> Result<Edition> {
135    if !manifest_path.exists() {
136        return Err(Error::file_not_found(format!(
137            "Cargo.toml not found at {}",
138            manifest_path.display()
139        )));
140    }
141    
142    let contents = fs::read_to_string(manifest_path).await?;
143    let manifest: toml::Value = toml::from_str(&contents)
144        .map_err(|e| Error::parse(format!("Failed to parse Cargo.toml: {}", e)))?;
145    
146    // Get edition from [package] section
147    let edition_str = manifest
148        .get("package")
149        .and_then(|p| p.get("edition"))
150        .and_then(|e| e.as_str())
151        .unwrap_or("2015"); // Default to 2015 if not specified
152    
153    Edition::from_str(edition_str)
154}
155
156/// Check edition compliance for a project
157pub async fn check_compliance(project_path: &Path) -> Result<EditionStatus> {
158    let manifest_path = project_path.join("Cargo.toml");
159    let edition = detect_edition(&manifest_path).await?;
160    
161    Ok(EditionStatus::new(edition, manifest_path))
162}
163
164/// Get edition migration recommendations
165pub fn get_migration_recommendations(status: &EditionStatus) -> Vec<String> {
166    let mut recommendations = Vec::new();
167    
168    if !status.is_latest {
169        recommendations.push(format!(
170            "Your project is using {}, but {} is now available",
171            status.current, status.latest
172        ));
173        
174        if !status.migration_path.is_empty() {
175            recommendations.push(format!(
176                "Recommended migration path: {}",
177                status.migration_path
178                    .iter()
179                    .map(|e| e.to_string())
180                    .collect::<Vec<_>>()
181                    .join(" → ")
182            ));
183        }
184        
185        recommendations.push(
186            "Run `ferrous-forge edition migrate` to start the migration process".to_string()
187        );
188    } else {
189        recommendations.push(format!(
190            "✅ Your project is already using the latest edition ({})",
191            status.latest
192        ));
193    }
194    
195    recommendations
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use tempfile::TempDir;
202    
203    #[test]
204    fn test_edition_from_str() {
205        assert_eq!(Edition::from_str("2015").unwrap(), Edition::Edition2015);
206        assert_eq!(Edition::from_str("2018").unwrap(), Edition::Edition2018);
207        assert_eq!(Edition::from_str("2021").unwrap(), Edition::Edition2021);
208        assert_eq!(Edition::from_str("2024").unwrap(), Edition::Edition2024);
209        assert!(Edition::from_str("2027").is_err());
210    }
211    
212    #[test]
213    fn test_edition_ordering() {
214        assert!(Edition::Edition2015 < Edition::Edition2018);
215        assert!(Edition::Edition2018 < Edition::Edition2021);
216        assert!(Edition::Edition2021 < Edition::Edition2024);
217    }
218    
219    #[test]
220    fn test_edition_next() {
221        assert_eq!(Edition::Edition2015.next(), Some(Edition::Edition2018));
222        assert_eq!(Edition::Edition2018.next(), Some(Edition::Edition2021));
223        assert_eq!(Edition::Edition2021.next(), Some(Edition::Edition2024));
224        assert_eq!(Edition::Edition2024.next(), None);
225    }
226    
227    #[test]
228    fn test_migration_path() {
229        let status = EditionStatus::new(Edition::Edition2015, PathBuf::from("Cargo.toml"));
230        assert_eq!(status.migration_path.len(), 3);
231        assert_eq!(status.migration_path[0], Edition::Edition2018);
232        assert_eq!(status.migration_path[1], Edition::Edition2021);
233        assert_eq!(status.migration_path[2], Edition::Edition2024);
234    }
235    
236    #[tokio::test]
237    async fn test_detect_edition() {
238        let temp_dir = TempDir::new().unwrap();
239        let manifest_path = temp_dir.path().join("Cargo.toml");
240        
241        let manifest_content = r#"
242[package]
243name = "test"
244version = "0.1.0"
245edition = "2021"
246"#;
247        
248        fs::write(&manifest_path, manifest_content).await.unwrap();
249        
250        let edition = detect_edition(&manifest_path).await.unwrap();
251        assert_eq!(edition, Edition::Edition2021);
252    }
253}