use anyhow::Result;
use camino::Utf8PathBuf;
use std::fs;
use crate::cli::DictateArgs;
use crate::config::load_dictate_config;
use crate::files::collect_all_files;
use crate::interactive::InteractiveFixer;
use crate::regime::init_regime_for_files;
pub fn run_dictate(
args: DictateArgs,
config_path: Option<Utf8PathBuf>,
profile: Option<String>,
) -> Result<()> {
if args.interactive {
run_interactive_dictate(&args.paths, config_path, profile)
} else {
run_batch_dictate(&args.paths, config_path, profile)
}
}
fn run_interactive_dictate(
paths: &[Utf8PathBuf],
config_path: Option<Utf8PathBuf>,
profile: Option<String>,
) -> Result<()> {
println!("🔧 Interactive Fix Mode");
println!("========================\n");
let decree_config = load_dictate_config(config_path.as_ref(), profile.as_deref())?;
let mut fixer = InteractiveFixer::new();
println!("🔍 Collecting fixable violations...");
fixer.collect_violations(paths, decree_config.as_ref())?;
if !fixer.has_violations() {
println!("✨ No fixable violations found!");
return Ok(());
}
println!("🔧 Found {} fixable violations\n", fixer.violation_count());
fixer.run_interactive(false)?;
Ok(())
}
fn run_batch_dictate(
paths: &[Utf8PathBuf],
config_path: Option<Utf8PathBuf>,
profile: Option<String>,
) -> Result<()> {
let files = collect_all_files(paths)?;
if files.is_empty() {
eprintln!("No files found");
return Ok(());
}
let decree_config = load_dictate_config(config_path.as_ref(), profile.as_deref())?;
let file_types = crate::files::detect_file_types(&files);
let mut regime = init_regime_for_files(&file_types, decree_config.as_ref());
if let Some(ref config) = decree_config {
for (name, settings) in &config.decree {
if matches!(
name.as_str(),
"supreme" | "ruby" | "typescript" | "golang" | "rust" | "python" | "frontmatter"
) {
continue;
}
if let Some(ref path) = settings.path
&& settings.enabled.unwrap_or(true)
{
regime.add_wasm_decree(path)?;
}
}
}
let mut fixed_count = 0;
let mut file_count = 0;
for path in files {
let Some(original) = crate::files::read_source_file(path.as_std_path()) else {
continue;
};
let source = dictator_core::Source {
path: path.as_path(),
text: &original,
};
let diags = regime.enforce(&[source])?;
let mut fixed_text = original.clone();
let mut file_was_fixed = false;
for diag in diags {
if diag.enforced
&& let Some(new_text) = apply_single_fix(&fixed_text, &diag)
&& new_text != fixed_text
{
fixed_text = new_text;
file_was_fixed = true;
}
}
if file_was_fixed {
fs::write(&path, &fixed_text)?;
println!("Fixed: {path}");
fixed_count += 1;
}
file_count += 1;
}
if fixed_count > 0 {
println!("Fixed {fixed_count} file(s) out of {file_count} checked.");
} else {
println!("All {file_count} file(s) already compliant.");
}
Ok(())
}
pub(crate) fn apply_single_fix(
content: &str,
diag: &dictator_decree_abi::Diagnostic,
) -> Option<String> {
match diag.rule.as_str() {
rule if rule.contains("trailing-whitespace") => {
let mut result = String::with_capacity(content.len());
for line in content.lines() {
result.push_str(line.trim_end_matches([' ', '\t']));
result.push('\n');
}
if !content.ends_with('\n') && result.ends_with('\n') {
result.pop();
}
Some(result)
}
rule if rule.contains("tab-character") => Some(content.replace('\t', " ")),
rule if rule.contains("missing-final-newline") => {
if content.ends_with('\n') {
None } else {
Some(format!("{content}\n"))
}
}
rule if rule.contains("mixed-line-endings") => Some(content.replace("\r\n", "\n")),
rule if rule.contains("blank-line-whitespace") => {
let mut result = String::with_capacity(content.len());
for line in content.lines() {
if !line.trim().is_empty() || line.is_empty() {
result.push_str(line);
}
result.push('\n');
}
if !content.ends_with('\n') && result.ends_with('\n') {
result.pop();
}
Some(result)
}
_ => None, }
}
#[cfg(test)]
fn fix_structural_issues(content: &str) -> String {
let mut result = String::with_capacity(content.len());
let normalized = content.replace("\r\n", "\n");
for line in normalized.split('\n') {
let trimmed = line.trim_end_matches([' ', '\t']);
result.push_str(trimmed);
result.push('\n');
}
if result.ends_with('\n') && !normalized.ends_with('\n') {
} else if result.ends_with("\n\n") && !normalized.ends_with("\n\n") {
result.pop();
}
while result.ends_with("\n\n") {
result.pop();
}
if !result.ends_with('\n') && !result.is_empty() {
result.push('\n');
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fix_trailing_whitespace() {
let input = "hello world \nfoo bar\t\t\n";
let expected = "hello world\nfoo bar\n";
assert_eq!(fix_structural_issues(input), expected);
}
#[test]
fn test_fix_crlf() {
let input = "hello\r\nworld\r\n";
let expected = "hello\nworld\n";
assert_eq!(fix_structural_issues(input), expected);
}
#[test]
fn test_add_final_newline() {
let input = "hello world";
let expected = "hello world\n";
assert_eq!(fix_structural_issues(input), expected);
}
#[test]
fn test_remove_extra_final_newlines() {
let input = "hello world\n\n\n";
let expected = "hello world\n";
assert_eq!(fix_structural_issues(input), expected);
}
}