Skip to content

Commit

Permalink
Merge pull request #1 from edgurgel/fix/different-fixes
Browse files Browse the repository at this point in the history
Fix type to string and _ => _ usage on maps
  • Loading branch information
edgurgel authored Aug 29, 2024
2 parents 9d98de7 + 035e9be commit 3956dae
Show file tree
Hide file tree
Showing 11 changed files with 112 additions and 28 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Ham

[![CI](https://github.com/edgurgel/ham/actions/workflows/ci.yml/badge.svg)](https://github.com/edgurgel/ham/actions/workflows/ci.yml)
[![Module Version](https://img.shields.io/hexpm/v/edgurgel.svg)](https://hex.pm/packages/ham)
[![Module Version](https://img.shields.io/hexpm/v/ham.svg)](https://hex.pm/packages/ham)
[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/ham)
[![Total Download](https://img.shields.io/hexpm/dt/ham.svg)](https://hex.pm/packages/ham)
[![License](https://img.shields.io/hexpm/l/ham.svg)](https://github.com/edgurgel/ham/blob/main/LICENSE)
Expand Down
9 changes: 6 additions & 3 deletions lib/ham/type_engine.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ defmodule Ham.TypeEngine do
alias Ham.Utils

@type_kinds [:type, :typep, :opaque]
@typep entry_type :: tuple

@spec match_type(term, entry_type) :: :ok | {:error, [term]}
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 ->
Expand All @@ -27,9 +29,10 @@ defmodule Ham.TypeEngine do
end
end

def match_type(_value, {:type, _, :any, []}) do
:ok
end
def match_type(_value, {:type, _, :any, []}), do: :ok

# Special case for %{ _ => _ } supported by Erlang
def match_type(_value, {:var, _, :_}), do: :ok

def match_type(value, {:type, _, :none, []} = type) do
type_mismatch(value, type)
Expand Down
25 changes: 2 additions & 23 deletions lib/ham/type_match_error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -123,27 +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
# 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()
|> Macro.to_string()
|> String.split("\n")
|> Enum.map_join(&String.replace(&1, ~r/ +/, " "))
|> String.split(" :: ")
|> case do
[_, type_string] -> type_string
[_, type_name, type_string] -> "#{type_string} (\"#{type_name}\")"
end
end
end
34 changes: 34 additions & 0 deletions lib/ham/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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, _, :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

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

defp type_parts(macro) do
case macro do
# The type has a name
{:"::", [], [{:foo, [], []}, {:"::", _, _}]} -> 3
_ -> 2
end
end
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Ham.MixProject do
use Mix.Project

@source_url "https://github.com/edgurgel/ham"
@version "0.1.0"
@version "0.2.0"

def project do
[
Expand Down
6 changes: 6 additions & 0 deletions src/test.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-module(test).
-type req() :: #{
binary() => binary(),
_ => _
}.
-export_type([req/0]).
19 changes: 19 additions & 0 deletions test/ham/type_checker_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1272,10 +1272,29 @@ defmodule Ham.TypeCheckerTest do
end
end

describe "multiple options" do
test "pass" do
assert_pass(:foo_multiple_options_annotated, [], {:ok, "a", "b"})
end

test "fail" do
assert_fail(
:foo_multiple_options_annotated,
[],
:ok,
~r/type \{:ok, binary\(\), req :: binary\(\)\} | \{:more, binary\(\), req :: binary\(\)\} \("result"\)/
)
end
end

test "nospec" do
assert_pass(:nospec_fun, [], :ok)
end

test "map with underscore: _ => _" do
assert_pass(:map_type_with_underscore, [], %{"method" => "GET", :foo => "bar"})
end

defp assert_pass(function_name, args \\ [], return_value) do
assert :ok = TypeChecker.validate(TestModule, function_name, args, return_value)

Expand Down
26 changes: 26 additions & 0 deletions test/ham/utils_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
defmodule Ham.UtilsTest do
use ExUnit.Case, async: true
alias Ham.Utils

defp fetch_test_type(type_name) do
{:ok, types} =
Code.Typespec.fetch_types(Ham.Test.TestModule)

Enum.find_value(types, fn
{:type, {^type_name, type, _}} -> type
_ -> nil
end)
end

describe "type_to_string/1" do
test "complex type" do
assert :complex |> fetch_test_type() |> Utils.type_to_string() ==
"{:ok, binary(), req :: binary()} | {:more, binary(), req :: binary()}"
end

test "complex type with annotation" do
assert :complex_annotated |> fetch_test_type() |> Utils.type_to_string() ==
"{:ok, binary(), req :: binary()} | {:more, binary(), req :: binary()} (\"result\")"
end
end
end
2 changes: 2 additions & 0 deletions test/support/behaviour.ex
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,6 @@ defmodule Ham.Test.Behaviour do
@callback foo_opaque_type() :: opaque_type

@callback foo_guarded(arg) :: [arg] when arg: integer()

@callback map_type_with_underscore() :: :test.req()
end
2 changes: 2 additions & 0 deletions test/support/impl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,6 @@ defmodule Ham.Test.Impl do
def foo_opaque_type, do: :opaque_value

def foo_guarded(_arg), do: [1]

def map_type_with_underscore, do: %{method: "GET"}
end
13 changes: 13 additions & 0 deletions test/support/module.ex
Original file line number Diff line number Diff line change
Expand Up @@ -240,4 +240,17 @@ defmodule Ham.Test.TestModule do
def foo_guarded(_arg), do: [1]

def nospec_fun, do: :ok

@spec map_type_with_underscore() :: :test.req()
def map_type_with_underscore, do: %{method: "GET"}

@type complex :: {:ok, binary(), req :: binary()} | {:more, binary(), req :: binary()}
@type complex_annotated ::
result :: {:ok, binary(), req :: binary()} | {:more, binary(), req :: binary()}

@spec foo_multiple_options_annotated :: complex_annotated
def foo_multiple_options_annotated, do: {:ok, "a", "b"}

@spec foo_multiple_options :: complex
def foo_multiple_options, do: {:ok, "a", "b"}
end

0 comments on commit 3956dae

Please sign in to comment.