Dialyzer bug with Behaviors?

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>>}

Spec does not break even without embedded typing:

defmodule Example.Behavior do
  @callback some_method(some_bool :: boolean()) :: {:ok, any()} | {:error, :access_denied | :bad_config}
end

defmodule Example.Behavior.Impl do
  @behaviour Example.Behavior

  @impl true
  def some_method(some_bool) do
    if some_bool do
      {:error, :should_break_spec}
    else
      {:ok, "test"}
    end
  end
end

For folks who see this in the future, see @kokolegorille post Type and spec - Dialyzer not detecting error

If at least one type matches, then Dialyzer will not generate a warning. See this base case:

@spec hello(integer()) :: map()
  def hello(int) do
    case int do
      1 -> %{hello: "world"}
      _ -> :doesnterror
    end
  end

Regarding the The type specification has too many types for the function errors, this is just because a spec is being defined with too many types for the function being created. The evaluation is different when behaviors are included.

2 Likes