Erlang/OTP 24 [erts-12.0.3] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [jit] [dtrace]
Elixir 1.12.2 (compiled with Erlang/OTP 24)
Hey folks!
I’ve noticed the following bug with Dialyzer when incorporating behaviors:
defmodule Example.Behavior do
@type error_reasons ::
:access_denied
| :bad_config
@type error :: {:error, error_reasons()}
@type execution_result :: {:ok, any()} | error()
@callback some_method(some_bool :: boolean()) :: execution_result()
end
defmodule Example.Behavior.Impl do
@behaviour Example.Behavior
@impl true
def some_method(some_bool) do
if some_bool do
{:error, :why_does_this_not_break_spec}
else
{:ok, "test"}
end
end
end
It seems that if at least one of the return types in the impl some_method
matches the spec in the behavior, then the dialyzer does not complain. However, if none of the return types match, dialyzer flags as expected:
defmodule Example.Behavior.Impl do
@behaviour Example.Behavior
@impl true
def some_method(some_bool) do
if some_bool do
{:error, :why_does_this_not_break_spec}
else
{:error, :blah}
end
end
end
Type mismatch for @callback some_method/1 in Example.Behavior behaviour.
Expected type:
{:error, :access_denied | :bad_config} | {:ok, _}
Actual type:
{:error, :blah | :why_does_this_not_break_spec}
If the impl
method is inheriting the behavior’s spec, which I assume it is doing behind the scenes, then I would expect it to flag as it does with any normal typespec return type mismatches:
defmodule Example do
@type error_reasons ::
:access_denied
| :bad_config
@type error :: {:error, error_reasons()}
@type execution_result :: {:ok, any()} | error()
@spec some_method(some_bool :: boolean()) :: execution_result()
def some_method(some_bool) do
if some_bool do
{:error, :flags_dialyzer}
else
{:ok, "test"}
end
end
end
The type specification has too many types for the function.
Function:
Example.some_method/1
Extra type:
{:error, :access_denied | :bad_config}
Success typing:
{:error, :flags_dialyzer} | {:ok, <<_::32>>}
Interestingly, when defining the spec in the behavior impl, this does not break either:
defmodule Example.Behavior do
@type error_reasons ::
:access_denied
| :bad_config
@type error :: {:error, error_reasons()}
@type execution_result :: {:ok, any()} | error()
@callback some_method(some_bool :: boolean()) :: execution_result()
end
defmodule Example.Behavior.Impl do
@behaviour Example.Behavior
@impl true
@spec some_method(some_bool :: boolean()) :: Example.Behavior.execution_result()
def some_method(some_bool) do
if some_bool do
{:error, :why_does_this_not_break_spec}
else
{:ok, "test"}
end
end
end
But this does:
defmodule Example.Behavior.Impl do
@behaviour Example.Behavior
@impl true
@spec some_method(some_bool :: boolean()) :: {:error, :access_denied | :bad_config} | {:ok, any()}
def some_method(some_bool) do
if some_bool do
{:error, :breaks_spec}
else
{:ok, "test"}
end
end
end
The type specification has too many types for the function.
Function:
Example.Behavior.Impl.some_method/1
Extra type:
{:error, :access_denied | :bad_config}
Success typing:
{:error, :breaks_spec} | {:ok, <<_::32>>}