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