use alef_core::config::{AlefConfig, Language};
use std::path::Path;
use std::process::Command;
use tracing::{debug, warn};
struct FormatterCommand {
command: &'static str,
args: &'static [&'static str],
}
struct FormatterSpec {
commands: &'static [FormatterCommand],
work_dir: &'static str,
}
fn get_default_formatter(lang: Language) -> Option<FormatterSpec> {
match lang {
Language::Python => Some(FormatterSpec {
commands: &[
FormatterCommand {
command: "ruff",
args: &["check", "--fix", "."],
},
FormatterCommand {
command: "ruff",
args: &["format", "."],
},
],
work_dir: "packages/python/",
}),
Language::Node => Some(FormatterSpec {
commands: &[FormatterCommand {
command: "biome",
args: &["format", "--write", "."],
}],
work_dir: "packages/typescript/",
}),
Language::Ruby => Some(FormatterSpec {
commands: &[FormatterCommand {
command: "rubocop",
args: &["-A", "--no-server"],
}],
work_dir: "packages/ruby/",
}),
Language::Php => Some(FormatterSpec {
commands: &[FormatterCommand {
command: "php-cs-fixer",
args: &["fix"],
}],
work_dir: "packages/php/",
}),
Language::Elixir => Some(FormatterSpec {
commands: &[FormatterCommand {
command: "mix",
args: &["format"],
}],
work_dir: "packages/elixir/",
}),
Language::Go => Some(FormatterSpec {
commands: &[FormatterCommand {
command: "gofmt",
args: &["-w", "."],
}],
work_dir: "packages/go/",
}),
Language::Java => Some(FormatterSpec {
commands: &[FormatterCommand {
command: "google-java-format",
args: &["-i"],
}],
work_dir: "packages/java/src/",
}),
Language::Csharp => Some(FormatterSpec {
commands: &[FormatterCommand {
command: "dotnet",
args: &["format"],
}],
work_dir: "packages/csharp/",
}),
Language::Wasm => Some(FormatterSpec {
commands: &[FormatterCommand {
command: "cargo",
args: &["fmt", "-p", "wasm"],
}],
work_dir: "packages/wasm/",
}),
Language::Ffi => Some(FormatterSpec {
commands: &[FormatterCommand {
command: "cargo",
args: &["fmt", "--all"],
}],
work_dir: "",
}),
Language::R => Some(FormatterSpec {
commands: &[FormatterCommand {
command: "Rscript",
args: &["-e", "styler::style_pkg('packages/r')"],
}],
work_dir: "",
}),
Language::Rust => None,
}
}
pub fn format_generated(
files: &[(Language, Vec<alef_core::backend::GeneratedFile>)],
config: &AlefConfig,
base_dir: &Path,
) {
let mut formatted_langs = std::collections::HashSet::new();
for (lang, _) in files {
if formatted_langs.contains(lang) {
continue;
}
let lang_str = lang.to_string().to_lowercase();
let format_cfg = config
.format_overrides
.get(&lang_str)
.cloned()
.unwrap_or_else(|| config.format.clone());
if !format_cfg.enabled {
debug!(" [{lang_str}] formatting disabled, skipping");
continue;
}
let formatter_cmd = if let Some(custom) = format_cfg.command {
if !run_custom_formatter(&custom, base_dir) {
warn!("[{lang_str}] custom formatter failed");
}
formatted_langs.insert(*lang);
continue;
} else if let Some(spec) = get_default_formatter(*lang) {
spec
} else {
debug!(" [{lang_str}] no default formatter configured");
continue;
};
let work_dir = if formatter_cmd.work_dir.is_empty() {
base_dir.to_path_buf()
} else {
base_dir.join(formatter_cmd.work_dir)
};
if !work_dir.exists() {
debug!(
" [{lang_str}] package directory does not exist: {}, skipping",
work_dir.display()
);
continue;
}
for step in formatter_cmd.commands {
if !is_tool_available(step.command) {
warn!("[{lang_str}] formatter not found: {} (skipping format)", step.command);
break;
}
match run_formatter(step.command, step.args, &work_dir) {
Ok(()) => {
debug!(" [{lang_str}] {} {:?} ok", step.command, step.args);
}
Err(e) => {
warn!("[{lang_str}] {} {:?} failed: {}", step.command, step.args, e);
break;
}
}
}
formatted_langs.insert(*lang);
}
}
fn is_tool_available(tool: &str) -> bool {
Command::new("which")
.arg(tool)
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
fn run_formatter(command: &str, args: &[&str], work_dir: &Path) -> anyhow::Result<()> {
let output = Command::new(command).args(args).current_dir(work_dir).output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow::anyhow!(
"formatter exited with code {:?}: {}",
output.status.code(),
stderr.trim()
));
}
Ok(())
}
fn run_custom_formatter(cmd: &str, work_dir: &Path) -> bool {
let output = Command::new("sh").arg("-c").arg(cmd).current_dir(work_dir).output();
match output {
Ok(out) => out.status.success(),
Err(e) => {
debug!("custom formatter error: {}", e);
false
}
}
}