Current section

Files

Jump to
cracker lib mermaid_generator.ex
Raw

lib/mermaid_generator.ex

defmodule Cracker.MermaidGenerator do
@moduledoc """
Generates Mermaid flowchart markup from a list of function call edges.
Each edge is represented as [caller, callee] where caller and callee
are strings in the format "module.function/arity"
"""
@doc """
Generate Mermaid flowchart markup from a list of function call edges.
## Example
edges = [
{"MyApp.ModuleA.post/1", "MyApp.Client.post/2"},
{"MyApp.ModuleA.post/1", "MyApp.ModuleA.persist_data/2"},
{"MyApp.Client.post/2", "MyApp.Client.encode_query/2"},
{"MyApp.Client.post/2", "MyApp.Client.send_request/2"}
]
Cracker.MermaidGenerator.generate(edges)
"""
def generate(edges) do
# Convert edges to structured format
structured_edges = Enum.map(edges, &to_edge_tuple/1)
# Group by caller
grouped_edges = group_by_caller(structured_edges)
# Group by module
module_grouped_edges = group_by_module(grouped_edges)
IO.iodata_to_binary([
"flowchart LR\n",
" %% Function call graph\n",
generate_caller_nodes(grouped_edges),
"\n",
generate_module_subgraphs(module_grouped_edges),
"\n %% External connections\n",
generate_connections(grouped_edges)
])
end
defp to_edge_tuple({from, to}) do
{parse_function(from), parse_function(to)}
end
defp parse_function(function_str) do
parts = String.split(function_str, ".")
{module_parts, [function_with_arity]} = Enum.split(parts, -1)
module_path = Enum.join(module_parts, ".")
[function_name, arity] = String.split(function_with_arity, "/")
{module_path, function_name, String.to_integer(arity)}
end
defp group_by_caller(edges) do
Enum.group_by(edges, &elem(&1, 0))
end
defp group_by_module(grouped_edges) do
Enum.group_by(grouped_edges, fn {{module, _func, _arity}, _callees} -> module end)
end
defp generate_caller_nodes(grouped_edges) do
# Find all functions that are called by others
called_functions =
grouped_edges
|> Enum.flat_map(fn {_caller, callees} ->
Enum.map(callees, fn {_caller, callee} -> callee end)
end)
|> MapSet.new()
grouped_edges
|> Enum.filter(fn {caller, _callees} -> not MapSet.member?(called_functions, caller) end)
|> Enum.map(fn {{module, func, arity}, _callees} ->
" #{node_id(module, func, arity)}[\"#{module}.#{func}/#{arity}\"]\n"
end)
end
defp generate_module_subgraphs(module_grouped_edges) do
Enum.map(module_grouped_edges, fn {module, edges} ->
module_container = module_container_id(module)
[
" subgraph #{module_container}[\"#{module}\"]\n",
" style #{module_container} stroke-dasharray: 5 5\n",
generate_function_subgraphs(edges),
" end\n"
]
end)
end
defp generate_function_subgraphs(grouped_edges) do
Enum.map(grouped_edges, fn {{caller_module, caller_func, caller_arity}, callees} ->
container_name = container_id(caller_module, caller_func, caller_arity)
[
" subgraph #{container_name}[\"#{caller_func}/#{caller_arity}\"]\n",
# Generate all nodes
generate_callee_nodes(caller_module, caller_func, caller_arity, callees),
# Generate internal connections
generate_callee_connections(caller_module, caller_func, caller_arity, callees),
" end\n"
]
end)
end
defp generate_callee_nodes(caller_module, caller_func, caller_arity, callees) do
callees
|> Enum.with_index()
|> Enum.map(fn {{_caller, {callee_module, callee_func, callee_arity}}, _idx} ->
node = container_scoped_node_id(caller_module, caller_func, caller_arity, callee_module, callee_func, callee_arity)
label =
if callee_module == caller_module do
# Internal call - simplified label
"#{callee_func}/#{callee_arity}"
else
# External call - full module path
"#{format_module_name(callee_module)}.#{callee_func}/#{callee_arity}"
end
" #{node}[\"#{label}\"]\n"
end)
end
defp generate_callee_connections(caller_module, caller_func, caller_arity, callees) do
callees
|> Enum.with_index()
# Get pairs of consecutive functions
|> Enum.chunk_every(2, 1, :discard)
|> Enum.map(fn [{{_caller1, callee1}, _idx1}, {{_caller2, callee2}, _idx2}] ->
source =
container_scoped_node_id(
caller_module,
caller_func,
caller_arity,
elem(callee1, 0),
elem(callee1, 1),
elem(callee1, 2)
)
target =
container_scoped_node_id(
caller_module,
caller_func,
caller_arity,
elem(callee2, 0),
elem(callee2, 1),
elem(callee2, 2)
)
" #{source} --> #{target}\n"
end)
end
defp generate_connections(grouped_edges) do
# Find all functions that are called by others
called_functions =
grouped_edges
|> Enum.flat_map(fn {_caller, callees} ->
Enum.map(callees, fn {_caller, callee} -> callee end)
end)
|> MapSet.new()
Enum.flat_map(grouped_edges, fn {{caller_module, caller_func, caller_arity} = caller, callees} ->
caller_node = node_id(caller_module, caller_func, caller_arity)
# Only create container connection if this function isn't called by others
container_connection =
if MapSet.member?(called_functions, caller) do
[]
else
[" #{caller_node} --> #{module_container_id(caller_module)}\n"]
end
# Connections to other containers
external_connections =
callees
|> Enum.filter(fn {_caller, callee} ->
# Only create connections for calls to functions that are callers themselves
Enum.any?(grouped_edges, fn {key, _} -> key == callee end)
end)
|> Enum.map(fn {_caller, {callee_module, callee_func, callee_arity}} ->
source_node =
container_scoped_node_id(
caller_module,
caller_func,
caller_arity,
callee_module,
callee_func,
callee_arity
)
if callee_module == caller_module && callee_func == caller_func && callee_arity == caller_arity do
# Self-referential connection within the same container
" #{source_node} --> #{source_node}\n"
else
target_container = container_id(callee_module, callee_func, callee_arity)
" #{source_node} -.-> #{target_container}\n"
end
end)
container_connection ++ external_connections
end)
end
defp node_id(module, function, arity) do
"#{String.replace(module, ".", "_")}_#{function}_#{arity}"
end
defp container_id(module, function, arity) do
"#{node_id(module, function, arity)}_container"
end
defp module_container_id(module) do
"#{String.replace(module, ".", "_")}_container"
end
defp container_scoped_node_id(container_module, container_func, container_arity, module, function, arity) do
"#{container_id(container_module, container_func, container_arity)}_#{node_id(module, function, arity)}"
end
defp format_module_name(module) do
module
end
end