1use crate::config::E2eConfig;
8use alef_core::backend::GeneratedFile;
9use std::collections::HashSet;
10use std::path::Path;
11use tracing::warn;
12
13fn default_formatter(lang: &str) -> Option<&'static str> {
37 match lang {
38 "rust" => Some("(cd {dir} && cargo fmt --all && cargo sort .)"),
39 "python" => Some("ruff format {dir}"),
40 "node" | "wasm" => Some("pnpm dlx oxfmt {dir}"),
41 _ => None,
42 }
43}
44
45pub fn run_formatters(files: &[GeneratedFile], e2e_config: &E2eConfig) {
53 let output_prefix = Path::new(e2e_config.effective_output());
57 let languages: HashSet<String> = files
58 .iter()
59 .filter_map(|f| {
60 let remainder = f.path.strip_prefix(output_prefix).ok()?;
61 let first = remainder.components().next()?;
62 Some(first.as_os_str().to_string_lossy().into_owned())
63 })
64 .collect();
65
66 for lang in &languages {
67 let cmd_template: &str = if let Some(custom) = e2e_config.format.get(lang.as_str()) {
69 custom.as_str()
70 } else if let Some(builtin) = default_formatter(lang.as_str()) {
71 builtin
72 } else {
73 continue;
74 };
75
76 let dir = format!("{}/{}", e2e_config.effective_output(), lang);
77 let cmd = cmd_template.replace("{dir}", &dir);
78
79 eprintln!(" Formatting {lang}: {cmd}");
80 let status = std::process::Command::new("sh").args(["-c", &cmd]).status();
81
82 match status {
83 Ok(s) if s.success() => {}
84 Ok(s) => {
85 warn!("Formatter for {lang} exited with {s}: {cmd}");
86 }
87 Err(e) => {
88 warn!("Failed to run formatter for {lang}: {e}");
89 }
90 }
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn test_default_formatter_rust_uses_cd_into_dir() {
100 let cmd = default_formatter("rust").expect("rust must have a default formatter");
101 assert!(
102 cmd.contains("cd {dir}") && cmd.contains("cargo fmt"),
103 "rust formatter must cd into {{dir}} before invoking cargo fmt so it works on \
104 standalone (non-workspace-member) e2e crates: {cmd}"
105 );
106 assert!(
107 !cmd.contains("--manifest-path"),
108 "rust formatter must not use --manifest-path; cargo-fmt does not accept it as a \
109 global flag: {cmd}"
110 );
111 assert!(
112 cmd.contains("{dir}"),
113 "rust formatter must include {{dir}} placeholder: {cmd}"
114 );
115 assert!(
116 cmd.contains("cargo sort"),
117 "rust formatter must run cargo sort to normalise Cargo.toml before hash \
118 finalisation so prek's cargo-sort hook is a no-op: {cmd}"
119 );
120 }
121
122 #[test]
123 fn test_default_formatter_python_uses_ruff_format() {
124 let cmd = default_formatter("python").expect("python must have a default formatter");
125 assert!(cmd.contains("ruff"), "python formatter must use ruff: {cmd}");
126 assert!(cmd.contains("format"), "python formatter must run ruff format: {cmd}");
127 assert!(
128 cmd.contains("{dir}"),
129 "python formatter must include {{dir}} placeholder: {cmd}"
130 );
131 }
132
133 #[test]
134 fn test_default_formatter_node_uses_oxfmt() {
135 let cmd = default_formatter("node").expect("node must have a default formatter");
136 assert!(cmd.contains("oxfmt"), "node formatter must use oxfmt: {cmd}");
137 assert!(
138 cmd.contains("{dir}"),
139 "node formatter must include {{dir}} placeholder: {cmd}"
140 );
141 }
142
143 #[test]
144 fn test_default_formatter_wasm_uses_oxfmt() {
145 let cmd = default_formatter("wasm").expect("wasm must have a default formatter");
146 assert!(cmd.contains("oxfmt"), "wasm formatter must use oxfmt: {cmd}");
147 assert!(
148 cmd.contains("{dir}"),
149 "wasm formatter must include {{dir}} placeholder: {cmd}"
150 );
151 }
152
153 #[test]
154 fn test_default_formatter_unknown_lang_returns_none() {
155 assert!(default_formatter("gleam").is_none());
156 assert!(default_formatter("zig").is_none());
157 assert!(default_formatter("java").is_none());
158 }
159}