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!

12 Likes

I’d love to check this out in my phoenix app. Could you provide a simple list of “getting started” on a current phoenix app (v1.2)? I am fairly new to dialyzer, and I remember jose tweeting that with 1.2 it should be a good experience now.

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
4 Likes

thank you very much! you should copy this to the github readme or a wiki page and link to it - I can imagine that there will be more folks that want to try dialyzer explicitly in phoenix :slight_smile:

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

3 Likes

I believe this is a Phoenix 1.2 bug. Didn’t yet try with 1.3 to see if it’s fixed. We’re still using 1.2 and just add @dialyzer :no_match in the view module, and a brief comment.

Aha, thanks Sasa! I had tried that but included :no_match in a tuple, oops!

Relatedly, can anyone clarify what differences exist between this library and dialyze ?

1 Like

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.

2 Likes

Instead of adding it to my 30+ *_view.ex modules I added it to the *.Web.view/1 with a TODO: to remind me to check if this has been fixed upstream yet and remove it if so. Much easier having it in one place, and it works. :slight_smile:

1 Like

Found another for your wiki page, Phoenix.Presence is also apparently bugged as this code:

  use Phoenix.Presence, otp_app: :my_server, pubsub_server: MyServer.PubSub # Line 75

Generates this dialyzer issue:

help_presence.ex:75: Expression produces a value of type {'ok',pid()}, but this value is unmatched
help_presence.ex:75: The inferred return type of init/1 ({'ok',#{}}) has nothing in common with {'error',_} | {'ok',pid()}, which is the expected return type for the callback of 'Elixir.Phoenix.Presence' behaviour
help_presence.ex:75: The inferred return type of track/3 ({'error',_} | {'ok',binary()}) has nothing in common with 'ok', which is the expected return type for the callback of 'Elixir.Phoenix.Presence' behaviour
help_presence.ex:75: The inferred return type of track/4 ({'error',_} | {'ok',binary()}) has nothing in common with 'ok', which is the expected return type for the callback of 'Elixir.Phoenix.Presence' behaviour
help_presence.ex:75: The inferred return type of update/3 ({'error',_} | {'ok',binary()}) has nothing in common with 'ok', which is the expected return type for the callback of 'Elixir.Phoenix.Presence' behaviour
help_presence.ex:75: The inferred return type of update/4 ({'error',_} | {'ok',binary()}) has nothing in common with 'ok', which is the expected return type for the callback of 'Elixir.Phoenix.Presence' behaviour

And this fixes most of them:

  @dialyzer [
              {:nowarn_function, 'init': 1},
              {:nowarn_function, 'track': 3},
              {:nowarn_function, 'track': 4},
              {:nowarn_function, 'update': 3},
              {:nowarn_function, 'update': 4},
            ]

However it leaves this:

help_presence.ex:75: Expression produces a value of type {'ok',pid()}, but this value is unmatched

And I am not sure what is happening here, any help? Is this one or the others my bug instead?

Is it possible to share an example?

The stock PhoenixFramework Presence example should do it as that is mostly all that mine is:

defmodule MyServer.HelpPresence do
  use Phoenix.Presence, otp_app: :my_server,
                        pubsub_server: MyServer.PubSub

  def fetch(_topic, entries) do
    entries
  end
end

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

Also it is not enough to just add the direct dependencies to the PLT (as I think both dialyze and dialyxir), you will likely need to add deps of deps. dialyzer won’t know about the types of deps of deps and may not be able to infer the spec of dependency functions without the deps of deps if they dont have @spec. A previous version of dialyze handled this but some people had memory issues.

Edit: Ah dialyxir can handle this situation I think if it gets all transitive deps by walking the whole deps tree. This should probably be the default otherwise false negatives due to dialyzer being unable to infer type information.

1 Like

Thanks for the reply, the thinking behind dialyze is really fascinating, glad you shared it.

This is partially correct; dialyxir has always had functionality to check for missing deps and add them if necessary, but the user has to run the mix dialyzer.plt task after they make dependency changes. Until recently there was an ommission where it did not check for updates in dependencies that were added to the PLT, but it does do that now. For a medium sized project this whole process takes several seconds, at least the way I’m doing it - so I’ve kept it as a separate task.

I agree, if I can do it quickly enough, and presently I haven’t been able to - just checking if there are missing apps takes a couple of seconds. Perhaps there could be a quick way to see if any dependencies of the project has changed, like a hash of the mix.lock file, that should trigger a recheck.

I just added support for doing this, and actually this is the reason I opened the thread is to solicit feedback on if it should be the default or not, I’m presently inclined towards making it so.

I do plan to adopt the dialyze method of PLT management, as I mentioned in the original post.

I’m open to changes in the default warnings, particularly removing -Wrace_conditions. The genesis of the default warnings used is an elixir-lang-core email from April 2013 which was my introduction to Dialyzer and one PR which added -Wunderspecs. I never made any changes as I didn’t think it was right to silently stop checking something that I used to check, even if it happened to be listed in a changelog. But my intention with the 0.4 release is clean-up some of these mistakes that have been left in the name of “compatibility”.

This whole experience has given me a very different perspective on worse-is-better. I wrote Dialyxir before I knew hardly anything about Dialyzer, and despite never mentioning it to anyone (until this OP) it sort of took on a life of its own and started getting PRs to fix some of the most glaring issues and to maintain compatibility with the evolving Elixir that I had lost touch with for a couple of years. Now for better (and often worse) a lot of people are trying to use it so I’ve taken a new interest in fixing it. Thanks for your feedback, it really does help.

2 Likes

Unfortunately I think this is fully correct but perhaps there is a miscommunication. If a dep is updated and this changes that dep so that modules are added or removed dialyxir does not add/remove these modules. Not that if a dep application is added. Dialyxir also never handles removal of removed deps. I think this will cause dialyxir to crash and require a rebuild of the PLT from scratch.

I did edit before your reply but you probably missed it, please see the edit, dialyxir is not adding transitive deps as I described in the edit.

Yes but I was trying to explain that the current PLT usage needs a lot of work and that is going to generate false negatives and false positives in the hope people will contribute. I decided to officially declare dialyze as not maintained in response to this post in the hope that you will stand up and produce a good task that I believe neither of us have been able to do so far.

The current defaults have beginner users wasting a lot of time. I have seen a lot of people struggling to deal with these warnings which shouldn’t be displayed and they get put off using dialyzer. Elixir can maintain the -Wunderspecs and other advance warnings because it has the contributors to hold itself to a higher standard than most other libraries, and it is not expected for beginners to use that, maybe not even more advanced users.

1 Like

Dialyxir will now (as of latest release) invoke the dialyzer --check_plt task which should handle updating apps modules when dependencies have had code changes. It is correct it does not remove apps, this is a liability of the global PLT and should definitely be fixed when I move to the dialyze style PLT management.

dialyxir gets transitive dependencies using Mix.Project.deps_paths - basically the same contents as the mix.lock file. I am not pulling in OTP applications from dependencies applications list and perhaps I should - is that what you see is missing?

Yes but I think we are talking passed each other as I was only talking about when a dep adds or removes modules.

Right! You may also had deps that aren’t needed using this method. dialyze uses .app files to do this. I am unsure if dialyze and dialyxir use the same license, if not I can provide a copy of dialyze under a matching license sans contributions from others, so you can re use what you’d like to.

The license is the same, apache 2.0, so there is no issue there.