use thiserror::Error;
use sqry_core::graph::unified::bind::scope::arena::ScopeKind;
use sqry_core::graph::unified::edge::kind::{EdgeKind, ExportKind};
use sqry_core::graph::unified::node::kind::NodeKind;
use sqry_core::schema::Visibility;
use super::compile::{BuildError, QueryBuilder, ScanFilters};
use super::ir::{
Direction, PathPattern, PlanNode, Predicate, PredicateValue, QueryPlan, RegexFlags,
RegexPattern, StringPattern,
};
pub fn parse_query(source: &str) -> Result<QueryPlan, ParseError> {
let mut parser = Parser::new(source);
let builder = parser.parse_chain()?;
parser.expect_eof()?;
builder.build().map_err(ParseError::from)
}
#[derive(Debug, Error, PartialEq, Eq, Clone)]
pub enum ParseError {
#[error("unexpected end of input at byte {offset}: expected {expected}")]
UnexpectedEnd {
offset: usize,
expected: &'static str,
},
#[error("unexpected character {ch:?} at byte {offset}: expected {expected}")]
UnexpectedChar {
ch: char,
offset: usize,
expected: &'static str,
},
#[error("unknown {kind} {value:?} at byte {offset}")]
UnknownIdent {
kind: &'static str,
value: String,
offset: usize,
},
#[error("invalid integer {value:?} at byte {offset}")]
InvalidInteger {
value: String,
offset: usize,
},
#[error("plan construction failed: {0}")]
Build(#[from] BuildError),
}
struct Parser<'a> {
src: &'a [u8],
pos: usize,
}
impl<'a> Parser<'a> {
fn new(source: &'a str) -> Self {
Self {
src: source.as_bytes(),
pos: 0,
}
}
fn parse_chain(&mut self) -> Result<QueryBuilder, ParseError> {
let mut builder = QueryBuilder::new();
self.skip_ws();
while !self.at_end() && !self.peek_is(b')') {
builder = self.parse_step(builder)?;
self.skip_ws();
}
Ok(builder)
}
fn parse_step(&mut self, builder: QueryBuilder) -> Result<QueryBuilder, ParseError> {
let start = self.pos;
let head = self.take_ident()?;
match head.as_str() {
"kind" => {
self.expect_byte(b':', "':' after 'kind'")?;
let ident = self.take_ident()?;
let offset = start;
let nk = NodeKind::parse(&ident).ok_or(ParseError::UnknownIdent {
kind: "node kind",
value: ident,
offset,
})?;
Ok(builder.scan(nk))
}
"visibility" => {
self.expect_byte(b':', "':' after 'visibility'")?;
let ident = self.take_ident()?;
let vis = Visibility::parse(&ident).ok_or(ParseError::UnknownIdent {
kind: "visibility",
value: ident,
offset: start,
})?;
Ok(apply_visibility(builder, vis))
}
"name" => {
self.expect_byte(b':', "':' after 'name'")?;
let pat = self.parse_string_pattern()?;
Ok(apply_name_pattern(builder, pat))
}
"returns" => {
self.expect_byte(b':', "':' after 'returns'")?;
let type_name = self.parse_bare_or_quoted()?;
Ok(builder.filter(Predicate::Returns(type_name)))
}
"in" => {
self.expect_byte(b':', "':' after 'in'")?;
let glob = self.parse_bare_or_quoted()?;
Ok(builder.filter(Predicate::InFile(PathPattern::new(glob))))
}
"scope" => {
self.expect_byte(b':', "':' after 'scope'")?;
let ident = self.take_ident()?;
let sk = parse_scope_kind(&ident).ok_or(ParseError::UnknownIdent {
kind: "scope kind",
value: ident,
offset: start,
})?;
Ok(builder.filter(Predicate::InScope(sk)))
}
"has" => {
self.expect_byte(b':', "':' after 'has'")?;
let ident = self.take_ident()?;
match ident.as_str() {
"caller" => Ok(builder.filter(Predicate::HasCaller)),
"callee" => Ok(builder.filter(Predicate::HasCallee)),
_ => Err(ParseError::UnknownIdent {
kind: "has-target (expected 'caller' or 'callee')",
value: ident,
offset: start,
}),
}
}
"unused" => Ok(builder.filter(Predicate::IsUnused)),
"traverse" => {
self.expect_byte(b':', "':' after 'traverse'")?;
let (direction, edge_kind, depth) = self.parse_traverse_args()?;
Ok(builder.traverse(direction, edge_kind, depth))
}
"callers" | "callees" | "imports" | "exports" | "implements" | "impl" => {
self.expect_byte(b':', "':' after relation predicate")?;
let value = self.parse_value()?;
let predicate = match head.as_str() {
"callers" => Predicate::Callers(value),
"callees" => Predicate::Callees(value),
"imports" => Predicate::Imports(value),
"exports" => Predicate::Exports(value),
"implements" | "impl" => Predicate::Implements(value),
_ => unreachable!("outer match covers every arm"),
};
Ok(builder.filter(predicate))
}
"references" => {
self.skip_ws();
if self.eat_bytes(b"~=") {
self.skip_ws();
let regex = self.parse_regex_literal()?;
Ok(builder.filter(Predicate::References(PredicateValue::Regex(regex))))
} else {
self.expect_byte(b':', "':' or '~=' after 'references'")?;
let value = self.parse_value()?;
Ok(builder.filter(Predicate::References(value)))
}
}
_ => Err(ParseError::UnknownIdent {
kind: "step keyword",
value: head,
offset: start,
}),
}
}
fn parse_value(&mut self) -> Result<PredicateValue, ParseError> {
self.skip_inline_ws();
if self.peek_is(b'(') {
self.pos += 1;
let sub_builder = self.parse_chain()?;
self.expect_byte(b')', "')' to close subquery")?;
let sub_plan = sub_builder.build().map_err(ParseError::from)?;
Ok(PredicateValue::Subquery(Box::new(sub_plan.root)))
} else if self.peek_is(b'/') {
let regex = self.parse_regex_literal()?;
Ok(PredicateValue::Regex(regex))
} else {
let pat = self.parse_string_pattern()?;
Ok(PredicateValue::Pattern(pat))
}
}
fn parse_string_pattern(&mut self) -> Result<StringPattern, ParseError> {
let raw = self.parse_bare_or_quoted()?;
let has_glob_meta = raw.contains(['*', '?', '[']);
let pattern = if has_glob_meta {
StringPattern::glob(raw)
} else {
StringPattern::exact(raw)
};
Ok(pattern)
}
fn parse_bare_or_quoted(&mut self) -> Result<String, ParseError> {
self.skip_inline_ws();
if self.peek_is(b'"') {
self.take_quoted_string()
} else {
let start = self.pos;
let tok = self.take_value_word()?;
if tok.is_empty() {
Err(ParseError::UnexpectedChar {
ch: self.peek_char().unwrap_or('\0'),
offset: start,
expected: "value (quoted string or bare word)",
})
} else {
Ok(tok)
}
}
}
fn parse_regex_literal(&mut self) -> Result<RegexPattern, ParseError> {
self.expect_byte(b'/', "'/' to open regex literal")?;
let start = self.pos;
while !self.at_end() && !self.peek_is(b'/') {
if self.peek_is(b'\\') && self.pos + 1 < self.src.len() {
self.pos += 2;
} else {
self.pos += 1;
}
}
if self.at_end() {
return Err(ParseError::UnexpectedEnd {
offset: self.pos,
expected: "'/' to close regex literal",
});
}
let body_bytes = &self.src[start..self.pos];
let body = std::str::from_utf8(body_bytes)
.map_err(|_| ParseError::UnexpectedChar {
ch: '\u{FFFD}',
offset: start,
expected: "valid UTF-8 in regex body",
})?
.to_owned();
self.pos += 1;
let mut flags = RegexFlags::default();
while let Some(b) = self.peek_byte() {
match b {
b'i' => {
flags.case_insensitive = true;
self.pos += 1;
}
b'm' => {
flags.multiline = true;
self.pos += 1;
}
b's' => {
flags.dot_all = true;
self.pos += 1;
}
_ => break,
}
}
Ok(RegexPattern::with_flags(body, flags))
}
fn parse_traverse_args(&mut self) -> Result<(Direction, EdgeKind, u32), ParseError> {
let dir_start = self.pos;
let dir_text = self.take_ident()?;
let direction = parse_direction(&dir_text).ok_or(ParseError::UnknownIdent {
kind: "traversal direction",
value: dir_text,
offset: dir_start,
})?;
self.expect_byte(b'(', "'(' after traversal direction")?;
self.skip_inline_ws();
let edge_start = self.pos;
let edge_text = self.take_ident()?;
let edge_kind = parse_edge_kind(&edge_text).ok_or(ParseError::UnknownIdent {
kind: "edge kind",
value: edge_text,
offset: edge_start,
})?;
self.skip_inline_ws();
self.expect_byte(b',', "',' between edge kind and depth")?;
self.skip_inline_ws();
let depth_start = self.pos;
let depth_text = self.take_digits()?;
let depth: u32 = depth_text.parse().map_err(|_| ParseError::InvalidInteger {
value: depth_text,
offset: depth_start,
})?;
self.skip_inline_ws();
self.expect_byte(b')', "')' to close traversal arguments")?;
Ok((direction, edge_kind, depth))
}
#[inline]
fn at_end(&self) -> bool {
self.pos >= self.src.len()
}
#[inline]
fn peek_byte(&self) -> Option<u8> {
self.src.get(self.pos).copied()
}
#[inline]
fn peek_is(&self, b: u8) -> bool {
self.peek_byte() == Some(b)
}
fn peek_char(&self) -> Option<char> {
self.src[self.pos..]
.utf8_chunks()
.next()
.and_then(|chunk| chunk.valid().chars().next())
}
fn eat_bytes(&mut self, needle: &[u8]) -> bool {
if self.src[self.pos..].starts_with(needle) {
self.pos += needle.len();
true
} else {
false
}
}
fn skip_ws(&mut self) {
while let Some(b) = self.peek_byte() {
if b.is_ascii_whitespace() {
self.pos += 1;
} else {
break;
}
}
}
fn skip_inline_ws(&mut self) {
while let Some(b) = self.peek_byte() {
if b == b' ' || b == b'\t' {
self.pos += 1;
} else {
break;
}
}
}
fn expect_byte(&mut self, byte: u8, expected: &'static str) -> Result<(), ParseError> {
self.skip_inline_ws();
match self.peek_byte() {
Some(b) if b == byte => {
self.pos += 1;
Ok(())
}
Some(_) => Err(ParseError::UnexpectedChar {
ch: self.peek_char().unwrap_or('\0'),
offset: self.pos,
expected,
}),
None => Err(ParseError::UnexpectedEnd {
offset: self.pos,
expected,
}),
}
}
fn expect_eof(&mut self) -> Result<(), ParseError> {
self.skip_ws();
if self.at_end() {
Ok(())
} else {
Err(ParseError::UnexpectedChar {
ch: self.peek_char().unwrap_or('\0'),
offset: self.pos,
expected: "end of query",
})
}
}
fn take_ident(&mut self) -> Result<String, ParseError> {
let start = self.pos;
while let Some(b) = self.peek_byte() {
let is_start = (start == self.pos) && (b.is_ascii_alphabetic() || b == b'_');
let is_continue = start != self.pos && (b.is_ascii_alphanumeric() || b == b'_');
if is_start || is_continue {
self.pos += 1;
} else {
break;
}
}
if self.pos == start {
return Err(ParseError::UnexpectedChar {
ch: self.peek_char().unwrap_or('\0'),
offset: self.pos,
expected: "identifier",
});
}
let slice = &self.src[start..self.pos];
let s = std::str::from_utf8(slice)
.expect("identifier is ASCII")
.to_ascii_lowercase();
Ok(s)
}
fn take_digits(&mut self) -> Result<String, ParseError> {
let start = self.pos;
while let Some(b) = self.peek_byte() {
if b.is_ascii_digit() {
self.pos += 1;
} else {
break;
}
}
if self.pos == start {
return Err(ParseError::UnexpectedChar {
ch: self.peek_char().unwrap_or('\0'),
offset: self.pos,
expected: "integer",
});
}
Ok(std::str::from_utf8(&self.src[start..self.pos])
.expect("digits are ASCII")
.to_owned())
}
fn take_value_word(&mut self) -> Result<String, ParseError> {
let start = self.pos;
while let Some(b) = self.peek_byte() {
if b.is_ascii_whitespace() || matches!(b, b')' | b'(') {
break;
}
self.pos += 1;
}
let slice = &self.src[start..self.pos];
std::str::from_utf8(slice)
.map(str::to_owned)
.map_err(|_| ParseError::UnexpectedChar {
ch: '\u{FFFD}',
offset: start,
expected: "valid UTF-8 in value",
})
}
fn take_quoted_string(&mut self) -> Result<String, ParseError> {
self.expect_byte(b'"', "'\"' to open quoted string")?;
let mut out = String::new();
loop {
match self.peek_byte() {
None => {
return Err(ParseError::UnexpectedEnd {
offset: self.pos,
expected: "'\"' to close quoted string",
});
}
Some(b'"') => {
self.pos += 1;
return Ok(out);
}
Some(b'\\') => {
if let Some(&next) = self.src.get(self.pos + 1) {
self.pos += 2;
match next {
b'\\' => out.push('\\'),
b'"' => out.push('"'),
b'n' => out.push('\n'),
b't' => out.push('\t'),
other => out.push(other as char),
}
} else {
return Err(ParseError::UnexpectedEnd {
offset: self.pos + 1,
expected: "escape character after '\\'",
});
}
}
Some(_) => {
let tail = &self.src[self.pos..];
let chunk = tail
.utf8_chunks()
.next()
.expect("non-empty tail yields a chunk");
if let Some(ch) = chunk.valid().chars().next() {
out.push(ch);
self.pos += ch.len_utf8();
} else {
return Err(ParseError::UnexpectedChar {
ch: '\u{FFFD}',
offset: self.pos,
expected: "valid UTF-8 inside quoted string",
});
}
}
}
}
}
}
fn apply_visibility(builder: QueryBuilder, visibility: Visibility) -> QueryBuilder {
let steps = builder_steps(&builder);
if let Some(existing) = steps.last()
&& let PlanNode::NodeScan {
kind,
visibility: existing_vis,
name_pattern,
} = existing
{
let kind = *kind;
let vis = existing_vis.unwrap_or(visibility);
let name_pattern = name_pattern.clone();
let mut trimmed = strip_last_step(builder);
trimmed = trimmed.scan_with(
ScanFilters::new()
.merge_kind(kind)
.with_visibility(vis)
.merge_name(name_pattern),
);
return trimmed;
}
builder.scan_with(ScanFilters::new().with_visibility(visibility))
}
fn apply_name_pattern(builder: QueryBuilder, pattern: StringPattern) -> QueryBuilder {
let steps = builder_steps(&builder);
if steps.is_empty() {
return builder.scan_with(ScanFilters {
kind: None,
visibility: None,
name_pattern: Some(pattern),
});
}
if let Some(existing) = steps.last()
&& let PlanNode::NodeScan {
kind,
visibility,
name_pattern: existing_name,
} = existing
&& existing_name.is_none()
{
let kind = *kind;
let vis = *visibility;
let mut trimmed = strip_last_step(builder);
trimmed = trimmed.scan_with(ScanFilters {
kind,
visibility: vis,
name_pattern: Some(pattern),
});
return trimmed;
}
builder.filter(Predicate::MatchesName(pattern))
}
fn builder_steps(builder: &QueryBuilder) -> Vec<PlanNode> {
if builder.is_empty() {
return Vec::new();
}
let cloned = builder.clone();
match cloned.build() {
Ok(plan) => match plan.root {
PlanNode::Chain { steps } => steps,
other => vec![other],
},
Err(_) => Vec::new(),
}
}
fn strip_last_step(builder: QueryBuilder) -> QueryBuilder {
let steps = builder_steps(&builder);
let mut out = QueryBuilder::new();
if steps.len() <= 1 {
return out;
}
out = rehydrate_from_steps(&steps[..steps.len() - 1]);
out
}
fn rehydrate_from_steps(steps: &[PlanNode]) -> QueryBuilder {
let mut b = QueryBuilder::new();
for step in steps {
match step {
PlanNode::NodeScan {
kind,
visibility,
name_pattern,
} => {
b = b.scan_with(ScanFilters {
kind: *kind,
visibility: *visibility,
name_pattern: name_pattern.clone(),
});
}
PlanNode::EdgeTraversal {
direction,
edge_kind,
max_depth,
} => match edge_kind {
Some(k) => {
b = b.traverse(*direction, k.clone(), *max_depth);
}
None => {
b = b.traverse_any(*direction, *max_depth);
}
},
PlanNode::Filter { predicate } => {
b = b.filter(predicate.clone());
}
PlanNode::SetOp { .. } | PlanNode::Chain { .. } => {
b = b.filter(Predicate::HasCaller);
}
}
}
b
}
trait ScanFiltersExt {
fn merge_kind(self, kind: Option<NodeKind>) -> Self;
fn merge_name(self, pattern: Option<StringPattern>) -> Self;
}
impl ScanFiltersExt for ScanFilters {
fn merge_kind(mut self, kind: Option<NodeKind>) -> Self {
if let Some(k) = kind {
self.kind = Some(k);
}
self
}
fn merge_name(mut self, pattern: Option<StringPattern>) -> Self {
if let Some(p) = pattern {
self.name_pattern = Some(p);
}
self
}
}
fn parse_direction(text: &str) -> Option<Direction> {
match text {
"forward" | "outgoing" | "out" => Some(Direction::Forward),
"reverse" | "incoming" | "in" => Some(Direction::Reverse),
"both" => Some(Direction::Both),
_ => None,
}
}
fn parse_scope_kind(text: &str) -> Option<ScopeKind> {
match text {
"module" => Some(ScopeKind::Module),
"function" => Some(ScopeKind::Function),
"class" => Some(ScopeKind::Class),
"namespace" => Some(ScopeKind::Namespace),
"trait" => Some(ScopeKind::Trait),
"impl" => Some(ScopeKind::Impl),
_ => None,
}
}
fn parse_edge_kind(text: &str) -> Option<EdgeKind> {
match text {
"calls" => Some(EdgeKind::Calls {
argument_count: 0,
is_async: false,
}),
"references" => Some(EdgeKind::References),
"imports" => Some(EdgeKind::Imports {
alias: None,
is_wildcard: false,
}),
"exports" => Some(EdgeKind::Exports {
kind: ExportKind::Direct,
alias: None,
}),
"implements" => Some(EdgeKind::Implements),
"inherits" => Some(EdgeKind::Inherits),
"defines" => Some(EdgeKind::Defines),
"contains" => Some(EdgeKind::Contains),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_kind_scan_produces_single_nodescan_step() {
let plan = parse_query("kind:function").expect("parse");
let PlanNode::Chain { steps } = plan.root else {
panic!("expected Chain root");
};
assert_eq!(steps.len(), 1);
assert!(matches!(
steps[0],
PlanNode::NodeScan {
kind: Some(NodeKind::Function),
..
}
));
}
#[test]
fn parse_has_caller_is_a_filter_step() {
let plan = parse_query("kind:function has:caller").expect("parse");
let PlanNode::Chain { steps } = plan.root else {
panic!("chain");
};
assert_eq!(steps.len(), 2);
assert!(matches!(
steps[1],
PlanNode::Filter {
predicate: Predicate::HasCaller,
}
));
}
#[test]
fn parse_traverse_accepts_all_three_directions() {
for (text, expected) in [
("forward", Direction::Forward),
("reverse", Direction::Reverse),
("both", Direction::Both),
] {
let src = format!("kind:function traverse:{text}(calls,1)");
let plan = parse_query(&src).expect("parse");
let PlanNode::Chain { steps } = plan.root else {
panic!("chain");
};
match &steps[1] {
PlanNode::EdgeTraversal {
direction,
max_depth,
..
} => {
assert_eq!(*direction, expected);
assert_eq!(*max_depth, 1);
}
other => panic!("expected EdgeTraversal, got {other:?}"),
}
}
}
#[test]
fn parse_unknown_ident_produces_unknown_error() {
let err = parse_query("kind:definitely_not_a_kind").unwrap_err();
match err {
ParseError::UnknownIdent { kind, .. } => assert_eq!(kind, "node kind"),
other => panic!("expected UnknownIdent, got {other:?}"),
}
}
#[test]
fn parse_regex_literal_with_flags() {
let plan = parse_query("kind:function references ~= /handle_.*/im").expect("parse");
let PlanNode::Chain { steps } = plan.root else {
panic!("chain");
};
match &steps[1] {
PlanNode::Filter {
predicate: Predicate::References(PredicateValue::Regex(rp)),
} => {
assert_eq!(rp.pattern, "handle_.*");
assert!(rp.flags.case_insensitive);
assert!(rp.flags.multiline);
assert!(!rp.flags.dot_all);
}
other => panic!("expected References(Regex), got {other:?}"),
}
}
#[test]
fn parse_subquery_value_produces_plan_node() {
let plan = parse_query("kind:function callers:(kind:method)").expect("parse");
let PlanNode::Chain { steps } = plan.root else {
panic!("chain");
};
match &steps[1] {
PlanNode::Filter {
predicate: Predicate::Callers(PredicateValue::Subquery(inner)),
} => match inner.as_ref() {
PlanNode::Chain { steps: sub_steps } => {
assert!(matches!(
sub_steps[0],
PlanNode::NodeScan {
kind: Some(NodeKind::Method),
..
}
));
}
other => panic!("expected Chain subquery, got {other:?}"),
},
other => panic!("expected Callers(Subquery), got {other:?}"),
}
}
#[test]
fn parse_glob_name_pattern_folds_into_scan() {
let plan = parse_query("kind:function name:parse_*").expect("parse");
let PlanNode::Chain { steps } = plan.root else {
panic!("chain");
};
assert_eq!(steps.len(), 1);
match &steps[0] {
PlanNode::NodeScan {
kind: Some(NodeKind::Function),
name_pattern: Some(pat),
..
} => {
assert_eq!(pat.raw, "parse_*");
}
other => panic!("expected folded NodeScan, got {other:?}"),
}
}
#[test]
fn parse_implements_and_impl_aliases_both_work() {
for src in ["kind:class implements:Visitor", "kind:class impl:Visitor"] {
let plan = parse_query(src).expect("parse");
let PlanNode::Chain { steps } = plan.root else {
panic!("chain");
};
assert!(matches!(
steps[1],
PlanNode::Filter {
predicate: Predicate::Implements(_),
}
));
}
}
#[test]
fn parse_unused_alone_is_a_filter() {
let plan = parse_query("kind:function unused").expect("parse");
let PlanNode::Chain { steps } = plan.root else {
panic!("chain");
};
assert_eq!(steps.len(), 2);
assert!(matches!(
steps[1],
PlanNode::Filter {
predicate: Predicate::IsUnused,
}
));
}
#[test]
fn parse_empty_query_errors_on_build() {
let err = parse_query("").unwrap_err();
assert!(matches!(err, ParseError::Build(_)));
}
#[test]
fn parse_returns_predicate_basic() {
let plan = parse_query("kind:function returns:error").expect("parse");
let PlanNode::Chain { steps } = plan.root else {
panic!("chain");
};
assert_eq!(steps.len(), 2);
match &steps[1] {
PlanNode::Filter {
predicate: Predicate::Returns(name),
} => {
assert_eq!(name, "error");
}
other => panic!("expected Filter(Returns), got {other:?}"),
}
}
#[test]
fn parse_returns_does_not_collide_with_name_predicate() {
let plan = parse_query("kind:function name:Foo returns:Bar").expect("parse");
let PlanNode::Chain { steps } = plan.root else {
panic!("chain");
};
assert_eq!(steps.len(), 2);
match &steps[0] {
PlanNode::NodeScan {
kind: Some(NodeKind::Function),
name_pattern: Some(pat),
..
} => {
assert_eq!(pat.raw, "Foo");
}
other => panic!("expected leading NodeScan with name_pattern, got {other:?}"),
}
match &steps[1] {
PlanNode::Filter {
predicate: Predicate::Returns(name),
} => {
assert_eq!(name, "Bar");
}
other => panic!("expected Filter(Returns), got {other:?}"),
}
}
#[test]
fn parse_returns_takes_value_byte_exact_no_glob_promotion() {
let plan = parse_query("kind:function returns:Result*").expect("parse");
let PlanNode::Chain { steps } = plan.root else {
panic!("chain");
};
match &steps[1] {
PlanNode::Filter {
predicate: Predicate::Returns(name),
} => {
assert_eq!(name, "Result*");
}
other => panic!("expected Filter(Returns), got {other:?}"),
}
}
#[test]
fn parse_returns_quoted_string_value() {
let plan = parse_query(r#"kind:function returns:"std::io::Error""#).expect("parse");
let PlanNode::Chain { steps } = plan.root else {
panic!("chain");
};
match &steps[1] {
PlanNode::Filter {
predicate: Predicate::Returns(name),
} => {
assert_eq!(name, "std::io::Error");
}
other => panic!("expected Filter(Returns), got {other:?}"),
}
}
#[test]
fn parse_returns_missing_value_is_an_error() {
let err = parse_query("kind:function returns:").unwrap_err();
assert!(matches!(
err,
ParseError::UnexpectedChar { .. } | ParseError::UnexpectedEnd { .. }
));
}
#[test]
fn parse_integer_rejects_non_digit() {
let err = parse_query("kind:function traverse:forward(calls,abc)").unwrap_err();
match err {
ParseError::UnexpectedChar { expected, .. } => {
assert_eq!(expected, "integer");
}
other => panic!("expected UnexpectedChar, got {other:?}"),
}
}
}