ferrous_forge/edition/
mod.rs1use crate::{Error, Result};
7use serde::{Deserialize, Serialize};
8use std::path::{Path, PathBuf};
9use tokio::fs;
10
11pub mod analyzer;
13pub mod migrator;
15
16pub use analyzer::EditionAnalyzer;
17pub use migrator::EditionMigrator;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
21pub enum Edition {
22 Edition2015,
24 Edition2018,
26 Edition2021,
28 Edition2024,
30}
31
32impl Edition {
33 pub fn parse_edition(s: &str) -> Result<Self> {
40 match s {
41 "2015" => Ok(Self::Edition2015),
42 "2018" => Ok(Self::Edition2018),
43 "2021" => Ok(Self::Edition2021),
44 "2024" => Ok(Self::Edition2024),
45 _ => Err(Error::parse(format!("Unknown edition: {}", s))),
46 }
47 }
48
49 pub fn as_str(&self) -> &'static str {
51 match self {
52 Self::Edition2015 => "2015",
53 Self::Edition2018 => "2018",
54 Self::Edition2021 => "2021",
55 Self::Edition2024 => "2024",
56 }
57 }
58
59 pub fn latest() -> Self {
61 Self::Edition2024
62 }
63
64 pub fn is_latest(&self) -> bool {
66 *self == Self::latest()
67 }
68
69 pub fn next(&self) -> Option<Self> {
71 match self {
72 Self::Edition2015 => Some(Self::Edition2018),
73 Self::Edition2018 => Some(Self::Edition2021),
74 Self::Edition2021 => Some(Self::Edition2024),
75 Self::Edition2024 => None,
76 }
77 }
78
79 pub fn migration_lints(&self) -> Vec<String> {
81 match self {
82 Self::Edition2015 => vec!["rust_2018_compatibility".to_string()],
83 Self::Edition2018 => vec!["rust_2021_compatibility".to_string()],
84 Self::Edition2021 => vec!["rust_2024_compatibility".to_string()],
85 Self::Edition2024 => vec![],
86 }
87 }
88}
89
90impl std::fmt::Display for Edition {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 write!(f, "Edition {}", self.as_str())
93 }
94}
95
96#[derive(Debug, Clone)]
98pub struct EditionStatus {
99 pub current: Edition,
101 pub latest: Edition,
103 pub is_latest: bool,
105 pub manifest_path: PathBuf,
107 pub migration_path: Vec<Edition>,
109}
110
111impl EditionStatus {
112 pub fn new(current: Edition, manifest_path: PathBuf) -> Self {
114 let latest = Edition::latest();
115 let is_latest = current == latest;
116
117 let mut migration_path = Vec::new();
119 let mut current_edition = current;
120
121 while let Some(next) = current_edition.next() {
122 if next <= latest {
123 migration_path.push(next);
124 current_edition = next;
125 } else {
126 break;
127 }
128 }
129
130 Self {
131 current,
132 latest,
133 is_latest,
134 manifest_path,
135 migration_path,
136 }
137 }
138}
139
140pub async fn detect_edition(manifest_path: &Path) -> Result<Edition> {
147 if !manifest_path.exists() {
148 return Err(Error::file_not_found(format!(
149 "Cargo.toml not found at {}",
150 manifest_path.display()
151 )));
152 }
153
154 let contents = fs::read_to_string(manifest_path).await?;
155 let manifest: toml::Value = toml::from_str(&contents)
156 .map_err(|e| Error::parse(format!("Failed to parse Cargo.toml: {}", e)))?;
157
158 let edition_str = manifest
160 .get("package")
161 .and_then(|p| p.get("edition"))
162 .and_then(|e| e.as_str())
163 .unwrap_or("2015"); Edition::parse_edition(edition_str)
166}
167
168pub async fn check_compliance(project_path: &Path) -> Result<EditionStatus> {
174 let manifest_path = project_path.join("Cargo.toml");
175 let edition = detect_edition(&manifest_path).await?;
176
177 Ok(EditionStatus::new(edition, manifest_path))
178}
179
180pub fn get_migration_recommendations(status: &EditionStatus) -> Vec<String> {
182 let mut recommendations = Vec::new();
183
184 if !status.is_latest {
185 recommendations.push(format!(
186 "Your project is using {}, but {} is now available",
187 status.current, status.latest
188 ));
189
190 if !status.migration_path.is_empty() {
191 recommendations.push(format!(
192 "Recommended migration path: {}",
193 status
194 .migration_path
195 .iter()
196 .map(|e| e.to_string())
197 .collect::<Vec<_>>()
198 .join(" → ")
199 ));
200 }
201
202 recommendations
203 .push("Run `ferrous-forge edition migrate` to start the migration process".to_string());
204 } else {
205 recommendations.push(format!(
206 "✅ Your project is already using the latest edition ({})",
207 status.latest
208 ));
209 }
210
211 recommendations
212}
213
214#[cfg(test)]
215#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
216mod tests {
217 use super::*;
218 use tempfile::TempDir;
219
220 #[test]
221 fn test_edition_from_str() {
222 assert_eq!(
223 Edition::parse_edition("2015").unwrap(),
224 Edition::Edition2015
225 );
226 assert_eq!(
227 Edition::parse_edition("2018").unwrap(),
228 Edition::Edition2018
229 );
230 assert_eq!(
231 Edition::parse_edition("2021").unwrap(),
232 Edition::Edition2021
233 );
234 assert_eq!(
235 Edition::parse_edition("2024").unwrap(),
236 Edition::Edition2024
237 );
238 assert!(Edition::parse_edition("2027").is_err());
239 }
240
241 #[test]
242 fn test_edition_ordering() {
243 assert!(Edition::Edition2015 < Edition::Edition2018);
244 assert!(Edition::Edition2018 < Edition::Edition2021);
245 assert!(Edition::Edition2021 < Edition::Edition2024);
246 }
247
248 #[test]
249 fn test_edition_next() {
250 assert_eq!(Edition::Edition2015.next(), Some(Edition::Edition2018));
251 assert_eq!(Edition::Edition2018.next(), Some(Edition::Edition2021));
252 assert_eq!(Edition::Edition2021.next(), Some(Edition::Edition2024));
253 assert_eq!(Edition::Edition2024.next(), None);
254 }
255
256 #[test]
257 fn test_migration_path() {
258 let status = EditionStatus::new(Edition::Edition2015, PathBuf::from("Cargo.toml"));
259 assert_eq!(status.migration_path.len(), 3);
260 assert_eq!(status.migration_path[0], Edition::Edition2018);
261 assert_eq!(status.migration_path[1], Edition::Edition2021);
262 assert_eq!(status.migration_path[2], Edition::Edition2024);
263 }
264
265 #[tokio::test]
266 async fn test_detect_edition() {
267 let temp_dir = TempDir::new().unwrap();
268 let manifest_path = temp_dir.path().join("Cargo.toml");
269
270 let manifest_content = r#"
271[package]
272name = "test"
273version = "0.1.0"
274edition = "2021"
275"#;
276
277 fs::write(&manifest_path, manifest_content).await.unwrap();
278
279 let edition = detect_edition(&manifest_path).await.unwrap();
280 assert_eq!(edition, Edition::Edition2021);
281 }
282}