198 lines
7.3 KiB
Elixir
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
|