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