mod array_splitter;
mod config;
mod dependency_analyzer;
mod domain_router;
mod error_recovery;
mod field_access_tracker;
mod file_analyzer;
mod glob_import_analyzer;
mod helper_dependency_tracker;
mod import_analyzer;
mod incremental;
mod macro_analyzer;
mod method_analyzer;
mod metrics_dashboard;
mod module_generator;
mod naming_strategy;
mod nested_mod_splitter;
mod scope_analyzer;
mod source_map;
mod test_generator;
#[cfg(feature = "smt")]
use splitrs::smt_cli;
#[cfg(feature = "smt")]
#[allow(unused_imports)]
use splitrs::extraction;
mod test_module_splitter;
mod trait_bound_analyzer;
mod trait_method_tracker;
mod workspace;
mod workspace_mode;
use anyhow::{Context, Result};
use clap::Parser;
use config::Config;
use file_analyzer::FileAnalyzer;
use module_generator::extract_test_module_path;
use std::fs;
use std::path::PathBuf;
use syn::{File, Item};
#[derive(Parser)]
#[command(name = "splitrs")]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(short, long)]
input: Option<PathBuf>,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(short = 'T', long)]
split_test_modules: bool,
#[arg(long)]
split_arrays: bool,
#[arg(value_name = "FILE")]
file: Option<PathBuf>,
#[arg(short, long)]
max_lines: Option<usize>,
#[arg(long)]
split_impl_blocks: Option<bool>,
#[arg(long)]
max_impl_lines: Option<usize>,
#[arg(short = 'n', long)]
dry_run: bool,
#[arg(short = 'c', long)]
config: Option<PathBuf>,
#[arg(short = 'I', long)]
interactive: bool,
#[arg(long)]
naming_strategy: Option<String>,
#[arg(long)]
incremental: bool,
#[arg(long)]
generate_tests: bool,
#[arg(long, default_value = "smart")]
merge_strategy: String,
#[arg(long)]
workspace: bool,
#[arg(long)]
parallel: bool,
#[arg(long, default_value = "0")]
threads: usize,
#[arg(long)]
continue_on_error: bool,
#[arg(long)]
rollback: bool,
#[arg(long, default_value = "500")]
target: usize,
#[arg(long)]
metrics: bool,
#[arg(long)]
metrics_output: Option<PathBuf>,
#[arg(long, default_value = "html")]
metrics_format: String,
#[arg(long)]
extract_tests: bool,
#[arg(long)]
deepen_super: bool,
#[arg(long, value_name = "TOML-FILE")]
target_modules: Option<PathBuf>,
#[arg(long)]
split_nested_mods: Option<bool>,
#[arg(long, value_name = "N")]
max_mod_depth: Option<usize>,
#[arg(long, value_name = "STYLE")]
facade: Option<String>,
#[cfg(feature = "smt")]
#[arg(long)]
verify_equiv: bool,
#[cfg(feature = "smt")]
#[arg(long, value_name = "FILE::FN")]
left: Option<String>,
#[cfg(feature = "smt")]
#[arg(long, value_name = "FILE::FN")]
right: Option<String>,
#[cfg(feature = "smt")]
#[arg(long)]
extract_pure: bool,
#[cfg(feature = "smt")]
#[arg(long)]
verify: bool,
}
fn main() -> Result<()> {
let args = Args::parse();
if args.split_test_modules {
let target_file = args.file.as_ref().or(args.input.as_ref()).ok_or_else(|| {
anyhow::anyhow!(
"--split-test-modules requires a FILE argument or --input.\n\
Example: splitrs --split-test-modules path/to/file.rs"
)
})?;
if !target_file.exists() {
anyhow::bail!(
"Input file does not exist: {}\n\
Please provide a valid Rust source file.",
target_file.display()
);
}
test_module_splitter::run_split_test_modules(target_file, args.dry_run)?;
return Ok(());
}
if args.split_arrays {
let target_file = args.input.as_ref().or(args.file.as_ref()).ok_or_else(|| {
anyhow::anyhow!(
"--split-arrays requires --input (or a positional FILE).\n\
Example: splitrs --split-arrays -i path/to/table.rs --max-lines 1800"
)
})?;
if !target_file.exists() {
anyhow::bail!(
"Input file does not exist: {}\n\
Please provide a valid Rust source file.",
target_file.display()
);
}
let max_lines = match args.max_lines {
Some(n) => n,
None => {
let cfg = if let Some(config_path) = &args.config {
Config::from_file(config_path).unwrap_or_default()
} else {
Config::load_from_current_dir()
};
cfg.splitrs.max_lines
}
};
let split = array_splitter::run_split_arrays(target_file, max_lines, args.dry_run)?;
if !split {
eprintln!(
"Nothing split: no array/slice literal exceeded {} lines.",
max_lines
);
}
return Ok(());
}
#[cfg(feature = "smt")]
if args.verify_equiv {
return smt_cli::run_verify_equiv(args.left.as_deref(), args.right.as_deref());
}
if args.workspace {
return workspace_mode::run_workspace_mode(&args);
}
let input = args.input.as_ref().ok_or_else(|| {
anyhow::anyhow!(
"--input is required (or use --split-test-modules for test-module splitting).\n\
Run 'splitrs --help' for usage."
)
})?;
let output = args.output.as_ref().ok_or_else(|| {
anyhow::anyhow!(
"--output is required.\n\
Run 'splitrs --help' for usage."
)
})?;
if !input.exists() {
anyhow::bail!(
"Input file does not exist: {:?}\n\
Please provide a valid Rust source file.",
input
);
}
if !input.is_file() {
anyhow::bail!(
"Input path is not a file: {:?}\n\
Please provide a path to a .rs file, not a directory.",
input
);
}
if let Some(ext) = input.extension() {
if ext != "rs" {
eprintln!(
"⚠️ Warning: Input file does not have .rs extension: {:?}",
input
);
eprintln!(" SplitRS is designed for Rust source files (.rs)");
}
}
let mut config = if let Some(config_path) = &args.config {
Config::from_file(config_path).context(format!(
"Failed to load configuration from {:?}\n\
Please ensure:\n\
- The config file exists\n\
- The file has valid TOML syntax\n\
- All required fields are present\n\
\n\
Example .splitrs.toml:\n\
[splitrs]\n\
max_lines = 1000\n\
max_impl_lines = 500\n\
split_impl_blocks = true",
config_path
))?
} else {
Config::load_from_current_dir()
};
config.merge_with_args(args.max_lines, args.max_impl_lines, args.split_impl_blocks);
config.merge_nested_args(
args.split_nested_mods,
args.max_mod_depth,
args.facade.as_deref(),
);
if args.extract_tests {
config.splitrs.extract_tests = true;
}
if let Some(tm_path) = &args.target_modules {
let spec = config::TargetModulesFile::from_file(tm_path).context(format!(
"Failed to load target-modules configuration from {:?}\n\
Please ensure:\n\
- The file exists and is readable\n\
- It contains valid TOML\n\
- It has [[target_modules]] entries with `name` and `items` fields",
tm_path
))?;
let mut merged = spec.target_modules;
merged.append(&mut config.target_modules);
config.target_modules = merged;
if spec.assign_unlisted.is_some() {
config.assign_unlisted = spec.assign_unlisted;
}
}
let split_nested_mods = config.splitrs.split_nested_mods;
let max_mod_depth = config.splitrs.max_mod_depth.max(1);
let facade = config::FacadeStyle::parse(&config.output.facade)?;
let assign_unlisted = config::AssignUnlisted::parse(config.assign_unlisted.as_deref())?;
let seeded = assign_unlisted == config::AssignUnlisted::Seeded;
config::validate_target_modules(&config.target_modules)?;
let all_rules = config.target_modules.clone();
let top_rules: Vec<config::TargetModule> = all_rules
.iter()
.filter(|rule| rule.parent.is_none())
.cloned()
.collect();
if !split_nested_mods && all_rules.iter().any(|rule| rule.parent.is_some()) {
anyhow::bail!(
"[[target_modules]] rules with `parent = ...` route items inside nested inline \
modules and require --split-nested-mods (or `split_nested_mods = true` in \
.splitrs.toml)"
);
}
println!("Configuration loaded:");
println!(" Max lines per module: {}", config.splitrs.max_lines);
println!(" Max lines per impl: {}", config.splitrs.max_impl_lines);
println!(" Split impl blocks: {}", config.splitrs.split_impl_blocks);
if config.splitrs.extract_tests {
println!(" Extract inline test mods: true");
}
if split_nested_mods {
println!(
" Split nested mods: true (max depth {}, facade {:?})",
max_mod_depth, config.output.facade
);
}
if !config.target_modules.is_empty() {
println!(" Target modules: {} rule(s)", config.target_modules.len());
if seeded {
println!(" Assign unlisted items: seeded");
}
}
let source_code = fs::read_to_string(input).context(format!(
"Failed to read input file: {:?}\n\
Please ensure:\n\
- The file exists\n\
- You have read permissions\n\
- The path is correct",
input
))?;
let syntax_tree: File = syn::parse_file(&source_code).context(format!(
"Failed to parse Rust source code in {:?}\n\
Common issues:\n\
- Syntax errors in the source file\n\
- Incomplete code blocks\n\
- Macro expansion required (try using 'cargo expand' first)\n\
\n\
Please ensure the file contains valid Rust code that compiles.",
input
))?;
#[cfg(feature = "smt")]
let mut syntax_tree = syntax_tree; #[cfg(feature = "smt")]
let extraction_outcomes: Vec<splitrs::extraction::ExtractionOutcome> = if args.extract_pure {
let outcomes =
splitrs::extraction::extract_pure_blocks(&mut syntax_tree, config.splitrs.max_lines);
splitrs::extraction::print_report(&outcomes);
outcomes
} else {
Vec::new()
};
println!("\nAnalyzing file: {:?}", input);
println!("Total items: {}", syntax_tree.items.len());
if config.splitrs.split_impl_blocks {
println!(
"Impl block splitting enabled (max {} lines per impl)",
config.splitrs.max_impl_lines
);
}
let mut analyzer = FileAnalyzer::new(
config.splitrs.split_impl_blocks,
config.splitrs.max_impl_lines,
);
analyzer.set_extract_tests(config.splitrs.extract_tests);
analyzer.set_target_modules(top_rules.clone());
analyzer.set_seeded_assignment(seeded);
if split_nested_mods {
analyzer.set_split_nested_mods(true, config.splitrs.max_lines);
}
analyzer.set_source(&source_code);
analyzer.analyze_with_test_files(&syntax_tree, input);
domain_router::check_unmatched_patterns(&domain_router::routable_names(&analyzer), &top_rules)?;
println!("Found {} types", analyzer.types.len());
println!("Found {} standalone items", analyzer.standalone_items.len());
let total_trait_impls: usize = analyzer.types.values().map(|t| t.trait_impls.len()).sum();
if total_trait_impls > 0 {
println!("Found {} trait implementations", total_trait_impls);
}
let nested_mods = analyzer.take_nested_mods();
let mut nested_plans: Vec<nested_mod_splitter::NestedModPlan> = Vec::new();
if !nested_mods.is_empty() {
let opts = nested_mod_splitter::NestedSplitOptions {
split_impl_blocks: config.splitrs.split_impl_blocks,
max_impl_lines: config.splitrs.max_impl_lines,
max_lines: config.splitrs.max_lines,
extract_tests: config.splitrs.extract_tests,
max_mod_depth,
seeded_assignment: seeded,
all_rules: &all_rules,
};
for nested in &nested_mods {
let mod_path = nested.ident.to_string();
nested_plans.push(nested_mod_splitter::plan_nested_split(
nested,
&source_code,
&opts,
&mod_path,
1,
)?);
}
println!("Descending into {} nested module(s)", nested_plans.len());
}
nested_mod_splitter::validate_parent_rules(&all_rules, &nested_plans)?;
if nested_plans.iter().any(|plan| plan.name == "tests") && !analyzer.extracted_tests.is_empty()
{
anyhow::bail!(
"a nested module named `tests` conflicts with the tests.rs produced by \
--extract-tests; re-run without --extract-tests or rename the module"
);
}
let mut modules = analyzer.group_by_module(config.splitrs.max_lines);
if !nested_plans.is_empty() {
let reserved: std::collections::HashSet<String> =
nested_plans.iter().map(|plan| plan.name.clone()).collect();
nested_mod_splitter::rename_module_collisions(&mut modules, &reserved);
let child_names: Vec<String> = nested_plans.iter().map(|plan| plan.name.clone()).collect();
nested_mod_splitter::add_child_mod_imports(&mut modules, &child_names);
}
if args.deepen_super {
for module in &mut modules {
module.deepen_super = true;
}
}
println!("Generated {} modules", modules.len());
let macro_count = analyzer.macro_analyzer().total_macro_count();
if macro_count > 0 {
println!(
" Macros found: {} ({} exported)",
macro_count,
analyzer.macro_analyzer().exported_macro_count()
);
let custom_derives = analyzer.macro_analyzer().all_custom_derives();
if !custom_derives.is_empty() {
println!(" Custom derives: {}", custom_derives.join(", "));
}
}
if args.dry_run {
println!("\n{}", "=".repeat(60));
println!("DRY RUN - Preview Mode");
println!("{}", "=".repeat(60));
println!("\n📊 Statistics:");
println!(" Original file: {} lines", source_code.lines().count());
println!(" Total modules to create: {}", modules.len());
println!("\n📁 Module Structure:");
for module in &modules {
let module_types = module.types.len();
let module_items = module.standalone_items.len();
let trait_impls = module.trait_impls.len();
print!(" 📄 {}.rs", module.name);
if module_types > 0 {
print!(" ({} types", module_types);
}
if module_items > 0 {
if module_types > 0 {
print!(", {} items", module_items);
} else {
print!(" ({} items", module_items);
}
}
if trait_impls > 0 {
if module_types > 0 || module_items > 0 {
print!(", {} trait impls", trait_impls);
} else {
print!(" ({} trait impls", trait_impls);
}
}
if module_types > 0 || module_items > 0 || trait_impls > 0 {
print!(")");
}
println!();
}
if !top_rules.is_empty() {
let mut printed_header = false;
for module in &modules {
let Some(lines) = domain_router::explain_named_module(module, &top_rules) else {
continue;
};
if !printed_header {
println!("\n🧭 Rule attribution (named modules):");
printed_header = true;
}
println!(" 📄 {}.rs", module.name);
for line in lines {
println!(" - {}", line);
}
}
}
if !nested_plans.is_empty() {
println!("\n📁 Nested module tree (--split-nested-mods):");
for plan in &nested_plans {
for line in nested_mod_splitter::dry_run_lines(plan, 1) {
println!("{}", line);
}
}
}
println!("\n💾 Files that would be created:");
println!(" 📁 {}/", output.display());
for module in &modules {
println!(" 📄 {}.rs", module.name);
}
for plan in &nested_plans {
for line in nested_mod_splitter::dry_run_lines(plan, 2) {
println!("{}", line);
}
}
println!(" 📄 mod.rs");
println!("\n{}", "=".repeat(60));
println!("✓ Preview complete - no files were created");
println!("{}", "=".repeat(60));
return Ok(());
}
if args.interactive {
println!("\n{}", "=".repeat(60));
println!("⚠️ INTERACTIVE MODE");
println!("{}", "=".repeat(60));
println!(
"\nThis will create {} module files in: {}",
modules.len(),
output.display()
);
print!("\nProceed with file generation? [y/N]: ");
use std::io::{self, Write};
io::stdout().flush()?;
let mut response = String::new();
io::stdin().read_line(&mut response)?;
if !response.trim().eq_ignore_ascii_case("y") {
println!("\n❌ Operation cancelled by user");
return Ok(());
}
println!();
}
let incremental_result = if args.incremental {
let merge_strategy = match args.merge_strategy.as_str() {
"add-only" => incremental::MergeStrategy::AddOnly,
"replace" => incremental::MergeStrategy::Replace,
"skip-customized" => incremental::MergeStrategy::SkipCustomized,
_ => incremental::MergeStrategy::Smart,
};
let mut refactor = incremental::IncrementalRefactor::new(output, merge_strategy);
if let Ok(state) = refactor.analyze_existing() {
if !state.modules.is_empty() {
println!("\n📁 Incremental mode: detected existing structure");
refactor.print_existing_state();
println!();
}
}
Some(refactor)
} else {
None
};
let backup_dir = std::env::temp_dir().join(format!(".splitrs_backup_{}", std::process::id()));
if input.exists() {
fs::create_dir_all(&backup_dir)?;
let backup_file = backup_dir.join("original.rs");
fs::copy(input, &backup_file)?;
println!("📦 Backup created at: {:?}", backup_dir);
}
fs::create_dir_all(output)?;
let mut type_to_module: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
for module in &modules {
for exported_type in module.get_exported_types() {
type_to_module.insert(exported_type, module.name.clone());
}
}
for module in &modules {
for item in &module.standalone_items {
if let Item::Trait(trait_item) = item {
let trait_name = trait_item.ident.to_string();
analyzer
.trait_tracker
.register_trait_module(&trait_name, &module.name);
}
}
}
let (mut needs_pub_super, cross_module_imports, fields_need_pub_super) =
analyzer.compute_cross_module_visibility(&modules);
let scope_uses = nested_mod_splitter::compute_parent_scope_items(
&nested_mods,
&analyzer.use_statements,
&modules,
&mut needs_pub_super,
args.deepen_super,
);
let needs_pub_super = needs_pub_super;
if !needs_pub_super.is_empty() {
println!(
"Upgrading {} private functions to pub(super) for cross-module access",
needs_pub_super.len()
);
}
if !fields_need_pub_super.is_empty() {
let total_fields: usize = fields_need_pub_super.values().map(|s| s.len()).sum();
println!(
"Upgrading {} struct fields to pub(super) for cross-module access",
total_fields
);
}
let mut created_count = 0;
let mut skipped_count = 0;
for module in &modules {
if let Some(ref refactor) = incremental_result {
if !refactor.should_update_module(&module.name) {
println!("Skipped: {}.rs (has customizations)", module.name);
skipped_count += 1;
continue;
}
}
let module_path = output.join(format!("{}.rs", module.name));
let content = module.generate_content(
&syntax_tree,
&analyzer.use_statements,
&type_to_module,
&needs_pub_super,
cross_module_imports.get(&module.name),
&fields_need_pub_super,
Some(&analyzer.trait_tracker),
);
fs::write(&module_path, &content).context(format!(
"Failed to write module file: {:?}\n\
Please ensure:\n\
- You have write permissions for the output directory\n\
- The disk has sufficient space\n\
- The file path is valid for your filesystem",
module_path
))?;
if let Err(e) = syn::parse_file(&content) {
eprintln!(
"⚠️ Warning: Generated module {:?} may contain syntax errors: {}",
module_path, e
);
eprintln!(
" This might be due to complex macro usage or edge cases.\n\
Please review the generated file and report this issue."
);
}
println!("Created: {:?}", module_path);
created_count += 1;
}
for plan in &nested_plans {
let files = nested_mod_splitter::write_plan(plan, output, facade)?;
for file in &files {
println!("Created: {:?}", file);
}
created_count += files.len();
}
let extracted_test_items = analyzer.take_extracted_tests();
let has_extracted_tests = !extracted_test_items.is_empty();
if has_extracted_tests {
let empty_imports = std::collections::HashMap::new();
let tests_sibling_imports = cross_module_imports.get("tests").unwrap_or(&empty_imports);
let tests_parent_resolvable: std::collections::HashSet<String> =
type_to_module.keys().cloned().collect();
let tests_content = module_generator::generate_tests_rs_full(
&extracted_test_items,
&analyzer.use_statements,
tests_sibling_imports,
args.deepen_super,
&tests_parent_resolvable,
);
let tests_path = output.join("tests.rs");
fs::write(&tests_path, &tests_content).context(format!(
"Failed to write tests.rs file: {:?}\n\
Please ensure you have write permissions for the output directory.",
tests_path
))?;
if let Err(e) = syn::parse_file(&tests_content) {
eprintln!(
"⚠️ Warning: Generated tests.rs may contain syntax errors: {}",
e
);
}
println!("Created: {:?}", tests_path);
}
let lib_rs_path = output.join("lib.rs");
if !lib_rs_path.exists() {
let test_module_path = extract_test_module_path(&syntax_tree);
let child_decls: Vec<syn::ItemMod> =
nested_plans.iter().map(|plan| plan.decl_item()).collect();
let mod_content = module_generator::generate_mod_rs_ext(
&modules,
output,
test_module_path.as_deref(),
has_extracted_tests,
&analyzer.file_inner_docs,
&[],
&child_decls,
facade,
&scope_uses,
)?;
let mod_path = output.join("mod.rs");
fs::write(&mod_path, &mod_content).context(format!(
"Failed to write mod.rs file: {:?}\n\
Please ensure you have write permissions for the output directory.",
mod_path
))?;
if let Err(e) = syn::parse_file(&mod_content) {
eprintln!(
"⚠️ Warning: Generated mod.rs may contain syntax errors: {}",
e
);
}
println!("Created: {:?}", mod_path);
} else if !nested_plans.is_empty() {
eprintln!(
"Note: {} exists, so no mod.rs was written. Declare the descended module(s) there \
manually: {}",
lib_rs_path.display(),
nested_plans
.iter()
.map(|plan| format!("`mod {};`", plan.name))
.collect::<Vec<_>>()
.join(", ")
);
}
if args.deepen_super {
let same_stem = match (output.file_name(), input.file_stem()) {
(Some(out_name), Some(in_stem)) => out_name == in_stem,
_ => false,
};
let same_parent = output.parent() == input.parent();
if same_stem && same_parent && input.is_file() {
fs::remove_file(input).with_context(|| {
format!(
"Failed to remove the original source file after in-place split: {:?}\n\
The generated `{}/` directory module now shadows it; remove it \
manually to resolve the ambiguous-module error.",
input,
output.display()
)
})?;
println!(
"Removed original (replaced by directory module): {:?}",
input
);
}
}
if args.generate_tests {
let test_path = output.join("refactoring_tests.rs");
let mut test_gen = test_generator::TestGenerator::new(
output
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("generated"),
);
test_gen.collect_from_file(&syntax_tree);
let test_content = test_gen.generate_tests();
fs::write(&test_path, &test_content)
.context(format!("Failed to write test file: {:?}", test_path))?;
println!("Created: {:?} (verification tests)", test_path);
}
if args.metrics {
let method_metrics = metrics_dashboard::ComplexityAnalyzer::analyze_file(&syntax_tree);
let module_metrics_list: Vec<metrics_dashboard::ModuleMetrics> = modules
.iter()
.map(|m| {
let type_count = m.types.len();
let total_lines = m.types.iter().map(|t| t.estimate_lines()).sum::<usize>()
+ m.standalone_items.len() * 10;
metrics_dashboard::build_module_metrics(
&m.name,
total_lines,
type_count,
method_metrics.clone(),
)
})
.collect();
let original_lines = source_code.lines().count();
let module_names: Vec<&str> = modules.iter().map(|m| m.name.as_str()).collect();
let dep_dot = metrics_dashboard::RefactoringReport::build_dependency_dot(&module_names);
let report = metrics_dashboard::RefactoringReport::new(
input.clone(),
original_lines,
module_metrics_list,
dep_dot,
);
let output_content = match args.metrics_format.as_str() {
"json" => metrics_dashboard::DashboardGenerator::generate_json(&report),
"text" => metrics_dashboard::DashboardGenerator::generate_text(&report),
_ => metrics_dashboard::DashboardGenerator::generate_html(&report),
};
let ext = match args.metrics_format.as_str() {
"json" => "json",
"text" => "txt",
_ => "html",
};
let metrics_path = args
.metrics_output
.clone()
.unwrap_or_else(|| output.join(format!("metrics.{}", ext)));
fs::write(&metrics_path, &output_content)
.with_context(|| format!("Failed to write metrics report to {:?}", metrics_path))?;
println!("\nMetrics report written to: {:?}", metrics_path);
}
println!("\n{}", "=".repeat(60));
println!("✓ Refactoring complete!");
println!("{}", "=".repeat(60));
println!("📊 Statistics:");
println!(" Original file: {} lines", source_code.lines().count());
println!(" Created {} module files", created_count);
if skipped_count > 0 {
println!(" Skipped {} modules (have customizations)", skipped_count);
}
println!(" Total types: {}", analyzer.types.len());
if let Some(strategy_name) = &args.naming_strategy {
println!(" Naming strategy: {}", strategy_name);
}
if args.incremental {
println!(" Mode: Incremental ({})", args.merge_strategy);
}
let total_methods: usize = analyzer
.types
.values()
.map(|t| {
t.impls.len()
+ t.trait_impls.len()
+ t.large_impls
.iter()
.map(|(_, groups)| groups.len())
.sum::<usize>()
})
.sum();
if total_methods > 0 {
println!(" Total impl blocks: {}", total_methods);
}
#[cfg(feature = "smt")]
if args.verify {
let relocated_items: usize = modules
.iter()
.map(|m| m.types.len() + m.standalone_items.len() + m.trait_impls.len())
.sum();
print_verify_report(&extraction_outcomes, relocated_items, args.extract_pure);
}
println!("\n💡 Next steps:");
println!(" 1. Review the generated modules in {:?}", output);
println!(" 2. Run 'cargo check' to verify the refactored code compiles");
println!(" 3. Run your test suite to ensure functionality is preserved");
if args.generate_tests {
println!(" 4. Run 'cargo test' to execute the verification tests");
}
if backup_dir.exists() {
println!("\n📦 Backup: {:?}", backup_dir);
println!(" (You can delete this after verifying the refactored code)");
}
Ok(())
}
#[cfg(feature = "smt")]
fn print_verify_report(
outcomes: &[splitrs::extraction::ExtractionOutcome],
relocated_items: usize,
extract_pure_was_set: bool,
) {
use splitrs::extraction::ExtractionOutcome;
println!("\n{}", "=".repeat(60));
println!("Semantic verification report");
println!("{}", "=".repeat(60));
println!("\nSMT-Verified (body rewrite):");
let committed: Vec<(&String, &String)> = outcomes
.iter()
.filter_map(|o| match o {
ExtractionOutcome::Committed {
fn_ident,
helper_ident,
} => Some((fn_ident, helper_ident)),
_ => None,
})
.collect();
if committed.is_empty() {
if extract_pure_was_set {
println!(" (none) — no pure fixed-width-integer extraction was proven and committed.");
} else {
println!(
" (none) — `--extract-pure` was not requested; that pre-pass is where SMT \
proofs apply."
);
}
} else {
for (fn_ident, helper_ident) in committed {
println!(
" \u{2713} {fn_ident}: function extraction — SMT-Verified equivalent \
(QF_BV, all inputs) \u{2192} helper {helper_ident}"
);
}
}
for outcome in outcomes {
match outcome {
ExtractionOutcome::SkippedRefuted { fn_ident, cx } => {
let inputs = cx
.inputs
.iter()
.map(|(n, v)| format!("{n}={v}"))
.collect::<Vec<_>>()
.join(", ");
println!(
" \u{2717} {fn_ident}: extraction REFUTED by the oracle (semantic drift \
caught); counterexample: {inputs}"
);
}
ExtractionOutcome::SkippedUnsupported { fn_ident, reason } => {
println!(" \u{2022} {fn_ident}: extraction skipped — {reason}");
}
ExtractionOutcome::Committed { .. } => {}
}
}
println!("\nStructural identity (NOT SMT-proven):");
println!(
" \u{2022} {relocated_items} items relocated unchanged (structural identity). \
Whole-item relocation preserves behavior by byte-identical move; \
name-resolution/visibility safety is the Rust compiler's responsibility \
(verify with `cargo check`), NOT proven by SMT."
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_impl_type_extraction() {
let code = r#"
struct Foo;
impl Foo {
fn bar() {}
}
"#;
let file = syn::parse_file(code).unwrap();
let mut analyzer = FileAnalyzer::new(false, 500);
analyzer.analyze(&file);
assert_eq!(analyzer.types.len(), 1);
assert_eq!(analyzer.types.get("Foo").unwrap().impls.len(), 1);
}
#[test]
fn test_generic_type_parameters_preserved() {
let code = r#"
struct Container<T, U> {
data: Vec<T>,
metadata: U,
}
impl<T, U> Container<T, U>
where
T: Clone,
U: Default,
{
fn new(data: Vec<T>, metadata: U) -> Self {
Self { data, metadata }
}
fn get_data(&self) -> &Vec<T> {
&self.data
}
fn clone_data(&self) -> Vec<T>
where
T: Clone,
{
self.data.clone()
}
}
"#;
let file = syn::parse_file(code).unwrap();
let mut analyzer = FileAnalyzer::new(true, 50); analyzer.analyze(&file);
assert_eq!(analyzer.types.len(), 1);
let container = analyzer.types.get("Container").unwrap();
assert_eq!(container.name, "Container");
let modules = analyzer.group_by_module(500);
assert!(!modules.is_empty());
let impl_modules: Vec<_> = modules
.iter()
.filter(|m| m.impl_type_name.is_some())
.collect();
for module in impl_modules {
if let Some(ref generics) = module.impl_generics {
assert!(!generics.params.is_empty(), "Generics should be preserved");
}
}
}
#[test]
fn test_lifetime_parameters_preserved() {
let code = r#"
struct Holder<'a, T> {
reference: &'a T,
}
impl<'a, T> Holder<'a, T> {
fn new(reference: &'a T) -> Self {
Self { reference }
}
fn get(&self) -> &'a T {
self.reference
}
}
"#;
let file = syn::parse_file(code).unwrap();
let mut analyzer = FileAnalyzer::new(true, 30);
analyzer.analyze(&file);
assert_eq!(analyzer.types.len(), 1);
let modules = analyzer.group_by_module(500);
let impl_modules: Vec<_> = modules
.iter()
.filter(|m| m.impl_type_name.is_some())
.collect();
for module in impl_modules {
if let Some(ref generics) = module.impl_generics {
assert!(
!generics.params.is_empty(),
"Lifetime parameters should be preserved"
);
}
}
}
#[test]
fn test_cfg_attributes_preserved() {
let code = r#"
struct PlatformSpecific {
data: Vec<u8>,
}
#[cfg(target_os = "linux")]
impl PlatformSpecific {
fn linux_only(&self) -> usize {
self.data.len()
}
fn another_method(&self) -> bool {
!self.data.is_empty()
}
fn method3(&self) -> usize { 0 }
fn method4(&self) -> usize { 1 }
fn method5(&self) -> usize { 2 }
fn method6(&self) -> usize { 3 }
}
#[cfg(target_os = "windows")]
impl PlatformSpecific {
fn windows_only(&self) -> usize {
self.data.len() + 1
}
fn win_method2(&self) -> bool { true }
fn win_method3(&self) -> usize { 0 }
fn win_method4(&self) -> usize { 1 }
}
"#;
let file = syn::parse_file(code).unwrap();
let mut analyzer = FileAnalyzer::new(true, 10); analyzer.analyze(&file);
assert_eq!(analyzer.types.len(), 1);
let modules = analyzer.group_by_module(500);
let impl_modules: Vec<_> = modules
.iter()
.filter(|m| m.impl_type_name.is_some())
.collect();
assert!(
!impl_modules.is_empty(),
"Should have generated impl modules"
);
let mut found_cfg = false;
for module in impl_modules {
if !module.impl_attrs.is_empty() {
let has_cfg = module.impl_attrs.iter().any(|attr| {
attr.path()
.segments
.first()
.map(|s| s.ident == "cfg")
.unwrap_or(false)
});
if has_cfg {
found_cfg = true;
break;
}
}
}
assert!(
found_cfg,
"At least one impl module should preserve cfg attributes"
);
}
#[test]
fn test_doc_comments_on_impl_blocks() {
let code = r#"
struct Document {
content: String,
}
/// Main implementation for Document
/// Provides core functionality
impl Document {
/// Creates a new document
pub fn new(content: String) -> Self {
Self { content }
}
/// Returns the content
pub fn get_content(&self) -> &str {
&self.content
}
/// Additional method 1
pub fn method1(&self) -> usize { 1 }
/// Additional method 2
pub fn method2(&self) -> usize { 2 }
/// Additional method 3
pub fn method3(&self) -> usize { 3 }
/// Additional method 4
pub fn method4(&self) -> usize { 4 }
}
"#;
let file = syn::parse_file(code).unwrap();
let mut analyzer = FileAnalyzer::new(true, 10); analyzer.analyze(&file);
assert_eq!(analyzer.types.len(), 1);
let modules = analyzer.group_by_module(500);
let impl_modules: Vec<_> = modules
.iter()
.filter(|m| m.impl_type_name.is_some())
.collect();
assert!(
!impl_modules.is_empty(),
"Should have generated impl modules"
);
let mut found_doc = false;
for module in impl_modules {
if !module.impl_attrs.is_empty() {
let has_doc = module.impl_attrs.iter().any(|attr| {
attr.path()
.segments
.first()
.map(|s| s.ident == "doc")
.unwrap_or(false)
});
if has_doc {
found_doc = true;
break;
}
}
}
assert!(
found_doc,
"At least one impl module should preserve doc comments"
);
}
#[test]
fn test_workspace_analyzer() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
fs::write(
temp_dir.path().join("Cargo.toml"),
r#"
[package]
name = "test-crate"
version = "0.1.0"
edition = "2021"
"#,
)
.unwrap();
let src_dir = temp_dir.path().join("src");
fs::create_dir_all(&src_dir).unwrap();
fs::write(
src_dir.join("main.rs"),
"fn main() {\n println!(\"Hello\");\n}\n",
)
.unwrap();
let analyzer = workspace::WorkspaceAnalyzer::new(temp_dir.path(), 100);
let analysis = analyzer.analyze().unwrap();
assert_eq!(analysis.crates.len(), 1);
assert_eq!(analysis.crates[0].name, "test-crate");
}
#[test]
fn test_error_recovery_diagnostic() {
let error = error_recovery::DiagnosticError::new(
"Test error",
error_recovery::ErrorSeverity::Error,
)
.with_location(PathBuf::from("test.rs"), 10, 5)
.with_suggestion("Try this fix");
let formatted = error.format();
assert!(formatted.contains("error"));
assert!(formatted.contains("test.rs:10:5"));
assert!(formatted.contains("Try this fix"));
}
#[test]
fn test_unicode_identifiers_in_types() {
let code = r#"
struct データ構造 {
値: i32,
}
impl データ構造 {
fn 新規作成(値: i32) -> Self {
Self { 値 }
}
fn 値取得(&self) -> i32 {
self.値
}
}
"#;
let file = syn::parse_file(code).unwrap();
let mut analyzer = FileAnalyzer::new(true, 30);
analyzer.analyze(&file);
assert_eq!(analyzer.types.len(), 1);
let modules = analyzer.group_by_module(500);
assert!(!modules.is_empty());
for module in &modules {
assert!(
module
.name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_'),
"Module name contains non-ASCII characters: {}",
module.name
);
}
}
#[test]
fn test_mixed_unicode_ascii_identifiers() {
let code = r#"
struct MixedData {
english_field: String,
日本語フィールド: i32,
}
impl MixedData {
fn new(english_field: String, 日本語フィールド: i32) -> Self {
Self { english_field, 日本語フィールド }
}
fn get_english(&self) -> &str {
&self.english_field
}
fn 日本語取得(&self) -> i32 {
self.日本語フィールド
}
}
"#;
let file = syn::parse_file(code).unwrap();
let mut analyzer = FileAnalyzer::new(false, 500);
analyzer.analyze(&file);
assert_eq!(analyzer.types.len(), 1);
let mixed_data = analyzer.types.get("MixedData").unwrap();
assert_eq!(mixed_data.name, "MixedData");
}
}