elipl/test/support/test_helper.ex
2025-07-11 21:45:31 +02:00

198 lines
7.3 KiB
Elixir

defmodule Til.TestHelpers do
import ExUnit.Assertions
alias Til.Parser
# For type-checking related helpers
alias Til.Typer
alias Til.AstUtils
# --- Node Access Helpers ---
def get_file_node_from_map(nodes_map) do
file_node =
Enum.find(Map.values(nodes_map), fn node ->
# Ensure the map is an AST node before checking its type
Map.has_key?(node, :ast_node_type) && node.ast_node_type == :file
end)
refute is_nil(file_node), "File node not found in #{inspect(nodes_map)}"
file_node
end
def get_node_by_id(nodes_map, node_id) do
node = Map.get(nodes_map, node_id)
refute is_nil(node), "Node with ID #{inspect(node_id)} not found in nodes_map."
node
end
def get_first_child_node(nodes_map, parent_node_id \\ nil) do
parent_node =
if is_nil(parent_node_id) do
get_file_node_from_map(nodes_map)
else
get_node_by_id(nodes_map, parent_node_id)
end
children_ids = Map.get(parent_node, :children, [])
if Enum.empty?(children_ids) do
flunk("Parent node #{parent_node.id} has no children, cannot get first child node.")
end
get_node_by_id(nodes_map, hd(children_ids))
end
def get_nth_child_node(nodes_map, index, parent_node_id \\ nil) do
parent_node =
if is_nil(parent_node_id) do
get_file_node_from_map(nodes_map)
else
get_node_by_id(nodes_map, parent_node_id)
end
children_ids = Map.get(parent_node, :children, [])
unless index >= 0 && index < length(children_ids) do
flunk(
"Child node at index #{index} not found for parent #{parent_node.id}. Parent has #{length(children_ids)} children. Children IDs: #{inspect(children_ids)}"
)
end
get_node_by_id(nodes_map, Enum.at(children_ids, index))
end
# --- Combined Parse/TypeCheck and Get Node Helpers ---
# These return {node, nodes_map}
def parse_and_get_first_node(source_string, file_name \\ "test_source") do
{:ok, parsed_nodes_map} = Parser.parse(source_string, file_name)
{get_first_child_node(parsed_nodes_map), parsed_nodes_map}
end
def parse_and_get_nth_node(source_string, index, file_name \\ "test_source") do
{:ok, parsed_nodes_map} = Parser.parse(source_string, file_name)
{get_nth_child_node(parsed_nodes_map, index), parsed_nodes_map}
end
def typecheck_and_get_first_node(source_string, file_name \\ "test_source") do
{:ok, parsed_nodes_map} = Parser.parse(source_string, file_name)
{:ok, typed_nodes_map} = Typer.type_check(parsed_nodes_map)
{get_first_child_node(typed_nodes_map), typed_nodes_map}
end
def typecheck_and_get_nth_node(source_string, index, file_name \\ "test_source") do
{:ok, parsed_nodes_map} = Parser.parse(source_string, file_name)
{:ok, typed_nodes_map} = Typer.type_check(parsed_nodes_map)
{get_nth_child_node(typed_nodes_map, index), typed_nodes_map}
end
# --- Type Assertion Helpers ---
# Strips :id fields recursively and resolves _id suffixed keys
# from a type definition structure, using nodes_map for lookups.
def deep_strip_id(type_definition, nodes_map) do
cond do
is_struct(type_definition, MapSet) ->
type_definition
|> MapSet.to_list()
# Recursively call on elements
|> Enum.map(&deep_strip_id(&1, nodes_map))
|> MapSet.new()
is_map(type_definition) ->
# Remove its own :id.
map_without_id = Map.delete(type_definition, :id)
# Recursively process its values, resolving _id keys.
# Special handling for type_annotation_mismatch error details
# Check if this map is a type definition itself (has :type_kind) before specific error checks
if Map.has_key?(type_definition, :type_kind) &&
type_definition.type_kind == :error &&
type_definition.reason == :type_annotation_mismatch do
actual_type_clean =
case Map.get(type_definition, :actual_type_id) do
nil -> nil
id -> deep_strip_id(Map.get(nodes_map, id), nodes_map)
end
expected_type_clean =
case Map.get(type_definition, :expected_type_id) do
nil -> nil
id -> deep_strip_id(Map.get(nodes_map, id), nodes_map)
end
%{
type_kind: :error,
reason: :type_annotation_mismatch,
actual_type: actual_type_clean,
expected_type: expected_type_clean
}
else
# Standard processing for other maps
Enum.reduce(map_without_id, %{}, fn {original_key, original_value}, acc_map ->
{final_key, final_value} =
cond do
# Handle :element_type_id -> :element_type
original_key == :element_type_id && is_atom(original_value) ->
resolved_def = Map.get(nodes_map, original_value)
{:element_type, deep_strip_id(resolved_def, nodes_map)}
# Handle :key_type_id -> :key_type
original_key == :key_type_id && is_atom(original_value) ->
resolved_def = Map.get(nodes_map, original_value)
{:key_type, deep_strip_id(resolved_def, nodes_map)}
# Handle :value_type_id -> :value_type (e.g., in index_signature or %{value_type_id: ..., optional: ...})
original_key == :value_type_id && is_atom(original_value) ->
resolved_def = Map.get(nodes_map, original_value)
{:value_type, deep_strip_id(resolved_def, nodes_map)}
is_map(original_value) ->
{original_key, deep_strip_id(original_value, nodes_map)}
is_list(original_value) ->
# Handles lists of type defs
{original_key, deep_strip_id(original_value, nodes_map)}
true ->
{original_key, original_value}
end
Map.put(acc_map, final_key, final_value)
end)
end
is_list(type_definition) ->
# Recursively call on elements for lists of type definitions
Enum.map(type_definition, &deep_strip_id(&1, nodes_map))
# Literals, atoms, numbers, nil, etc. (leaf nodes in the type structure)
true ->
type_definition
end
end
def assert_node_typed_as(node, typed_nodes_map, expected_type_definition_clean) do
type_key = node.type_id
assert is_atom(type_key),
"Type ID for node #{inspect(node.id)} (#{node.raw_string}) should be an atom key, got: #{inspect(type_key)}"
actual_type_definition_from_map = Map.get(typed_nodes_map, type_key)
refute is_nil(actual_type_definition_from_map),
"Type definition for key #{inspect(type_key)} (from node #{node.id}, raw: '#{node.raw_string}') not found in nodes_map."
actual_type_definition_clean =
deep_strip_id(actual_type_definition_from_map, typed_nodes_map)
assert actual_type_definition_clean == expected_type_definition_clean,
"Type mismatch for node #{node.id} ('#{node.raw_string}').\nExpected (clean):\n#{inspect(expected_type_definition_clean, pretty: true, limit: :infinity)}\nGot (clean):\n#{inspect(actual_type_definition_clean, pretty: true, limit: :infinity)}"
end
def inspect_nodes(node_map) do
AstUtils.build_debug_ast_data(node_map)
|> IO.inspect(pretty: true, limit: :infinity)
end
end