"Dialyzer is usually never wrong", but I can't figure out how my @spec is incorrect

1.1k views Asked by At

I have some code that is failing dialyzer and I cannot understand why. No matter what I put into the @spec at the top of the function, calls to that function return a puzzling dialyzer error. Here is a simplification of the function. As far as I can tell, I have spec'd the function correctly.

@spec balances(uuid :: String.t(), retries :: non_neg_integer) ::
        {:ok, list()}
        | {:internal_server_error, String.t(), String.t()}
        | {:internal_server_error, map | list, String.t()}
def balances(uuid, retries \\ 0) do
  url = "/url/for/balances" |> process_url

  case HTTPoison.get(
         url,
         [, {"Content-Type", "application/json"}],
         []
       ) do
    {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
      response = Poison.decode!(body, as: %{"message" => [%Currency{}]})

      cond response["message"] do
        length(bal) > 0 ->
          {:ok, bal}

        retries >= 1 ->
          {:ok, []}

        true ->
          init(uuid)
          balances(uuid, retries + 1)
      end

    {:error, %HTTPoison.Error{reason: reason}} ->
      Notifier.notify(url, reason, Helpers.line_info(__ENV__))
      {:internal_server_error, reason, url}

    {_, %HTTPoison.Response{body: body} = res} ->
      response = Poison.decode!(body)
      Notifier.notify(url, response, Helpers.line_info(__ENV__))

      {:internal_server_error, response, url}
  end
end

My issue is that every call across the codebase to this function is failing if I expect to get anything other than {:ok, balances}:

  user_balances =
    case balances(uuid) do
      {:ok, user_balances} -> user_balances
      _ -> [] # Dialyzer error here
    end

Dialyzer warns that The variable _ can never match since previous clauses completely covered the type {'ok',[map()]}. I read this to mean that any call to balances will always return {:ok, balances}, but that can't be true as the case statement for HTTPoison.get is the last thing evaluated in the function, and it appears to have only three possible results:

  • {:ok, list}
  • {:internal_server_error, String.t(), String.t()}
  • {:internal_server_error, map | list, String.t()}.

I understand that I am likely missing something very obvious but I can't figure out what it is. Any help would be greatly appreciated. Thank You!

1

There are 1 answers

1
localshred On BEST ANSWER

Thanks to @legoscia's comment, I investigated the call to Notifier.notify, and sure enough there is a dialyzer warning in that function as well (I have PR out to an open source project to fix the spec that is causing the notify function to fail dialyzer). If I modify the notify function such that no warning occurs, sure enough the calls to balances no longer produce dialyzer warnings.

tl;dr If dialyzer gives you a warning about a function that doesn't appear to be incorrectly specified, start going through the function calls within your function to find a downstream dialyzer error.