robbplo
Dialyzer: inferring types when using Mox
I ran into this issue the other day and it’s been bugging me ever since. Below is a basic behaviour/implementation, and function which returns a module at runtime. This is the recommended way to decouple your modules when using Mox. It seems to me that this breaks Dialyzer analysis, since no errors are produced with this clearly incorrect typing.
defmodule MyBehaviour do
@callback get_integer() :: integer
end
defmodule MyImplementation do
@behaviour MyBehaviour
@impl MyBehaviour
def get_integer, do: 100
end
defmodule DialyzerMoxBehaviours do
@moduledoc """
Using the recommended implementation of a behaviour for Mox, Dialyzer is unable to infer the type of `get_integer()`.
By decoupling this module from `MyImplementation`, we lose the typespec information from the behaviour.
"""
@spec dialyze_me() :: binary
def dialyze_me, do: "hello" # valid type
@spec dialyze_me_two() :: binary
def dialyze_me_two, do: impl().get_integer() # invalid type, but dialyzer does not complain.
@spec impl() :: MyBehaviour
defp impl(), do: Application.get_env(:dialyzer_mox_behaviours, :impl, MyImplementation)
end
Am I missing something? I like using Mox but this seems like a major drawback to me. If anyone else has encountered this issue I’m interested in how you’ve solved it.
Marked As Solved
LostKobrakai
Mox.stub_with can help there.
Also Liked
LostKobrakai
The approach as stated does loose type information, but not because of mox or behaviours, but because of the runtime selection of the used implementation. No static analysis can catch errors based on information not available statically in the code.
If you make impl() return MyImplementation statically or correctly hardcode the typespec return value of it as MyImplementation then dialyzer can help you. But loosening the type information to not return a specific module means dropping to any module(), which is an alias for atom(). There’s nothing to check if the returned module is unknown.
The typesystem dialyzer uses doesn’t allow you to say “but the module returned implements that behaviour”. It’s either one specific module or any module for that typesystem.
Given the typesystem doesn’t help you can however go the “statically known” route. If you don’t need runtime selection you can compile the configured implementation into the module statically.
@impl Application.compile_env(:dialyzer_mox_behaviours, :impl, MyImplementation)
@spec impl() :: module()
defp impl(), do: @impl







