@@ -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///|
3867test " 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