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

1

1

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!

Elbertelberta answered 14/11, 2018 at 14:24 Comment(3)
Do you get the same Dialyzer error if you comment out the calls to Notifier.notify?Hagiographer
@Hagiographer oddly no I don't. If I go and check that error there's a dialyzer error in that function as well (which I have a PR out for an open source library to fix). So is that the issue? Dialyzer can't analyze the other 2 paths, thus the {:ok, balances} branch is the only one it's aware of?Elbertelberta
Exactly. Since Dialyzer thinks that Notifier.notify always crashes, it concludes that your function can never actually return :internal_server_error.Hagiographer
E
5

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.

Elbertelberta answered 14/11, 2018 at 15:10 Comment(1)
SO won't let me for a few days, I'll do so then.Elbertelberta

© 2022 - 2024 — McMap. All rights reserved.