Skip to content

Commit d275961

Browse files
committed
add ExprQuery::captures_from_ast
1 parent a7fc3e1 commit d275961

4 files changed

Lines changed: 137 additions & 6 deletions

File tree

query/moon.pkg

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,9 @@ import {
99
"moonbitlang/parser",
1010
"moonbitlang/parser/syntax",
1111
}
12+
13+
import {
14+
"moonbitlang/core/list",
15+
"moonbitlang/lexer",
16+
"moonbitlang/parser/handrolled_parser",
17+
} for "test"

query/pkg.generated.mbti

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
type ExprQuery
1414
pub fn ExprQuery::ExprQuery(String) -> Self raise
1515
pub fn ExprQuery::captures(Self, source_name~ : String, String) -> ArrayView[Map[String, @untyped_ast.Node]] raise
16+
pub fn ExprQuery::captures_from_ast(Self, ast~ : @untyped_ast.Node) -> ArrayView[Map[String, @untyped_ast.Node]]
1617

1718
// Type aliases
1819

query/query.mbt

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,22 @@ pub fn ExprQuery::captures(
5454
for node in impls {
5555
let impl_source = source_text_for_impl(source, node)
5656
if @rule_prefilter.rule_is_relevant_to_source(self.rule, impl_source) {
57-
collect_impl_matches(self.pattern, node, matches)
57+
collect_node_matches(self.pattern, @untyped_ast.from_impl(node), matches)
5858
}
5959
}
6060
matches
6161
}
6262

63+
///|
64+
pub fn ExprQuery::captures_from_ast(
65+
self : ExprQuery,
66+
ast~ : @untyped_ast.Node,
67+
) -> ArrayView[Map[String, @untyped_ast.Node]] {
68+
let matches : Array[Map[String, @untyped_ast.Node]] = []
69+
collect_node_matches(self.pattern, ast, matches)
70+
matches
71+
}
72+
6373
///|
6474
fn anonymous_pattern_rule(pattern : String) -> RawRuleSpec {
6575
{
@@ -76,14 +86,12 @@ fn anonymous_pattern_rule(pattern : String) -> RawRuleSpec {
7686
}
7787

7888
///|
79-
fn collect_impl_matches(
89+
fn collect_node_matches(
8090
pattern : CompiledRulePattern,
81-
node : @syntax.Impl,
91+
ast : @untyped_ast.Node,
8292
matches : Array[Map[String, @untyped_ast.Node]],
8393
) -> Unit {
84-
@untyped_ast.visit_node_scoped_expr_roots(@untyped_ast.from_impl(node), fn(
85-
scoped_expr,
86-
) {
94+
@untyped_ast.visit_node_scoped_expr_roots(ast, fn(scoped_expr) {
8795
collect_scoped_node_matches(pattern, scoped_expr, matches)
8896
@untyped_ast.NodeVisitAction::NodeVisitPrune
8997
})

query/query_test.mbt

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,35 @@ fn query_constant_value(node : @untyped_ast.Node) -> String? {
3434
query_leaf_string_child(node, "value")
3535
}
3636

37+
///|
38+
fn parse_query_expr(source : String) -> @syntax.Expr raise {
39+
let lex_result = @lexer.tokens_from_string(
40+
source,
41+
comment=true,
42+
name="query expr test",
43+
)
44+
guard lex_result.errors.length() == 0 else {
45+
fail("expected expression to lex: \{source}")
46+
}
47+
let (expr, reports) = @handrolled_parser.parse_expr(lex_result.tokens)
48+
guard reports is [] else { fail("expected expression to parse: \{source}") }
49+
expr
50+
}
51+
52+
///|
53+
fn parse_query_impls(source : String) -> @list.List[@syntax.Impl] raise {
54+
let (impls, reports) = @parser.parse_string(source, name="query impl test")
55+
guard reports is [] else { fail("expected impls to parse: \{source}") }
56+
impls
57+
}
58+
59+
///|
60+
fn parse_query_impl(source : String) -> @syntax.Impl raise {
61+
let impls = parse_query_impls(source).to_array()
62+
guard impls.length() == 1 else { fail("expected one impl: \{source}") }
63+
impls[0]
64+
}
65+
3766
///|
3867
test "query matches simple expression and returns empty capture map" {
3968
let query = ExprQuery::ExprQuery("target()")
@@ -145,3 +174,90 @@ test "query raises when pattern is invalid" {
145174
_ => fail("expected invalid pattern")
146175
}
147176
}
177+
178+
///|
179+
test "query captures_from_ast finds matches under an impl list root" {
180+
let query = ExprQuery::ExprQuery("target()")
181+
let source =
182+
#|fn sample {
183+
#| target();
184+
#| other()
185+
#|}
186+
#|
187+
let ast = @untyped_ast.from_impls(parse_query_impls(source))
188+
let matches = query.captures_from_ast(ast~)
189+
assert_eq(matches.length(), 1)
190+
assert_eq(matches[0].length(), 0)
191+
}
192+
193+
///|
194+
test "query captures_from_ast matches a direct expression root" {
195+
let query = ExprQuery::ExprQuery("target($(literal:const))")
196+
let ast = @untyped_ast.from_expr(parse_query_expr("target(42)"))
197+
let matches = query.captures_from_ast(ast~)
198+
assert_eq(matches.length(), 1)
199+
guard matches[0].get("literal") is Some(literal) else {
200+
fail("expected literal capture")
201+
}
202+
guard query_constant_value(literal) is Some(value) else {
203+
fail("expected literal value")
204+
}
205+
inspect(value, content="42")
206+
}
207+
208+
///|
209+
test "query captures_from_ast finds nested expressions from a non-expression impl root" {
210+
let query = ExprQuery::ExprQuery("target()")
211+
let ast = @untyped_ast.from_impl(parse_query_impl("fn sample { target() }"))
212+
assert_true(ast.kind == Impl_TopFuncDef)
213+
let matches = query.captures_from_ast(ast~)
214+
assert_eq(matches.length(), 1)
215+
}
216+
217+
///|
218+
test "query captures_from_ast preserves expression id and constant captures" {
219+
let query = ExprQuery::ExprQuery(
220+
"sink($(callee:id), $(value:exp), $(literal:const))",
221+
)
222+
let source =
223+
#|fn sample {
224+
#| sink(source, make(), "danger")
225+
#|}
226+
#|
227+
let source_matches = query.captures(source_name="sample.mbt", source)
228+
let ast_matches = query.captures_from_ast(
229+
ast=@untyped_ast.from_impls(parse_query_impls(source)),
230+
)
231+
assert_eq(ast_matches.length(), source_matches.length())
232+
assert_eq(ast_matches.length(), 1)
233+
guard ast_matches[0].get("callee") is Some(callee) else {
234+
fail("expected callee capture")
235+
}
236+
guard query_leaf_string(callee) is Some(callee_name) else {
237+
fail("expected callee name")
238+
}
239+
inspect(callee_name, content="source")
240+
guard ast_matches[0].get("value") is Some(value) else {
241+
fail("expected expression capture")
242+
}
243+
assert_true(value.kind == Expr_Apply)
244+
guard ast_matches[0].get("literal") is Some(literal) else {
245+
fail("expected literal capture")
246+
}
247+
guard query_constant_value(literal) is Some(literal_value) else {
248+
fail("expected literal value")
249+
}
250+
inspect(literal_value, content="danger")
251+
}
252+
253+
///|
254+
test "query captures_from_ast prunes nested expressions after an outer match" {
255+
let query = ExprQuery::ExprQuery("wrap($(value:exp))")
256+
let ast = @untyped_ast.from_expr(parse_query_expr("wrap(wrap(1))"))
257+
let matches = query.captures_from_ast(ast~)
258+
assert_eq(matches.length(), 1)
259+
guard matches[0].get("value") is Some(value) else {
260+
fail("expected value capture")
261+
}
262+
assert_true(value.kind == Expr_Apply)
263+
}

0 commit comments

Comments
 (0)