Skip to main content

bynk/
fmt.rs

1//! `bynk fmt` — format `.bynk` source files in place.
2//!
3//! Runs the formatter in-process over `bynk-fmt` (v0.138, #487), mirroring
4//! `bynkc fmt` byte-for-byte: each input is formatted and rewritten only when it
5//! changes; `--check` reports non-canonical files without writing; `-` reads
6//! stdin and writes the formatted result to stdout. The `BYNK_BYNKC` override
7//! shells the pinned compiler instead, like `bynk check`.
8
9use std::ffi::OsString;
10use std::process::ExitCode;
11
12use bynk_driver::{FmtArgs, IndentKind};
13
14use crate::compiler::{Compiler, Origin, SkewAsker, skew_gate};
15
16/// Run `bynk fmt`. `compiler` carries the driver's resolution so a `BYNK_BYNKC`
17/// override can be honoured by shelling the pinned `bynkc`.
18pub fn run(compiler: &Compiler, args: FmtArgs) -> ExitCode {
19    if let (Some(Origin::Override), Some(bynkc)) = (compiler.origin, compiler.path.as_deref()) {
20        // #1675: a second compiler, so its skew from the driver is acted on.
21        let asker = SkewAsker {
22            command: Some("fmt"),
23            has_flag: false,
24        };
25        if !skew_gate(compiler, asker, false) {
26            return ExitCode::FAILURE;
27        }
28        return crate::shell::delegate(bynkc, delegated_argv(&args));
29    }
30    fmt_in_process(args)
31}
32
33/// The argv for the shelled `bynkc fmt`. Every flag is respelled, style flags
34/// included (#968): forwarding only `--check` would have a `BYNK_BYNKC`
35/// override quietly format to the canonical style while the developer asked
36/// for another one. `bynk` and `bynkc` flatten the same [`FmtArgs`], so the
37/// flag names below are the ones the child parses.
38///
39/// Only flags the run actually passed are respelled (#972). The child does its
40/// own `bynk.toml` `[fmt]` lookup — it inherits this process's working
41/// directory and gets the same input paths, so it reaches the same manifest —
42/// and forwarding a *resolved* value would defeat that: `--max-line-width 100`
43/// synthesised from the default would override a project's `120`, and one argv
44/// cannot express the several manifests a multi-project run may resolve.
45fn delegated_argv(args: &FmtArgs) -> Vec<OsString> {
46    let mut argv: Vec<OsString> = vec!["fmt".into()];
47    if args.check {
48        argv.push("--check".into());
49    }
50    if let Some(kind) = args.indent {
51        argv.push("--indent".into());
52        argv.push(
53            match kind {
54                IndentKind::Tab => "tab",
55                IndentKind::Spaces => "spaces",
56            }
57            .into(),
58        );
59    }
60    if let Some(width) = args.indent_width {
61        argv.push("--indent-width".into());
62        argv.push(width.to_string().into());
63    }
64    if let Some(width) = args.max_line_width {
65        argv.push("--max-line-width".into());
66        argv.push(width.to_string().into());
67    }
68    if args.trailing_comma {
69        argv.push("--trailing-comma".into());
70    }
71    if args.no_trailing_comma {
72        argv.push("--no-trailing-comma".into());
73    }
74    if args.no_config {
75        argv.push("--no-config".into());
76    }
77    // Flags first, then `--`, then the paths: `bynk` already parsed these as
78    // positionals (the user may have used their own `--`), so a path that
79    // begins with a dash must not be re-read as a flag by the child. `-`
80    // itself still means stdin after the separator.
81    argv.push("--".into());
82    argv.extend(args.inputs.iter().map(|p| p.as_os_str().to_os_string()));
83    argv
84}
85
86/// The default path: the shared command body (#521, [`bynk_driver::run_fmt`]).
87fn fmt_in_process(args: FmtArgs) -> ExitCode {
88    bynk_driver::run_fmt("bynk", &args)
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    fn argv_of(args: FmtArgs) -> Vec<String> {
96        delegated_argv(&args)
97            .into_iter()
98            .map(|a| a.to_string_lossy().into_owned())
99            .collect()
100    }
101
102    fn base() -> FmtArgs {
103        FmtArgs {
104            inputs: vec!["a.bynk".into()],
105            check: false,
106            indent: None,
107            indent_width: None,
108            max_line_width: None,
109            trailing_comma: false,
110            no_trailing_comma: false,
111            no_config: false,
112        }
113    }
114
115    #[test]
116    fn an_unflagged_run_forwards_no_style_at_all() {
117        // #972: not even a synthesised `--max-line-width 100`. The child does
118        // its own `bynk.toml` lookup, and a resolved default forwarded as a
119        // flag would override the project's own `[fmt]`.
120        assert_eq!(argv_of(base()), vec!["fmt", "--", "a.bynk"]);
121    }
122
123    #[test]
124    fn style_overrides_reach_the_pinned_compiler() {
125        // The regression this guards: a `BYNK_BYNKC` override that formatted
126        // to the canonical style while the developer asked for another one.
127        let args = FmtArgs {
128            check: true,
129            indent: Some(IndentKind::Spaces),
130            indent_width: Some(4),
131            max_line_width: Some(120),
132            no_trailing_comma: true,
133            ..base()
134        };
135        assert_eq!(
136            argv_of(args),
137            vec![
138                "fmt",
139                "--check",
140                "--indent",
141                "spaces",
142                "--indent-width",
143                "4",
144                "--max-line-width",
145                "120",
146                "--no-trailing-comma",
147                "--",
148                "a.bynk",
149            ]
150        );
151    }
152
153    #[test]
154    fn an_explicit_tab_is_forwarded_so_it_can_beat_a_manifest() {
155        // `--indent tab` is not a no-op once `[fmt] indent = "spaces"` exists:
156        // it is how a run overrides the project back to tabs, so the child must
157        // be told, even though tabs are also the spec default.
158        let args = FmtArgs {
159            indent: Some(IndentKind::Tab),
160            ..base()
161        };
162        let argv = argv_of(args);
163        assert!(
164            argv.windows(2).any(|w| w == ["--indent", "tab"]),
165            "an explicit `--indent tab` must be forwarded: {argv:?}"
166        );
167    }
168
169    #[test]
170    fn trailing_comma_and_no_config_are_forwarded() {
171        let args = FmtArgs {
172            trailing_comma: true,
173            no_config: true,
174            ..base()
175        };
176        let argv = argv_of(args);
177        assert!(argv.contains(&"--trailing-comma".to_string()), "{argv:?}");
178        assert!(argv.contains(&"--no-config".to_string()), "{argv:?}");
179    }
180
181    #[test]
182    fn every_input_is_forwarded_after_the_separator() {
183        // A path beginning with a dash is a path, not a flag — `bynk` already
184        // parsed it as a positional, so the child must too. `-` still means
185        // stdin after the separator.
186        let args = FmtArgs {
187            inputs: vec!["-".into(), "b/c.bynk".into(), "-weird.bynk".into()],
188            ..base()
189        };
190        let argv = argv_of(args);
191        let sep = argv
192            .iter()
193            .position(|a| a == "--")
194            .expect("a `--` separator");
195        assert_eq!(
196            &argv[sep + 1..],
197            ["-", "b/c.bynk", "-weird.bynk"],
198            "every input must follow the separator, in order: {argv:?}"
199        );
200    }
201}