defmodule Mix.Tasks.Arch.Explore.Apps do use Mix.Task alias Archeometer.Repo alias Archeometer.Explore.Project alias Archeometer.Util.DumpStats @moduledoc """ Mix Task to collect application names, module application id, module implemented behaviours, module defined structs and store them into a Archeometer database. Usage: mix arch.explore.apps [options] The following options are accepted: * `--include-deps` - Wildcard glob style to filter dependencies For more information see: [Basic usage guide](guides/introduction/basic_usage.md#include-dependencies-in-the-analysis) """ @shortdoc "Dumps modules aplication id of current project into Archeometer DB" defp missing_app_modules(), do: Repo.all("SELECT m.name, m.path FROM modules m WHERE m.app_id IS NULL;") defp guess_app_and_format(%Repo.Result{rows: rows}) do rows |> Enum.map(fn [name, path] -> case Project.guess_app_from_path(path) do {:ok, app} -> [{name, app}] {:error, _} -> [] end end) |> Enum.concat() |> Enum.group_by(&elem(&1, 1), &elem(&1, 0)) end defp get_args(argv) do {opts, _args, invalid} = OptionParser.parse( argv, strict: [ include_deps: :keep ] ) case invalid do [] -> deps_filter = Keyword.get_values(opts, :include_deps) validate_options(deps_filter) _ -> {:error, :wrong_arguments} end end @impl Mix.Task def run(argv) do if Archeometer.Repo.db_ready?(:basic) do case get_args(argv) do [ include_deps: deps_filter ] -> _run(deps_filter) {:error, error} -> Mix.shell().error("Error: #{error}") print_help() end else Mix.shell().error("Please run static analysis first") end end defp _run(deps_filter) do Mix.shell().info("Starting application classification analysis... \n") Mix.Task.run("compile") extra_deps = append_deps(deps_filter) Mix.shell().info("Saving apps...") Project.apps() |> Enum.concat(extra_deps) |> Archeometer.Util.DumpStats.save_apps() Mix.shell().info("Saving apps modules...") Project.local_modules_by_app(extra_deps) |> Enum.map(fn {app, modules} -> %{app: app, modules: modules} end) |> DumpStats.add_modules_app_id() missing_app_modules() |> guess_app_and_format() |> Enum.map(fn {app, modules} -> %{app: app, modules: modules} end) |> DumpStats.add_modules_app_id() Mix.shell().info("Saving behaviours...") Project.local_modules(extra_deps) |> Project.list_behaviours() |> DumpStats.save_behaviours() Mix.shell().info("Saving structs...") Project.local_modules(extra_deps) |> Project.list_modules_with_struct() |> DumpStats.add_structs() Mix.shell().info("Done!\n") end defp validate_options(deps_filter) do case Project.filter_deps(deps_filter) do [] -> {:error, "Filter doesn't match any dependency"} _ -> [ include_deps: deps_filter ] end end defp append_deps(deps_filter) do deps = Project.filter_deps(deps_filter) if deps, do: Keyword.keys(deps), else: [] end defp print_help() do Mix.shell().info(""" Usage: mix arch.explore.apps [opts] opts: --include-deps 'deps_filter' Where `deps_filter` is a glob Unix-like pattern, matches dependencies name --include-deps can be used more than once. - `*` matches none or many tokens - `?` matches exactly one token - `[abc]` matches a set of tokens - `[a-z]` matches a range of tokens - `[!...]` matches anything but a set of tokens """) end end