1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
use std::ops::Range;
use std::sync::Arc;
use ariadne::{Color, Label, Report, ReportKind, Source};
use cratestack_core::SourceSpan;
/// A schema error, identified by which file it came from (cratestack#916).
///
/// `file`/`source_text` start out empty: every constructor below (`new`,
/// `span_error`) fires deep inside parsing/validation, dozens of call sites
/// that have no path in scope and share one property — every error a single
/// parse produces always belongs to the one file that parse was given.
/// Rather than thread a path through every one of those call sites for no
/// behavioral gain, [`SchemaError::with_file`] is applied exactly once, at
/// the boundary where a path *is* known (`parse_schema_named`,
/// `parse_schema_diagnostics`, `parse_schema_file`, `parse_schema_unvalidated`
/// in `entry.rs`). Entry points that take no path tag the error with
/// [`crate::ANONYMOUS_SCHEMA`] and the real source, so a rendered diagnostic
/// always has a code frame — but only the `*_named`/`*_file` paths can name
/// the actual file. Prefer those.
#[derive(Clone, thiserror::Error)]
#[error("{message}")]
pub struct SchemaError {
message: String,
span: Range<usize>,
line: usize,
file: Arc<str>,
source_text: Arc<str>,
}
impl std::fmt::Debug for SchemaError {
/// Deliberately omits `source_text`. A derived `Debug` prints the entire
/// schema file, so every `.unwrap()` panic on a `Result<_, SchemaError>`
/// and every `{:?}` log line would dump the whole `.cstack` — a silent
/// regression with no compile error to catch it. The byte length is
/// enough to tell "source attached" from "source missing".
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SchemaError")
.field("message", &self.message)
.field("span", &self.span)
.field("line", &self.line)
.field("file", &self.file)
.field("source_text_len", &self.source_text.len())
.finish()
}
}
impl SchemaError {
pub(crate) fn new(message: impl Into<String>, span: Range<usize>, line: usize) -> Self {
Self {
message: message.into(),
span,
line,
file: Arc::from(""),
source_text: Arc::from(""),
}
}
/// Attach this error's file identity and that file's source text.
///
/// `source` is an `Arc<str>` the caller already holds (not a fresh
/// `String`) so tagging every error collected from one parse — several,
/// with [`crate::parse_schema_diagnostics`] — is a refcount bump each,
/// not a copy of the whole schema per error.
pub(crate) fn with_file(mut self, file: &Arc<str>, source: &Arc<str>) -> Self {
self.file = Arc::clone(file);
self.source_text = Arc::clone(source);
self
}
pub fn message(&self) -> &str {
&self.message
}
pub fn span(&self) -> Range<usize> {
self.span.clone()
}
pub fn line(&self) -> usize {
self.line
}
/// The file this error belongs to. Errors from the `*_named`/`*_file`
/// entry points carry the real path; those from the path-less entry
/// points carry [`crate::ANONYMOUS_SCHEMA`]. Empty only for an error
/// constructed internally and never passed through [`Self::with_file`],
/// which no public entry point returns.
pub fn file(&self) -> &str {
&self.file
}
/// Render this error as a human-readable diagnostic.
///
/// Takes no arguments: the error already knows both its file (`self.file`)
/// and that file's source (`self.source_text`), attached once at the
/// parse/diagnostics boundary. Before cratestack#916 this took a `(path,
/// source)` pair supplied by the caller, which had to happen to match the
/// file the error actually came from — nothing enforced that once more
/// than one file was involved.
pub fn render(&self) -> String {
let mut output = Vec::new();
let file = self.file.to_string();
Report::build(ReportKind::Error, (file.clone(), self.span.clone()))
.with_message(&self.message)
.with_label(
Label::new((file.clone(), self.span.clone()))
.with_message(&self.message)
.with_color(Color::Red),
)
.finish()
.write((file, Source::from(self.source_text.as_ref())), &mut output)
.expect("diagnostic rendering should succeed");
String::from_utf8(output).expect("ariadne should emit utf-8")
}
}
pub(crate) fn span_error(message: impl Into<String>, span: SourceSpan) -> SchemaError {
SchemaError::new(message, span.start..span.end, span.line)
}