command: Operand carries a source span
Each WHERE-expression Operand now records the byte span of the terminal it was built from — the precise per-literal highlight target for an expression WARNING (finishing ADR-0027 §2's highlight/hint wiring). parse_operand captures MatchedItem::span; the RowFilter::eq convenience constructor uses Operand::NO_SPAN. PartialEq is hand-written to ignore the span — it is editor metadata, so Command equality stays whitespace- and position-independent, which the Expr test corpus relies on. No behaviour change; 1100 tests still pass, clippy clean.
This commit is contained in:
+30
-15
@@ -520,22 +520,28 @@ impl<'a> ExprParser<'a> {
|
||||
}
|
||||
|
||||
/// `operand := literal | column_ref`.
|
||||
///
|
||||
/// Every operand records the byte `span` of the terminal it
|
||||
/// was built from — the precise highlight target for an
|
||||
/// expression WARNING (ADR-0027).
|
||||
fn parse_operand(&mut self) -> Result<Operand, ValidationError> {
|
||||
let item = self
|
||||
.advance()
|
||||
.ok_or_else(|| drift_error("expected an operand"))?;
|
||||
let span = item.span;
|
||||
let literal = |value: Value| Operand::Literal { value, span };
|
||||
match &item.kind {
|
||||
MatchedKind::Ident { role: "expr_column", .. } => {
|
||||
Ok(Operand::Column(item.text.clone()))
|
||||
Ok(Operand::Column { name: item.text.clone(), span })
|
||||
}
|
||||
MatchedKind::Word("null") => Ok(Operand::Literal(Value::Null)),
|
||||
MatchedKind::Word("true") => Ok(Operand::Literal(Value::Bool(true))),
|
||||
MatchedKind::Word("false") => Ok(Operand::Literal(Value::Bool(false))),
|
||||
MatchedKind::Word("null") => Ok(literal(Value::Null)),
|
||||
MatchedKind::Word("true") => Ok(literal(Value::Bool(true))),
|
||||
MatchedKind::Word("false") => Ok(literal(Value::Bool(false))),
|
||||
MatchedKind::NumberLit => {
|
||||
Ok(Operand::Literal(Value::Number(item.text.clone())))
|
||||
Ok(literal(Value::Number(item.text.clone())))
|
||||
}
|
||||
MatchedKind::StringLit => {
|
||||
Ok(Operand::Literal(Value::Text(item.text.clone())))
|
||||
Ok(literal(Value::Text(item.text.clone())))
|
||||
}
|
||||
_ => Err(drift_error("expected a column or literal operand")),
|
||||
}
|
||||
@@ -598,11 +604,24 @@ mod tests {
|
||||
}
|
||||
|
||||
fn col(name: &str) -> Operand {
|
||||
Operand::Column(name.to_string())
|
||||
Operand::Column {
|
||||
name: name.to_string(),
|
||||
span: Operand::NO_SPAN,
|
||||
}
|
||||
}
|
||||
|
||||
/// A literal operand with no source span — the span is
|
||||
/// ignored by `Operand`'s `PartialEq`, so test trees built
|
||||
/// with this compare equal to walked ones.
|
||||
fn lit(value: Value) -> Operand {
|
||||
Operand::Literal {
|
||||
value,
|
||||
span: Operand::NO_SPAN,
|
||||
}
|
||||
}
|
||||
|
||||
fn num(n: &str) -> Operand {
|
||||
Operand::Literal(Value::Number(n.to_string()))
|
||||
lit(Value::Number(n.to_string()))
|
||||
}
|
||||
|
||||
fn compare(left: Operand, op: CompareOp, right: Operand) -> Expr {
|
||||
@@ -644,16 +663,12 @@ mod tests {
|
||||
compare(
|
||||
col("Name"),
|
||||
CompareOp::Eq,
|
||||
Operand::Literal(Value::Text("Ada".to_string())),
|
||||
lit(Value::Text("Ada".to_string())),
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_expr("Active = true"),
|
||||
compare(
|
||||
col("Active"),
|
||||
CompareOp::Eq,
|
||||
Operand::Literal(Value::Bool(true)),
|
||||
),
|
||||
compare(col("Active"), CompareOp::Eq, lit(Value::Bool(true))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_expr("a = -7"),
|
||||
@@ -708,7 +723,7 @@ mod tests {
|
||||
parse_expr("Name like 'A%'"),
|
||||
Expr::Predicate(Predicate::Like {
|
||||
target: col("Name"),
|
||||
pattern: Operand::Literal(Value::Text("A%".to_string())),
|
||||
pattern: lit(Value::Text("A%".to_string())),
|
||||
negated: false,
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user