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