Current section
Files
Jump to
Current section
Files
lib/mix/tasks/dialyzer.ex
defmodule Mix.Tasks.Dialyzer do
@shortdoc "Runs dialyzer with default or project-defined flags."
@moduledoc """
This task compiles the mix project, creates a PLT with dependencies if needed and runs `dialyzer`. Much of its behavior can be managed in configuration as described below.
If executed outside of a mix project, it will build the core PLT files and exit.
## Command line options
* `--no-compile` - do not compile even if needed
* `--no-check` - do not perform (quick) check to see if PLT needs update
* `--force-check` - force PLT check also if lock file is unchanged useful
when dealing with local deps.
* `--ignore-exit-status` - display warnings but do not halt the VM or
return an exit status code
* `--list-unused-filters` - list unused ignore filters useful for CI. do
not use with `mix do`.
* `--plt` - only build the required PLT(s) and exit
* `--format short` - format the warnings in a compact format
* `--format raw` - format the warnings in format returned before Dialyzer formatting
* `--format dialyxir` - format the warnings in a pretty printed format
* `--format dialyzer` - format the warnings in the original Dialyzer format
* `--format github` - format the warnings in the Github Actions message format
* `--format ignore_file` - format the warnings to be suitable for adding to Elixir Format ignore file
* `--quiet` - suppress all informational messages
* `--quiet-with-result` - suppress all informational messages except for the final result message
Warning flags passed to this task are passed on to `:dialyzer` - e.g.
mix dialyzer --unmatched_returns
## Configuration
All configuration is included under a dialyzer key in the mix project keyword list.
### Flags
You can specify any `dialyzer` command line argument with the :flags keyword.
Dialyzer supports a number of warning flags used to enable or disable certain kinds of analysis features. Until version 0.4, `dialyxir` used by default the additional warning flags shown in the example below. However some of these create warnings that are often more confusing than helpful, particularly to new users of Dialyzer. As of 0.4, there are no longer any flags used by default. To get the old behavior, specify them in your Mix project file. For compatibility reasons you can use either the `-Wwarning` convention of the dialyzer CLI, or (preferred) the `WarnOpts` atoms supported by the [API](http://erlang.org/doc/man/dialyzer.html#gui-1). e.g.
```elixir
def project do
[
app: :my_app,
version: "0.0.1",
deps: deps,
dialyzer: [flags: ["-Wunmatched_returns", :error_handling, :underspecs]]
]
end
```
### PLT Configuration
The task will build a PLT with default core Erlang applications: `:erts :kernel :stdlib :crypto` and re-use this core file in multiple projects - another core file is created for Elixir.
OTP application dependencies are (transitively) added to your project's PLT by default. The applications added are the same as you would see displayed with the command `mix app.tree`. There is also a `:plt_add_deps` option you can set to control the dependencies added. The following options are supported:
* `:apps_direct` - Only Direct OTP runtime application dependencies - not the entire tree
* `:app_tree` - Transitive OTP runtime application dependencies e.g. `mix app.tree` (default)
```
def project do
[
app: :my_app,
version: "0.0.1",
deps: deps,
dialyzer: [plt_add_deps: :apps_direct, plt_add_apps: [:wx]]
]
end
```
You can also configure applications to include in the PLT more directly:
* `dialyzer: :plt_add_apps` - applications to include
*in addition* to the core applications and project dependencies.
* `dialyzer: :plt_ignore_apps` - applications to ignore from the list of core
applications and dependencies.
* `dialyzer: :plt_apps` - a list of applications to include that will replace the default,
include all the apps you need e.g.
### Other Configuration
* `dialyzer: :plt_file` - Deprecated - specify the PLT file name to create and use - default is to create one in the project's current build environment (e.g. _build/dev/) specific to the Erlang/Elixir version used. Note that use of this key in version 0.4 or later will produce a deprecation warning - you can silence the warning by providing a pair with key :no_warn e.g. `plt_file: {:no_warn,"filename"}`.
* `dialyzer: :plt_local_path` - specify the PLT directory name to create and use - default is the project's current build environment (e.g. `_build/dev/`).
* `dialyzer: :plt_core_path` - specify an alternative to `MIX_HOME` to use to store the Erlang and Elixir core files.
* `dialyzer: :ignore_warnings` - specify file path to filter well-known warnings.
"""
use Mix.Task
import System, only: [user_home!: 0]
import Dialyxir.Output
alias Dialyxir.Project
alias Dialyxir.Plt
alias Dialyxir.Dialyzer
defmodule Build do
@shortdoc "Build the required PLT(s) and exit."
@moduledoc """
This task compiles the mix project and creates a PLT with dependencies if needed.
It is equivalent to running `mix dialyzer --plt`
## Command line options
* `--no-compile` - do not compile even if needed.
"""
use Mix.Task
def run(args) do
Mix.Tasks.Dialyzer.run(["--plt" | args])
end
end
defmodule Clean do
@shortdoc "Delete PLT(s) and exit."
@moduledoc """
This task deletes PLT files and hash files.
## Command line options
* `--all` - delete also core PLTs.
"""
use Mix.Task
@command_options [all: :boolean]
def run(args) do
{opts, _, _dargs} = OptionParser.parse(args, strict: @command_options)
Mix.Tasks.Dialyzer.clean(opts)
end
end
@default_warnings [:unknown]
@old_options [
halt_exit_status: :boolean
]
@command_options Keyword.merge(@old_options,
force_check: :boolean,
ignore_exit_status: :boolean,
list_unused_filters: :boolean,
no_check: :boolean,
no_compile: :boolean,
plt: :boolean,
quiet: :boolean,
quiet_with_result: :boolean,
raw: :boolean,
format: :string
)
def run(args) do
{opts, _, dargs} = OptionParser.parse(args, strict: @command_options)
original_shell = Mix.shell()
if opts[:quiet] || opts[:quiet_with_result], do: Mix.shell(Mix.Shell.Quiet)
opts = Keyword.delete(opts, :quiet)
check_dialyzer()
compatibility_notice()
if Mix.Project.get() do
Project.check_config()
unless opts[:no_compile], do: Mix.Task.run("compile")
_ =
unless no_check?(opts) do
info("Finding suitable PLTs")
force_check? = Keyword.get(opts, :force_check, false)
check_plt(force_check?)
end
default = Dialyxir.Project.default_ignore_warnings()
ignore_warnings = Dialyxir.Project.dialyzer_ignore_warnings()
cond do
!ignore_warnings && File.exists?(default) ->
info("""
No :ignore_warnings opt specified in mix.exs. Using default: #{default}.
""")
ignore_warnings && File.exists?(ignore_warnings) ->
info("""
ignore_warnings: #{ignore_warnings}
""")
ignore_warnings ->
info("""
:ignore_warnings opt specified in mix.exs: #{ignore_warnings}, but file does not exist.
""")
true ->
info("""
No :ignore_warnings opt specified in mix.exs and default does not exist.
""")
end
warn_old_options(opts)
unless opts[:plt], do: run_dialyzer(opts, dargs)
else
info("No mix project found - checking core PLTs...")
Project.plts_list([], false) |> Plt.check()
end
Mix.shell(original_shell)
end
def clean(opts, fun \\ &delete_plt/4) do
check_dialyzer()
compatibility_notice()
if opts[:all], do: Project.plts_list([], false) |> Plt.check(fun)
if Mix.Project.get() do
{apps, _hash} = dependency_hash()
info("Deleting PLTs")
Project.plts_list(apps, true, true) |> Plt.check(fun)
info("About to delete PLT hash file: #{plt_hash_file()}")
File.rm(plt_hash_file())
end
end
def delete_plt(plt, _, _, _) do
info("About to delete PLT file: #{plt}")
File.rm(plt)
end
defp no_check?(opts) do
case {in_child?(), no_plt?()} do
{true, true} ->
info("In an Umbrella child and no PLT found - building that first.")
build_parent_plt()
true
{true, false} ->
info("In an Umbrella child, not checking PLT...")
true
_ ->
opts[:no_check]
end
end
defp check_plt(force_check?) do
info("Checking PLT...")
{apps, hash} = dependency_hash()
if not force_check? and check_hash?(hash) do
info("PLT is up to date!")
else
Project.plts_list(apps) |> Plt.check()
File.write(plt_hash_file(), hash)
end
end
defp run_dialyzer(opts, dargs) do
args = [
{:check_plt, opts[:force_check] || false},
{:init_plt, String.to_charlist(Project.plt_file())},
{:files, Project.dialyzer_files()},
{:warnings, dialyzer_warnings(dargs)},
{:format, opts[:format]},
{:raw, opts[:raw]},
{:list_unused_filters, opts[:list_unused_filters]},
{:ignore_exit_status, opts[:ignore_exit_status]},
{:quiet_with_result, opts[:quiet_with_result]}
]
{status, exit_status, [time | result]} = Dialyzer.dialyze(args)
info(time)
quiet_with_result? = opts[:quiet_with_result]
report =
cond do
status == :ok && quiet_with_result? ->
fn text ->
Mix.shell(Mix.Shell.IO)
info(text)
Mix.shell(Mix.Shell.Quiet)
end
status == :ok ->
&info/1
true ->
&error/1
end
Enum.each(result, report)
unless exit_status == 0 || opts[:ignore_exit_status] do
error("Halting VM with exit status #{exit_status}")
System.halt(exit_status)
end
end
defp dialyzer_warnings(dargs) do
raw_opts = Project.dialyzer_flags() ++ Enum.map(dargs, &elem(&1, 0))
transform(raw_opts) ++ (@default_warnings -- Project.dialyzer_removed_defaults())
end
defp transform(options) when is_list(options), do: Enum.map(options, &transform/1)
defp transform(option) when is_atom(option), do: option
defp transform(option) when is_binary(option) do
option
|> String.replace_leading("-W", "")
|> String.replace("--", "")
|> String.to_atom()
end
defp in_child? do
case Project.no_umbrella?() do
true -> false
false -> String.contains?(Mix.Project.config()[:lockfile], "..")
end
end
defp no_plt? do
not File.exists?(Project.deps_plt())
end
defp build_parent_plt() do
parent = Mix.Project.config()[:lockfile] |> Path.expand() |> Path.dirname()
opts = [into: IO.stream(:stdio, :line), stderr_to_stdout: true, cd: parent]
# It would seem more natural to use Mix.in_project here to start in our parent project.
# However part of the app.tree resolution includes loading all sub apps, and we will
# hit an exception when we try to do that for *this* child, which is already loaded.
{out, rc} = System.cmd("mix", ["dialyzer", "--plt"], opts)
unless rc == 0 do
info("Error building parent PLT, process returned code: #{rc}\n#{out}")
end
end
if Version.match?(System.version(), ">= 1.15.0") do
defp check_dialyzer do
Mix.ensure_application!(:dialyzer)
end
else
defp check_dialyzer do
if not Code.ensure_loaded?(:dialyzer) do
error("""
DEPENDENCY MISSING
------------------------
If you are reading this message, then Elixir and Erlang are installed but the
Erlang Dialyzer is not available. Probably this is because you installed Erlang
with your OS package manager and the Dialyzer package is separate.
On Debian/Ubuntu:
`apt-get install erlang-dialyzer`
Fedora:
`yum install erlang-dialyzer`
Arch and Homebrew include Dialyzer in their base erlang packages. Please report a Github
issue to add or correct distribution-specific information.
""")
:erlang.halt(3)
end
end
end
defp warn_old_options(opts) do
for {opt, _} <- opts, @old_options[opt] do
error("#{opt} is no longer a valid CLI argument.")
end
nil
end
defp compatibility_notice do
old_plt = "#{user_home!()}/.dialyxir_core_*.plt"
if File.exists?(old_plt) &&
(!File.exists?(Project.erlang_plt()) || !File.exists?(Project.elixir_plt())) do
info("""
COMPATIBILITY NOTICE
------------------------
Previous usage of a pre-0.4 version of Dialyxir detected. Please be aware that the 0.4 release
makes a number of changes to previous defaults. Among other things, the PLT task is automatically
run when dialyzer is run, PLT paths have changed,
transitive dependencies are included by default in the PLT, and no additional warning flags
beyond the dialyzer defaults are included. All these properties can be changed in configuration.
(see `mix help dialyzer`).
If you no longer use the older Dialyxir in any projects and do not want to see this notice each time you upgrade your Erlang/Elixir distribution, you can delete your old pre-0.4 PLT files. (`rm ~/.dialyxir_core_*.plt`)
""")
end
end
@spec check_hash?(binary()) :: boolean()
defp check_hash?(hash) do
case File.read(plt_hash_file()) do
{:ok, stored_hash} -> hash == stored_hash
_ -> false
end
end
defp plt_hash_file, do: Project.plt_file() <> ".hash"
@spec dependency_hash :: {[atom()], binary()}
def dependency_hash do
apps = Project.cons_apps()
apps |> inspect() |> info()
hash = :crypto.hash(:sha, lock_file() <> :erlang.term_to_binary(apps))
{apps, hash}
end
defp lock_file() do
lockfile = Mix.Project.config()[:lockfile]
read_res = File.read(lockfile)
case read_res do
{:ok, data} ->
data
{:error, :enoent} ->
# If there is no lock file, an empty bitstring will do to indicate there is none there
<<>>
{:error, reason} ->
raise File.Error,
reason: reason,
action: "read file",
path: lockfile
end
end
end