defmodule Archeometer.Analysis.Xref do @moduledoc """ Functions for generating a dependency graph from a list of given modules. Accepted output formats are "dot" (graphviz), "png" and "mermaid". """ alias Archeometer.Graphs.Graphviz alias Archeometer.Graphs.Mermaid alias Archeometer.Repo import Archeometer.Analysis.Xref.Data @supported_formats ["png", "dot", "mermaid", "svg"] @doc """ Creates a dependency graph between the modules given as parameters ## Parameters - `modules` is either a list of module names, e.g. `[Foo.Bar, Foo.Rex, Foo.Zorg]` or a tuple `{application, namespace}` where application is the name of a specific applciation or `:none` for all applications, and namespace is a specific namespace or `"*"`. - `format` can be one of "dot" (graphviz), "png", or "mermaid". - `db_name` is the filename of the DB to be used. If not given uses default DB. ## Returns - The binary representing the graph, if the operation was completed successfully. - `{:error, reason}` if not. """ def graph(opts) do opts = Enum.into(opts, %{}) case opts do %{ app: :none, namespace: "*", origin: false, dest: false } -> gen_graph(opts.modules, opts.format, opts.db_name) %{ app: app, namespace: namespace, origin: false, dest: false } -> gen_graph({app, namespace}, opts.format, opts.db_name) %{ app: :none, namespace: "*", modules: modules, origin: origin, dest: dest } -> gen_graph({modules, direction(origin, dest)}, opts.format, opts.db_name) end end defp direction(origin, dest) do cond do origin and dest -> :both origin and not dest -> :outgoing true -> :incoming end end def gen_graph(modules, format, db_name \\ Repo.default_db_name()) def gen_graph({modules, direction}, format, db_name) when format in @supported_formats and is_atom(direction) do modules_result = Enum.reduce(modules, [], fn module, acc -> %{rows: neighbors} = neighbors(module, direction, db_name) acc ++ List.flatten(neighbors) end) modules_result = Enum.uniq(modules_result ++ modules) gen_graph(modules_result ++ modules, format, db_name) end def gen_graph({app, ns}, format, db_name) when format in @supported_formats do case get_modules(app, ns, db_name) do [] -> {:error, :no_modules_matched} modules -> gen_graph(modules, format, db_name) end end def gen_graph(modules, format, db_name) when format in @supported_formats do try do do_gen_graph(modules, format, db_name) rescue e in RuntimeError -> {:error, e.message} _ in MatchError -> {:error, :no_modules_matched} end end def gen_graph(_modules, _format, _db_name) do {:error, :unsupported_format} end defp do_gen_graph(modules, format, db_name) do Enum.each(modules, fn module -> if not module_exists?(module, db_name) do raise RuntimeError, message: "unknown module '#{module}'" end end) modules |> xrefs(db_name) |> render(format) end defp xrefs(modules, db_name) do modules |> Enum.map(&callees(&1, db_name, modules)) |> Enum.into(%{}) end defp callees(module, db_name, modules_list) do others = (modules_list -- [module]) |> List.flatten() %{rows: xrefs} = neighbors(module, :outgoing, db_name) callees = xrefs |> List.flatten() |> Enum.uniq() |> Enum.filter(&Enum.member?(others, &1)) {module, callees} end @doc ~S""" Takes a map representing the adjacency list of a graph, and renders it using the appropriante backend. This can be both plain text graph formats (mermaid or dot) or image formats (svg or png). iex> Archeometer.Analysis.Xref.render(%{"a" => ["b"], "b" => []}, "dot") =~ ...> ~r/digraph G {\n .*\n.*\n "a";\n "b";\n "a" -> "b";\n}\n/ true iex> Archeometer.Analysis.Xref.render(%{"a" => ["b"], "b" => []}, "mermaid") ~s/graph TD;\n id_0([a]);\n id_1([b]);\n id_0-->id_1;\n/ """ def render(content, format) def render(refs, "dot") do Graphviz.render_dot(refs) end def render(refs, format) when format in ["png", "svg"] do Graphviz.render_image(refs, format) end def render(refs, "mermaid") do Mermaid.render(refs) end end