Current section
Files
Jump to
Current section
Files
lib/dialyxir/plt.ex
# Credits: this code was originally part of the `dialyze` task
# Copyright by James Fish
# https://github.com/fishcakez/dialyze
defmodule Dialyxir.Plt do
@moduledoc false
import Dialyxir.Output
alias Dialyxir.Formatter
def check(plts, fun \\ &check_plt/4) do
find_plts(plts, [], fun)
end
defp find_plts([{plt, apps} | plts], acc, fun) do
case plt_files(plt) do
nil ->
find_plts(plts, [{plt, apps, nil} | acc], fun)
beams ->
apps_rest = Enum.flat_map(plts, fn {_plt2, apps2} -> apps2 end)
apps = Enum.uniq(apps ++ apps_rest)
check_plts([{plt, apps, beams} | acc], fun)
end
end
defp find_plts([], acc, fun) do
check_plts(acc, fun)
end
defp check_plts(plts, fun) do
_ =
Enum.reduce(plts, {nil, MapSet.new(), %{}}, fn {plt, apps, beams}, acc ->
fun.(plt, apps, beams, acc)
end)
end
defp check_plt(plt, apps, old_beams, {prev_plt, prev_beams, prev_cache}) do
info("Finding applications for #{Path.basename(plt)}")
cache = resolve_apps(apps, prev_cache)
mods = cache_mod_diff(cache, prev_cache)
info("Finding modules for #{Path.basename(plt)}")
beams = resolve_modules(mods, prev_beams)
check_beams(plt, beams, old_beams, prev_plt)
{plt, beams, cache}
end
defp cache_mod_diff(new, old) do
Enum.flat_map(new, fn {app, {mods, _deps}} ->
case Map.has_key?(old, app) do
true -> []
false -> mods
end
end)
end
defp resolve_apps(apps, cache) do
apps
|> Enum.uniq()
|> Enum.filter(&(not Map.has_key?(cache, &1)))
|> Enum.map(&app_info/1)
|> Enum.into(cache)
end
defp app_info(app) do
app_file = Atom.to_charlist(app) ++ ~c".app"
case :code.where_is_file(app_file) do
:non_existing ->
error("Unknown application #{inspect(app)}")
{app, {[], []}}
app_file ->
Path.expand(app_file)
|> read_app_info(app)
end
end
defp read_app_info(app_file, app) do
case :file.consult(app_file) do
{:ok, [{:application, ^app, info}]} ->
parse_app_info(info, app)
{:error, reason} ->
Mix.raise("Could not read #{app_file}: #{:file.format_error(reason)}")
end
end
defp parse_app_info(info, app) do
mods = Keyword.get(info, :modules, [])
apps = Keyword.get(info, :applications, [])
inc_apps = Keyword.get(info, :included_applications, [])
runtime_deps = get_runtime_deps(info)
{app, {mods, runtime_deps ++ inc_apps ++ apps}}
end
defp get_runtime_deps(info) do
Keyword.get(info, :runtime_dependencies, [])
|> Enum.map(&parse_runtime_dep/1)
end
defp parse_runtime_dep(runtime_dep) do
runtime_dep = IO.chardata_to_string(runtime_dep)
regex = ~r/^(.+)\-\d+(?|\.\d+)*$/
[app] = Regex.run(regex, runtime_dep, capture: :all_but_first)
String.to_atom(app)
end
defp resolve_modules(modules, beams) do
Enum.reduce(modules, beams, &resolve_module/2)
end
defp resolve_module(module, beams) do
beam = Atom.to_charlist(module) ++ ~c".beam"
case :code.where_is_file(beam) do
path when is_list(path) ->
path = Path.expand(path)
MapSet.put(beams, path)
:non_existing ->
error("Unknown module #{inspect(module)}")
beams
end
end
defp check_beams(plt, beams, nil, prev_plt) do
plt_ensure(plt, prev_plt)
case plt_files(plt) do
nil ->
Mix.raise("Could not open #{plt}: #{:file.format_error(:enoent)}")
old_beams ->
check_beams(plt, beams, old_beams)
end
end
defp check_beams(plt, beams, old_beams, _prev_plt) do
check_beams(plt, beams, old_beams)
end
defp check_beams(plt, beams, old_beams) do
remove = MapSet.difference(old_beams, beams)
plt_remove(plt, remove)
check = MapSet.intersection(beams, old_beams)
plt_check(plt, check)
add = MapSet.difference(beams, old_beams)
plt_add(plt, add)
end
defp plt_ensure(plt, nil), do: plt_new(plt)
defp plt_ensure(plt, prev_plt), do: plt_copy(prev_plt, plt)
defp plt_new(plt) do
info("Creating #{Path.basename(plt)}")
plt = erl_path(plt)
_ = plt_run(analysis_type: :plt_build, output_plt: plt, apps: [:erts])
:ok
end
defp plt_copy(plt, new_plt) do
info("Copying #{Path.basename(plt)} to #{Path.basename(new_plt)}")
new_plt
|> Path.dirname()
|> File.mkdir_p!()
File.cp!(plt, new_plt)
end
defp plt_add(plt, files) do
case MapSet.size(files) do
0 ->
:ok
n ->
Mix.shell().info("Adding #{n} modules to #{Path.basename(plt)}")
plt = erl_path(plt)
files = erl_files(files)
{duration_us, _} =
:timer.tc(fn -> plt_run(analysis_type: :plt_add, init_plt: plt, files: files) end)
Mix.shell().info(Formatter.formatted_time(duration_us))
:ok
end
end
defp plt_remove(plt, files) do
case MapSet.size(files) do
0 ->
:ok
n ->
info("Removing #{n} modules from #{Path.basename(plt)}")
plt = erl_path(plt)
files = erl_files(files)
_ = plt_run(analysis_type: :plt_remove, init_plt: plt, files: files)
:ok
end
end
defp plt_check(plt, files) do
case MapSet.size(files) do
0 ->
:ok
n ->
Mix.shell().info("Checking #{n} modules in #{Path.basename(plt)}")
plt = erl_path(plt)
_ = plt_run(analysis_type: :plt_check, init_plt: plt)
:ok
end
end
defp plt_run(opts) do
try do
:dialyzer.run([check_plt: false] ++ opts)
catch
{:dialyzer_error, msg} ->
error(color(":dialyzer.run error: #{msg}", :red))
end
end
defp plt_info(plt) do
erl_path(plt)
|> :dialyzer.plt_info()
end
defp erl_files(files) do
Enum.reduce(files, [], &[erl_path(&1) | &2])
end
defp erl_path(path) do
encoding = :file.native_name_encoding()
:unicode.characters_to_list(path, encoding)
end
defp plt_files(plt) do
info("Looking up modules in #{Path.basename(plt)}")
case plt_info(plt) do
{:ok, info} ->
Keyword.fetch!(info, :files)
|> Enum.reduce(MapSet.new(), &MapSet.put(&2, Path.expand(&1)))
{:error, :no_such_file} ->
nil
{:error, reason} ->
Mix.raise("Could not open #{plt}: #{:file.format_error(reason)}")
end
end
end