dialyzer.ex (13645B)
1 defmodule Mix.Tasks.Dialyzer do 2 @shortdoc "Runs dialyzer with default or project-defined flags." 3 4 @moduledoc """ 5 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. 6 7 If executed outside of a mix project, it will build the core PLT files and exit. 8 9 ## Command line options 10 11 * `--no-compile` - do not compile even if needed 12 * `--no-check` - do not perform (quick) check to see if PLT needs update 13 * `--force-check` - force PLT check also if lock file is unchanged useful 14 when dealing with local deps. 15 * `--ignore-exit-status` - display warnings but do not halt the VM or 16 return an exit status code 17 * `--list-unused-filters` - list unused ignore filters useful for CI. do 18 not use with `mix do`. 19 * `--plt` - only build the required PLT(s) and exit 20 * `--format short` - format the warnings in a compact format 21 * `--format raw` - format the warnings in format returned before Dialyzer formatting 22 * `--format dialyxir` - format the warnings in a pretty printed format 23 * `--format dialyzer` - format the warnings in the original Dialyzer format 24 * `--format github` - format the warnings in the Github Actions message format 25 * `--format ignore_file` - format the warnings to be suitable for adding to Elixir Format ignore file 26 * `--quiet` - suppress all informational messages 27 28 Warning flags passed to this task are passed on to `:dialyzer` - e.g. 29 30 mix dialyzer --unmatched_returns 31 32 ## Configuration 33 34 All configuration is included under a dialyzer key in the mix project keyword list. 35 36 ### Flags 37 38 You can specify any `dialyzer` command line argument with the :flags keyword. 39 40 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. 41 42 ```elixir 43 def project do 44 [ 45 app: :my_app, 46 version: "0.0.1", 47 deps: deps, 48 dialyzer: [flags: ["-Wunmatched_returns", :error_handling, :underspecs]] 49 ] 50 end 51 ``` 52 53 ### PLT Configuration 54 55 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. 56 57 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: 58 59 * `:apps_direct` - Only Direct OTP runtime application dependencies - not the entire tree 60 * `:app_tree` - Transitive OTP runtime application dependencies e.g. `mix app.tree` (default) 61 62 ``` 63 def project do 64 [ 65 app: :my_app, 66 version: "0.0.1", 67 deps: deps, 68 dialyzer: [plt_add_deps: :apps_direct, plt_add_apps: [:wx]] 69 ] 70 end 71 ``` 72 73 You can also configure applications to include in the PLT more directly: 74 75 * `dialyzer: :plt_add_apps` - applications to include 76 *in addition* to the core applications and project dependencies. 77 78 * `dialyzer: :plt_ignore_apps` - applications to ignore from the list of core 79 applications and dependencies. 80 81 * `dialyzer: :plt_apps` - a list of applications to include that will replace the default, 82 include all the apps you need e.g. 83 84 ### Other Configuration 85 86 * `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"}`. 87 88 * `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/`). 89 90 * `dialyzer: :plt_core_path` - specify an alternative to `MIX_HOME` to use to store the Erlang and Elixir core files. 91 92 * `dialyzer: :ignore_warnings` - specify file path to filter well-known warnings. 93 """ 94 95 use Mix.Task 96 import System, only: [user_home!: 0] 97 import Dialyxir.Output, only: [info: 1, error: 1] 98 alias Dialyxir.Project 99 alias Dialyxir.Plt 100 alias Dialyxir.Dialyzer 101 102 defmodule Build do 103 @shortdoc "Build the required PLT(s) and exit." 104 105 @moduledoc """ 106 This task compiles the mix project and creates a PLT with dependencies if needed. 107 It is equivalent to running `mix dialyzer --plt` 108 109 ## Command line options 110 111 * `--no-compile` - do not compile even if needed. 112 """ 113 use Mix.Task 114 115 def run(args) do 116 Mix.Tasks.Dialyzer.run(["--plt" | args]) 117 end 118 end 119 120 defmodule Clean do 121 @shortdoc "Delete PLT(s) and exit." 122 123 @moduledoc """ 124 This task deletes PLT files and hash files. 125 126 ## Command line options 127 128 * `--all` - delete also core PLTs. 129 """ 130 use Mix.Task 131 132 @command_options [all: :boolean] 133 def run(args) do 134 {opts, _, _dargs} = OptionParser.parse(args, strict: @command_options) 135 Mix.Tasks.Dialyzer.clean(opts) 136 end 137 end 138 139 @default_warnings [:unknown] 140 141 @old_options [ 142 halt_exit_status: :boolean 143 ] 144 145 @command_options Keyword.merge(@old_options, 146 force_check: :boolean, 147 ignore_exit_status: :boolean, 148 list_unused_filters: :boolean, 149 no_check: :boolean, 150 no_compile: :boolean, 151 plt: :boolean, 152 quiet: :boolean, 153 raw: :boolean, 154 format: :string 155 ) 156 157 def run(args) do 158 {opts, _, dargs} = OptionParser.parse(args, strict: @command_options) 159 original_shell = Mix.shell() 160 if opts[:quiet], do: Mix.shell(Mix.Shell.Quiet) 161 opts = Keyword.delete(opts, :quiet) 162 check_dialyzer() 163 compatibility_notice() 164 165 if Mix.Project.get() do 166 Project.check_config() 167 168 unless opts[:no_compile], do: Mix.Task.run("compile") 169 170 _ = 171 unless no_check?(opts) do 172 info("Finding suitable PLTs") 173 force_check? = Keyword.get(opts, :force_check, false) 174 check_plt(force_check?) 175 end 176 177 default = Dialyxir.Project.default_ignore_warnings() 178 ignore_warnings = Dialyxir.Project.dialyzer_ignore_warnings() 179 180 cond do 181 !ignore_warnings && File.exists?(default) -> 182 info(""" 183 No :ignore_warnings opt specified in mix.exs. Using default: #{default}. 184 """) 185 186 ignore_warnings && File.exists?(ignore_warnings) -> 187 info(""" 188 ignore_warnings: #{ignore_warnings} 189 """) 190 191 ignore_warnings -> 192 info(""" 193 :ignore_warnings opt specified in mix.exs: #{ignore_warnings}, but file does not exist. 194 """) 195 196 true -> 197 info(""" 198 No :ignore_warnings opt specified in mix.exs and default does not exist. 199 """) 200 end 201 202 warn_old_options(opts) 203 unless opts[:plt], do: run_dialyzer(opts, dargs) 204 else 205 info("No mix project found - checking core PLTs...") 206 Project.plts_list([], false) |> Plt.check() 207 end 208 209 Mix.shell(original_shell) 210 end 211 212 def clean(opts, fun \\ &delete_plt/4) do 213 check_dialyzer() 214 compatibility_notice() 215 if opts[:all], do: Project.plts_list([], false) |> Plt.check(fun) 216 217 if Mix.Project.get() do 218 {apps, _hash} = dependency_hash() 219 info("Deleting PLTs") 220 Project.plts_list(apps, true, true) |> Plt.check(fun) 221 info("About to delete PLT hash file: #{plt_hash_file()}") 222 File.rm(plt_hash_file()) 223 end 224 end 225 226 def delete_plt(plt, _, _, _) do 227 info("About to delete PLT file: #{plt}") 228 File.rm(plt) 229 end 230 231 defp no_check?(opts) do 232 case {in_child?(), no_plt?()} do 233 {true, true} -> 234 info("In an Umbrella child and no PLT found - building that first.") 235 build_parent_plt() 236 true 237 238 {true, false} -> 239 info("In an Umbrella child, not checking PLT...") 240 true 241 242 _ -> 243 opts[:no_check] 244 end 245 end 246 247 defp check_plt(force_check?) do 248 info("Checking PLT...") 249 {apps, hash} = dependency_hash() 250 251 if not force_check? and check_hash?(hash) do 252 info("PLT is up to date!") 253 else 254 Project.plts_list(apps) |> Plt.check() 255 File.write(plt_hash_file(), hash) 256 end 257 end 258 259 defp run_dialyzer(opts, dargs) do 260 args = [ 261 {:check_plt, opts[:force_check] || false}, 262 {:init_plt, String.to_charlist(Project.plt_file())}, 263 {:files, Project.dialyzer_files()}, 264 {:warnings, dialyzer_warnings(dargs)}, 265 {:format, opts[:format]}, 266 {:raw, opts[:raw]}, 267 {:list_unused_filters, opts[:list_unused_filters]}, 268 {:ignore_exit_status, opts[:ignore_exit_status]} 269 ] 270 271 {status, exit_status, [time | result]} = Dialyzer.dialyze(args) 272 info(time) 273 report = if status == :ok, do: &info/1, else: &error/1 274 Enum.each(result, report) 275 276 unless exit_status == 0 || opts[:ignore_exit_status] do 277 error("Halting VM with exit status #{exit_status}") 278 System.halt(exit_status) 279 end 280 end 281 282 defp dialyzer_warnings(dargs) do 283 raw_opts = Project.dialyzer_flags() ++ Enum.map(dargs, &elem(&1, 0)) 284 transform(raw_opts) ++ (@default_warnings -- Project.dialyzer_removed_defaults()) 285 end 286 287 defp transform(options) when is_list(options), do: Enum.map(options, &transform/1) 288 defp transform(option) when is_atom(option), do: option 289 290 defp transform(option) when is_binary(option) do 291 option 292 |> String.replace_leading("-W", "") 293 |> String.replace("--", "") 294 |> String.to_atom() 295 end 296 297 defp in_child? do 298 String.contains?(Mix.Project.config()[:lockfile], "..") 299 end 300 301 defp no_plt? do 302 not File.exists?(Project.deps_plt()) 303 end 304 305 defp build_parent_plt() do 306 parent = Mix.Project.config()[:lockfile] |> Path.expand() |> Path.dirname() 307 opts = [into: IO.stream(:stdio, :line), stderr_to_stdout: true, cd: parent] 308 # It would seem more natural to use Mix.in_project here to start in our parent project. 309 # However part of the app.tree resolution includes loading all sub apps, and we will 310 # hit an exception when we try to do that for *this* child, which is already loaded. 311 {out, rc} = System.cmd("mix", ["dialyzer", "--plt"], opts) 312 313 unless rc == 0 do 314 info("Error building parent PLT, process returned code: #{rc}\n#{out}") 315 end 316 end 317 318 defp check_dialyzer do 319 if not Code.ensure_loaded?(:dialyzer) do 320 error(""" 321 DEPENDENCY MISSING 322 ------------------------ 323 If you are reading this message, then Elixir and Erlang are installed but the 324 Erlang Dialyzer is not available. Probably this is because you installed Erlang 325 with your OS package manager and the Dialyzer package is separate. 326 327 On Debian/Ubuntu: 328 329 `apt-get install erlang-dialyzer` 330 331 Fedora: 332 333 `yum install erlang-dialyzer` 334 335 Arch and Homebrew include Dialyzer in their base erlang packages. Please report a Github 336 issue to add or correct distribution-specific information. 337 """) 338 339 :erlang.halt(3) 340 end 341 end 342 343 defp warn_old_options(opts) do 344 for {opt, _} <- opts, @old_options[opt] do 345 error("#{opt} is no longer a valid CLI argument.") 346 end 347 348 nil 349 end 350 351 defp compatibility_notice do 352 old_plt = "#{user_home!()}/.dialyxir_core_*.plt" 353 354 if File.exists?(old_plt) && 355 (!File.exists?(Project.erlang_plt()) || !File.exists?(Project.elixir_plt())) do 356 info(""" 357 COMPATIBILITY NOTICE 358 ------------------------ 359 Previous usage of a pre-0.4 version of Dialyxir detected. Please be aware that the 0.4 release 360 makes a number of changes to previous defaults. Among other things, the PLT task is automatically 361 run when dialyzer is run, PLT paths have changed, 362 transitive dependencies are included by default in the PLT, and no additional warning flags 363 beyond the dialyzer defaults are included. All these properties can be changed in configuration. 364 (see `mix help dialyzer`). 365 366 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`) 367 """) 368 end 369 end 370 371 @spec check_hash?(binary()) :: boolean() 372 defp check_hash?(hash) do 373 case File.read(plt_hash_file()) do 374 {:ok, stored_hash} -> hash == stored_hash 375 _ -> false 376 end 377 end 378 379 defp plt_hash_file, do: Project.plt_file() <> ".hash" 380 381 @spec dependency_hash :: {[atom()], binary()} 382 def dependency_hash do 383 apps = Project.cons_apps() 384 apps |> inspect() |> info() 385 hash = :crypto.hash(:sha, lock_file() <> :erlang.term_to_binary(apps)) 386 {apps, hash} 387 end 388 389 def lock_file() do 390 Mix.Project.config()[:lockfile] |> File.read!() 391 end 392 end