parser: structural error rendering, source echo, and caret pointer
The old humanise() fell back to chumsky's terse Display for non-
custom errors and appended "(near `X`)", which on top of an
already-cryptic "found 'i' expected ':'" turned the message into
a puzzle. Now humanise() reads the structured RichReason, lists
expected RichPatterns in plain prose, and prefixes the message
with the consumed context.
Before: parse error: found 'i' expected ':' (near `i`)
After: parse error: after `change column Rich`, expected `:`,
found `i`
dispatch_dsl additionally echoes the source line on parse failure
(matching the success path's "running: ...") and prints a `^` caret
under the failure position, so the user can see what got submitted
and where the parser broke without re-reading from scratch.
Known limit: keyword_ci's custom-error mismatches don't aggregate
across choice alternatives, so messages like "expected DATA or
TABLE" (bison-equivalent) aren't yet possible. That's a structural
fix to the keyword matcher, deferred to a future parser-affordances
ADR.
Tests: +2 structural-error regression tests.
This commit is contained in:
+28
@@ -717,6 +717,22 @@ impl App {
|
||||
}
|
||||
Err(ParseError::Empty) => Vec::new(),
|
||||
Err(err) => {
|
||||
// Echo the source line so the user can see what
|
||||
// got submitted (and copy-paste it back to fix).
|
||||
self.push_output(OutputLine {
|
||||
text: format!("running: {input}"),
|
||||
kind: OutputKind::Echo,
|
||||
mode_at_submission: submission_mode,
|
||||
});
|
||||
// Caret pointer at the failure position, when we
|
||||
// have one. Aligned to the "running: " prefix so
|
||||
// the caret sits under the offending character.
|
||||
if let ParseError::Invalid { position, .. } = &err {
|
||||
let prefix = "running: ";
|
||||
let trimmed_offset = leading_trim_offset(input);
|
||||
let pad = prefix.chars().count() + trimmed_offset + position;
|
||||
self.note_error(format!("{}^", " ".repeat(pad)));
|
||||
}
|
||||
self.note_error(format!("parse error: {}", parse_error_message(&err)));
|
||||
Vec::new()
|
||||
}
|
||||
@@ -820,6 +836,9 @@ impl App {
|
||||
// Wrap the command portion in quotes so the message
|
||||
// reads cleanly: "...failed: <reason>" rather than the
|
||||
// command running into "failed: ..." with no break.
|
||||
// `note_error` splits on newlines internally — refusal
|
||||
// diagnostics from `change column …` (ADR-0017 §7) flow
|
||||
// through as a multi-line bordered table.
|
||||
self.note_error(format!(
|
||||
"\"{} {}\" failed: {error}",
|
||||
command.verb(),
|
||||
@@ -1154,6 +1173,7 @@ impl App {
|
||||
" drop column [from] [table] <T>: <col>",
|
||||
" rename column [in] [table] <T>: <old> to <new>",
|
||||
" change column [in] [table] <T>: <col> (<newtype>)",
|
||||
" [--force-conversion | --dont-convert]",
|
||||
" add 1:n relationship [as <name>] from <P>.<col> to <C>.<col>",
|
||||
" [on delete <action>] [on update <action>] [--create-fk]",
|
||||
" drop relationship <name>",
|
||||
@@ -1251,6 +1271,14 @@ fn parse_error_message(err: &ParseError) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of leading whitespace characters in `s`. The parser
|
||||
/// trims its input before parsing, so a position returned by the
|
||||
/// parser is relative to the trimmed string. The caret needs the
|
||||
/// pre-trim offset to align under the user's literal input.
|
||||
fn leading_trim_offset(s: &str) -> usize {
|
||||
s.chars().take_while(|c| c.is_whitespace()).count()
|
||||
}
|
||||
|
||||
fn render_cascade_effect(effect: &CascadeEffect) -> String {
|
||||
use crate::dsl::ReferentialAction;
|
||||
let what = match effect.action {
|
||||
|
||||
Reference in New Issue
Block a user