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.

michalmuskala

michalmuskala

Neither mix nor eex should be in the applications list. The applications list is used for runtime dependencies of the application - both mix and eex are not used at runtime - they are used exclusively at compile-time.

The exception is, of course, the runtime use of eex eval functions - in that case adding eex to the applications list is indeed the right thing to do.

jeremyjh

jeremyjh

I think the main difference is that dialyze is controlled by command line switches, and dialyxir is driven more by configuration. dialyxir does take a couple of args, and you can pass any dialyzer arg and it gets passed on. dialyze is a single task and it will just do whatever is required to setup the PLT when you run it. When you are just doing analysis with dialyze you probably want to pass --no-check so that it doesn’t check for updated apps every time you analyze your project. With dialyxir you have to run the separate dialyzer.plt task to create/update/check the PLT. This lets it default to --no-check.

The implementations are also different; dialyze uses the erlang :dialyzer module to perform all operations. dialyxir builds a command-line string and shells out to dialyzer. It prints this string which is handy for debugging; several issues (usually user environment/project specific) that have been reported to the issue tracker I was able to figure out just by reading the command string it generated.

dialyxir was started first but probably lacked some features when dialyze came out. I think dialyze has had more sensible defaults, for example it has always included all direct dependencies in the PLT. I do not think it lets you change that though; dialyxir gives you full control of what goes in the PLT. The thing is I do not know if anyone wants that. dialyxir has lots of configuration and I have little idea of how much of it is used.

Where Next?

Popular in Libraries Top

scohen
Lexical Lexical is a next-generation language server for the Elixir programming language. Features Context aware code completion As-you...
New
RobertDober
Earmark is a pure-Elixir Markdown converter. It is intended to be used as a library (just call Earmark.as_html), but can also be used as...
239 11851 134
New
sasajuric
I’d like to announce a small library called boundaries. This is an experimental project which explores the idea of enforcing boundaries ...
New
praveenperera
FastRSS Parse RSS feeds very quickly: This is rust NIF built using rustler Uses the RSS rust crate to do the actual RSS parsing Speed...
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
woutdp
Hi! I wanted to introduce my latest project LiveSvelte. It allows you to render Svelte inside LiveView with end-to-end reactivity. It’s ...
New
aditya7iyengar
Rummage.Ecto and Rummage.Phoenix provide ways to perform Searching, Sorting and Pagination over Ecto queries and Phoenix collections. Fo...
New
benlime
LiveMotion enables high performance animations declared on the server and run on the client. As a follow up to my previous thread A libr...
New
gabrielpoca
Hello everyone! I want to share with you something that I’m really proud of: https://stillstatic.io/ Still is a static site builder for...
New
Azolo
Hey everyone, I just released WebSockex which is a Elixir WebSocket client. WebSockex strives to work as a OTP special process, be RFC6...
New

Other popular topics Top

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...
3268 119930 1237
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
William
I would like to know that is there any online source for learning Phoenix Framework for building E-Commerce Store? Any advantage on build...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

Sub Categories:

We're in Beta

About us Mission Statement