The Shoemaker’s Son

Time for a new Erlang story, kids! This one is about one of my favourite Erlang tools: dialyzer.

You may know this already, but I always recommend checking all your Erlang code with dialyzer as soon as you write it. You can hear me preaching that exact thing at this Functional Geekery interview. We’re so fond of these tools (like dialyzer, xref and elvis) at Inaka, that we even created Meta Test Suites that you can use to run dialyzer as part of your common test suites every time.

And that’s how this story begins…

Adam Sandler — The Cobbler (2014)

Running Dialyzer

Dialyzer is both a shell tool that you can run on your console and, of course, en Erlang application that you can run from within your Erlang VMs. For Katana-Test’s meta test suite, we needed to run dialyzer from code, as you might have expected.

A simplified version of what we needed to do is shown below…


That module contains 2 main functions: plt/0 and run/1. They both end up calling dialyzer:run/1 with different options. The main difference is analysis_type: While plt/0 is used to build the plt that dialyzer needs to analyze your code, run/1 is used to actually check the successful types in your modules and functions.

dialyzer:run/1 returns a list of warnings in Erlang form (more on that below) that you can later run through dialyzer:format_warning/1,2 to get proper messages in string() format.

So far, so good. Now, let’s see what happens when we try to analyze this module itself, as we tried to do with our meta suite…

1> c(dia, [debug_info]).
2> dia:plt().
Creating PLT dia.plt ... done in 0m11.30s
3> dia:run(["dia.beam"]).
Checking whether the PLT dia.plt is up-to-date... yes
Proceeding with analysis... done in 0m0.15s
dia.erl:28: The call dialyzer:format_warning(Warning::{atom(),{_,_},{_,_}}) breaks the contract (raw_warning()) -> string()

Oh, man! We’re breaking a contract!

What’s going on here?

Dialyzer is telling us that we’re using an inappropriate parameter when calling dialyzer:format_warning/1. That function expects a raw_warning() and we’re passing a tuple as its parameter. Where is that tuple coming from? It’s actually one of the elements that we get in the result from dialyzer:run/1. So let’s check that function spec:

-spec run(dial_options()) -> [dial_warning()].

So, yeah… run/1 returns a list of dial_warning(). But format_warning/1 expects a raw_warning() instead. What can we do? Well… check the spec of format_warning/2:

-spec format_warning(raw_warning() | dial_warning(), fopt()) -> string().

We can totally use this function, then! We just need to find a fopt() that we can use. What’s a fopt() anyway? Check it out!

-type fopt() :: 'basename' | 'fullpath'.

It’s basically determining if the paths printed out in the messages should be absolute or relative. To get the results we want, we can totally use ‘basename’ as the second parameter and be done with it…

new dia.erl

But… wait a second… isn’t that exactly what format_warning/1 is doing?

format_warning(W) ->
format_warning(W, basename).

So… why is format_warning/1 not accepting dial_warning() as its parameter? Probably because the shoemaker’s son always goes barefoot, i.e. the specs on the Erlang module that checks the specs of Erlang modules are wrong ;)

…and now you have a PR to fix that :)

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.