Skip to main content

bynk_render/
lib.rs

1//! Bynk's shared diagnostic-rendering layer.
2//!
3//! The presentation layer over [`bynk_syntax::CompileError`]: ariadne human
4//! output and the `short`/`json`-feeding line forms. Every renderer takes
5//! `&[CompileError]` + `source` + `filename` — it is agnostic about *where* the
6//! errors came from. Both CLI front-ends adopt it so they render identically
7//! (ADR 0100).
8//!
9//! **Invariant (ADR 0100):** this crate depends on `bynk-syntax` **only** (plus
10//! `ariadne`). It must never see `AttributedError`/`ProjectFailure` (which live
11//! in `bynk-emit`): the `AttributedError → CompileError` flattening stays *above*
12//! render, in the front-end, so there is no `render → emit` cycle. A function
13//! here taking a `ProjectFailure` would not even compile — the dependency isn't
14//! present, by design.
15//!
16//! Extracted from `bynkc` as slice 6 of the crate-decomposition track.
17
18use std::path::Path;
19
20use ariadne::Source;
21use bynk_syntax::error::Severity;
22use bynk_syntax::{CompileError, span};
23
24/// Render a list of compile errors to a string (for tests) using the given
25/// filename as the diagnostic source label.
26pub fn render_errors(errors: &[CompileError], source: &str, filename: &str) -> String {
27    let mut out = Vec::new();
28    let mut cache = (filename, Source::from(source));
29    for err in errors {
30        err.report(filename)
31            .write(&mut cache, &mut out)
32            .expect("write to Vec<u8> cannot fail");
33    }
34    String::from_utf8_lossy(&out).into_owned()
35}
36
37/// Render a list of compile errors to a string with colour disabled and the
38/// given filename as the source label. Unlike [`render_errors`], the output
39/// contains no ANSI escape codes, so it is byte-stable — suitable for the
40/// committed diagnostic transcripts under `site/src/diagnostics/`.
41pub fn render_errors_plain(errors: &[CompileError], source: &str, filename: &str) -> String {
42    let mut out = Vec::new();
43    let mut cache = (filename, Source::from(source));
44    for err in errors {
45        err.report_plain(filename)
46            .write(&mut cache, &mut out)
47            .expect("write to Vec<u8> cannot fail");
48    }
49    String::from_utf8_lossy(&out).into_owned()
50}
51
52/// Render to stderr with color, used by the CLI.
53pub fn print_errors(errors: &[CompileError], source: &str, filename: &str) {
54    let mut cache = (filename, Source::from(source));
55    for err in errors {
56        let _ = err.report(filename).eprint(&mut cache);
57    }
58}
59
60/// Render project-level errors as plain `[category] message` lines — the
61/// fallback for errors with no file attribution. Rich, source-context rendering
62/// lives in the front-end's project-failure renderer (v0.24).
63pub fn print_project_errors(root: &Path, errors: &[CompileError]) {
64    let _ = root;
65    for err in errors {
66        eprintln!("[{}] {}", err.category, err.message);
67        for note in &err.notes {
68            eprintln!("  note: {note}");
69        }
70    }
71}
72
73/// v0.38 (ADR 0071): one terse line per diagnostic for tooling consumers
74/// (`bynkc check --format short`):
75/// `path:line:col: <severity>[<category>]: <message>`. Line/column are
76/// 1-indexed, computed from the byte span against the source. The VS Code
77/// `bynkc` problem-matcher keys off this exact shape — keep it stable.
78pub fn print_errors_short(errors: &[CompileError], source: &str, filename: &str) {
79    eprint!("{}", render_errors_short(errors, source, filename));
80}
81
82/// The string form of [`print_errors_short`] — one `…[category]: message` line
83/// per error, each newline-terminated. The renderer behind the CLI's `--format
84/// short`, exposed for testing.
85pub fn render_errors_short(errors: &[CompileError], source: &str, filename: &str) -> String {
86    let mut out = String::new();
87    for err in errors {
88        out.push_str(&short_line(filename, source, err));
89        out.push('\n');
90    }
91    out
92}
93
94/// One terse `path:line:col: severity[category]: message` line for a single
95/// error against its source. The front-end's project-failure short renderer
96/// flattens an attributed error to `(label, text, error)` and calls this.
97pub fn short_line(filename: &str, source: &str, err: &CompileError) -> String {
98    let (line, col) = span::line_col(source, err.span.start);
99    format!(
100        "{filename}:{line}:{col}: {}[{}]: {}",
101        severity_word(err),
102        err.category,
103        err.message
104    )
105}
106
107/// `"error"` / `"warning"` for an error's [`Severity`].
108pub fn severity_word(err: &CompileError) -> &'static str {
109    match Severity::for_error(err) {
110        Severity::Error => "error",
111        Severity::Warning => "warning",
112    }
113}
114
115/// Render a list of compile errors as plain `[category] message` lines (with
116/// notes), for test assertion.
117pub fn render_project_errors(errors: &[CompileError]) -> String {
118    let mut out = String::new();
119    for err in errors {
120        out.push_str(&format!("[{}] {}\n", err.category, err.message));
121        for note in &err.notes {
122            out.push_str(&format!("  note: {note}\n"));
123        }
124    }
125    out
126}