Skip to main content

alef_e2e/
format.rs

1//! Post-generation formatter support for e2e test projects.
2//!
3//! Reads formatter commands from `E2eConfig.format` and runs them for each
4//! language that had files generated. The `{dir}` placeholder in the command
5//! is replaced with the actual output directory.
6
7use crate::config::E2eConfig;
8use alef_core::backend::GeneratedFile;
9use std::collections::HashSet;
10use std::path::Path;
11use tracing::warn;
12
13/// Default per-language formatter commands for e2e directories.
14///
15/// These are run automatically when the language's directory was generated and
16/// no custom override is present in `e2e_config.format`. The `{dir}` placeholder
17/// is replaced with the actual output directory path before execution.
18///
19/// * `rust` — `cargo fmt --manifest-path {dir}/Cargo.toml` formats the
20///   standalone e2e crate without requiring it to be a workspace member.
21/// * `python` — `ruff format {dir}` normalises whitespace/newlines in the
22///   generated test files so prek's ruff hook is a no-op.
23fn default_formatter(lang: &str) -> Option<&'static str> {
24    match lang {
25        "rust" => Some("cargo fmt --manifest-path {dir}/Cargo.toml"),
26        "python" => Some("ruff format {dir}"),
27        _ => None,
28    }
29}
30
31/// Run per-language formatters for all languages that had files generated.
32///
33/// For each language present in `files`, picks the command from
34/// `e2e_config.format[lang]` when available, then falls back to
35/// [`default_formatter`] for languages that have a built-in default (rust,
36/// python). The `{dir}` placeholder is replaced with `{output}/{lang}`.
37/// Failures are logged as warnings and do not abort the process.
38pub fn run_formatters(files: &[GeneratedFile], e2e_config: &E2eConfig) {
39    // Collect the set of languages that had files generated by inspecting
40    // file paths. E2e files are written to `{output}/{lang}/...`, so the
41    // first path component after the output prefix is the language name.
42    let output_prefix = Path::new(e2e_config.effective_output());
43    let languages: HashSet<String> = files
44        .iter()
45        .filter_map(|f| {
46            let remainder = f.path.strip_prefix(output_prefix).ok()?;
47            let first = remainder.components().next()?;
48            Some(first.as_os_str().to_string_lossy().into_owned())
49        })
50        .collect();
51
52    for lang in &languages {
53        // User override takes precedence; then built-in default.
54        let cmd_template: &str = if let Some(custom) = e2e_config.format.get(lang.as_str()) {
55            custom.as_str()
56        } else if let Some(builtin) = default_formatter(lang.as_str()) {
57            builtin
58        } else {
59            continue;
60        };
61
62        let dir = format!("{}/{}", e2e_config.effective_output(), lang);
63        let cmd = cmd_template.replace("{dir}", &dir);
64
65        eprintln!("  Formatting {lang}: {cmd}");
66        let status = std::process::Command::new("sh").args(["-c", &cmd]).status();
67
68        match status {
69            Ok(s) if s.success() => {}
70            Ok(s) => {
71                warn!("Formatter for {lang} exited with {s}: {cmd}");
72            }
73            Err(e) => {
74                warn!("Failed to run formatter for {lang}: {e}");
75            }
76        }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn test_default_formatter_rust_uses_manifest_path() {
86        let cmd = default_formatter("rust").expect("rust must have a default formatter");
87        assert!(
88            cmd.contains("--manifest-path"),
89            "rust formatter must use --manifest-path so it works on standalone e2e crates: {cmd}"
90        );
91        assert!(
92            cmd.contains("{dir}"),
93            "rust formatter must include {{dir}} placeholder: {cmd}"
94        );
95    }
96
97    #[test]
98    fn test_default_formatter_python_uses_ruff_format() {
99        let cmd = default_formatter("python").expect("python must have a default formatter");
100        assert!(cmd.contains("ruff"), "python formatter must use ruff: {cmd}");
101        assert!(cmd.contains("format"), "python formatter must run ruff format: {cmd}");
102        assert!(
103            cmd.contains("{dir}"),
104            "python formatter must include {{dir}} placeholder: {cmd}"
105        );
106    }
107
108    #[test]
109    fn test_default_formatter_unknown_lang_returns_none() {
110        assert!(default_formatter("gleam").is_none());
111        assert!(default_formatter("zig").is_none());
112        assert!(default_formatter("java").is_none());
113    }
114}