Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions crates/plotnik-lib/src/query/invariants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,6 @@ pub fn ensure_both_branch_kinds<'a>(
}
}

#[inline]
pub fn ensure_capture_has_inner<T>(inner: Option<T>) -> T {
inner.expect(
"shape_cardinalities: Capture without inner expression \
(parser uses checkpoint, inner is guaranteed)",
)
}

#[inline]
pub fn ensure_quantifier_has_inner<T>(inner: Option<T>) -> T {
inner.expect(
"shape_cardinalities: Quantifier without inner expression \
(parser uses checkpoint, inner is guaranteed)",
)
}

#[inline]
pub fn ensure_ref_has_name<T>(name: Option<T>) -> T {
name.expect(
Expand Down
12 changes: 7 additions & 5 deletions crates/plotnik-lib/src/query/shapes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
//! undefined refs, etc.).

use super::Query;
use super::invariants::{
ensure_capture_has_inner, ensure_quantifier_has_inner, ensure_ref_has_name,
};
use super::invariants::ensure_ref_has_name;
use crate::diagnostics::DiagnosticKind;
use crate::parser::{Expr, FieldExpr, Ref, SeqExpr, SyntaxNode};

Expand Down Expand Up @@ -55,12 +53,16 @@ impl Query<'_> {
Expr::SeqExpr(seq) => self.seq_cardinality(seq),

Expr::CapturedExpr(cap) => {
let inner = ensure_capture_has_inner(cap.inner());
let Some(inner) = cap.inner() else {
return ShapeCardinality::Invalid;
};
self.get_or_compute(&inner)
}

Expr::QuantifiedExpr(q) => {
let inner = ensure_quantifier_has_inner(q.inner());
let Some(inner) = q.inner() else {
return ShapeCardinality::Invalid;
};
self.get_or_compute(&inner)
}

Expand Down
65 changes: 65 additions & 0 deletions crates/plotnik-lib/src/query/shapes_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,3 +413,68 @@ fn invalid_ref_to_bodyless_def() {
Ref⁻ X
");
}

#[test]
fn invalid_capture_without_inner() {
// Error recovery: `extra` is invalid, but `@y` still creates a Capture node
let query = Query::try_from("(call extra @y)").unwrap();
assert!(!query.is_valid());
insta::assert_snapshot!(query.dump_with_cardinalities(), @r"
Root¹
Def¹
NamedNode¹ call
CapturedExpr⁻ @y
");
insta::assert_snapshot!(query.dump_diagnostics(), @r"
error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)`
|
1 | (call extra @y)
| ^^^^^
");
}

#[test]
fn invalid_capture_without_inner_standalone() {
// Standalone capture without preceding expression
let query = Query::try_from("@x").unwrap();
assert!(!query.is_valid());
insta::assert_snapshot!(query.dump_diagnostics(), @r"
error: `@` must follow an expression to capture
|
1 | @x
| ^
");
}

#[test]
fn invalid_multiple_captures_with_error() {
let query = Query::try_from("(call (Undefined) @x extra @y)").unwrap();
assert!(!query.is_valid());
insta::assert_snapshot!(query.dump_with_cardinalities(), @r"
Root¹
Def¹
NamedNode¹ call
CapturedExpr⁻ @x
Ref⁻ Undefined
CapturedExpr⁻ @y
");
}

#[test]
fn invalid_quantifier_without_inner() {
// Error recovery: `extra` is invalid, but `*` still creates a Quantifier node
let query = Query::try_from("(foo extra*)").unwrap();
assert!(!query.is_valid());
insta::assert_snapshot!(query.dump_with_cardinalities(), @r"
Root¹
Def¹
NamedNode¹ foo
QuantifiedExpr⁻ *
");
insta::assert_snapshot!(query.dump_diagnostics(), @r"
error: bare identifier is not a valid expression; wrap in parentheses: `(identifier)`
|
1 | (foo extra*)
| ^^^^^
");
}