Dialyzer can be a tricky tool to figure out, but it can also be really helpful in spotting bugs or inaccurate typespecs for us, so I’m going to go through a couple of the more common warnings that you’ll see in your daily use of Dialyzer to help understand what’s happening, some of the causes of those common warnings, and how you can resolve them.
For this series I’ll be using dialyxir
1.0.0-rc4, and I’m going to show both
the new Elixir formatted warnings and the old Erlang formatted warnings for
these cases. If you’re not using dialyxir
1.0.0 yet, I highly recommend it!
The formatting of warnings is much more helpful than the original
formatting.
Here’s the code that we’re going to be using for the examples today:
defmodule Test
@spec concat(atom(), String.t()) :: String.t()
def concat(first, second) do
Atom.to_string(first) <> second
end
@spec call_concat() :: String.t()
def call_concat() do
concat("string", :atom)
end
end
That small bit of code has one fairly obvious bug, but because of that bug Dialyzer gives us two warnings, and these two are among the more common (and important!) warnings you’ll see. One bug causing multiple warnings in Dialyzer is a fairly common situation, by the way, so when you’re looking at your Dialyzer output, trying to group things together can often give you much more information about how to resolve those warnings than trying to tackle one warning at a time.
Let’s start with arguably the most important - but also most confusing -
warnings: no_return
.
no_return
When we run Dialyzer on that very small program, one of the warnings we see looks like this with the new formatting:
lib/my_app/test.ex:8:no_return
Function call_concat/0 has no local return.
or like this with the old Dialyzer formatting:
lib/my_app/test.ex:8: Function call_concat/0 has no local return
This warning is telling us that the function call_concat/0
does not return.
Basically, there is no way that you can call this function and have it do
anything other than raise an exception.
This usually happens in two situations:
1) We’re always raising an exception on purpose 2) We’re always raising an exception even though we don’t intend to
If your function is intentionally raising an exception every time it’s called,
you can resolve this warning by explicitly annotating that in the spec for that
function like so: @spec call_concat() :: no_return()
.
But it’s that second case where Dialyzer is most helpful here. It’s screaming at us and saying “You have a serious bug here!”
It’s important to note here that Dialyzer can only tell us that we definitely have a bug here because it knows for a fact what the values of the variables in that example are because we have a string and atom literal. For example, if we have this code:
defmodule Test do
@spec concat(atom(), String.t()) :: String.t()
def concat(first, second) do
Atom.to_string(first) <> second
end
@spec call_concat(String.t(), atom()) :: String.t()
def call_concat(string, atom) do
concat(string, atom)
end
end
Then Dialyzer won’t give us a no_return
warning. It gives us a different
warning, but it can’t say for certain that there’s a function that will not
return because an exception was raised. Only if call_concat/2
were called
somewhere with something that Dialyzer can determine the type of with 100%
confidence (usually a literal) will it give us the no_return
warning.
So, Dialyzer says we have a bug, but unfortunately it’s not really telling us where that bug is in this warning. In order to resolve this warning we need to fix our bug, but that’s gonna be pretty tricky to find with just this information. Luckily, that’s where the next fairly common warning comes in.
call
The second warning that we get for that code is:
lib/test.ex:9:call
The call:
Test.concat("string", :atom)
will never return since the success typing is:
(atom(), binary()) :: binary()
and the contract is
(atom(), String.t()) :: String.t()
or:
lib/test.ex:9: The call 'Elixir.PotionProxy.Client.MainWorker':concat(#{#<115>(8, 1, 'integer', ['unsigned', 'big']), #<116>(8, 1, 'integer', ['unsigned', 'big']), #<114>(8, 1, 'integer', ['unsigned', 'big']), #<105>(8, 1, 'integer', ['unsigned', 'big']), #<110>(8, 1, 'integer', ['unsigned', 'big']), #<103>(8, 1, 'integer', ['unsigned', 'big'])}#,'atom') will never return since the success typing is (atom(),binary()) -> binary() and the contract is (atom(),'Elixir.String':t()) -> 'Elixir.String':t()
This example really shows how much more helpful the new formatting is!
So, what this warning is telling us (and telling us much more clearly in the new
formatting) is that we’re calling concat/2
with "string"
and :atom
as
arguments, but that it will never return if we call it with those arguments.
Ok, now we’re getting somewhere - this looks like it’s the function call that’s
causing our bug! The unfortunate thing here is that this warning is directly
related to the no_return
warning that we just went over, but there’s sadly no
connection to that in the messages. In the message for no_return
it was
warning about call_concat
, but now in the message for call
it’s warning
about concat
. At first glance these two warnings don’t seem connected, and yet
they are.
The one helpful thing that’s always good to keep in mind is that the warnings
are grouped by file and ordered by line number, so if you see a
no_return
warning and then below that a call
warning a couple lines down in
the same file, there’s a good chance the two warnings are related somehow.
So, back to that warning. It’s saying that what we’re trying to do is never
going to work. It then tells us what the success typing is for that concat/2
function, which in this case is (atom(), binary()) :: binary()
, and the
contract for that function which is slightly different
(atom(), String.t()) :: String.t()
.
The difference between a success typing and a contract is the success typing is
the absolute minimum requirement that Dialyzer has worked out that are
necessary for that function to not fail, while the contract is the requirement
for using that function that a human programmer has written. Because humans
frequently make mistakes, and because Dialyzer’s success typing is sometimes not
able to be as specific as a human, it’s generally a good idea to take whichever
of the two options is more strict when you’re deciding on how to use a
particular function. In this case, String.t()
is more strict than just
binary
, so you should probably only call concat
with strings and not any old
binary.
So, how to resolve this warning? Well, you need to use the function according
to how that function wants to be used! In this case, we need to change
concat("string", :atom)
to concat(:atom, "string")
so the arguments are in
the correct order. Once we’ve done that we can re-run Dialyzer and see that we
now have no more warnings - bug found and fixed!