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), 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