walker: diagnostics-severity model + input_verdict (ADR-0027 step A)
Adds `Severity` (Error / Warning, ordered so Error > Warning)
and `Diagnostic { severity, span, message }` in
`walker::outcome`, plus a `diagnostics` field on `WalkResult`
— the schema-aware findings layered on a structurally-valid
parse (ADR-0027 §2).
`input_verdict(source, schema)` is the validity-indicator
entry point: `None` when the input would run clean (and for
empty input), `Some(Error)` for a parse failure or unknown
command, `Some(Warning)` for the ADR-0026 expression flags.
The verdict is the highest severity across the parse outcome
and the diagnostics set.
`diagnostics` is empty at this step — the schema-existence
(ERROR) and expression (WARNING) passes that fill it land
next. Covered by `input_verdict` unit tests.
This commit is contained in:
@@ -29,6 +29,7 @@ use crate::dsl::walker::outcome::{
|
||||
|
||||
pub use context::ColumnInfo;
|
||||
pub use highlight::highlight_runs;
|
||||
pub use outcome::{Diagnostic, Severity};
|
||||
|
||||
/// Resolve the hint-panel mode at the end of `source`
|
||||
/// (ADR-0024 §HintMode-per-node, §Phase D §typed-value-slots).
|
||||
@@ -291,6 +292,44 @@ pub fn completion_probe(
|
||||
}
|
||||
}
|
||||
|
||||
/// The validity-indicator verdict for `source` (ADR-0027 §3).
|
||||
///
|
||||
/// `None` — the input would run clean (the indicator shows
|
||||
/// nothing); empty / whitespace-only input is also `None`.
|
||||
/// `Some(Error)` — pressing Enter now fails (a structural
|
||||
/// parse failure, or a schema-existence diagnostic).
|
||||
/// `Some(Warning)` — it runs, but is very likely not intended
|
||||
/// (the ADR-0026 expression flags).
|
||||
///
|
||||
/// The verdict is the highest severity across the parse
|
||||
/// outcome and the `diagnostics` set (ADR-0027 §2).
|
||||
#[must_use]
|
||||
pub fn input_verdict(
|
||||
source: &str,
|
||||
schema: Option<&crate::completion::SchemaCache>,
|
||||
) -> Option<outcome::Severity> {
|
||||
use outcome::Severity;
|
||||
if source.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut ctx = schema.map_or_else(
|
||||
context::WalkContext::new,
|
||||
context::WalkContext::with_schema,
|
||||
);
|
||||
let (result, _cmd) = walk(source, outcome::WalkBound::EndOfInput, &mut ctx);
|
||||
let Some(result) = result else {
|
||||
// The first token is not a registered command word —
|
||||
// typing this and pressing Enter fails.
|
||||
return Some(Severity::Error);
|
||||
};
|
||||
let outcome_severity = match result.outcome {
|
||||
outcome::WalkOutcome::Match { .. } => None,
|
||||
_ => Some(Severity::Error),
|
||||
};
|
||||
let diag_severity = result.diagnostics.iter().map(|d| d.severity).max();
|
||||
outcome_severity.into_iter().chain(diag_severity).max()
|
||||
}
|
||||
|
||||
/// What the grammar would accept at the end of `source`
|
||||
/// (ADR-0024 §architecture, Phase F walker-driven completion).
|
||||
///
|
||||
@@ -570,6 +609,9 @@ pub fn walk<'a>(
|
||||
matched_path: path,
|
||||
per_byte_class: per_byte,
|
||||
tail_expected,
|
||||
// Filled by the schema-existence and expression passes
|
||||
// (ADR-0027 §2); empty here at the structural layer.
|
||||
diagnostics: Vec::new(),
|
||||
};
|
||||
(Some(result), cmd)
|
||||
}
|
||||
@@ -1236,6 +1278,45 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
// ---- input_verdict (ADR-0027 §3) --------------------------
|
||||
|
||||
#[test]
|
||||
fn input_verdict_clean_command_is_none() {
|
||||
assert_eq!(super::input_verdict("quit", None), None);
|
||||
assert_eq!(super::input_verdict("show table Customers", None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_verdict_empty_input_is_none() {
|
||||
assert_eq!(super::input_verdict("", None), None);
|
||||
assert_eq!(super::input_verdict(" ", None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_verdict_incomplete_command_is_error() {
|
||||
assert_eq!(
|
||||
super::input_verdict("create table", None),
|
||||
Some(super::Severity::Error),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_verdict_unknown_command_is_error() {
|
||||
assert_eq!(
|
||||
super::input_verdict("frobnicate the gizmo", None),
|
||||
Some(super::Severity::Error),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_verdict_mismatched_token_is_error() {
|
||||
// `quit` takes no argument — trailing junk fails.
|
||||
assert_eq!(
|
||||
super::input_verdict("quit now", None),
|
||||
Some(super::Severity::Error),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walker_parses_insert_with_explicit_column_list() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -159,11 +159,52 @@ pub struct ByteClass {
|
||||
pub class: HighlightClass,
|
||||
}
|
||||
|
||||
/// Severity of a pre-submit [`Diagnostic`] (ADR-0027 §1).
|
||||
///
|
||||
/// Ordered so `Error > Warning` — taking the `max` across a
|
||||
/// diagnostic set gives the validity indicator's verdict.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Severity {
|
||||
/// Valid and runnable, but very likely not what the user
|
||||
/// intends — a type-mismatched comparison, `= NULL`
|
||||
/// (ADR-0026 §7).
|
||||
Warning,
|
||||
/// Known to fail if submitted now — a parse error, or a
|
||||
/// reference to a table / column that does not exist.
|
||||
Error,
|
||||
}
|
||||
|
||||
/// A problem the walker found in the input *before* submission
|
||||
/// (ADR-0027 §2). The severity drives the validity indicator;
|
||||
/// the span drives highlighting; the message is the hint text.
|
||||
///
|
||||
/// Note: the parse outcome (`Incomplete` / `Mismatch` /
|
||||
/// `ValidationFailed`) is *not* recorded as a `Diagnostic` —
|
||||
/// the indicator reads it from `WalkOutcome` directly
|
||||
/// (ADR-0027 §2, "the highest severity across the outcome and
|
||||
/// the diagnostics"). `diagnostics` carries the schema-aware
|
||||
/// findings layered on top of a structurally-valid parse.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Diagnostic {
|
||||
pub severity: Severity,
|
||||
/// Byte range in the source the diagnostic refers to.
|
||||
pub span: (usize, usize),
|
||||
/// Ready-to-show message — already catalog-translated by
|
||||
/// whichever pass produced the diagnostic.
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WalkResult {
|
||||
pub outcome: WalkOutcome,
|
||||
pub matched_path: MatchedPath,
|
||||
pub per_byte_class: Vec<ByteClass>,
|
||||
/// Schema-aware pre-submit findings layered on a
|
||||
/// structurally-valid parse (ADR-0027): unknown table /
|
||||
/// column references (ERROR), type-mismatched WHERE
|
||||
/// comparisons and `= NULL` (WARNING). Empty when the
|
||||
/// input does not parse — those are the outcome's job.
|
||||
pub diagnostics: Vec<Diagnostic>,
|
||||
/// Optional-Optional expectations the walker could have
|
||||
/// accepted but didn't because the outer shape ran out at a
|
||||
/// node boundary (ADR-0024 §architecture, round-5 follow-up).
|
||||
|
||||
Reference in New Issue
Block a user