defmodule Mix.Tasks.Arch.Collect.Apps do
use Mix.Task
alias Archeometer.Repo
alias Archeometer.Collect.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.collect.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"
@impl Mix.Task
def run(argv) do
case get_args(argv) do
[
include_deps: deps_filter
] ->
if Archeometer.Repo.db_ready?(:basic) do
_run(deps_filter)
else
Mix.shell().error("Please run static analysis first")
print_help()
{:error, :no_static_analysis_found}
end
{:error, error} ->
Mix.shell().error("Error: #{error}")
print_help()
{:error, error}
end
end
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
defp filtered_deps(app, full_apps) do
with {:ok, deps} <- Project.get_app_deps(app) do
{:ok, Enum.filter(deps, &(&1 in full_apps))}
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...")
all_apps = Project.apps() ++ extra_deps
all_apps
|> Project.tag_external_apps()
|> Archeometer.Util.DumpStats.save_apps()
Mix.shell().info("Saving apps xrefs...")
all_apps
|> Enum.flat_map(fn app ->
case filtered_deps(app, all_apps) do
{:error, _} -> []
{:ok, deps} -> Enum.map(deps, &{app, &1})
end
end)
|> Archeometer.Util.DumpStats.save_apps_xrefs()
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.collect.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