defmodule Til.TupleParserTest do use ExUnit.Case, async: true alias Til.Parser alias Til.TestHelpers describe "parse/2 - Tuple Expressions" do test "parses an empty tuple" do source = "{}" {:ok, nodes_map} = Parser.parse(source) # file_node + tuple_node assert map_size(nodes_map) == 2 file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file)) refute is_nil(file_node) tuple_node = TestHelpers.get_first_child_node(nodes_map) assert tuple_node.ast_node_type == :tuple_expression assert tuple_node.parent_id == file_node.id assert tuple_node.children == [] # "{}" assert tuple_node.location == [0, 1, 1, 2, 1, 3] assert tuple_node.raw_string == "{}" end test "parses a simple tuple with integers" do source = "{1 22 -3}" {:ok, nodes_map} = Parser.parse(source) # 1 file_node + 1 tuple_expression node + 3 integer nodes = 5 nodes assert map_size(nodes_map) == 5 file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file)) refute is_nil(file_node) tuple_node = TestHelpers.get_first_child_node(nodes_map) refute is_nil(tuple_node) assert tuple_node.ast_node_type == :tuple_expression assert tuple_node.parent_id == file_node.id assert length(tuple_node.children) == 3 children_nodes = Enum.map(tuple_node.children, &TestHelpers.get_node_by_id(nodes_map, &1)) assert Enum.map(children_nodes, & &1.value) == [1, 22, -3] Enum.each(children_nodes, fn child -> assert child.parent_id == tuple_node.id assert child.ast_node_type == :literal_integer end) # "{1 22 -3}" assert tuple_node.location == [0, 1, 1, 9, 1, 10] end test "parses a simple tuple with symbols" do source = "{foo bar baz}" {tuple_node, nodes_map} = TestHelpers.parse_and_get_first_node(source) # 1 file_node, 1 tuple_expr, 3 symbols = 5 nodes assert map_size(nodes_map) == 5 file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file)) refute is_nil(file_node) refute is_nil(tuple_node) assert tuple_node.ast_node_type == :tuple_expression assert tuple_node.parent_id == file_node.id assert length(tuple_node.children) == 3 children_nodes = Enum.map(tuple_node.children, &TestHelpers.get_node_by_id(nodes_map, &1)) assert Enum.map(children_nodes, & &1.name) == ["foo", "bar", "baz"] end test "parses nested tuples" do source = "{a {b 1} c}" {outer_tuple, nodes_map} = TestHelpers.parse_and_get_first_node(source) # Nodes: 1 file_node, outer_tuple, a, inner_tuple, c, b, 1 => 7 nodes assert map_size(nodes_map) == 7 file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file)) refute is_nil(file_node) refute is_nil(outer_tuple) assert outer_tuple.ast_node_type == :tuple_expression assert outer_tuple.parent_id == file_node.id assert length(outer_tuple.children) == 3 child1 = TestHelpers.get_nth_child_node(nodes_map, 0, outer_tuple.id) inner_tuple = TestHelpers.get_nth_child_node(nodes_map, 1, outer_tuple.id) child3 = TestHelpers.get_nth_child_node(nodes_map, 2, outer_tuple.id) assert child1.ast_node_type == :symbol && child1.name == "a" assert inner_tuple.ast_node_type == :tuple_expression assert child3.ast_node_type == :symbol && child3.name == "c" assert inner_tuple.parent_id == outer_tuple.id assert length(inner_tuple.children) == 2 grandchild1 = TestHelpers.get_nth_child_node(nodes_map, 0, inner_tuple.id) grandchild2 = TestHelpers.get_nth_child_node(nodes_map, 1, inner_tuple.id) assert grandchild1.ast_node_type == :symbol && grandchild1.name == "b" assert grandchild2.ast_node_type == :literal_integer && grandchild2.value == 1 end test "parses tuples with varied spacing" do source = "{ foo 1\nbar }" {tuple_node, nodes_map} = TestHelpers.parse_and_get_first_node(source) file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file)) refute is_nil(file_node) refute is_nil(tuple_node) assert tuple_node.ast_node_type == :tuple_expression assert tuple_node.parent_id == file_node.id assert length(tuple_node.children) == 3 children_names_values = Enum.map(tuple_node.children, fn id -> node = TestHelpers.get_node_by_id(nodes_map, id) if node.ast_node_type == :symbol, do: node.name, else: node.value end) assert children_names_values == ["foo", 1, "bar"] end test "handles unclosed tuple" do source = "{foo bar" {tuple_node, nodes_map} = TestHelpers.parse_and_get_first_node(source) # Expect 1 file_node, 1 tuple_expression node (error), 2 symbol nodes = 4 nodes assert map_size(nodes_map) == 4 file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file)) refute is_nil(file_node) refute is_nil(tuple_node) assert tuple_node.ast_node_type == :tuple_expression assert tuple_node.parent_id == file_node.id assert tuple_node.parsing_error == "Unclosed tuple" # foo, bar assert length(tuple_node.children) == 2 # "{foo bar" assert tuple_node.location == [0, 1, 1, 8, 1, 9] end test "handles unexpected closing curly brace at top level" do source = "foo } bar" {:ok, nodes_map} = Parser.parse(source) # 1 file_node, foo, error_node_for_}, bar = 4 nodes assert map_size(nodes_map) == 4 file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file)) refute is_nil(file_node) top_level_children = Enum.map(file_node.children, &TestHelpers.get_node_by_id(nodes_map, &1)) error_node = Enum.find(top_level_children, &(&1.ast_node_type == :unknown && &1.raw_string == "}")) refute is_nil(error_node) assert error_node.parent_id == file_node.id assert error_node.parsing_error == "Unexpected '}'" # location of "}" assert error_node.location == [4, 1, 5, 5, 1, 6] symbol_foo = Enum.find(top_level_children, &(&1.ast_node_type == :symbol && &1.name == "foo")) refute is_nil(symbol_foo) assert symbol_foo.parent_id == file_node.id symbol_bar = Enum.find(top_level_children, &(&1.ast_node_type == :symbol && &1.name == "bar")) refute is_nil(symbol_bar) assert symbol_bar.parent_id == file_node.id end test "parses a tuple with mixed elements including strings, S-expressions, lists, and other tuples" do source = "{1 'hello' (a b) [x y] {z} 'end'}" {:ok, nodes_map} = Parser.parse(source) # Expected items: 1 outer tuple, 1 int, 1 str, 1 s-expr (2 children), 1 list (2 children), 1 inner tuple (1 child), 1 str_end # Node counts: outer_tuple (1) + int (1) + str (1) + s-expr (1) + sym_a (1) + sym_b (1) + list (1) + sym_x (1) + sym_y (1) + inner_tuple (1) + sym_z (1) + str_end (1) = 12 nodes # Plus 1 file_node = 13 nodes assert map_size(nodes_map) == 13 file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file)) refute is_nil(file_node) outer_tuple_node = TestHelpers.get_first_child_node(nodes_map) refute is_nil(outer_tuple_node) assert outer_tuple_node.ast_node_type == :tuple_expression assert outer_tuple_node.parent_id == file_node.id assert length(outer_tuple_node.children) == 6 children = Enum.map(outer_tuple_node.children, &TestHelpers.get_node_by_id(nodes_map, &1)) assert Enum.at(children, 0).ast_node_type == :literal_integer assert Enum.at(children, 0).value == 1 assert Enum.at(children, 1).ast_node_type == :literal_string assert Enum.at(children, 1).value == "hello" s_expr_child = Enum.at(children, 2) assert s_expr_child.ast_node_type == :s_expression assert length(s_expr_child.children) == 2 s_expr_children = Enum.map(s_expr_child.children, &TestHelpers.get_node_by_id(nodes_map, &1)) assert Enum.map(s_expr_children, & &1.name) == ["a", "b"] list_child = Enum.at(children, 3) assert list_child.ast_node_type == :list_expression assert length(list_child.children) == 2 list_children = Enum.map(list_child.children, &TestHelpers.get_node_by_id(nodes_map, &1)) assert Enum.map(list_children, & &1.name) == ["x", "y"] inner_tuple_child = Enum.at(children, 4) assert inner_tuple_child.ast_node_type == :tuple_expression assert length(inner_tuple_child.children) == 1 inner_tuple_children = Enum.map(inner_tuple_child.children, &TestHelpers.get_node_by_id(nodes_map, &1)) assert Enum.map(inner_tuple_children, & &1.name) == ["z"] assert Enum.at(children, 5).ast_node_type == :literal_string assert Enum.at(children, 5).value == "end" end test "symbol cannot contain curly braces" do # Test "foo{" source1 = "foo{" {:ok, nodes_map1} = Parser.parse(source1) # 1 file_node, "foo" symbol, "{" (unclosed tuple_expression) = 3 nodes assert map_size(nodes_map1) == 3 file_node1 = Enum.find(Map.values(nodes_map1), &(&1.ast_node_type == :file)) refute is_nil(file_node1) children1 = Enum.map(file_node1.children, &TestHelpers.get_node_by_id(nodes_map1, &1)) foo_node = Enum.find(children1, &(&1.name == "foo")) tuple_node = Enum.find(children1, &(&1.ast_node_type == :tuple_expression)) refute is_nil(foo_node) assert foo_node.parent_id == file_node1.id refute is_nil(tuple_node) assert tuple_node.parent_id == file_node1.id assert tuple_node.parsing_error == "Unclosed tuple" assert tuple_node.children == [] # Test "foo{bar" source2 = "foo{bar" {:ok, nodes_map2} = Parser.parse(source2) # 1 file_node, "foo" symbol, "{" (tuple_expression), "bar" symbol inside tuple = 4 nodes assert map_size(nodes_map2) == 4 file_node2 = Enum.find(Map.values(nodes_map2), &(&1.ast_node_type == :file)) refute is_nil(file_node2) children2 = Enum.map(file_node2.children, &TestHelpers.get_node_by_id(nodes_map2, &1)) foo_node2 = Enum.find(children2, &(&1.name == "foo")) tuple_node2 = Enum.find(children2, &(&1.ast_node_type == :tuple_expression)) refute is_nil(foo_node2) assert foo_node2.parent_id == file_node2.id refute is_nil(tuple_node2) assert tuple_node2.parent_id == file_node2.id assert tuple_node2.parsing_error == "Unclosed tuple" assert length(tuple_node2.children) == 1 bar_node_id = hd(tuple_node2.children) bar_node = TestHelpers.get_node_by_id(nodes_map2, bar_node_id) assert bar_node.name == "bar" assert bar_node.parent_id == tuple_node2.id end end end