From ab42f01db57390e6c60fa7659010902605134773 Mon Sep 17 00:00:00 2001 From: edgurgel Date: Tue, 27 Aug 2024 21:59:34 +1200 Subject: [PATCH] WIP --- lib/ham/type_engine.ex | 13 ++++++++++++- lib/ham/type_match_error.ex | 30 ++---------------------------- lib/ham/utils.ex | 34 ++++++++++++++++++++++++++++++++++ src/test.erl | 6 ++++++ test/ham/type_checker_test.exs | 9 +++++++-- test/ham/utils_test.exs | 17 +++++++++++++++++ test/support/module.ex | 12 +++++++++++- 7 files changed, 89 insertions(+), 32 deletions(-) create mode 100644 src/test.erl create mode 100644 test/ham/utils_test.exs diff --git a/lib/ham/type_engine.ex b/lib/ham/type_engine.ex index 6761703..17f5cb8 100644 --- a/lib/ham/type_engine.ex +++ b/lib/ham/type_engine.ex @@ -5,7 +5,18 @@ defmodule Ham.TypeEngine do alias Ham.Utils @type_kinds [:type, :typep, :opaque] - + @typep entry_type :: tuple + @typep value :: term + + @type reason :: + {:protocol_type_mismatch, value, module} + | {:remote_type_fetch_failure, {module, atom, arity}} + | {:module_fetch_failure, module} + | {:struct_name_type_mismatch, module | nil, module} + | {:required_field_unfulfilled_map_type_mismatch, entry_type} + | {:type_mismatch, value, entry_type} + + @spec match_type(term, entry_type) :: :ok | {:error, [reason]} def match_type(value, {:type, _, :union, union_types} = union) when is_list(union_types) do results = Enum.reduce_while(union_types, [], fn type, reason_stacks -> diff --git a/lib/ham/type_match_error.ex b/lib/ham/type_match_error.ex index 0ab2bd6..8b5faff 100644 --- a/lib/ham/type_match_error.ex +++ b/lib/ham/type_match_error.ex @@ -4,9 +4,11 @@ defmodule Ham.TypeMatchError do match types defined in typespecs. """ defexception [:reasons] + @type t :: %__MODULE__{} alias Ham.Utils + import Ham.Utils, only: [type_to_string: 1] @impl Exception def exception({:error, reasons}), do: %__MODULE__{reasons: reasons} @@ -123,32 +125,4 @@ defmodule Ham.TypeMatchError do padding <> string end - - defp type_to_string({:type, _, :map_field_exact, [type1, type2]}) do - "required(#{type_to_string(type1)}) => #{type_to_string(type2)}" - end - - defp type_to_string({:type, _, :map_field_assoc, [type1, type2]}) do - "optional(#{type_to_string(type1)}) => #{type_to_string(type2)}" - end - - defp type_to_string(type) do - dbg(type) - # We really want to access Code.Typespec.typespec_to_quoted/1 here but it's - # private... this hack needs to suffice. - {:foo, type, []} - |> Code.Typespec.type_to_quoted() - |> IO.inspect(label: :type_to_quoted) - |> Macro.to_string() - |> IO.inspect(label: :macro_to_string) - |> String.split("\n") - |> Enum.map_join(&String.replace(&1, ~r/ +/, " ")) - |> IO.inspect(label: :before_split) - |> String.split(" :: ", parts: 3) - |> IO.inspect(label: :split) - |> case do - [_, type_string] -> type_string - [_, type_name, type_string] -> "#{type_string} (\"#{type_name}\")" - end - end end diff --git a/lib/ham/utils.ex b/lib/ham/utils.ex index 18198cb..bb47131 100644 --- a/lib/ham/utils.ex +++ b/lib/ham/utils.ex @@ -63,4 +63,38 @@ defmodule Ham.Utils do defp suffix(2), do: "nd" defp suffix(3), do: "rd" defp suffix(_), do: "th" + + def type_to_string(type) do + # We really want to access Code.Typespec.typespec_to_quoted/1 here but it's + # private... this hack needs to suffice. + macro = + {:foo, type, []} + |> Code.Typespec.type_to_quoted() + + macro + |> Macro.to_string() + |> String.split("\n") + |> Enum.map_join(&String.replace(&1, ~r/ +/, " ")) + |> String.split(" :: ", parts: type_parts(macro)) + |> case do + [_foo, type_string] -> type_string + [_foo, type_name, type_string] -> "#{type_string} (\"#{type_name}\")" + end + end + + def type_to_string({:type, _, :map_field_exact, [type1, type2]}) do + "required(#{type_to_string(type1)}) => #{type_to_string(type2)}" + end + + def type_to_string({:type, _, :map_field_assoc, [type1, type2]}) do + "optional(#{type_to_string(type1)}) => #{type_to_string(type2)}" + end + + defp type_parts(macro) do + case macro do + # The type has a name + {:"::", [], [{:foo, [], []}, {:"::", _, _}]} -> 3 + _ -> 2 + end + end end diff --git a/src/test.erl b/src/test.erl new file mode 100644 index 0000000..48e2fae --- /dev/null +++ b/src/test.erl @@ -0,0 +1,6 @@ +-module(test). +-type req() :: #{ + binary() => binary(), + _ => _ +}. +-export_type([req/0]). diff --git a/test/ham/type_checker_test.exs b/test/ham/type_checker_test.exs index 41aa8a3..2442b80 100644 --- a/test/ham/type_checker_test.exs +++ b/test/ham/type_checker_test.exs @@ -1274,11 +1274,16 @@ defmodule Ham.TypeCheckerTest do describe "multiple options" do test "pass" do - assert_pass(:foo_multiple_options, [], {:ok, "a", "b"}) + assert_pass(:foo_multiple_options_annotated, [], {:ok, "a", "b"}) end test "fail" do - assert_fail(:foo_multiple_options, [], :ok, "error") + assert_fail( + :foo_multiple_options_annotated, + [], + :ok, + ~r/type \{:ok, binary\(\), req :: binary\(\)\} | \{:more, binary\(\), req :: binary\(\)\} \("result"\)/ + ) end end diff --git a/test/ham/utils_test.exs b/test/ham/utils_test.exs new file mode 100644 index 0000000..e471345 --- /dev/null +++ b/test/ham/utils_test.exs @@ -0,0 +1,17 @@ +defmodule Ham.UtilsTest do + use ExUnit.Case, async: true + alias Ham.Utils + + describe "type_to_string/1" do + test "complex type" do + {:ok, [type: {:complex_with_name, type_with_name, _}, type: {:complex, type, _}]} = + Code.Typespec.fetch_types(Complex) + + assert Utils.type_to_string(type) == + "{:ok, binary(), req :: binary()} | {:more, binary(), req :: binary()}" + + assert Utils.type_to_string(type_with_name) == + "{:ok, binary(), req :: binary()} | {:more, binary(), req :: binary()} (\"result\")" + end + end +end diff --git a/test/support/module.ex b/test/support/module.ex index 5ffb8ef..217937e 100644 --- a/test/support/module.ex +++ b/test/support/module.ex @@ -244,7 +244,17 @@ defmodule Ham.Test.TestModule do @spec map_type_with_underscore() :: :test.req() def map_type_with_underscore, do: %{method: "GET"} + @spec foo_multiple_options_annotated :: + result :: {:ok, binary(), req :: binary()} | {:more, binary(), req :: binary()} + def foo_multiple_options_annotated, do: {:ok, "a", "b"} + @spec foo_multiple_options :: - {:ok, binary(), req :: binary()} | {:more, binary(), req :: binary()} + result :: {:ok, binary(), req :: binary()} | {:more, binary(), req :: binary()} def foo_multiple_options, do: {:ok, "a", "b"} end + +defmodule Complex do + @type complex :: {:ok, binary(), req :: binary()} | {:more, binary(), req :: binary()} + @type complex_with_name :: + result :: {:ok, binary(), req :: binary()} | {:more, binary(), req :: binary()} +end