defmodule Til.MapParserTest do use ExUnit.Case, async: true alias Til.Parser alias Til.TestHelpers describe "parse/2 - Map Expressions" do test "parses an empty map" do source = "m{}" {:ok, nodes_map} = Parser.parse(source) # file_node + map_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) map_node = TestHelpers.get_first_child_node(nodes_map) assert map_node.ast_node_type == :map_expression assert map_node.parent_id == file_node.id assert map_node.children == [] # "m{}" assert map_node.location == [0, 1, 1, 3, 1, 4] assert map_node.raw_string == "m{}" end test "parses a simple map with symbol keys and values" do source = "m{key1 val1 key2 val2}" {:ok, nodes_map} = Parser.parse(source) # 1 file_node + 1 map_expression node + 4 symbol nodes = 6 nodes assert map_size(nodes_map) == 6 file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file)) refute is_nil(file_node) map_node = TestHelpers.get_first_child_node(nodes_map) refute is_nil(map_node) assert map_node.ast_node_type == :map_expression assert map_node.parent_id == file_node.id assert length(map_node.children) == 4 children_nodes = Enum.map(map_node.children, &TestHelpers.get_node_by_id(nodes_map, &1)) assert Enum.map(children_nodes, & &1.name) == ["key1", "val1", "key2", "val2"] Enum.each(children_nodes, fn child -> assert child.parent_id == map_node.id assert child.ast_node_type == :symbol end) # "m{key1 val1 key2 val2}" assert map_node.location == [0, 1, 1, 22, 1, 23] assert map_node.raw_string == "m{key1 val1 key2 val2}" end test "parses a map with mixed type values" do source = "m{s 'a string' i 123 sym value}" {map_node, nodes_map} = TestHelpers.parse_and_get_first_node(source) # 1 file_node + 1 map, 3 keys (symbols), 1 string val, 1 int val, 1 symbol val = 1 + 1 + 3 + 3 = 8 nodes assert map_size(nodes_map) == 8 file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file)) refute is_nil(file_node) refute is_nil(map_node) assert map_node.ast_node_type == :map_expression assert map_node.parent_id == file_node.id assert length(map_node.children) == 6 children_nodes = Enum.map(map_node.children, &TestHelpers.get_node_by_id(nodes_map, &1)) # s assert Enum.at(children_nodes, 0).ast_node_type == :symbol assert Enum.at(children_nodes, 0).name == "s" # 'a string' assert Enum.at(children_nodes, 1).ast_node_type == :literal_string assert Enum.at(children_nodes, 1).value == "a string" # i assert Enum.at(children_nodes, 2).ast_node_type == :symbol assert Enum.at(children_nodes, 2).name == "i" # 123 assert Enum.at(children_nodes, 3).ast_node_type == :literal_integer assert Enum.at(children_nodes, 3).value == 123 # sym assert Enum.at(children_nodes, 4).ast_node_type == :symbol assert Enum.at(children_nodes, 4).name == "sym" # value assert Enum.at(children_nodes, 5).ast_node_type == :symbol assert Enum.at(children_nodes, 5).name == "value" end test "parses nested maps" do source = "m{outer_key m{inner_key inner_val}}" {:ok, nodes_map} = Parser.parse(source) # Nodes: 1 file_node, outer_map, outer_key, inner_map, inner_key, inner_val => 6 nodes assert map_size(nodes_map) == 6 file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file)) refute is_nil(file_node) outer_map = TestHelpers.get_first_child_node(nodes_map) refute is_nil(outer_map) assert outer_map.ast_node_type == :map_expression assert outer_map.parent_id == file_node.id # outer_key, inner_map assert length(outer_map.children) == 2 outer_key_node = TestHelpers.get_nth_child_node(nodes_map, 0, outer_map.id) inner_map_node = TestHelpers.get_nth_child_node(nodes_map, 1, outer_map.id) assert outer_key_node.ast_node_type == :symbol assert outer_key_node.name == "outer_key" assert inner_map_node.ast_node_type == :map_expression assert inner_map_node.parent_id == outer_map.id # inner_key, inner_val assert length(inner_map_node.children) == 2 inner_key_node = TestHelpers.get_nth_child_node(nodes_map, 0, inner_map_node.id) inner_val_node = TestHelpers.get_nth_child_node(nodes_map, 1, inner_map_node.id) assert inner_key_node.ast_node_type == :symbol assert inner_key_node.name == "inner_key" assert inner_val_node.ast_node_type == :symbol assert inner_val_node.name == "inner_val" end test "parses map with varied spacing" do source = "m{ key1 val1\n key2 val2 }" {map_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(map_node) assert map_node.ast_node_type == :map_expression assert map_node.parent_id == file_node.id assert length(map_node.children) == 4 children_names_values = Enum.map(map_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 == ["key1", "val1", "key2", "val2"] end test "handles unclosed map" do source = "m{key1 val1" {map_node, nodes_map} = TestHelpers.parse_and_get_first_node(source) # Expect 1 file_node, 1 map_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(map_node) assert map_node.ast_node_type == :map_expression assert map_node.parent_id == file_node.id assert map_node.parsing_error == "Unclosed map" # key1, val1 assert length(map_node.children) == 2 # "m{key1 val1" assert map_node.location == [0, 1, 1, 11, 1, 12] assert map_node.raw_string == "m{key1 val1" end test "handles unexpected closing curly brace at top level (not map specific, but related)" do # This } is not part of m{} source = "foo } bar" {:ok, nodes_map} = Parser.parse(source) # 1 file_node + 3 items 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] end test "parses map with odd number of elements (parser accepts, semantic check later)" do source = "m{key1 val1 key2}" {map_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(map_node) assert map_node.ast_node_type == :map_expression assert map_node.parent_id == file_node.id # key1, val1, key2 assert length(map_node.children) == 3 children_nodes = Enum.map(map_node.children, &TestHelpers.get_node_by_id(nodes_map, &1)) assert Enum.map(children_nodes, & &1.name) == ["key1", "val1", "key2"] end test "map within an S-expression" do source = "(do-something m{a 1 b 2})" {s_expr_node, nodes_map} = TestHelpers.parse_and_get_first_node(source) # 1 file_node, s-expr, do-something, map, a, 1, b, 2 => 8 nodes assert map_size(nodes_map) == 8 file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file)) refute is_nil(file_node) refute is_nil(s_expr_node) assert s_expr_node.ast_node_type == :s_expression assert s_expr_node.parent_id == file_node.id # do-something, map_node assert length(s_expr_node.children) == 2 map_node = TestHelpers.get_nth_child_node(nodes_map, 1, s_expr_node.id) assert map_node.ast_node_type == :map_expression assert map_node.parent_id == s_expr_node.id # a, 1, b, 2 assert length(map_node.children) == 4 end test "map as a value in another map" do source = "m{data m{x 10 y 20}}" {outer_map_node, nodes_map} = TestHelpers.parse_and_get_first_node(source) # 1 file_node, outer_map, data_symbol, inner_map, x_symbol, 10_int, y_symbol, 20_int => 8 nodes assert map_size(nodes_map) == 8 file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file)) refute is_nil(file_node) refute is_nil(outer_map_node) assert outer_map_node.ast_node_type == :map_expression assert outer_map_node.parent_id == file_node.id # data_symbol, inner_map_node assert length(outer_map_node.children) == 2 inner_map_node = TestHelpers.get_nth_child_node(nodes_map, 1, outer_map_node.id) assert inner_map_node.ast_node_type == :map_expression assert inner_map_node.parent_id == outer_map_node.id # x, 10, y, 20 assert length(inner_map_node.children) == 4 end end end