Dialyxir - Recent Updates and Request for Feedback

I wrote dialyze because I wanted a dialyzer task to make very different decisions to dialyxir. I would have liked to have contributed these changes but I wanted to change nearly everything.

dialyze is designed for zero/minimal configuration, repeatable/accurate warnings, smart PLT creation, sane defaults and to automatically make decisions it knows should be done without manual user commands.

Zero/minimal configuration:

In the majority of cases all the information required to build a PLT already exists if an application adheres to OTP principles. The PLT should contain the dependencies of the project and on application should have all it dependent applications listed in its applications. This means dialyze won’t work if not creating OTP compliant applications. I think a lot of people don’t. For example I think phoenix users can’t use this task very well because phoenix does not generate the correct applications list.

Therefore dialyxir will likely be more appropriate if you are a new user because it will show unknown warnings when run and then the relevant application(s) can be manually added to the configuration. I am probably more anal than average about OTP principles so I was confident for my own use that dialyze wouldn’t be a problem here. However if you need the configuration then dialyxir is your only option.

Repeatable/accurate warnings:

Since the modules added to the PLT are completely controlled by the dialyze task it can change the PLT to reflect changes in dependencies. It does this by adding 2 extra phases to --check-plt that dialyxir doesn’t do. It builds the application dependency tree (by using the zero configuration PLT approach above) and works out which files should be in the PLT. Then it removes any modules that shouldn’t be there, then it checks the modules in the PLT are up to date with the latest versions of the modules and then it adds any missing modules. The 3 stage “check plt” is run every time by default (and only takes about a second if no changes to be made) so the PLT should always be the correct one. This feature might crash for a number of reasons prior to OTP 18.3 due to bugs in dialyzer itself.

Even though dialyxir works at the application level it will only perform the middle change using modules that are already in the PLT. Therefore if deps change adds/removes modules the PLT is out of sync which can cause false positives and negatives. I think the only way to avoid this with dialyxir is to completely rebuild the PLT when a dep changes but please correct me if this is wrong.

I could be wrong but I imagine the very vast majority of dialyxir users aren’t doing this and are missing warnings and getting invalid warnings.

Smart PLT creation:

dialyze will create 1 PLT per project per Elixir version per OTP version. The reason for this is that different projects may use different dependencies and different versions of dependencies. This may sound like it would be very slow but dialyze does smart caching of PLTs so if another project has already created a PLT for that version of Elixir and OTP, or just for OTP, all or part of the PLT can be reused. This means a project that doesn’t have any dependencies except for Elixir (and dependencies of Elixir like :kernel and :stdlib) it will build in a matter of seconds. Without this caching it will probably take a long time, maybe more than 10 minutes. This means getting started using dialyzer on a new project is fast. Because of the smart “check-plt” for repeatable warnings feature (above) the minimal changes are done to a PLT automatically when a dependency is added and the PLT does not need to rebuilt.

On the other hand dialyxir uses a global PLT per OTP version, which means you would can only use one global version of elixir and deps. Otherwise you will need to rebuild the PLT or end up using the incorrect PLT! It is configurable to avoid this but it is not the default. This default could mean using another project could break the PLT usage for different project.

I could be wrong but I imagine for many dialyxir users they don’t realise that PLTs from one project can interact with another. And for all dialyxir users that have realised this and set local PLTs they will have had to spend a lot of extra time waiting for PLTs to build.

Sane defaults:

As well as the (forced) defaults above dialyze does not add any extra warnings that dialyzer does not run. Apart from opting out of checking the PLT or opting out of running analysis this is the only part of dialyze that can be configured and requires command line arguments to turn warnings on and off.

However dialyxir adds extra default warnings: "-Wunmatched_returns", "-Werror_handling", "-Wrace_conditions", "-Wunderspecs". These warnings are not on by default because success typing should not produce false positives. All of these except -Wrace_conditions can cause them. These warnings are extra features that dialyzer is able to produce.

The first "-Wunmatched_returns" is a useful warning but perhaps a little to strict for a beginner, in most cases of this warning it is fine to ignore the return of a function - false positive. Requiring _ = call() in these cases is of questionable style. I like it but some don’t.

The second "-Werror_handling" can lead to false positive because it might be intentional that a function will always raise. I am unsure of a case where dialyzer won’t producer other warnings when this is not intentional.

The third "-Wrace_conditions" tries to find race conditions in the code. I am unsure what the warnings are because it has never found any in my code. Please let me know if it has found any for you. However it can add a lot of extra time to running dialyzer, I think it tries brute force. Some people have even reported it get stuck in an apparent infinite loop on the erlang mailing list. Therefore I would advise against running this warning. If you need race condition testing try concuerror it is much better at this job.

The fourth "-Wunderspecs" shows when specifications are too allowing. It may not be practical or helpful to use the more specific specification, i.e. false positives. For example a macro may return a subset of Macro.t and this warning will appear if dialyzer a subset that it can produce. However to a user of the macro it should only be applicable that the macro returns Macro.t. Enforcing such a strict spec often leads to requiring a backwards incompatible change in spec for what would otherwise be a backwards compatible change.

Automatically run commands that need to be run:

If a computer can work out that something needs to run then it should run it for the user without being prompted. dialyze will automatically do everything required: ensure the PLT is using the current deps and is up to date unless explicitly told not.

dialyxir requires manually building and checking the PLT. Even if checking was automatic the check would not be enough to ensure the PLT is up to date with deps.

Another difference is that dialyzer runs dialyzer inside the same VM. Whereas dialyxir starts another VM. I am unsure if there are bugs here like getting the wrong version of Erlang but if dialyer is slow and the user decides to kill mix, the extra VM will keep running and wont exit until the analysis finishes. You will need to look up the processes and kill it manually. There is a benefit to using the CLI though, unknown warnings are shown by default, whereas for the erlang API there are not and have to be turned on. Therefore dialyze does not show unknown warnings by default but dialyxir does.

Importantly dialyze is no longer maintained because no one was contributing and only people complaining. Instead I maintain the rebar3 provider which has all the features of dialyze (but is faster) plus the same (and more) configuration that dialyxir has. The rebar3 provider also adds various colours to different parts of the warnings to make them easier to understand and groups warnings by module.

I would suggest that any one interested in using dialyzer in elixir gets behind dialyxir and resolves the issues I have mentioned above. Otherwise the experience of using dialyzer will remain poor (and IMO broken) in elixir: Wasting ~10 minutes per PLT build (after the first global) that is not going to be managed properly and generate false positives and false negatives. Once that has been handled people may want to look at formatting the errors in a clearer style.

6 Likes