jeremyjh

jeremyjh

Dialyxir - Recent Updates and Request for Feedback

Today I dusted off my Elixir compiler and made a couple of updates to Dialyxir that had long been asked for and the updates are released on hex.pm in version 0.3.5. I’ve seen posts about issues due to lack of these features so thought I might bring them to attention:

0.3.5 Features

* Option to include transitive dependencies (your entire dependency graph) in the PLT (plt_add_deps: :transitive)
* Check the PLT for required updates to existing dependencies when you run mix dialyzer.plt 

Other notable changes in the last 6-7 months include improved umbrella project support (thanks vadim-moz), automatic compilation (seriously this took 2.5 years Jeremy?!) and the --halt-exit-status option (thanks to schnittchen) which will give you the return code from Dialyzer for CI tools.

0.4.0 and Dependency Changes

I also wanted to get some feedback on a specific question I have about potentially changing some default behaviors in the 0.4 release with regard to dependency configuration.

From the beginning it seemed obvious that configuration to control which OTP applications get built into the PLT is required - including them all would simply take too long and most projects only need a handful. I settled on a few specific ones that are commonly used and were suggested in a mailing list post, and added mechanisms to replace or add to this list.

I handled project dependencies the same way without giving it a whole lot of thought. I started this project in May 2013 (Elixir 0.8.2); and at that time most dependency graphs were quite shallow. The personal project I was working on at the time had only two dependencies outside of OTP - one of those was only macros.

I threw in the plt_add_deps option as an afterthought - if you specified a plt_add_deps: true it would add your mix project dependencies to the PLT when the dialyzer.plt task is run. After seeing some questions that come into the issue tracker, here and elsewhere I tend to think it should be actually be the default to do this.

I think one reason I did not make plt_add_deps true by default is that I’ve never been sure its a great idea to put project dependencies into a shared PLT file, and I’ve always been a fan of the shared PLT because it takes so long to build a base PLT with OTP and Elixir libraries. I’ve thought probably if you want to add your dependencies to the PLT, probably you should use the plt_file: “mypltfile.plt” to specific a private project PLT.

But fishcakez came up with a better solution to this two years ago; his library uses a shared core PLT but copies it to a local path (in _build) and then adds the project dependencies. So, I’m thinking I will implement this in the 0.4.0 release but would like some feedback. I’d also like to consider whether I should just go ahead and include transitive dependencies by default.

Transitive Dependency “Controversy”

My initial stance on transitive dependencies was that they don’t belong in your Elixir project code - if you plan to call a function in a module then you are treating it as a direct dependency, so its best to add it to your deps list.

The thing is, not everyone agrees. At least, the Phoenix project generator doesn’t agree. It has several transitive dependencies that are used in the generated code such as ecto and plug - these get pulled in by other dependencies when you build your deps but they are not in the deps list in the generated mix.exs. In order to successfully dialyze a newly generated Phoenix project you’ve got to track these down and add them to your project file, then run dialyzer.plt again. With today’s release now you can use plt_add_deps :transitive to add them all automatically, and it works great.

I think having a smooth experience for new Phoenix and Dialyzer users would be a good thing. If you could gen a Phoenix project, add Dialyxir to it, successfully generate the correct PLT and start benefiting straight away from static analysis without learning any more about the blizzard of Dialyxir configuration options I think there would be more adoption.

The only downside is a lot of projects do not need or expect transitive dependencies to be added to their PLT as it adds a lot of time. If I made transitive a default and you are using plt_add_deps: true then that won’t change - this would continue to use the :project configuration which would only include direct dependencies. But perhaps the default if you don’t have any plt_add_deps key in your configuration at all, should be to include transitive dependencies. This is what I need the most feedback on.

Any thoughts and suggestions on this or other related topics is most appreciated in this thread!

Most Liked

fishcakez

fishcakez

Ecto Core Team

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.

jeremyjh

jeremyjh

Sure, it is really straightforward.

Assuming you’ve just generated a new Phoenix project.

Edit your mix.exs:

In deps add:

{:dialyxir, "~> 0.3.5"}

In project add:

dialyzer: [plt_add_deps: :transitive]

At the command line run:

mix do deps.get, deps.compile, dialyzer.plt

Now you are all set, you can run mix dialyzer anytime you want to check your project.

except…

Currently there are some spurious warnings in the default project. At least some of these will be fixed upstream, but for now you need to add a couple annotations to get rid of them.

Example file names for a project named myapp:

In lib/myapp/repo.ex add the @dialyzer attribute as below:

defmodule Myapp.Repo do
  use Ecto.Repo, otp_app: :myapp
  @dialyzer {:nowarn_function, rollback: 1}
end

Similarly, add this attribute in web/gettext.ex:

  @dialyzer [{:nowarn_function, 'MACRO-dgettext': 3},
             {:nowarn_function, 'MACRO-dgettext': 4},
             {:nowarn_function, 'MACRO-dngettext': 5},
             {:nowarn_function, 'MACRO-dngettext': 6},
            ]

The default PageView module (which includes web/page/index.html.eex) will emit the warning:
index.html.eex:1: The pattern {'safe', _@2} can never match the type binary()

Add @dialyzer :no_match to your PageView to resolve it.

web/views/page_view.ex

defmodule Myapp.PageView do
  use Chat.Web, :view
  @dialyzer :no_match
end
jeremyjh

jeremyjh

Good idea, I added a wiki page for it and put a plug for it in the Github readme.

Where Next?

Popular in Announcing Top

OvermindDL1
I created a new library (rather I pulled out a couple files from my big project), it manages an operating system PID file for the BEAM. ...
New
dbern
I’m excited to announce that TaxJar has developed and open-sourced DateTimeParser. We developed it because we found a need to parse user ...
New
mplatts
With HEEX released we decided to start a components library using Tailwind CSS - check it out here: Petal Components. We also have a boi...
New
devonestes
Introducing assertions, the library that helps you write really great test assertions! GitHub: GitHub - devonestes/assertions: Helpful a...
New
brainlid
LangChain is short for Language Chain. An LLM, or Large Language Model, is the “Language” part. This library makes it easier for Elixir a...
New
Crowdhailer
Experimenting with this code. OK.try do user <- fetch_user(1) cart <- fetch_cart(1) order = checkout(cart, user) save_orde...
New
MRdotB
I needed to reuse React components from my Chrome extension in my Phoenix/LiveView backend. I noticed that for Svelte/Vue, there are live...
New
treble37
Just looking for a little feedback on a tiny helper library I built - Sometimes I find the need to convert maps with atom keys to maps w...
New
woylie
I released Doggo, a collection of unstyled Phoenix components. https://github.com/woylie/doggo Features Unstyled Phoenix components....
New
mattludwigs
Grizzly is a library for working with Z-Wave devices. Z-Wave is a low-frequency radio protocol for controlling smart home devices on a me...
New

Other popular topics Top

danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29377 241
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
Qqwy
Update: How to use the Blogs & Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 126479 1222
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New

We're in Beta

About us Mission Statement