Repository: msz/hammox (elixir)

Issue:
Hammox breaks when there is a @typep
This code in type_engine.ex accepts only `@type`:

```elixir
  defp get_type(type_list, type_name, arity) do
    case Enum.find(type_list, fn {:type, {name, _type, params}} ->
           name == type_name and length(params) == arity
         end) do
      nil -> {:error, {:type_not_found, {type_name, arity}}}
      type -> {:ok, type}
    end
  end
```

However, Typespecs accepts `@type`, `@typep` and `@opaque` ([source](https://hexdocs.pm/elixir/typespecs.html#user-defined-types)). This causes hammox to break:

```elixir
     ** (FunctionClauseError) no function clause matching in anonymous fn/1 in Hammox.TypeEngine.get_type/3

     The following arguments were given to anonymous fn/1 in Hammox.TypeEngine.get_type/3:

         # 1
         {:typep, {:names, {:remote_type, 133, [{:atom, 0, MapSet}, {:atom, 0, :t}, []]}, []}}

     stacktrace:
       (hammox 0.2.4) lib/hammox/type_engine.ex:695: anonymous fn/1 in Hammox.TypeEngine.get_type/3
       (elixir 1.10.2) lib/enum.ex:3305: Enum.find_list/3
       (hammox 0.2.4) lib/hammox/type_engine.ex:695: Hammox.TypeEngine.get_type/3
       (hammox 0.2.4) lib/hammox/type_engine.ex:656: Hammox.TypeEngine.resolve_remote_type/1
       (hammox 0.2.4) lib/hammox/type_engine.ex:621: Hammox.TypeEngine.match_type/2
       (hammox 0.2.4) lib/hammox.ex:312: Hammox.match_return_value/2
       (hammox 0.2.4) lib/hammox.ex:280: Hammox.match_call/3
       (hammox 0.2.4) lib/hammox.ex:264: anonymous fn/4 in Hammox.check_call/3
       (elixir 1.10.2) lib/enum.ex:3686: Enumerable.List.reduce/3
       (elixir 1.10.2) lib/enum.ex:2161: Enum.reduce_while/3
       (hammox 0.2.4) lib/hammox.ex:263: Hammox.check_call/3
       (hammox 0.2.4) lib/hammox.ex:256: Hammox.protected_code/3
```

In my case, I'm using `Ecto.Multi.t()` as the return value of a function, and this structure defines `@typep`: https://github.com/elixir-ecto/ecto/blob/v3.4.4/lib/ecto/multi.ex#L133

Pull request:
support all type kinds
Fixes #41.

Hammox blows up when encountering `@typep` and `@opaque`. For now we will treat them like any other type.

Further work on opaque types in #43 

Patch:
diff --git a/lib/hammox/type_engine.ex b/lib/hammox/type_engine.ex
index cab981d..14fac90 100644
--- a/lib/hammox/type_engine.ex
+++ b/lib/hammox/type_engine.ex
@@ -3,6 +3,8 @@ defmodule Hammox.TypeEngine do
 
   alias Hammox.Utils
 
+  @type_kinds [:type, :typep, :opaque]
+
   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 ->
@@ -653,7 +655,8 @@ defmodule Hammox.TypeEngine do
        )
        when is_atom(module_name) and is_atom(type_name) and is_list(args) do
     with {:ok, types} <- fetch_types(module_name),
-         {:ok, {:type, {_name, type, vars}}} <- get_type(types, type_name, length(args)) do
+         {:ok, {type_kind, {_name, type, vars}}} when type_kind in @type_kinds <-
+           get_type(types, type_name, length(args)) do
       resolved_type =
         args
         |> Enum.zip(vars)
@@ -688,7 +691,8 @@ defmodule Hammox.TypeEngine do
   end
 
   defp get_type(type_list, type_name, arity) do
-    case Enum.find(type_list, fn {:type, {name, _type, params}} ->
+    case Enum.find(type_list, fn {type_kind, {name, _type, params}}
+                                 when type_kind in @type_kinds ->
            name == type_name and length(params) == arity
          end) do
       nil -> {:error, {:type_not_found, {type_name, arity}}}


Test patch:
diff --git a/test/hammox_test.exs b/test/hammox_test.exs
index 6279591..20d8013 100644
--- a/test/hammox_test.exs
+++ b/test/hammox_test.exs
@@ -1163,6 +1163,26 @@ defmodule HammoxTest do
     end
   end
 
+  describe "private types" do
+    test "pass" do
+      assert_pass(:foo_private_type, :private_value)
+    end
+
+    test "fail" do
+      assert_fail(:foo_private_type, :other)
+    end
+  end
+
+  describe "opaque types" do
+    test "pass" do
+      assert_pass(:foo_opaque_type, :opaque_value)
+    end
+
+    test "fail" do
+      assert_fail(:foo_opaque_type, :other)
+    end
+  end
+
   defp assert_pass(function_name, value) do
     TestMock |> expect(function_name, fn -> value end)
     assert value == apply(TestMock, function_name, [])
diff --git a/test/support/behaviour.ex b/test/support/behaviour.ex
index aa8ab8f..aa69862 100644
--- a/test/support/behaviour.ex
+++ b/test/support/behaviour.ex
@@ -118,4 +118,11 @@ defmodule Hammox.Test.Behaviour do
           value: param
         }
   @callback foo_multiline_param_type() :: multiline_param_type(:arg)
+
+  @typep private_type :: :private_value
+  @type type_including_private_type :: private_type()
+  @callback foo_private_type() :: type_including_private_type()
+
+  @opaque opaque_type :: :opaque_value
+  @callback foo_opaque_type() :: opaque_type
 end