Skip to main content

bynk_driver/
lib.rs

1//! bynk-driver — the shared front-end of the `bynkc` and `bynk` CLIs (#521).
2//!
3//! Both binaries expose `fmt` and `check` with identical semantics; before
4//! this crate each re-implemented the command bodies (and the project-failure
5//! flattening layer, and the project-rooting rule) as by-hand copies pinned
6//! only by comments and a skip-able parity test. The single implementation
7//! lives here, parameterised by the program name that prefixes messages.
8
9use std::path::{Path, PathBuf};
10use std::process::ExitCode;
11
12use bynk_emit::project::{self, CompileOptions, read_project_paths};
13use bynk_fmt::{FormatOptions, format_source};
14
15/// Root a directory project the way every project command should (#46): a
16/// `bynk.toml` or a `src/` subdir selects **project** mode, whose flat
17/// `[paths] include`/`exclude` layout (v0.113, DECISION S) defaults to the
18/// conventional roots that exist (`src`, `tests`) or the project root itself;
19/// otherwise the legacy **single-tree** where `<dir>` is itself the root.
20/// `check`, `compile`, `test`, and `dev` all route through this so the
21/// conventional layout works the same from any of them.
22pub fn project_options(input: &Path) -> CompileOptions {
23    if input.join("bynk.toml").exists() || input.join("src").is_dir() {
24        CompileOptions::split(input.to_path_buf(), read_project_paths(input))
25    } else {
26        CompileOptions::single(input.to_path_buf())
27    }
28}
29
30/// Render a project build failure with per-file ariadne context, exactly as
31/// single-file mode had rich rendering. Unattributed (project-level) errors
32/// keep the plain form.
33///
34/// This is the **flattening layer** (ADR 0100): it attributes each
35/// `AttributedError` to its file snapshot and delegates the actual rendering to
36/// [`bynk_render::print_errors`]. The `ProjectFailure → CompileError` flattening
37/// stays here, above `bynk-render`, so there is no `render → emit` edge.
38pub fn print_project_failure(failure: &project::ProjectFailure) {
39    let texts: std::collections::HashMap<&Path, &str> = failure
40        .snapshots
41        .iter()
42        .map(|(p, t)| (p.as_path(), t.as_str()))
43        .collect();
44    for ae in &failure.errors {
45        match ae
46            .source_path
47            .as_deref()
48            .and_then(|p| texts.get(p).map(|t| (p, *t)))
49        {
50            Some((path, text)) => {
51                let label = path.to_string_lossy().replace('\\', "/");
52                bynk_render::print_errors(std::slice::from_ref(&ae.error), text, &label);
53            }
54            None => {
55                eprintln!("[{}] {}", ae.error.category, ae.error.message);
56                for note in &ae.error.notes {
57                    eprintln!("  note: {note}");
58                }
59            }
60        }
61    }
62}
63
64/// v0.89 (ADR 0117): print a successful build's non-failing warnings. A
65/// successful build keeps no per-file snapshots, so warnings render in the
66/// plain `warning[<category>]: <message>` form (with the owning file, when
67/// known) rather than ariadne source context.
68pub fn print_project_warnings(warnings: &[project::AttributedError]) {
69    for w in warnings {
70        let where_ = w
71            .source_path
72            .as_deref()
73            .map(|p| format!("{}: ", p.to_string_lossy().replace('\\', "/")))
74            .unwrap_or_default();
75        eprintln!("{where_}warning[{}]: {}", w.error.category, w.error.message);
76        for note in &w.error.notes {
77            eprintln!("  note: {note}");
78        }
79    }
80}
81
82/// The project-failure analogue of [`bynk_render::print_errors_short`]: each
83/// attributed error is positioned against its file's snapshot; an unattributed
84/// (project-level) error falls back to `<severity>[<category>]: <message>`.
85pub fn print_project_failure_short(failure: &project::ProjectFailure) {
86    for line in project_failure_short_lines(failure) {
87        eprintln!("{line}");
88    }
89}
90
91/// The string form of [`print_project_failure_short`]: one `path:line:col:
92/// severity[category]: message` line per attributed error (an unattributed
93/// project-level error falls back to `severity[category]: message`). Backs both
94/// the printer above and the `bynkc test --format json` compile-error document,
95/// whose `diagnostics` the VS Code `bynkc` problem-matcher re-parses.
96///
97/// The flattening layer (ADR 0100): it delegates the per-error formatting to
98/// [`bynk_render::short_line`] / [`bynk_render::severity_word`].
99pub fn project_failure_short_lines(failure: &project::ProjectFailure) -> Vec<String> {
100    let texts: std::collections::HashMap<&Path, &str> = failure
101        .snapshots
102        .iter()
103        .map(|(p, t)| (p.as_path(), t.as_str()))
104        .collect();
105    failure
106        .errors
107        .iter()
108        .map(|ae| {
109            match ae
110                .source_path
111                .as_deref()
112                .and_then(|p| texts.get(p).map(|t| (p, *t)))
113            {
114                Some((path, text)) => {
115                    let label = path.to_string_lossy().replace('\\', "/");
116                    bynk_render::short_line(&label, text, &ae.error)
117                }
118                None => format!(
119                    "{}[{}]: {}",
120                    bynk_render::severity_word(&ae.error),
121                    ae.error.category,
122                    ae.error.message
123                ),
124            }
125        })
126        .collect()
127}
128
129/// The `fmt` command body shared by `bynkc fmt` and `bynk fmt`: each input is
130/// formatted and rewritten only when it changes; `--check` reports
131/// non-canonical files without writing; `-` reads stdin and writes the
132/// formatted result to stdout. `prog` prefixes messages (`bynk fmt: …`).
133pub fn run_fmt(prog: &str, inputs: &[PathBuf], check: bool) -> ExitCode {
134    let opts = FormatOptions::default();
135    if inputs.is_empty() {
136        eprintln!("{prog} fmt: no input files (pass file paths or `-` for stdin)");
137        return ExitCode::FAILURE;
138    }
139    let mut had_diff = false;
140    let mut had_error = false;
141    for input in inputs {
142        if input.as_os_str() == "-" {
143            use std::io::Read;
144            let mut source = String::new();
145            if let Err(e) = std::io::stdin().read_to_string(&mut source) {
146                eprintln!("{prog} fmt: read from stdin: {e}");
147                return ExitCode::FAILURE;
148            }
149            match format_source(&source, &opts) {
150                Ok(formatted) => print!("{formatted}"),
151                Err(e) => {
152                    bynk_render::print_errors(&e.errors, &source, "<stdin>");
153                    return ExitCode::FAILURE;
154                }
155            }
156            continue;
157        }
158        let source = match std::fs::read_to_string(input) {
159            Ok(s) => s,
160            Err(e) => {
161                eprintln!("{prog} fmt: read `{}`: {e}", input.display());
162                had_error = true;
163                continue;
164            }
165        };
166        let filename = input.display().to_string();
167        match format_source(&source, &opts) {
168            Ok(formatted) => {
169                if check {
170                    if formatted != source {
171                        eprintln!(
172                            "{prog} fmt: {} is not canonically formatted",
173                            input.display()
174                        );
175                        had_diff = true;
176                    }
177                } else if formatted != source
178                    && let Err(e) = std::fs::write(input, formatted)
179                {
180                    eprintln!("{prog} fmt: write `{}`: {e}", input.display());
181                    had_error = true;
182                }
183            }
184            Err(e) => {
185                bynk_render::print_errors(&e.errors, &source, &filename);
186                had_error = true;
187            }
188        }
189    }
190    if had_error || (check && had_diff) {
191        ExitCode::FAILURE
192    } else {
193        ExitCode::SUCCESS
194    }
195}
196
197/// The `check` command body shared by `bynkc check` and `bynk check`: a
198/// directory routes through [`project::compile_project`], a single file
199/// through [`bynk_emit::compile_with_warnings`]. `short` selects the one-line
200/// `--format short` rendering. `prog` prefixes messages (`bynk: …`).
201pub fn run_check(prog: &str, input: &Path, short: bool) -> ExitCode {
202    if input.is_dir() {
203        match project::compile_project(&project_options(input)) {
204            Ok(out) => {
205                print_project_warnings(&out.warnings);
206                ExitCode::SUCCESS
207            }
208            Err(failure) => {
209                if short {
210                    print_project_failure_short(&failure);
211                } else {
212                    print_project_failure(&failure);
213                }
214                ExitCode::FAILURE
215            }
216        }
217    } else {
218        let source = match std::fs::read_to_string(input) {
219            Ok(s) => s,
220            Err(e) => {
221                eprintln!("{prog}: could not read `{}`: {e}", input.display());
222                return ExitCode::FAILURE;
223            }
224        };
225        let filename = input.display().to_string();
226        match bynk_emit::compile_with_warnings(&source, &filename) {
227            Ok(compiled) => {
228                if !compiled.warnings.is_empty() {
229                    if short {
230                        bynk_render::print_errors_short(&compiled.warnings, &source, &filename);
231                    } else {
232                        bynk_render::print_errors(&compiled.warnings, &source, &filename);
233                    }
234                }
235                ExitCode::SUCCESS
236            }
237            Err(errors) => {
238                if short {
239                    bynk_render::print_errors_short(&errors, &source, &filename);
240                } else {
241                    bynk_render::print_errors(&errors, &source, &filename);
242                }
243                ExitCode::FAILURE
244            }
245        }
246    }
247}