Skip to main content

bynk_syntax/
error.rs

1//! Compiler diagnostics.
2//!
3//! Every error has a category (a dotted namespace string like
4//! `bynk.parse.expected_token`), a primary span, a primary message, and
5//! optionally some secondary labels and notes. Rendering goes through
6//! [`ariadne`] for source-pointing colour output.
7
8use ariadne::{Color, Config, Label, Report, ReportKind};
9
10use crate::span::Span;
11
12/// A compile error.
13#[derive(Debug, Clone)]
14pub struct CompileError {
15    pub category: &'static str,
16    pub span: Span,
17    pub message: String,
18    pub labels: Vec<(Span, String)>,
19    pub notes: Vec<String>,
20    /// v0.26 (ADR 0054): machine-applicable fixes, authored at the diagnosis
21    /// site — the only place the exact spans and replacement are known.
22    /// Consumed by the LSP (`codeAction`) and, later, a CLI `--fix`.
23    pub suggestions: Vec<Suggestion>,
24}
25
26/// A structured fix for the error it is attached to (v0.26, ADR 0054).
27///
28/// `edits` are span → replacement: an empty replacement deletes the span; an
29/// empty span inserts at its position. Spans are offsets into the same source
30/// text as the error's own span.
31#[derive(Debug, Clone)]
32pub struct Suggestion {
33    /// Human-facing action title, e.g. "remove `Clock` from the `given` clause".
34    pub message: String,
35    pub edits: Vec<(Span, String)>,
36    pub applicability: Applicability,
37}
38
39/// Whether a [`Suggestion`] can be applied without review (mirrors rustc;
40/// gates a future CLI `--fix` and the LSP's one-click apply).
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum Applicability {
43    /// The fix is exactly right — safe to apply mechanically.
44    MachineApplicable,
45    /// The fix contains placeholder text a human must complete; never
46    /// auto-applied.
47    HasPlaceholders,
48}
49
50impl CompileError {
51    pub fn new(category: &'static str, span: Span, message: impl Into<String>) -> Self {
52        Self {
53            category,
54            span,
55            message: message.into(),
56            labels: Vec::new(),
57            notes: Vec::new(),
58            suggestions: Vec::new(),
59        }
60    }
61
62    pub fn with_label(mut self, span: Span, label: impl Into<String>) -> Self {
63        self.labels.push((span, label.into()));
64        self
65    }
66
67    pub fn with_note(mut self, note: impl Into<String>) -> Self {
68        self.notes.push(note.into());
69        self
70    }
71
72    /// Attach a machine-applicable fix (v0.26). Mirrors [`Self::with_note`];
73    /// the suggestion is authored where the diagnostic is raised.
74    pub fn with_suggestion(
75        mut self,
76        message: impl Into<String>,
77        edits: Vec<(Span, String)>,
78        applicability: Applicability,
79    ) -> Self {
80        self.suggestions.push(Suggestion {
81            message: message.into(),
82            edits,
83            applicability,
84        });
85        self
86    }
87
88    /// Build an [`ariadne::Report`] for this error, anchored to the given
89    /// filename. Colour is on (for the CLI and human-facing test output).
90    pub fn report<'a>(
91        &'a self,
92        filename: &'a str,
93    ) -> Report<'a, (&'a str, std::ops::Range<usize>)> {
94        self.report_with_config(filename, Config::default())
95    }
96
97    /// Build a colourless [`ariadne::Report`], for transcripts committed to the
98    /// repo — no ANSI escape codes, so the output is byte-stable across machines.
99    pub fn report_plain<'a>(
100        &'a self,
101        filename: &'a str,
102    ) -> Report<'a, (&'a str, std::ops::Range<usize>)> {
103        self.report_with_config(filename, Config::default().with_color(false))
104    }
105
106    fn report_with_config<'a>(
107        &'a self,
108        filename: &'a str,
109        config: Config,
110    ) -> Report<'a, (&'a str, std::ops::Range<usize>)> {
111        let primary_span = (filename, self.span.range());
112        let mut builder = Report::build(ReportKind::Error, primary_span.clone())
113            .with_config(config)
114            .with_code(self.category)
115            .with_message(&self.message)
116            .with_label(
117                Label::new(primary_span)
118                    .with_message(&self.message)
119                    .with_color(Color::Red),
120            );
121
122        for (span, label) in &self.labels {
123            builder = builder.with_label(
124                Label::new((filename, span.range()))
125                    .with_message(label)
126                    .with_color(Color::Yellow),
127            );
128        }
129
130        for note in &self.notes {
131            builder = builder.with_note(note);
132        }
133
134        builder.finish()
135    }
136}