From 714e76ca11b2cba9b80a75a917e8fdc4afde64af Mon Sep 17 00:00:00 2001 From: Taus Date: Mon, 4 May 2026 13:13:18 +0000 Subject: [PATCH] Add yeast test suite 12 tests covering parsing, queries, tree building, desugaring rules, cursor navigation, and the shorthand rule! syntax. Tests use a custom output node-types.yml with named fields for all children (parameter, stmt, index), loaded via schema_from_yaml_with_language. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- shared/yeast/tests/node-types.yml | 63 +++++ shared/yeast/tests/test.rs | 368 ++++++++++++++++++++++++++++++ 2 files changed, 431 insertions(+) create mode 100644 shared/yeast/tests/node-types.yml create mode 100644 shared/yeast/tests/test.rs diff --git a/shared/yeast/tests/node-types.yml b/shared/yeast/tests/node-types.yml new file mode 100644 index 00000000000..978ef147543 --- /dev/null +++ b/shared/yeast/tests/node-types.yml @@ -0,0 +1,63 @@ +# Output node types for yeast test rules. +# Inspired by tree-sitter-ruby, but with all children in named fields +# (no unnamed children). This represents the desugared output schema. + +named: + program: + stmt*: [assignment, call, identifier, for] + + assignment: + left: [identifier, left_assignment_list] + right: [identifier, integer, call, element_reference] + + left_assignment_list: + item*: identifier + + element_reference: + object: identifier + index: [integer, identifier] + + for: + pattern: [identifier, left_assignment_list] + value: in + body: do + + in: + value: [identifier, call] + + do: + stmt*: [assignment, identifier, call] + + call: + receiver: [identifier, call] + method: identifier + arguments?: argument_list + block?: block + + argument_list: + argument*: [identifier, integer, call] + + block: + parameters: block_parameters + body: block_body + + block_parameters: + parameter*: identifier + + block_body: + stmt*: [assignment, identifier, call] + + identifier: + integer: + +unnamed: + - "=" + - "," + - "(" + - ")" + - "for" + - "in" + - "do" + - "end" + - "|" + - "." diff --git a/shared/yeast/tests/test.rs b/shared/yeast/tests/test.rs new file mode 100644 index 00000000000..639bed1d437 --- /dev/null +++ b/shared/yeast/tests/test.rs @@ -0,0 +1,368 @@ +#![cfg(test)] + +use yeast::dump::dump_ast; +use yeast::*; + +const OUTPUT_SCHEMA_YAML: &str = include_str!("node-types.yml"); + +/// Helper: parse Ruby source with no rules, return dump. +fn parse_and_dump(input: &str) -> String { + let runner = Runner::new(tree_sitter_ruby::LANGUAGE.into(), vec![]); + let ast = runner.run(input).unwrap(); + dump_ast(&ast, ast.get_root(), input) +} + +/// Helper: parse Ruby source with a custom output schema and rules, return dump. +fn run_and_dump(input: &str, rules: Vec) -> String { + let lang: tree_sitter::Language = tree_sitter_ruby::LANGUAGE.into(); + let schema = yeast::node_types_yaml::schema_from_yaml_with_language( + OUTPUT_SCHEMA_YAML, &lang + ).unwrap(); + let runner = Runner::with_schema(lang, schema, rules); + let ast = runner.run(input).unwrap(); + dump_ast(&ast, ast.get_root(), input) +} + +// ---- Parsing tests ---- + +#[test] +fn test_parse_assignment() { + let dump = parse_and_dump("x = 1"); + assert_eq!(dump.trim(), "\ +program + assignment + left: identifier \"x\" + right: integer \"1\""); +} + +#[test] +fn test_parse_multiple_assignment() { + let dump = parse_and_dump("x, y = foo()"); + assert_eq!(dump.trim(), "\ +program + assignment + left: + left_assignment_list + identifier \"x\" + identifier \"y\" + right: + call + arguments: + argument_list + method: identifier \"foo\""); +} + +#[test] +fn test_parse_for_loop() { + let dump = parse_and_dump("for x in list do\n y\nend"); + assert_eq!(dump.trim(), "\ +program + for + body: + do + identifier \"y\" + pattern: identifier \"x\" + value: + in + identifier \"list\""); +} + +// ---- Query tests ---- + +#[test] +fn test_query_match() { + let runner = Runner::new(tree_sitter_ruby::LANGUAGE.into(), vec![]); + let ast = runner.run("x = 1").unwrap(); + + let query = yeast::query!( + (program + child: (assignment + left: (_) @left + right: (_) @right + ) + ) + ); + + let mut captures = yeast::captures::Captures::new(); + let matched = query.do_match(&ast, ast.get_root(), &mut captures).unwrap(); + assert!(matched); + assert!(captures.get_var("left").is_ok()); + assert!(captures.get_var("right").is_ok()); +} + +#[test] +fn test_query_no_match() { + let runner = Runner::new(tree_sitter_ruby::LANGUAGE.into(), vec![]); + let ast = runner.run("x = 1").unwrap(); + + let query = yeast::query!( + (program + child: (call + method: (_) @m + ) + ) + ); + + let mut captures = yeast::captures::Captures::new(); + let matched = query.do_match(&ast, ast.get_root(), &mut captures).unwrap(); + assert!(!matched); +} + +#[test] +fn test_query_repeated_capture() { + let runner = Runner::new(tree_sitter_ruby::LANGUAGE.into(), vec![]); + let ast = runner.run("x, y, z = 1").unwrap(); + + let query = yeast::query!( + (assignment + left: (left_assignment_list + (identifier)* @names + ) + ) + ); + + // Match against the assignment node (first named child of program) + let mut cursor = AstCursor::new(&ast); + cursor.goto_first_child(); + let assignment_id = cursor.node().id(); + + let mut captures = yeast::captures::Captures::new(); + let matched = query.do_match(&ast, assignment_id, &mut captures).unwrap(); + assert!(matched); + assert_eq!(captures.get_all("names").len(), 3); +} + +// ---- Tree builder tests ---- + +#[test] +fn test_tree_builder() { + let runner = Runner::new(tree_sitter_ruby::LANGUAGE.into(), vec![]); + let mut ast = runner.run("x = 1").unwrap(); + let input = "x = 1"; + + let query = yeast::query!( + (program + child: (assignment + left: (_) @left + right: (_) @right + ) + ) + ); + + let mut captures = yeast::captures::Captures::new(); + query.do_match(&ast, ast.get_root(), &mut captures).unwrap(); + + // Swap left and right + let fresh = yeast::tree_builder::FreshScope::new(); + let mut ctx = yeast::build::BuildCtx::new(&mut ast, &captures, &fresh); + let new_id = yeast::tree!(ctx, + (program + child: (assignment + left: {ctx.capture("right")} + right: {ctx.capture("left")} + ) + ) + ); + + let dump = dump_ast(ctx.ast, new_id, input); + assert_eq!(dump.trim(), "\ +program + assignment + left: integer \"1\" + right: identifier \"x\""); +} + +// ---- Rule tests ---- + +// These rules use field names from node-types.yml, which extends the +// tree-sitter-ruby grammar with named fields for nodes that only have +// unnamed children in tree-sitter (e.g. block_body.stmt, block_parameters.parameter). +fn ruby_rules() -> Vec { + let assign_rule = yeast::rule!( + (assignment + left: (left_assignment_list + (identifier)* @left + ) + right: (_) @right + ) + => + (assignment + left: (identifier $tmp) + right: {right} + ) + {..left.iter().enumerate().map(|(i, &lhs)| + yeast::tree!( + (assignment + left: {lhs} + right: (element_reference + object: (identifier $tmp) + index: (integer #{i}) + ) + ) + ) + )} + ); + + let for_rule = yeast::rule!( + (for + pattern: (_) @pat + value: (in (_) @val) + body: (do (_)* @body) + ) + => + (call + receiver: {val} + method: (identifier "each") + block: (block + parameters: (block_parameters + parameter: (identifier $tmp) + ) + body: (block_body + stmt: {..{ + let assign = yeast::tree!(__yeast_ctx, + (assignment + left: {pat} + right: (identifier $tmp) + ) + ); + let mut stmts = vec![assign]; + stmts.extend(body); + stmts + }} + ) + ) + ) + ); + + vec![assign_rule, for_rule] +} + +#[test] +fn test_desugar_multiple_assignment() { + let dump = run_and_dump("x, y = e", ruby_rules()); + assert_eq!(dump.trim(), "\ +program + assignment + left: identifier \"$tmp-0\" + right: identifier \"e\" + assignment + left: identifier \"x\" + right: + element_reference + object: identifier \"$tmp-0\" + index: integer \"0\" + assignment + left: identifier \"y\" + right: + element_reference + object: identifier \"$tmp-0\" + index: integer \"1\""); +} + +#[test] +fn test_desugar_for_loop() { + let dump = run_and_dump("for x in list do\n y\nend", ruby_rules()); + assert_eq!(dump.trim(), "\ +program + call + block: + block + body: + block_body + stmt: + assignment + left: identifier \"x\" + right: identifier \"$tmp-0\" + identifier \"y\" + parameters: + block_parameters + parameter: identifier \"$tmp-0\" + method: identifier \"each\" + receiver: identifier \"list\""); +} + +#[test] +fn test_shorthand_rule() { + let rule = yeast::rule!( + (assignment + left: (_) @method + right: (_) @receiver + ) + => call + ); + + let dump = run_and_dump("x = 1", vec![rule]); + assert_eq!(dump.trim(), "\ +program + call + method: identifier \"x\" + receiver: integer \"1\""); +} + +// ---- Cursor tests ---- + +#[test] +fn test_cursor_navigation() { + let runner = Runner::new(tree_sitter_ruby::LANGUAGE.into(), vec![]); + let ast = runner.run("x = 1").unwrap(); + let mut cursor = AstCursor::new(&ast); + + // Start at root + assert_eq!(cursor.node().kind(), "program"); + + // Go to first child (assignment) + assert!(cursor.goto_first_child()); + assert_eq!(cursor.node().kind(), "assignment"); + + // No sibling + assert!(!cursor.goto_next_sibling()); + + // Go to first child of assignment + assert!(cursor.goto_first_child()); + assert!(cursor.node().is_named()); + + // Go back up + assert!(cursor.goto_parent()); + assert_eq!(cursor.node().kind(), "assignment"); + + assert!(cursor.goto_parent()); + assert_eq!(cursor.node().kind(), "program"); + + // Can't go further up + assert!(!cursor.goto_parent()); +} + +#[test] +fn test_desugar_for_with_multiple_assignment() { + let dump = run_and_dump("for a, b in list do\n x\nend", ruby_rules()); + assert_eq!(dump.trim(), "\ +program + call + block: + block + body: + block_body + stmt: + assignment + left: identifier \"$tmp-1\" + right: identifier \"$tmp-0\" + assignment + left: identifier \"a\" + right: + element_reference + object: identifier \"$tmp-1\" + index: integer \"0\" + assignment + left: identifier \"b\" + right: + element_reference + object: identifier \"$tmp-1\" + index: integer \"1\" + identifier \"x\" + parameters: + block_parameters + parameter: identifier \"$tmp-0\" + method: identifier \"each\" + receiver: identifier \"list\""); +} +