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
def 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> render(%{"a" => ["b"], "b" => []}, "dot") =~
...> ~r/digraph G {\n .*\n.*\n "a" .*;\n "b" .*;\n "a" -> "b";\n}\n/
true
iex> 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, __MODULE__.Centrality.degree_centrality(refs))
end
def render(refs, format) when format in ["png", "svg"] do
Graphviz.render_image(refs, __MODULE__.Centrality.degree_centrality(refs), format)
end
def render(refs, "mermaid") do
Mermaid.render(refs)
end
end