1pub mod apply;
7pub mod classify;
8pub mod model;
9pub mod pathfix;
10pub mod plan;
11pub mod project;
12pub mod util;
13pub mod verify;
14
15use std::path::{Path, PathBuf};
16
17use anyhow::{Context, Result};
18
19use crate::apply::{format_files, write_plan};
20use crate::model::{Config, FileOutcome};
21use crate::plan::build_plan;
22use crate::verify::{cargo_check, error_excerpt, find_manifest_dir};
23
24pub struct CrateCtx {
26 pub manifest_dir: Option<PathBuf>,
27 pub edition: String,
28 pub baseline_ok: Option<bool>,
30}
31
32impl CrateCtx {
33 pub fn resolve(path: &Path, verify: bool) -> CrateCtx {
35 let manifest_dir = find_manifest_dir(path);
36 let edition = manifest_dir
37 .as_deref()
38 .and_then(detect_edition)
39 .unwrap_or_else(|| "2021".to_string());
40 let baseline_ok = if verify {
41 manifest_dir.as_deref().map(|d| cargo_check(d, false).ok)
42 } else {
43 None
44 };
45 CrateCtx { manifest_dir, edition, baseline_ok }
46 }
47}
48
49pub fn detect_edition(manifest_dir: &Path) -> Option<String> {
51 let text = std::fs::read_to_string(manifest_dir.join("Cargo.toml")).ok()?;
52 for line in text.lines() {
53 let t = line.trim();
54 if let Some(rest) = t.strip_prefix("edition") {
55 if let Some(eq) = rest.find('=') {
56 let val = rest[eq + 1..].trim().trim_matches(['"', '\'']);
57 if !val.is_empty() {
58 return Some(val.to_string());
59 }
60 }
61 }
62 }
63 None
64}
65
66pub fn split_file_with(path: &Path, config: &Config, ctx: &CrateCtx) -> Result<FileOutcome> {
68 let src = std::fs::read_to_string(path)
69 .with_context(|| format!("reading {}", path.display()))?;
70
71 let plan = build_plan(path, &src)?;
72 if plan.is_noop() {
73 return Ok(FileOutcome::Skipped(format!(
74 "only {} module file(s) would result; nothing to split",
75 plan.files.len()
76 )));
77 }
78
79 if config.dry_run {
80 return Ok(FileOutcome::Split {
81 files: plan.files.iter().map(|f| plan.out_dir.join(format!("{}.rs", f.stem))).collect(),
82 });
83 }
84
85 let applied = write_plan(&plan, config)?;
86
87 if config.rustfmt {
88 format_files(&applied.files, &ctx.edition);
89 }
90
91 let want_verify = config.verify && ctx.baseline_ok == Some(true) && ctx.manifest_dir.is_some();
92 if want_verify {
93 let dir = ctx.manifest_dir.as_deref().unwrap();
94 let post = cargo_check(dir, false);
95 if !post.ok {
96 applied.rollback()?;
97 return Ok(FileOutcome::RolledBack(error_excerpt(&post.stderr)));
98 }
99 }
100
101 Ok(FileOutcome::Split { files: applied.files })
102}
103
104pub fn split_file(path: &Path, config: &Config) -> Result<FileOutcome> {
106 let ctx = CrateCtx::resolve(path, config.verify);
107 split_file_with(path, config, &ctx)
108}
109
110pub fn split_project(root: &Path, config: &Config) -> Result<Vec<(PathBuf, FileOutcome)>> {
112 let candidates = project::find_candidates(root, config)?;
113 let ctx = CrateCtx::resolve(root, config.verify);
114 let mut results = Vec::new();
115 for path in candidates {
116 let outcome = split_file_with(&path, config, &ctx)?;
117 results.push((path, outcome));
118 }
119 Ok(results)
120}