From 21313c766da91ae92481b0c546d10bc26871d594 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Tue, 9 Jul 2024 10:04:38 -0300 Subject: [PATCH] ci: add dialyzer mix task --- lib/mix/tasks/emqx.dialyzer.ex | 164 +++++++++++++++++++++++++++++++++ mix.exs | 8 +- 2 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 lib/mix/tasks/emqx.dialyzer.ex diff --git a/lib/mix/tasks/emqx.dialyzer.ex b/lib/mix/tasks/emqx.dialyzer.ex new file mode 100644 index 000000000..9a2a5ad16 --- /dev/null +++ b/lib/mix/tasks/emqx.dialyzer.ex @@ -0,0 +1,164 @@ +defmodule Mix.Tasks.Emqx.Dialyzer do + use Mix.Task + + alias EMQXUmbrella.MixProject, as: UMP + + if UMP.new_mix_build?() do + Code.require_file("emqx.ct.ex", __DIR__) + end + + alias Mix.Tasks.Emqx.Ct, as: ECt + + @requirements ["compile", "loadpaths"] + + @excluded_mods ( + [ + :emqx_exproto_v_1_connection_unary_handler_bhvr, + :emqx_exproto_v_1_connection_handler_client, + :emqx_exproto_v_1_connection_handler_bhvr, + :emqx_exproto_v_1_connection_adapter_client, + :emqx_exproto_v_1_connection_adapter_bhvr, + :emqx_exproto_v_1_connection_unary_handler_client, + :emqx_exhook_v_2_hook_provider_client, + :emqx_exhook_v_2_hook_provider_bhvr, + Mix.Tasks.Compile.Grpc, + Mix.Tasks.Compile.CopySrcs, + ] + |> MapSet.new(&to_string/1) + ) + + @impl true + def run(_args) do + ECt.add_to_path_and_cache(:dialyzer) + + %{ + umbrella_apps: umbrella_apps, + dep_apps: dep_apps + } = resolve_apps() + umbrella_files = Enum.flat_map(umbrella_apps, & resolve_files/1) + dep_files = Enum.flat_map(dep_apps, & resolve_files/1) + files = + (umbrella_files ++ dep_files) + |> Enum.reject(fn path -> + name = Path.basename(path, ".beam") + MapSet.member?(@excluded_mods, name) + end) + |> Enum.map(&to_charlist/1) + warning_files = + umbrella_files + |> Enum.reject(fn path -> + name = Path.basename(path, ".beam") + MapSet.member?(@excluded_mods, name) + end) + |> Enum.map(&to_charlist/1) + warning_apps = Enum.sort(umbrella_apps) + + try do + :dialyzer.run( + analysis_type: :incremental, + warnings: [ + :unmatched_returns, + :error_handling + ], + # plt_location: ~c".", + # plt_prefix: ~c"emqx_dialyzer", + warning_files: warning_files, + warning_files_rec: warning_files, + # apps: umbrella_apps ++ dep_apps, + # warning_apps: warning_apps, + get_warnings: false, + files: files, + files_rec: files + ) + catch + {:dialyzer_error, msg} -> + {:dialyzer_error, to_string(msg)} + err -> + {:throw, err} + end + |> IO.inspect(limit: :infinity) + end + + defp resolve_apps() do + base_apps = MapSet.new([:erts, :crypto]) + # excluded_apps = MapSet.new([:elixir]) + excluded_apps = MapSet.new() + acc = %{ + umbrella_apps: [], + dep_apps: base_apps + } + + Mix.Dep.Umbrella.loaded() + |> Enum.reduce(acc, fn dep, acc -> + # IO.inspect(dep) + props = dep.opts[:app_properties] + optional_apps = Keyword.get(props, :optional_applications, []) + apps = Keyword.get(props, :applications, []) + included_apps = Keyword.get(props, :included_applications, []) + dep_apps = MapSet.new(optional_apps ++ apps ++ included_apps) + acc + |> Map.update!(:umbrella_apps, & [dep.app | &1]) + |> Map.update!(:dep_apps, & MapSet.union(&1, dep_apps)) + end) + |> then(fn acc -> + dep_apps = + acc.dep_apps + |> MapSet.difference(MapSet.new(acc.umbrella_apps)) + |> MapSet.difference(excluded_apps) + |> Enum.reduce(MapSet.new(), &find_nested_apps/2) + |> MapSet.difference(excluded_apps) + |> Enum.filter(&app_present?/1) + %{acc | dep_apps: dep_apps} + end) + end + + defp app_present?(app) do + match?({:ok, _}, ebin_dir(app)) + end + + defp find_nested_apps(app, seen) do + if MapSet.member?(seen, app) do + seen + else + seen = MapSet.put(seen, app) + apps = case :application.get_key(app, :applications) do + {:ok, apps} -> apps + :undefined -> [] + end + included_apps = case :application.get_key(app, :included_applications) do + {:ok, apps} -> apps + :undefined -> [] + end + optional_apps = case :application.get_key(app, :optional_applications) do + {:ok, apps} -> apps + :undefined -> [] + end + Enum.reduce(apps ++ included_apps, seen, &find_nested_apps/2) + end + end + + defp resolve_files(app) do + with {:ok, dir} <- ebin_dir(app) do + Mix.Utils.extract_files([dir], [:beam]) + else + _ -> [] + end + end + + defp ebin_dir(app) do + with dir when is_list(dir) <- :code.lib_dir(app, :ebin), + dir = to_string(dir), + true <- File.dir?(dir) || {:error, :not_a_dir} do + {:ok, to_string(dir)} + else + error -> + Mix.shell().info(IO.ANSI.format([ + [:yellow, + "Unknown application: #{app}; error: #{inspect(error)}", + "; if this is is an optional application, ignore." + ], + ])) + :error + end + end +end diff --git a/mix.exs b/mix.exs index 9c1a8a5f5..cb508643e 100644 --- a/mix.exs +++ b/mix.exs @@ -1297,7 +1297,8 @@ defmodule EMQXUmbrella.MixProject do [ ct: &do_ct/1, eunit: &do_eunit/1, - proper: &do_proper/1 + proper: &do_proper/1, + dialyzer: &do_dialyzer/1 ] end @@ -1326,6 +1327,11 @@ defmodule EMQXUmbrella.MixProject do Mix.Task.run("emqx.proper", args) end + defp do_dialyzer(args) do + Code.require_file("lib/mix/tasks/emqx.dialyzer.ex") + Mix.Task.run("emqx.dialyzer", args) + end + defp ensure_test_mix_env!() do Mix.env() |> to_string()