zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

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