Current section

Files

Jump to
archeometer lib archeometer analysis xref.ex
Raw

lib/archeometer/analysis/xref.ex

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".
"""
import Archeometer.Query
alias Archeometer.Repo
alias Archeometer.Schema.XRef
alias Archeometer.Schema.Module
alias Archeometer.Graphs.Graphviz
alias Archeometer.Graphs.Mermaid
@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 gen_graph(modules, format, db_name \\ Repo.default_db_name())
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 get_modules(:none, "*", db_name) do
%{rows: modules} =
Repo.all(
from(m in Module,
select: [
name: m.name
]
),
[],
db_name
)
modules
end
defp get_modules(:none, ns, db_name) do
%{rows: modules} =
Repo.all(
from(m in Module,
select: [name: m.name],
where: m.name == ^ns or like(m.name, ^(ns <> ".%"))
),
[],
db_name
)
modules
end
defp get_modules(app, "*", db_name) do
%{rows: modules} =
Repo.all(
from(m in Module,
select: [
name: m.name
],
where: m.application.name == ^app
),
[],
db_name
)
modules
end
defp get_modules(app, ns, db_name) do
%{rows: modules} =
Repo.all(
from(m in Module,
select: [
name: m.name
],
where: m.application.name == ^app and (m.name == ^ns or like(m.name, ^(ns <> ".%")))
),
[],
db_name
)
modules
end
defp do_gen_graph(modules, format, db_name) do
modules
|> xrefs(db_name)
|> render(format)
end
defp xrefs(modules, db_name) do
module_ids = module_ids(modules, db_name)
modules_map = Enum.zip(module_ids, modules) |> Enum.into(%{})
module_ids
|> Enum.map(&callees(&1, db_name, modules_map))
|> Enum.into(%{})
end
defp module_ids(modules, db_name) do
Enum.map(modules, &module_id(&1, db_name))
end
defp module_id(m, db_name) do
%{rows: module} =
Repo.all(
from(m in Module,
select: [
id: m.id
],
where: m.name == ^m
),
[],
db_name
)
case module do
[[id]] -> id
[] -> raise RuntimeError, message: "unknown module '#{m}'"
end
end
defp callees(id, db_name, modules_map) do
others = Map.keys(modules_map) -- [id]
%{rows: xrefs_ids} =
Repo.all(
from(x in XRef,
select: [
callee_id: x.callee.id
],
where: x.caller.id == ^id
),
[],
db_name
)
callees =
xrefs_ids
|> List.flatten()
|> Enum.uniq()
|> Enum.filter(fn callee_id -> Enum.member?(others, callee_id) end)
|> Enum.map(fn callee_id -> Map.get(modules_map, callee_id) end)
{Map.get(modules_map, id), callees}
end
defp render(refs, "dot") do
Graphviz.render_dot(refs)
end
defp render(refs, format) when format in ["png", "svg"] do
Graphviz.render_image(refs, format)
end
defp render(refs, "mermaid") do
Mermaid.render(refs)
end
end