Current section
Files
Jump to
Current section
Files
lib/frontier/visualization.ex
defmodule Frontier.Visualization do
@moduledoc "Builds a dependency graph from Frontier config and tracer references for `mix frontier.visualize`."
alias Frontier.Classifier
alias Frontier.Graph
alias Frontier.Rules
alias Frontier.Store
@doc "Builds a graph from Frontier config and tracer references."
@spec build :: Frontier.Graph.t()
def build do
contexts = Store.contexts()
ignored = Store.all_ignored()
app_name =
case Store.root() do
{root_mod, _} -> inspect(root_mod)
nil -> Keyword.fetch!(Mix.Project.config(), :app) |> Atom.to_string() |> Macro.camelize()
end
graph = Graph.new("#{app_name} Frontiers")
graph =
contexts
|> Enum.sort_by(fn {mod, _} -> inspect(mod) end)
|> Enum.reduce(graph, fn {module, _config}, g ->
node_type = if module in ignored, do: :ignored, else: :normal
Graph.add_node(g, inspect(module), node_type)
end)
references = Store.references()
if references != [] do
build_from_references(graph, references, contexts)
else
build_from_config(graph, contexts)
end
end
defp build_from_references(graph, references, contexts) do
edge_data = collect_edge_data(references, contexts)
graph = add_edges_from_data(graph, edge_data)
violations = collect_violations(edge_data)
Graph.set_violations(graph, violations)
end
defp collect_edge_data(references, contexts) do
Enum.reduce(references, %{}, fn ref, acc ->
classify_reference(ref, contexts, acc)
end)
end
defp classify_reference(ref, contexts, acc) do
caller_context = Classifier.owning_context(ref.from)
callee_context = Classifier.owning_context(ref.to)
if cross_context_call?(caller_context, callee_context, contexts) do
update_edge_data(acc, caller_context, callee_context, ref)
else
acc
end
end
defp cross_context_call?(caller_context, callee_context, contexts) do
caller_context && callee_context &&
caller_context != callee_context &&
Map.has_key?(contexts, caller_context)
end
defp update_edge_data(acc, caller_context, callee_context, ref) do
edge_key = {caller_context, callee_context}
{edge_type, violation_pair} =
case Rules.check(ref.from, ref.to) do
:ok -> {:allowed, nil}
{:violation, _, _} -> {:violation, {ref.from, ref.to}}
end
existing = Map.get(acc, edge_key, %{type: nil, violations: MapSet.new()})
updated_type = merge_edge_type(existing.type, edge_type)
updated_violations =
if violation_pair,
do: MapSet.put(existing.violations, violation_pair),
else: existing.violations
Map.put(acc, edge_key, %{type: updated_type, violations: updated_violations})
end
defp merge_edge_type(nil, type), do: type
defp merge_edge_type(:violation, _), do: :violation
defp merge_edge_type(_, :violation), do: :violation
defp merge_edge_type(:skipped, _), do: :skipped
defp merge_edge_type(:allowed, :allowed), do: :allowed
defp add_edges_from_data(graph, edge_data) do
Enum.reduce(edge_data, graph, fn {{from, to}, %{type: edge_type}}, g ->
caller_config = Store.context(from)
final_type =
if caller_config && to in caller_config.skip_violations,
do: :skipped,
else: edge_type
Graph.add_edge(g, inspect(from), inspect(to), final_type)
end)
end
defp collect_violations(edge_data) do
edge_data
|> Enum.flat_map(fn {_key, %{violations: violations}} -> MapSet.to_list(violations) end)
|> Enum.sort()
|> Enum.map(fn {caller, callee} -> {inspect(caller), inspect(callee)} end)
end
defp build_from_config(graph, contexts) do
Enum.reduce(contexts, graph, fn {module, config}, g ->
add_config_edges(g, module, config, contexts)
end)
end
defp add_config_edges(graph, _module, %{reaches: nil}, _contexts), do: graph
defp add_config_edges(graph, module, %{reaches: reaches} = config, contexts) do
Enum.reduce(reaches, graph, fn target, g ->
if Map.has_key?(contexts, target) do
edge_type = if target in config.skip_violations, do: :skipped, else: :allowed
Graph.add_edge(g, inspect(module), inspect(target), edge_type)
else
g
end
end)
end
end