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