Packages

Automatically compare two implementations of the same problem with property-based testing and performance benchmarks. Perfect for refactoring, algorithm comparison, and validating that different implementations produce identical results.

Current section

Files

Jump to
ab lib ab struct_validator.ex
Raw

lib/ab/struct_validator.ex

defmodule AB.StructValidator do
@moduledoc """
Validates consistency between struct type definitions and function typespecs.
This module catches inconsistencies where @type definitions don't match @spec
definitions, ensuring that generated structs work correctly with the functions
that are supposed to accept them.
"""
import ExUnit.Assertions
alias AB.{TypeParser, Validators}
@doc """
Validates struct type consistency for a module.
Checks if @type definitions are consistent with @spec definitions.
"""
@spec run_struct_consistency_validation(module()) :: :ok
def run_struct_consistency_validation(module) do
case Code.ensure_loaded(module) do
{:module, _} ->
validate_module_struct_consistency(module)
_ ->
flunk("Could not load module #{module}")
end
end
@spec validate_module_struct_consistency(module()) :: :ok
defp validate_module_struct_consistency(module) do
with {:ok, types} <- Code.Typespec.fetch_types(module),
true <- has_struct_type?(types),
{:ok, specs} <- Code.Typespec.fetch_specs(module) do
validate_each_spec(module, specs)
else
_ -> :ok
end
end
@spec has_struct_type?([any()]) :: boolean()
defp has_struct_type?(types) do
Enum.any?(types, fn
{:type, {:t, _, []}} -> true
_ -> false
end)
end
@spec validate_each_spec(module(), [any()]) :: :ok
defp validate_each_spec(module, specs) do
Enum.each(specs, fn {{function_name, _arity}, [spec | _]} ->
validate_spec_against_type(module, function_name, spec)
end)
end
@spec validate_spec_against_type(module(), atom(), any()) :: :ok
defp validate_spec_against_type(module, function_name, spec) do
case TypeParser.parse_spec(spec) do
{:ok, {[_input_type], _output_type}} ->
validate_function_with_generated_struct(module, function_name)
_ ->
:ok
end
end
@spec validate_function_with_generated_struct(module(), atom()) :: :ok
defp validate_function_with_generated_struct(module, function_name) do
case TypeParser.create_struct_from_type_definition(module) do
nil ->
:ok
gen ->
test_struct_against_function(module, function_name, gen)
end
end
@spec test_struct_against_function(module(), atom(), Enumerable.t()) :: :ok
defp test_struct_against_function(module, function_name, gen) do
test_input = gen |> Enum.take(1) |> List.first()
try do
apply(module, function_name, [test_input])
rescue
e ->
flunk(
"Type inconsistency detected: @type definition creates structs that don't work with @spec for #{function_name}/1. Error: #{inspect(e)}"
)
end
end
@doc """
Validates consistency between @type field definitions and @spec field definitions.
"""
@spec validate_struct_field_consistency(module(), atom(), [any()]) :: :ok
def validate_struct_field_consistency(module, function_name, type_field_types) do
with {:ok, {[spec_input_type], spec_output_type}} <-
TypeParser.get_function_spec(module, function_name),
type_fields when not is_nil(type_fields) <-
TypeParser.extract_struct_fields(type_field_types),
spec_fields when not is_nil(spec_fields) <-
extract_struct_fields_from_spec(spec_input_type) do
validate_all_fields(module, function_name, type_fields, spec_fields, spec_output_type)
else
_ -> :ok
end
end
@spec validate_all_fields(
module(),
atom(),
[{atom(), any()}] | nil,
%{atom() => any()} | nil,
any()
) :: :ok
defp validate_all_fields(module, function_name, type_fields, spec_fields, spec_output_type) do
Enum.each(type_fields, fn {field_name, type_field_type} ->
validate_single_field(
module,
function_name,
field_name,
type_field_type,
spec_fields,
spec_output_type
)
end)
end
@spec validate_single_field(module(), atom(), atom(), any(), %{atom() => any()}, any()) :: :ok
defp validate_single_field(
module,
function_name,
field_name,
type_field_type,
spec_fields,
spec_output_type
) do
case Map.get(spec_fields, field_name) do
nil ->
:ok
^type_field_type ->
:ok
spec_field_type ->
validate_field_type_mismatch(
module,
function_name,
field_name,
type_field_type,
spec_field_type,
spec_output_type
)
end
end
@spec validate_field_type_mismatch(module(), atom(), atom(), any(), any(), any()) :: :ok
defp validate_field_type_mismatch(
module,
function_name,
field_name,
type_field_type,
spec_field_type,
spec_output_type
) do
if TypeParser.types_equivalent?(type_field_type, spec_field_type) do
:ok
else
test_field_inconsistency(
module,
function_name,
field_name,
type_field_type,
spec_field_type,
spec_output_type
)
end
end
@spec test_field_inconsistency(module(), atom(), atom(), any(), any(), any()) :: :ok
defp test_field_inconsistency(
module,
function_name,
field_name,
type_field_type,
spec_field_type,
spec_output_type
) do
case TypeParser.create_struct_from_type_definition(module) do
nil ->
:ok
type_generator ->
test_struct = type_generator |> Enum.take(1) |> List.first()
try do
result = apply(module, function_name, [test_struct])
output_validator = Validators.create_output_validator(spec_output_type)
unless output_validator.(result) do
raise_type_inconsistency_error(
module,
field_name,
type_field_type,
spec_field_type,
"This causes function to return invalid output."
)
end
rescue
e ->
raise_type_inconsistency_error(
module,
field_name,
type_field_type,
spec_field_type,
"Error: #{inspect(e)}"
)
end
end
end
@spec raise_type_inconsistency_error(module(), atom(), any(), any(), String.t()) :: no_return()
defp raise_type_inconsistency_error(
module,
field_name,
type_field_type,
spec_field_type,
suffix
) do
raise "Type inconsistency: @type #{module}.t defines field :#{field_name} as #{inspect(type_field_type)} but @spec expects #{inspect(spec_field_type)}. #{suffix}"
end
@spec extract_struct_fields_from_spec(any()) :: %{atom() => any()} | nil
defp extract_struct_fields_from_spec({:type, _, :map, field_types}) do
TypeParser.extract_struct_fields(field_types)
end
defp extract_struct_fields_from_spec(_), do: nil
end