defmodule Mix.Tasks.Arch.Xref do
@moduledoc """
Mix Task to generate a dependency graph given some module names.
Usage:
mix arch.xref [options] mod1 mod2 .. modN
The following options are accepted:
* `--db` - Database filename
* `--format` - Can be one of `dot`, `png` or `svg`
* `--out` - Output filename
* `--app` - Application name
* `--ns` - Namespace of the modules to consider
* `--origin` - Subgraph of modules that are called from the given module
* `--dest` - Subgraph of modules that calls a given module
"""
@shortdoc "Generates a dependency graph from a list of modules"
require Logger
require EEx
use Mix.Task
use Archeometer.Repo
@supported_formats ["png", "svg", "dot", "mermaid"]
@impl Mix.Task
def run(argv) do
case parse_args(argv) do
{:ok, opts} ->
Archeometer.Analysis.Xref.graph(opts)
|> write(Keyword.get(opts, :out_fname))
{:error, e} = error ->
Mix.shell().error("Error: #{e}")
print_help()
error
end
end
defp print_help() do
Mix.shell().info("""
Usage:
mix arch.xref [opts] Mod1 Mod2 ... ModN
or
mix arch.xref [opts] [filter_opts]
opts: --db db_file_name --format (dot, png, svg, mermaid) --out out_fname
filter_opts: --app application_name --ns namespace --origin --dest
""")
end
defp parse_args(argv) do
{opts, modules, invalid_switches} =
OptionParser.parse(
argv,
strict: [
db: :string,
format: :string,
out: :string,
app: :string,
ns: :string,
origin: :boolean,
dest: :boolean
]
)
case invalid_switches do
[] ->
db_name = Keyword.get(opts, :db, default_db_name())
format = Keyword.get(opts, :format, "dot")
out_fname = Keyword.get(opts, :out, "console")
app = Keyword.get(opts, :app, :none)
namespace = Keyword.get(opts, :ns, "*")
origin = Keyword.get(opts, :origin, false)
dest = Keyword.get(opts, :dest, false)
case validate_options(
%{
db: db_name,
format: format,
out: out_fname,
app: app,
ns: namespace,
origin: origin,
dest: dest
},
modules
) do
:ok ->
{:ok,
[
app: app,
namespace: namespace,
db_name: db_name,
modules: modules,
format: format,
out_fname: out_fname,
origin: origin,
dest: dest
]}
{:error, e} ->
{:error, e}
end
_ ->
{:error, :wrong_arguments}
end
end
defp validate_options(
%{
format: format,
out: out_fname,
app: app,
ns: namespace,
db: db_name,
origin: origin,
dest: dest
},
modules
) do
origin_dest = origin or dest
app_namespace = app != :none or namespace != "*"
[
{
not Archeometer.Repo.db_ready?(:full, db_name),
{:error, "Database is not existent or doesn't have the required tables."}
},
{
length(modules) > 0 and app_namespace,
{:error, :incompatible_parameters}
},
{
app_namespace and origin_dest,
{:error, :incompatible_parameters}
},
{
length(modules) < 1 and origin_dest,
{:error, :no_modules_matched}
},
{
format not in @supported_formats,
{:error, :unsupported_output_format}
},
{
format == "png" and out_fname == "console",
{:error, :png_not_writable_to_console}
}
]
|> Enum.find_value(:ok, fn {x, e} -> if x, do: e end)
end
defp write({:error, error}, _) do
Mix.shell().error("Error: #{error}")
Mix.shell().error("No XRef is generated.")
{:error, error}
end
defp write(content, "console") do
Mix.shell().info(content)
Mix.shell().info("Graph ready at: console")
end
defp write(content, out_fname) do
File.write!(out_fname, content)
Mix.shell().info("Graph ready at: '#{out_fname}'")
end
end