ferrous_forge/edition/
mod.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
19pub enum Edition {
20 Edition2015,
22 Edition2018,
24 Edition2021,
26 Edition2024,
28}
29
30impl Edition {
31 pub fn parse_edition(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 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 pub fn latest() -> Self {
54 Self::Edition2024
55 }
56
57 pub fn is_latest(&self) -> bool {
59 *self == Self::latest()
60 }
61
62 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 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#[derive(Debug, Clone)]
91pub struct EditionStatus {
92 pub current: Edition,
94 pub latest: Edition,
96 pub is_latest: bool,
98 pub manifest_path: PathBuf,
100 pub migration_path: Vec<Edition>,
102}
103
104impl EditionStatus {
105 pub fn new(current: Edition, manifest_path: PathBuf) -> Self {
107 let latest = Edition::latest();
108 let is_latest = current == latest;
109
110 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
133pub 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 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"); Edition::parse_edition(edition_str)
154}
155
156pub 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
164pub 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
178 .migration_path
179 .iter()
180 .map(|e| e.to_string())
181 .collect::<Vec<_>>()
182 .join(" → ")
183 ));
184 }
185
186 recommendations
187 .push("Run `ferrous-forge edition migrate` to start the migration process".to_string());
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)]
199#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
200mod tests {
201 use super::*;
202 use tempfile::TempDir;
203
204 #[test]
205 fn test_edition_from_str() {
206 assert_eq!(
207 Edition::parse_edition("2015").unwrap(),
208 Edition::Edition2015
209 );
210 assert_eq!(
211 Edition::parse_edition("2018").unwrap(),
212 Edition::Edition2018
213 );
214 assert_eq!(
215 Edition::parse_edition("2021").unwrap(),
216 Edition::Edition2021
217 );
218 assert_eq!(
219 Edition::parse_edition("2024").unwrap(),
220 Edition::Edition2024
221 );
222 assert!(Edition::parse_edition("2027").is_err());
223 }
224
225 #[test]
226 fn test_edition_ordering() {
227 assert!(Edition::Edition2015 < Edition::Edition2018);
228 assert!(Edition::Edition2018 < Edition::Edition2021);
229 assert!(Edition::Edition2021 < Edition::Edition2024);
230 }
231
232 #[test]
233 fn test_edition_next() {
234 assert_eq!(Edition::Edition2015.next(), Some(Edition::Edition2018));
235 assert_eq!(Edition::Edition2018.next(), Some(Edition::Edition2021));
236 assert_eq!(Edition::Edition2021.next(), Some(Edition::Edition2024));
237 assert_eq!(Edition::Edition2024.next(), None);
238 }
239
240 #[test]
241 fn test_migration_path() {
242 let status = EditionStatus::new(Edition::Edition2015, PathBuf::from("Cargo.toml"));
243 assert_eq!(status.migration_path.len(), 3);
244 assert_eq!(status.migration_path[0], Edition::Edition2018);
245 assert_eq!(status.migration_path[1], Edition::Edition2021);
246 assert_eq!(status.migration_path[2], Edition::Edition2024);
247 }
248
249 #[tokio::test]
250 async fn test_detect_edition() {
251 let temp_dir = TempDir::new().unwrap();
252 let manifest_path = temp_dir.path().join("Cargo.toml");
253
254 let manifest_content = r#"
255[package]
256name = "test"
257version = "0.1.0"
258edition = "2021"
259"#;
260
261 fs::write(&manifest_path, manifest_content).await.unwrap();
262
263 let edition = detect_edition(&manifest_path).await.unwrap();
264 assert_eq!(edition, Edition::Edition2021);
265 }
266}