defmodule Mix.Tasks.Arch.Dsm do
@moduledoc """
Mix Task to perform a Design Structure Matrix (DSM) analysis.
Usage:
mix arch.dsm [options]
The following options are accepted:
* `--app` - Application name
* `--ns` - Namespace of the modules considered
* `--db` - Database filename
* `--format` - Can be one of `txt` (default) or `svg`
* `--out` - Output filename. Defaults to console
* `--skip-tests` - Skips test related modules (default)
* `--no-skip-tests` - Avoids skipping test related modules
Both options `--app` or `--ns` can be used at the same time in order to have
fine grained control of the set of modules considered in the analysis.
If none of `--app` or `--ns` options are given,
all modules in the project are considered.
Namespace is interpreted very broadly since Elixir doesn't have that concept.
Namespace can be interpreted as a string matching the beginning of the module names to
be considered, such as "Foo" or "Foo.Bar", after which a last part of the name must
be given to create a full module name.
For example the "namespace" `Foo` will include in the analysis modules such as
`Foo.Bar`, `Foo.Baz` and `Foo.Bar.Buzz`, but not `FooBar` or `Food`.
"""
@shortdoc "Performs a Design Structure Matrix analysis"
require Logger
use Mix.Task
use Archeometer.Repo
alias Archeometer.Analysis.DSM
@supported_formats ["svg", "txt"]
@impl Mix.Task
def run(argv) do
case get_args(argv) do
[
app: app,
namespace: namespace,
db_name: dbname,
format: format,
out: out_fname,
skip_tests: skip_tests
] ->
case dsm_analysis(app, namespace, dbname, skip_tests) do
{:ok, dsm, module_names} ->
dsm
|> render(module_names, format)
|> write_report(out_fname)
{:error, e} = error ->
Mix.shell().error("Error: #{e}")
Mix.shell().error("No DSM is generated.")
error
end
{:error, e} = error ->
Mix.shell().error("Error: #{e}")
print_help()
error
end
end
defp get_args(argv) do
{opts, _args, invalid} =
OptionParser.parse(
argv,
strict: [
app: :string,
ns: :string,
db: :string,
format: :string,
out: :string,
skip_tests: :boolean
]
)
case invalid do
[] ->
app = Keyword.get(opts, :app, :none)
namespace = Keyword.get(opts, :ns, "*")
db_name = Keyword.get(opts, :db, default_db_name())
format = Keyword.get(opts, :format, "txt")
out = Keyword.get(opts, :out, "console")
skip_tests = Keyword.get(opts, :skip_tests, true)
case validate_options(%{format: format, db: db_name, out: out}) do
:ok ->
[
app: app,
namespace: namespace,
db_name: db_name,
format: format,
out: out,
skip_tests: skip_tests
]
{:error, error} ->
{:error, error}
end
_ ->
{:error, :wrong_arguments}
end
end
defp validate_options(%{format: format, db: db_name}) do
cond do
format not in @supported_formats ->
{:error, :unsupported_output_format}
not Archeometer.Repo.db_ready?(:full, db_name) ->
{:error, "Database is not existent or doesn't have the required tables."}
true ->
:ok
end
end
defp print_help() do
Mix.shell().info("""
Usage: mix arch.dsm [opts] namespace
opts: --app app --ns namespace ---db db_file_name --format (svg, txt) --out fname --skip-tests (default)
""")
end
defp dsm_analysis(app, namespace, db_name, skip_tests) do
case DSM.gen_dsm(app, namespace, db_name, skip_tests) do
{:ok, dsm, module_names} ->
{:ok, DSM.triangularize(dsm) |> DSM.cyclic_deps_groups(), module_names}
{:error, error} ->
{:error, error}
end
end
defp render(dsm, module_names, "txt") do
DSM.ConsoleRender.render(dsm, module_names)
end
defp render(dsm, module_names, "svg") do
DSM.SVGRender.render(dsm, module_names)
end
defp write_report(str, "console") do
IO.puts(str)
end
defp write_report(str, file_name) do
file_name |> Path.dirname() |> File.mkdir_p()
File.write!(file_name, str)
Mix.shell().info("Report ready in '#{file_name}'!")
end
end