Current section
Files
Jump to
Current section
Files
lib/reach/cli/render/map.ex
defmodule Reach.CLI.Render.Map do
@moduledoc false
@compile {:no_warn_undefined, [Boxart.Render.PieChart, Boxart.Render.PieChart.PieChart]}
@dialyzer {:nowarn_function, render_depth_row_graph: 2}
alias Reach.CLI.BoxartGraph
alias Reach.CLI.Format
alias Reach.CLI.Requirements
alias Reach.Project.Query
@section_order [:hotspots, :boundaries, :coupling, :modules, :effects, :depth, :data, :xref]
def render(result, "json") do
Requirements.json!()
IO.puts(Jason.encode!(json_envelope(result), pretty: true))
end
def render(result, "oneline"), do: render_oneline_map(result)
def render(result, _format), do: render_text_map(result)
def render_graph(project, sections, data) do
BoxartGraph.require!()
cond do
:coupling in sections or :modules in sections ->
BoxartGraph.render_module_graph(project)
:effects in sections ->
render_effect_graph(data.effects)
:depth in sections ->
render_depth_graph(project, data.depth)
true ->
Mix.raise(
"--graph is supported with --modules, --coupling, or --depth. For target graphs, use mix reach.inspect TARGET --graph"
)
end
end
defp render_text_map(%{summary: summary, sections: sections}) do
if map_size(sections) == 1 do
[{key, data}] = Map.to_list(sections)
IO.puts(Format.header(section_header(key, data)))
render_text_section(key, data)
else
IO.puts(Format.header("Reach Map"))
IO.puts(" modules=#{summary.modules} functions=#{summary.functions}")
IO.puts(
" call_graph=#{summary.call_graph_vertices} vertices/#{summary.call_graph_edges} edges"
)
IO.puts(" dependence_graph=#{summary.graph_nodes} nodes/#{summary.graph_edges} edges")
Enum.each(ordered_sections(sections), fn {key, data} ->
IO.puts(Format.section(section_title(key)))
render_text_section(key, data)
end)
end
end
defp render_oneline_map(%{summary: summary, sections: sections}) do
IO.puts(
"summary modules=#{summary.modules} functions=#{summary.functions} call_edges=#{summary.call_graph_edges} graph_edges=#{summary.graph_edges}"
)
Enum.each(ordered_sections(sections), fn
{:modules, modules} ->
Enum.each(
modules,
&IO.puts(
"module #{&1.name} functions=#{&1.total_functions} complexity=#{&1.total_complexity}"
)
)
{:hotspots, hotspots} ->
Enum.each(
hotspots,
&IO.puts(
"hotspot #{&1.function} score=#{&1.score} branches=#{&1.branches} callers=#{&1.callers}"
)
)
{:boundaries, boundaries} ->
Enum.each(
boundaries,
&IO.puts("boundary #{&1.function} effects=#{Enum.join(&1.effects, "+")}")
)
{:effects, %{distribution: distribution}} ->
Enum.each(distribution, fn row -> IO.puts("effect #{row.effect}=#{row.count}") end)
{:effects, effects} ->
Enum.each(effects, fn {effect, count} -> IO.puts("effect #{effect}=#{count}") end)
{:data, data} ->
Enum.each(data.top_functions, &IO.puts("data #{&1.function} edges=#{&1.data_edges}"))
Enum.each(
data.cross_function_edges || [],
&IO.puts("xref #{&1.from} -> #{&1.to} edges=#{&1.edges}")
)
{:coupling, data} ->
Enum.each(
data.modules,
&IO.puts("coupling #{&1.name} ca=#{&1.afferent} ce=#{&1.efferent} i=#{&1.instability}")
)
{:depth, rows} ->
Enum.each(
rows,
&IO.puts("depth #{&1.function} depth=#{&1.depth} branches=#{&1.branch_count}")
)
end)
end
defp render_text_section(:modules, []), do: IO.puts(" " <> Format.empty())
defp render_text_section(:modules, modules) do
Enum.each(modules, fn module ->
behaviours = module_behaviour_label(module.callbacks)
IO.puts(" #{Format.bright(module.name)}#{Format.cyan(behaviours)}")
IO.puts(
" #{module.public_count} public, #{module.private_count} private, complexity #{module.total_complexity}"
)
if module.biggest_function,
do: IO.puts(" biggest: #{Format.yellow(module.biggest_function)}")
if module.file, do: IO.puts(" #{Format.faint(Format.path(module.file))}")
IO.puts("")
end)
end
defp render_text_section(:hotspots, []), do: IO.puts(" " <> Format.empty())
defp render_text_section(:hotspots, hotspots) do
IO.puts(" #{Format.faint("score combines branch count with caller impact")}")
Enum.each(hotspots, fn hotspot ->
label = Map.get(hotspot, :display_function, hotspot.function)
IO.puts(
" #{Format.bright(label)} score=#{hotspot.score} branches=#{hotspot.branches} callers=#{hotspot.callers}"
)
IO.puts(" #{Format.loc(hotspot.file, hotspot.line)}")
end)
end
defp render_text_section(:coupling, %{modules: [], cycles: []}),
do: IO.puts(" " <> Format.empty())
defp render_text_section(:coupling, %{modules: modules, cycles: cycles}) do
IO.puts(
" #{Format.faint("incoming=afferent dependencies, outgoing=efferent dependencies, instability=outgoing/(incoming+outgoing)")}"
)
Enum.each(modules, fn module ->
IO.puts(
" #{Format.bright(module.name)} incoming=#{Format.count(module.afferent)} outgoing=#{Format.count(module.efferent)} instability=#{instability_label(module.instability)}"
)
end)
if cycles != [] do
IO.puts(" cycles:")
Enum.each(cycles, &IO.puts(" #{Enum.join(&1.modules, " -> ")}"))
end
end
defp render_text_section(:effects, %{distribution: [], unknown_calls: []}),
do: IO.puts(" " <> Format.empty())
defp render_text_section(:effects, %{distribution: distribution, unknown_calls: unknown_calls}) do
Enum.each(distribution, fn row ->
IO.puts(" #{Format.effect(row.effect)}: #{row.count} (#{percent(row.ratio)})")
end)
if unknown_calls != [] do
IO.puts(" unknown calls:")
Enum.each(unknown_calls, &IO.puts(" #{&1.module}.#{&1.function}: #{&1.count}"))
end
end
defp render_text_section(:effects, []), do: IO.puts(" " <> Format.empty())
defp render_text_section(:effects, effects),
do:
Enum.each(effects, fn {effect, count} -> IO.puts(" #{Format.effect(effect)}: #{count}") end)
defp render_text_section(:boundaries, []), do: IO.puts(" " <> Format.empty())
defp render_text_section(:boundaries, boundaries) do
Enum.each(boundaries, fn boundary ->
IO.puts(
" #{Format.bright(boundary.display_function)} effects=#{Format.effects_join(boundary.effects, "+")}"
)
Enum.each(boundary.calls, fn call ->
IO.puts(" #{Format.effect(call.effect)} #{call.call}")
end)
IO.puts(" #{Format.loc(boundary.file, boundary.line)}")
end)
end
defp render_text_section(:depth, []), do: IO.puts(" " <> Format.empty())
defp render_text_section(:depth, rows) do
Enum.each(rows, fn row ->
IO.puts(" #{Format.bright(row.function)} depth=#{row.depth} branches=#{row.branch_count}")
IO.puts(" #{Format.loc(row.file, row.line)}")
end)
end
defp render_text_section(:data, data) do
IO.puts(" total_data_edges=#{data.total_data_edges}")
IO.puts(
" #{Format.faint("parameter_in flows into a call; parameter_out flows from a call result")}"
)
render_cross_function_flows(Map.get(data, :cross_function_edges, []))
render_top_data_functions(data.top_functions)
end
defp render_cross_function_flows([]) do
IO.puts("\n Cross-function flows:")
IO.puts(" " <> Format.empty())
end
defp render_cross_function_flows(edges) do
IO.puts("\n Cross-function flows:")
Enum.each(edges, &render_cross_function_flow/1)
end
defp render_cross_function_flow(row) do
labels = row.labels |> Enum.map_join(", ", fn {label, count} -> "#{label}=#{count}" end)
variables = Enum.join(row.variables, ", ")
IO.puts(" #{Format.bright(row.from)} → #{Format.bright(row.to)} edges=#{row.edges}")
IO.puts(" labels: #{labels}")
if variables != "", do: IO.puts(" vars: #{variables}")
end
defp render_top_data_functions([]) do
IO.puts("\n Functions by data edges:")
IO.puts(" " <> Format.empty())
end
defp render_top_data_functions(rows) do
IO.puts("\n Functions by data edges:")
Enum.each(rows, fn row ->
IO.puts(" #{Format.bright(row.function)} data_edges=#{row.data_edges}")
IO.puts(" #{Format.loc(row.file, row.line)}")
end)
end
defp ordered_sections(sections) do
Enum.sort_by(sections, fn {key, _data} ->
Enum.find_index(@section_order, &(&1 == key)) || 999
end)
end
defp percent(value) when is_float(value),
do: :erlang.float_to_binary(value * 100, decimals: 1) <> "%"
defp percent(value), do: to_string(value)
defp instability_label(value) when is_number(value) and value >= 0.8,
do: Format.yellow(to_string(value))
defp instability_label(value) when is_number(value), do: to_string(value)
defp instability_label(value), do: to_string(value)
defp section_title(:modules), do: "Modules"
defp section_title(:hotspots), do: "Hotspots"
defp section_title(:coupling), do: "Coupling"
defp section_title(:effects), do: "Effects"
defp section_title(:boundaries), do: "Effect Boundaries"
defp section_title(:depth), do: "Control Depth"
defp section_title(:data), do: "Data Flow"
defp section_header(:modules, data), do: "Modules (#{length(data)})"
defp section_header(:hotspots, data), do: "Hotspots (#{length(data)})"
defp section_header(:coupling, data), do: "Module Coupling (#{length(data.modules)})"
defp section_header(:effects, data), do: "Effect Distribution (#{data.total_calls} calls)"
defp section_header(:boundaries, data), do: "Effect Boundaries (#{length(data)})"
defp section_header(:depth, data), do: "Dominator Depth (#{length(data)})"
defp section_header(:data, data), do: "Data Flow (#{length(data.top_functions)})"
defp render_depth_graph(_project, []), do: IO.puts(" (no functions found)")
defp render_depth_graph(project, [row | _]), do: render_depth_row_graph(project, row)
defp render_depth_row_graph(project, row) do
func = Query.find_function_at_location(project, row.file, row.line)
if func do
BoxartGraph.render_cfg(func, row.file)
:ok
else
Mix.raise("Function not found: #{row.function}")
end
end
defp module_behaviour_label([]), do: ""
defp module_behaviour_label([behaviour | _]) when is_binary(behaviour), do: " (#{behaviour})"
defp module_behaviour_label(_callbacks), do: ""
defp render_effect_graph(result) do
BoxartGraph.require_pie_chart!()
chart_module = Module.concat([Boxart, Render, PieChart, PieChart])
slices =
result.distribution
|> Enum.reject(&(&1.count == 0))
|> Enum.map(&{&1.effect, &1.ratio * 100})
chart =
struct!(chart_module,
title: "Effect Distribution (#{result.total_calls} calls)",
slices: slices,
show_data: true
)
IO.puts(Boxart.Render.PieChart.render(chart))
end
defp json_envelope(%{command: command} = data),
do: %Reach.CLI.JSONEnvelope{command: command, data: Map.delete(data, :command)}
end