Repository: msz/hammox (elixir)

Issue:
Errors on Parametrized Types
From `Ecto.Changeset`, there is a `t()` defined such as:

```elixir
  @type t(data_type) :: %Changeset{valid?: boolean(),
                        ...
                        data: data_type,
                        ...
                        types: nil | %{atom => Ecto.Type.t}}
```

In our behaviour, we have something like:

```elixir
  @type user_changeset :: Ecto.Changeset.t(User.t())

  @callback password_operation(user_changeset) :: user_changeset
  @callback password_operation(user_changeset, attrs :: map) :: user_changeset
```

In our implementation:

```elixir
  @type user_changeset :: Ecto.Changeset.t(User.t())
  @impl CredentialsHelper
  @spec password_operation(user_changeset) :: user_changeset
  @spec password_operation(user_changeset, map) :: user_changeset
  def password_operation(cs, attrs \\ %{}),
    do: ...
```

When trying to `stub` or `expect`, Hammox raises:

```
     ** (FunctionClauseError) no function clause matching in Hammox.TypeEngine.match_type/2

     The following arguments were given to Hammox.TypeEngine.match_type/2:
     
         # 1
         nil
     
         # 2
         {:var, 282, :data_type}
```

Note: we tried every possible combination of typing here - directly `Ecto.Changeset.t`, `Ecto.Changeset.t(any)` etc. Only replacing the `changeset` type by `any` *on the behaviour* seems to fix the problem.


Pull request:
fix multiline parametrized types
Fixes #29.

There was a bug where during looking for the param name in the parametrized type definition to replace it with type arg, we were matching against the full syntax tuple of the original declaration of the param, including the line number information. This meant that if the definition of the parametrized type spanned multiple lines, the syntax tuple of the occurrence would be different from the original declaration tuple.

This changes the behaviour to only match the param name.

Patch:
diff --git a/lib/hammox/type_engine.ex b/lib/hammox/type_engine.ex
index cfef527..cab981d 100644
--- a/lib/hammox/type_engine.ex
+++ b/lib/hammox/type_engine.ex
@@ -672,8 +672,10 @@ defmodule Hammox.TypeEngine do
   end
 
   defp fill_type_var(type, var, arg) do
+    {:var, _, var_name} = var
+
     Utils.type_map(type, fn
-      ^var -> arg
+      {:var, _, ^var_name} -> arg
       other -> other
     end)
   end


Test patch:
diff --git a/test/hammox_test.exs b/test/hammox_test.exs
index 0076fac..6279591 100644
--- a/test/hammox_test.exs
+++ b/test/hammox_test.exs
@@ -1157,6 +1157,12 @@ defmodule HammoxTest do
     end
   end
 
+  describe "multiline parametrized types" do
+    test "works when param usage is on a line other than declaration line" do
+      assert_pass(:foo_multiline_param_type, %{value: :arg})
+    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 2957771..aa8ab8f 100644
--- a/test/support/behaviour.ex
+++ b/test/support/behaviour.ex
@@ -113,4 +113,9 @@ defmodule Hammox.Test.Behaviour do
   @type param_type_1(arg1) :: arg1
   @type param_type_2(arg2) :: param_type_1(arg2)
   @callback foo_nested_param_types() :: param_type_2(:param)
+
+  @type multiline_param_type(param) :: %{
+          value: param
+        }
+  @callback foo_multiline_param_type() :: multiline_param_type(:arg)
 end