zf

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

plt.ex (6398B)


      1 # Credits: this code was originally part of the `dialyze` task
      2 # Copyright by James Fish
      3 # https://github.com/fishcakez/dialyze
      4 
      5 defmodule Dialyxir.Plt do
      6   @moduledoc false
      7 
      8   import Dialyxir.Output
      9   alias Dialyxir.Formatter
     10 
     11   def check(plts, fun \\ &check_plt/4) do
     12     find_plts(plts, [], fun)
     13   end
     14 
     15   defp find_plts([{plt, apps} | plts], acc, fun) do
     16     case plt_files(plt) do
     17       nil ->
     18         find_plts(plts, [{plt, apps, nil} | acc], fun)
     19 
     20       beams ->
     21         apps_rest = Enum.flat_map(plts, fn {_plt2, apps2} -> apps2 end)
     22         apps = Enum.uniq(apps ++ apps_rest)
     23         check_plts([{plt, apps, beams} | acc], fun)
     24     end
     25   end
     26 
     27   defp find_plts([], acc, fun) do
     28     check_plts(acc, fun)
     29   end
     30 
     31   defp check_plts(plts, fun) do
     32     _ =
     33       Enum.reduce(plts, {nil, MapSet.new(), %{}}, fn {plt, apps, beams}, acc ->
     34         fun.(plt, apps, beams, acc)
     35       end)
     36   end
     37 
     38   defp check_plt(plt, apps, old_beams, {prev_plt, prev_beams, prev_cache}) do
     39     info("Finding applications for #{Path.basename(plt)}")
     40     cache = resolve_apps(apps, prev_cache)
     41     mods = cache_mod_diff(cache, prev_cache)
     42     info("Finding modules for #{Path.basename(plt)}")
     43     beams = resolve_modules(mods, prev_beams)
     44     check_beams(plt, beams, old_beams, prev_plt)
     45     {plt, beams, cache}
     46   end
     47 
     48   defp cache_mod_diff(new, old) do
     49     Enum.flat_map(new, fn {app, {mods, _deps}} ->
     50       case Map.has_key?(old, app) do
     51         true -> []
     52         false -> mods
     53       end
     54     end)
     55   end
     56 
     57   defp resolve_apps(apps, cache) do
     58     apps
     59     |> Enum.uniq()
     60     |> Enum.filter(&(not Map.has_key?(cache, &1)))
     61     |> Enum.map(&app_info/1)
     62     |> Enum.into(cache)
     63   end
     64 
     65   defp app_info(app) do
     66     app_file = Atom.to_charlist(app) ++ '.app'
     67 
     68     case :code.where_is_file(app_file) do
     69       :non_existing ->
     70         error("Unknown application #{inspect(app)}")
     71         {app, {[], []}}
     72 
     73       app_file ->
     74         Path.expand(app_file)
     75         |> read_app_info(app)
     76     end
     77   end
     78 
     79   defp read_app_info(app_file, app) do
     80     case :file.consult(app_file) do
     81       {:ok, [{:application, ^app, info}]} ->
     82         parse_app_info(info, app)
     83 
     84       {:error, reason} ->
     85         Mix.raise("Could not read #{app_file}: #{:file.format_error(reason)}")
     86     end
     87   end
     88 
     89   defp parse_app_info(info, app) do
     90     mods = Keyword.get(info, :modules, [])
     91     apps = Keyword.get(info, :applications, [])
     92     inc_apps = Keyword.get(info, :included_applications, [])
     93     runtime_deps = get_runtime_deps(info)
     94     {app, {mods, runtime_deps ++ inc_apps ++ apps}}
     95   end
     96 
     97   defp get_runtime_deps(info) do
     98     Keyword.get(info, :runtime_dependencies, [])
     99     |> Enum.map(&parse_runtime_dep/1)
    100   end
    101 
    102   defp parse_runtime_dep(runtime_dep) do
    103     runtime_dep = IO.chardata_to_string(runtime_dep)
    104     regex = ~r/^(.+)\-\d+(?|\.\d+)*$/
    105     [app] = Regex.run(regex, runtime_dep, capture: :all_but_first)
    106     String.to_atom(app)
    107   end
    108 
    109   defp resolve_modules(modules, beams) do
    110     Enum.reduce(modules, beams, &resolve_module/2)
    111   end
    112 
    113   defp resolve_module(module, beams) do
    114     beam = Atom.to_charlist(module) ++ '.beam'
    115 
    116     case :code.where_is_file(beam) do
    117       path when is_list(path) ->
    118         path = Path.expand(path)
    119         MapSet.put(beams, path)
    120 
    121       :non_existing ->
    122         error("Unknown module #{inspect(module)}")
    123         beams
    124     end
    125   end
    126 
    127   defp check_beams(plt, beams, nil, prev_plt) do
    128     plt_ensure(plt, prev_plt)
    129 
    130     case plt_files(plt) do
    131       nil ->
    132         Mix.raise("Could not open #{plt}: #{:file.format_error(:enoent)}")
    133 
    134       old_beams ->
    135         check_beams(plt, beams, old_beams)
    136     end
    137   end
    138 
    139   defp check_beams(plt, beams, old_beams, _prev_plt) do
    140     check_beams(plt, beams, old_beams)
    141   end
    142 
    143   defp check_beams(plt, beams, old_beams) do
    144     remove = MapSet.difference(old_beams, beams)
    145     plt_remove(plt, remove)
    146     check = MapSet.intersection(beams, old_beams)
    147     plt_check(plt, check)
    148     add = MapSet.difference(beams, old_beams)
    149     plt_add(plt, add)
    150   end
    151 
    152   defp plt_ensure(plt, nil), do: plt_new(plt)
    153   defp plt_ensure(plt, prev_plt), do: plt_copy(prev_plt, plt)
    154 
    155   defp plt_new(plt) do
    156     info("Creating #{Path.basename(plt)}")
    157     plt = erl_path(plt)
    158     _ = plt_run(analysis_type: :plt_build, output_plt: plt, apps: [:erts])
    159     :ok
    160   end
    161 
    162   defp plt_copy(plt, new_plt) do
    163     info("Copying #{Path.basename(plt)} to #{Path.basename(new_plt)}")
    164 
    165     new_plt
    166     |> Path.dirname()
    167     |> File.mkdir_p!()
    168 
    169     File.cp!(plt, new_plt)
    170   end
    171 
    172   defp plt_add(plt, files) do
    173     case MapSet.size(files) do
    174       0 ->
    175         :ok
    176 
    177       n ->
    178         Mix.shell().info("Adding #{n} modules to #{Path.basename(plt)}")
    179         plt = erl_path(plt)
    180         files = erl_files(files)
    181 
    182         {duration_us, _} =
    183           :timer.tc(fn -> plt_run(analysis_type: :plt_add, init_plt: plt, files: files) end)
    184 
    185         Mix.shell().info(Formatter.formatted_time(duration_us))
    186         :ok
    187     end
    188   end
    189 
    190   defp plt_remove(plt, files) do
    191     case MapSet.size(files) do
    192       0 ->
    193         :ok
    194 
    195       n ->
    196         info("Removing #{n} modules from #{Path.basename(plt)}")
    197         plt = erl_path(plt)
    198         files = erl_files(files)
    199         _ = plt_run(analysis_type: :plt_remove, init_plt: plt, files: files)
    200         :ok
    201     end
    202   end
    203 
    204   defp plt_check(plt, files) do
    205     case MapSet.size(files) do
    206       0 ->
    207         :ok
    208 
    209       n ->
    210         Mix.shell().info("Checking #{n} modules in #{Path.basename(plt)}")
    211         plt = erl_path(plt)
    212         _ = plt_run(analysis_type: :plt_check, init_plt: plt)
    213         :ok
    214     end
    215   end
    216 
    217   defp plt_run(opts) do
    218     try do
    219       :dialyzer.run([check_plt: false] ++ opts)
    220     catch
    221       {:dialyzer_error, msg} ->
    222         error(color(":dialyzer.run error: #{msg}", :red))
    223     end
    224   end
    225 
    226   defp plt_info(plt) do
    227     erl_path(plt)
    228     |> :dialyzer.plt_info()
    229   end
    230 
    231   defp erl_files(files) do
    232     Enum.reduce(files, [], &[erl_path(&1) | &2])
    233   end
    234 
    235   defp erl_path(path) do
    236     encoding = :file.native_name_encoding()
    237     :unicode.characters_to_list(path, encoding)
    238   end
    239 
    240   defp plt_files(plt) do
    241     info("Looking up modules in #{Path.basename(plt)}")
    242 
    243     case plt_info(plt) do
    244       {:ok, info} ->
    245         Keyword.fetch!(info, :files)
    246         |> Enum.reduce(MapSet.new(), &MapSet.put(&2, Path.expand(&1)))
    247 
    248       {:error, :no_such_file} ->
    249         nil
    250 
    251       {:error, reason} ->
    252         Mix.raise("Could not open #{plt}: #{:file.format_error(reason)}")
    253     end
    254   end
    255 end