ElixirLS 0.2: Better builds, code formatter, and incremental Dialyzer

Some months ago, I released ElixirLS, an IDE “smartness” server for Elixir that powers a VS Code plugin. Now that Elixir 1.6 is looking fairly close to release, I figured it was time to update ElixirLS with some improvements — a new build system, code formatting, and an experimental, incremental Dialyzer server.

Rewritten build system

The initial release of ElixirLS included a fork of Elixir 1.4’s compiler with some really nasty hacks to get build warnings and errors to show up in the IDE. It didn’t work reliably at all — umbrella projects failed completely, for example. These hacks were necessary because there was no good way to get warning and error information from Mix.

I contributed some changes to Elixir 1.6 to report build diagnostics from compilers. The built-in compilers now conform to a new behaviour, Mix.Task.Compiler, and return their status and diagnostics in a standardized way. If third-party compilers implement this behaviour, their warnings and errors will show up automatically in ElixirLS too.

This let me completely remove the custom compiler from ElixirLS, and builds are much more reliable now. If you’re using Elixir >= 1.6, build diagnostics will show up automatically in the editor, but if you’re using a previous version, you’ll have to look at the text log from the extension to see build warnings and errors. Builds will run automatically when you save a file. If you want this to happen automatically when you type, turn on “autosave” in your IDE.

Code formatting

Elixir 1.6 includes a code formatter akin to Prettier for Javascript or gofmt for Go. It uses logic similar to inspect to format your code for a certain target line length (by default 100 characters). I’ve found it a great help, and it’s available in ElixirLS 0.2 if you’re using Elixir >= 1.6. Hit Shift + Alt + F in VS Code to format the current file. You can also set editor.formatOnSave to true if you want it to run every time you manually save a file.

Automatic, incremental Dialyzer

Dialyzer is a static-analysis tool for Erlang and Elixir. It’s very useful for catching errors having to do with type-checking, but it can be a pain to use. The analysis is notoriously slow, so programmers typically use a tool like Dialyxir to maintain a PLT file (“Persistent Lookup Table”) with the saved analysis of files that change infrequently, such as core Elixir libraries or project dependencies. Once a PLT file is generated, Dialyzer’s analysis can be acceptably fast.

But even with tools like Dialyxir, using Dialyzer isn’t as fast or easy as it could be. For example, you need to determine which core Erlang or Elixir libraries such as :ets or Mix to include when you build your PLT. If you forget to include them, Dialyzer can’t give you useful analysis when you use them in your code. Dialyzer also re-analyzes all the modules in your project every time, even if no modules they reference have changed.

I took a stab at addressing these shortcomings, and ElixirLS 0.2 includes an experimental, incremental Dialyzer server that runs after each successful build. It uses undocumented, internal Dialyzer APIs, and it’s only compatible with Erlang/OTP 20. Future Erlang changes might break it, but so far it seems pretty reliable when used with OTP 20. You can set elixirLS.dialyzerEnabled to false to disable it if it breaks.

The server keeps a manifest similar to a PLT file in .elixir_ls. After each build, it looks for modules that changed and only re-analyzes modules when necessary. It also looks at the abstract code in your compiled beam files to determine which core Erlang or Elixir modules you use. If it sees that you’re referencing a library that isn’t already analyzed, it includes it in the analysis automatically. That way, you never get “unknown module” warnings and you don’t have to manually specify which applications you reference. It just works, and after the initial analysis, it’s quite fast.

You can change the setting elixirLS.dialyzerWarnOpts to control which warnings are shown, though Dialyzer’s defaults are good for most projects. You can also set the module attribute @dialyzer to show or hide warnings at a module or function level.

I’m hoping that fast and easy static analysis can smooth over the problems that come from dynamic typing. Getting immediate feedback when you pass an argument of the wrong type or write an incorrect pattern-match can take a lot of the guesswork out of writing correct code.

Installing Elixir 1.6, Erlang/OTP 20, and ElixirLS

To take advantage of these new features, you’ll need Elixir 1.6 and Erlang OTP/20. Since Elixir 1.6 isn’t released yet, you’ll need to install a pre-release version. I highly recommend installing both Elixir and Erlang from source using the tools kiex and kerl, respectively. That’ll let you use go-to-definition to jump from your code to the source code for core Elixir and Erlang modules. To install the latest pre-release Elixir with kiex, run kiex install master. To install ElixirLS, search “elixirls” in the extensions pane in VS Code. It’ll automatically use whatever Elixir and Erlang installations are the default in your shell.

Feedback is very welcome. Happy coding!