Current section
Files
Jump to
Current section
Files
lib/pi/code_map.ex
defmodule Pi.CodeMap.FunctionRef do
@moduledoc "A resolved function in a Reach project."
use JSONCodec
defstruct [:target, :mfa, :file, :line, :kind, clauses: []]
@type t :: %__MODULE__{
target: String.t() | nil,
mfa: term(),
file: String.t() | nil,
line: non_neg_integer() | nil,
kind: atom() | nil,
clauses: [term()]
}
end
defmodule Pi.CodeMap.Hotspot do
@moduledoc "A Reach hotspot finding for a function."
use JSONCodec
defstruct [
:display_function,
:module,
:function,
:file,
:line,
:score,
:branches,
:callers,
clauses: []
]
@type t :: %__MODULE__{
display_function: String.t() | nil,
module: String.t() | nil,
function: String.t() | nil,
file: String.t() | nil,
line: non_neg_integer() | nil,
score: number() | nil,
branches: non_neg_integer() | nil,
callers: non_neg_integer() | nil,
clauses: [term()]
}
end
defmodule Pi.CodeMap.Boundary do
@moduledoc "A mixed-effect boundary candidate."
use JSONCodec
defstruct [:display_function, :module, :function, :file, :line, effects: [], calls: []]
@type t :: %__MODULE__{
display_function: String.t() | nil,
module: String.t() | nil,
function: String.t() | nil,
file: String.t() | nil,
line: non_neg_integer() | nil,
effects: [String.t()],
calls: [map()]
}
end
defmodule Pi.CodeMap.Smell do
@moduledoc "A Reach smell finding normalized for eval."
use JSONCodec
defstruct [:kind, :message, :file, :line, :confidence, :raw]
@type t :: %__MODULE__{
kind: String.t() | nil,
message: String.t() | nil,
file: String.t() | nil,
line: non_neg_integer() | nil,
confidence: String.t() | nil,
raw: map() | nil
}
end
defmodule Pi.CodeMap.Reflection do
@moduledoc "Post-edit semantic reflection result."
use JSONCodec
defstruct command: "Pi.CodeMap.reflect",
paths: [],
changed_functions: [],
summary: %{},
hotspots: [],
boundaries: [],
smells: [],
contexts: [],
recommendation: nil
@type t :: %__MODULE__{
command: String.t(),
paths: [String.t()],
changed_functions: [Pi.CodeMap.FunctionRef.t()],
summary: map(),
hotspots: [Pi.CodeMap.Hotspot.t()],
boundaries: [Pi.CodeMap.Boundary.t()],
smells: [Pi.CodeMap.Smell.t()],
contexts: [map()],
recommendation: String.t() | nil
}
end
defmodule Pi.CodeMap do
@moduledoc """
Reach-backed semantic code map and reflection helpers for eval.
`Pi.CodeMap` turns Reach's project graph APIs into compact eval workflows. It
is intentionally evidence-oriented: use it after edits to ask what changed,
what depends on it, and whether Reach sees hotspots/smells/refactor leads.
Eval preloads `alias Pi.CodeMap, as: CodeMap`.
"""
alias Pi.CodeMap.{Boundary, FunctionRef, Hotspot, Reflection, Smell}
alias Pi.Project.Context
alias Pi.Protocol.Tool.OutputPart
alias Reach.Check.Smells, as: ReachSmells
alias Reach.Inspect.Context, as: ReachContext
alias Reach.Inspect.Impact
alias Reach.IR.Helpers, as: IRHelpers
alias Reach.Map.Analysis, as: MapAnalysis
alias Reach.Project.Query
@default_top 10
@default_depth 3
@reflection_hotspot_top 8
@reflection_smell_top 12
@high_impact_smells ~w[
bare_rescue
dual_key_access
ets_partial_key_match
false_success_error
map_contract_drift
missing_external_resource
return_contract_drift
side_effect_order_drift
unsafe_atom_creation
unsafe_binary_to_term
validation_drift
]
@doc "Returns true when Reach is available in the current project BEAM."
def available?, do: Code.ensure_loaded?(Reach.Project)
@doc "Builds a Reach project graph for the current Mix project or selected paths."
def project(opts \\ []) do
ensure_reach!()
context = Context.current(opts)
paths =
cond do
paths = opts[:paths] -> expand_paths(paths, context)
glob = opts[:glob] -> expand_paths([glob], context)
path = opts[:path] -> expand_paths(List.wrap(path), context)
true -> project_source_paths(context)
end
Reach.Project.from_sources(paths, project_opts(opts))
end
@doc "Returns a project-wide Reach summary."
def summary(opts \\ []) do
project = Keyword.get_lazy(opts, :project, fn -> project(opts) end)
MapAnalysis.summary(project, opts[:path]) |> normalize()
end
@doc "Returns module metrics."
def modules(opts \\ []), do: section(:modules, opts)
@doc "Returns high-risk functions ranked by Reach hotspot score."
def hotspots(opts \\ []) do
:hotspots
|> raw_section(opts)
|> Enum.map(&hotspot/1)
end
@doc "Returns module coupling and cycles."
def coupling(opts \\ []), do: section(:coupling, opts)
@doc "Returns effectful call summaries."
def effects(opts \\ []), do: section(:effects, opts)
@doc "Returns mixed-effect boundary candidates."
def boundaries(opts \\ []) do
:boundaries
|> raw_section(opts)
|> Enum.map(&boundary/1)
end
@doc "Returns dominator-depth metrics."
def depth(opts \\ []), do: section(:depth, opts)
@doc "Returns cross-function data-flow summary."
def data_flow(opts \\ []), do: section(:data, opts)
@doc "Resolves a target string, MFA, or file:line into a function summary."
def find(target, opts \\ []) do
project = Keyword.get_lazy(opts, :project, fn -> project(opts) end)
with {:ok, _mfa, func} <- resolve(project, target) do
function_summary(func)
end
end
@doc "Returns callers for a target."
def callers(target, opts \\ []) do
project = Keyword.get_lazy(opts, :project, fn -> project(opts) end)
depth = Keyword.get(opts, :depth, @default_depth)
with {:ok, mfa, _func} <- resolve(project, target) do
project
|> Query.callers(mfa, depth)
|> Enum.map(&Map.update!(&1, :id, fn id -> %{mfa: id, label: mfa_string(id)} end))
|> normalize()
end
end
@doc "Returns callees for a target."
def callees(target, opts \\ []) do
project = Keyword.get_lazy(opts, :project, fn -> project(opts) end)
depth = Keyword.get(opts, :depth, @default_depth)
with {:ok, mfa, _func} <- resolve(project, target) do
project
|> Query.callees(mfa, depth)
|> normalize_call_tree()
end
end
@doc "Returns an agent-readable context bundle for a target."
def context(target, opts \\ []) do
project = Keyword.get_lazy(opts, :project, fn -> project(opts) end)
with {:error, _reason} <- function_context(project, target, opts),
{:error, _reason} <- module_context(project, target, opts) do
{:error, "Function or module not found: #{inspect(target)}"}
end
end
@doc "Returns Reach impact analysis for a target."
def impact(target, opts \\ []) do
project = Keyword.get_lazy(opts, :project, fn -> project(opts) end)
depth = Keyword.get(opts, :depth, @default_depth)
with {:ok, mfa, _func} <- resolve(project, target) do
project |> Impact.analyze(mfa, depth) |> normalize()
end
end
@doc "Returns Reach smell findings, optionally filtered by `:path`."
def smells(opts \\ []) do
project = Keyword.get_lazy(opts, :project, fn -> project(opts) end)
path = opts[:path]
project
|> ReachSmells.run([])
|> Enum.map(&smell/1)
|> filter_by_path(path)
|> Enum.sort_by(&smell_sort_key/1)
|> Enum.take(opts[:top] || @default_top)
end
@doc "Runs a post-edit semantic reflection."
def reflect(opts \\ []) do
project = Keyword.get_lazy(opts, :project, fn -> project(opts) end)
paths = reflection_paths(opts)
changed = changed_functions(project, paths)
changed_targets = Enum.map(changed, & &1.target)
hotspots = reflection_hotspots(project, paths, changed_targets, opts)
boundaries = reflection_boundaries(project, paths, opts)
smells = reflection_smells(project, paths, opts)
contexts = reflection_contexts(project, changed, opts)
%Reflection{
paths: paths,
changed_functions: changed,
summary: summary(project: project),
hotspots: hotspots,
boundaries: boundaries,
smells: smells,
contexts: contexts,
recommendation: recommendation(changed, hotspots, boundaries, smells)
}
end
@doc "Renders `reflect/1` as a compact tree output."
def reflect_output(opts \\ []), do: reflect(opts) |> Pi.output(opts)
defp section(key, opts), do: key |> raw_section(opts) |> normalize()
defp raw_section(key, opts) do
project = Keyword.get_lazy(opts, :project, fn -> project(opts) end)
MapAnalysis.section_data(project, key, opts_with_top(opts), opts[:path])
end
defp opts_with_top(opts), do: Keyword.put_new(opts, :top, @default_top)
defp project_opts(opts), do: Keyword.take(opts, [:plugins, :source_only])
defp expand_paths(paths, context) do
paths
|> List.wrap()
|> Enum.flat_map(fn path ->
resolved = Context.resolve(context, to_string(path))
cond do
File.dir?(resolved) -> Path.wildcard(Path.join(resolved, "**/*.{ex,erl,gleam,js,ts}"))
String.contains?(resolved, "*") -> Path.wildcard(resolved)
true -> [resolved]
end
end)
|> Enum.filter(&File.regular?/1)
|> Enum.uniq()
|> Enum.sort()
end
defp project_source_paths(context) do
["lib", "src", "apps/*/lib", "apps/*/src"]
|> Enum.map(&Path.join(&1, "**/*.{ex,erl,gleam,js,ts}"))
|> expand_paths(context)
end
defp function_context(project, target, opts) do
with {:ok, mfa, func} <- resolve(project, target) do
project
|> ReachContext.build(mfa, func, opts)
|> normalize()
end
end
defp resolve(project, target) do
mfa = if Query.mfa?(target), do: target, else: Query.resolve_target(project, target)
with mfa when not is_nil(mfa) <- mfa,
func when not is_nil(func) <- Query.find_function(project, mfa) do
{:ok, mfa, func}
else
_ -> {:error, "Function not found: #{inspect(target)}"}
end
end
defp module_context(project, target, opts) do
with name when is_binary(name) <- module_name(target),
metric when not is_nil(metric) <- module_metric(project, target, name, opts) do
file = metric["file"]
%{
kind: :module,
target: name,
module: metric,
functions: module_functions(project, target, name),
hotspots: hotspots(project: project, path: file, top: opts[:top] || @default_top),
boundaries: boundaries(project: project, path: file, top: opts[:top] || @default_top),
smells: smells(project: project, path: file, top: opts[:smell_top] || @default_top)
}
|> normalize()
else
_ -> {:error, "Module not found: #{inspect(target)}"}
end
end
defp module_name(module) when is_atom(module), do: module |> Atom.to_string() |> module_name()
defp module_name(module) when is_binary(module), do: String.trim_leading(module, "Elixir.")
defp module_name(_target), do: nil
defp module_metric(project, target, module_name, opts) do
reach_metric =
project
|> MapAnalysis.section_data(:modules, Keyword.put(opts, :top, 10_000), opts[:path])
|> normalize()
|> Enum.find(&(field(&1, "name") == module_name))
reach_metric || loaded_module_metric(target, module_name)
end
defp loaded_module_metric(target, module_name) do
with module when is_atom(module) <- loaded_module(target),
true <- Code.ensure_loaded?(module) do
functions = module.__info__(:functions)
macros = module.__info__(:macros)
file = module.module_info(:compile)[:source]
%{
"name" => module_name,
"file" => file && List.to_string(file),
"functions" => length(functions) + length(macros),
"public" => length(functions),
"public_count" => length(functions),
"macro_count" => length(macros)
}
else
_ -> nil
end
end
defp loaded_module(module) when is_atom(module), do: module
defp loaded_module(module) when is_binary(module) do
with name when is_binary(name) <- module_name(module) do
Module.concat([name])
end
rescue
ArgumentError -> nil
end
defp loaded_module(_target), do: nil
defp module_functions(project, target, module_name) do
reach_functions =
project.nodes
|> Enum.map(fn {_id, node} -> node end)
|> Enum.filter(&(function_def?(&1) and module_name(&1.meta[:module]) == module_name))
|> Enum.map(&function_summary/1)
(reach_functions ++ loaded_module_functions(target, module_name))
|> Enum.uniq_by(& &1.target)
|> Enum.sort_by(&{&1.file || "", &1.line || 0, &1.target || ""})
end
defp loaded_module_functions(target, module_name) do
with module when is_atom(module) <- loaded_module(target),
true <- Code.ensure_loaded?(module) do
for {name, arity} <- module.__info__(:functions) ++ module.__info__(:macros) do
%FunctionRef{target: "#{module_name}.#{name}/#{arity}", mfa: {module, name, arity}}
end
else
_ -> []
end
end
defp function_def?(node), do: node.type == :function_def
defp reflection_paths(opts) do
cond do
opts[:paths] -> opts[:paths] |> List.wrap() |> Enum.map(&to_string/1)
opts[:path] -> [to_string(opts[:path])]
opts[:changed] == false -> []
true -> changed_files()
end
|> Enum.filter(&(Path.extname(&1) in ~w[.ex .erl .gleam .js .ts]))
|> Enum.uniq()
|> Enum.sort()
end
defp changed_files do
context = Context.current()
{unstaged, _} =
Context.command(context, "git", ["diff", "--name-only"], stderr_to_stdout: true)
{staged, _} =
Context.command(context, "git", ["diff", "--cached", "--name-only"], stderr_to_stdout: true)
{untracked, _} =
Context.command(context, "git", ["ls-files", "--others", "--exclude-standard"],
stderr_to_stdout: true
)
(String.split(unstaged, "\n", trim: true) ++
String.split(staged, "\n", trim: true) ++ String.split(untracked, "\n", trim: true))
|> Enum.uniq()
rescue
_ in ErlangError -> []
end
defp changed_functions(_project, []), do: []
defp changed_functions(project, paths) do
paths
|> Enum.flat_map(&changed_functions_for_path(project, &1))
|> Enum.uniq_by(& &1.target)
|> Enum.sort_by(&{&1.file || "", &1.line || 0})
end
defp changed_functions_for_path(project, path) do
case changed_line_ranges(path) do
[] ->
functions_in_file(project, path)
ranges ->
ranges
|> Enum.map(fn {line, _count} -> Query.find_function_at_location(project, path, line) end)
|> Enum.reject(&is_nil/1)
|> Enum.map(&function_summary/1)
end
end
defp functions_in_file(project, path) do
for {_id, node} <- project.nodes,
(node.type == :function_def and node.source_span) &&
Query.file_matches?(node.source_span.file, path),
do: function_summary(node)
end
defp changed_line_ranges(path) do
context = Context.current()
relative_path = Context.relative(context, path)
case Context.command(context, "git", ["diff", "--unified=0", "--", relative_path],
stderr_to_stdout: true
) do
{diff, 0} -> parse_hunks(diff)
{diff, _} -> parse_hunks(diff)
end
rescue
_ in ErlangError -> []
end
defp parse_hunks(diff) do
diff
|> String.split("\n")
|> Enum.flat_map(&parse_hunk_line/1)
end
defp parse_hunk_line("@@ " <> header) do
with [_removed, "+" <> added | _context] <- String.split(header, " ", parts: 3),
[start_text | count_text] <- String.split(added, ",", parts: 2),
{start, ""} <- Integer.parse(start_text),
{:ok, count} <- parse_hunk_count(count_text) do
[{start, count}]
else
_ -> []
end
end
defp parse_hunk_line(_line), do: []
defp parse_hunk_count([]), do: {:ok, 1}
defp parse_hunk_count([count_text]) do
case Integer.parse(count_text) do
{count, ""} -> {:ok, count}
_ -> :error
end
end
defp function_summary(func) do
mfa = {func.meta[:module], func.meta[:name], func.meta[:arity]}
%FunctionRef{
target: mfa_string(mfa),
mfa: mfa,
file: func.source_span && func.source_span.file,
line: func.source_span && func.source_span.start_line,
kind: func.meta[:kind],
clauses: clause_labels(func)
}
end
defp reflection_hotspots(project, paths, changed_targets, opts) do
path_hotspots =
paths
|> Enum.flat_map(fn path ->
raw_section(:hotspots,
project: project,
path: path,
top: opts[:top] || @reflection_hotspot_top
)
end)
changed_hotspots =
Enum.filter(path_hotspots, &(field(&1, :display_function) in changed_targets))
(changed_hotspots ++ path_hotspots)
|> Enum.uniq()
|> Enum.take(opts[:top] || @reflection_hotspot_top)
|> Enum.map(&hotspot/1)
end
defp reflection_boundaries(project, paths, opts) do
paths
|> Enum.flat_map(fn path ->
raw_section(:boundaries,
project: project,
path: path,
top: opts[:top] || @reflection_hotspot_top
)
end)
|> Enum.take(opts[:top] || @reflection_hotspot_top)
|> Enum.map(&boundary/1)
end
defp reflection_smells(project, [], opts),
do: smells(project: project, top: opts[:smell_top] || @reflection_smell_top)
defp reflection_smells(project, paths, opts) do
paths
|> Enum.flat_map(fn path ->
smells(project: project, path: path, top: opts[:smell_top] || @reflection_smell_top)
end)
|> Enum.uniq()
|> Enum.take(opts[:smell_top] || @reflection_smell_top)
end
defp reflection_contexts(project, changed, opts) do
changed
|> Enum.take(opts[:context_top] || 5)
|> Enum.map(fn %FunctionRef{mfa: mfa} ->
context(mfa, project: project, depth: opts[:depth] || 2)
end)
end
defp recommendation([], [], [], []) do
"No changed functions or Reach review leads detected. If edits were non-code or generated, no follow-up refactor is suggested."
end
defp recommendation(_changed, [], [], []) do
"Changed functions have no Reach hotspots, mixed-effect boundaries, or smell findings in the inspected scope. Prefer stopping unless human review spots a naming/API issue."
end
defp recommendation(_changed, hotspots, boundaries, smells) do
leads = []
leads = if hotspots == [], do: leads, else: ["hotspots" | leads]
leads = if boundaries == [], do: leads, else: ["mixed-effect boundaries" | leads]
leads = if smells == [], do: leads, else: ["smells" | leads]
"Review #{Enum.reverse(leads) |> Enum.join(", ")} before final. Apply one small behavior-preserving cleanup if it is in scope; otherwise document why it is deferred."
end
defp reflection_summary(%Reflection{} = reflection) do
changed = length(reflection.changed_functions)
hotspots = length(reflection.hotspots)
boundaries = length(reflection.boundaries)
smells = length(reflection.smells)
cond do
changed == 0 ->
"No changed functions detected; skip follow-up refactor unless edits were generated or non-code."
hotspots + boundaries + smells == 0 ->
"No follow-up refactor suggested: #{count(changed, "changed func")}, no hotspots/boundaries/smells."
true ->
"Review before final: #{count(changed, "changed func")}; #{lead_counts(hotspots, boundaries, smells)}."
end
end
defp lead_counts(hotspots, boundaries, smells) do
[
count_if_present(hotspots, "hotspot"),
count_if_present(boundaries, "boundary", "boundaries"),
count_if_present(smells, "smell")
]
|> Enum.reject(&is_nil/1)
|> Enum.join(", ")
end
defp count_if_present(0, _singular), do: nil
defp count_if_present(value, singular), do: count(value, singular)
defp count_if_present(0, _singular, _plural), do: nil
defp count_if_present(value, singular, plural), do: count(value, singular, plural)
defp count(1, singular), do: "1 #{singular}"
defp count(value, singular), do: "#{value} #{singular}s"
defp count(1, singular, _plural), do: "1 #{singular}"
defp count(value, _singular, plural), do: "#{value} #{plural}"
defp field(value, key) when is_struct(value), do: value |> Map.from_struct() |> Map.get(key)
defp field(value, key) when is_map(value),
do: Map.get(value, key) || Map.get(value, to_string(key))
defp field(_value, _key), do: nil
defp filter_by_path(findings, nil), do: findings
defp filter_by_path(findings, path) do
Enum.filter(findings, fn finding ->
file = finding.file || get_in(finding.raw || %{}, ["source", "file"])
is_binary(file) and Query.file_matches?(file, path)
end)
end
defp hotspot(value), do: value |> JSONCodec.dump() |> Hotspot.from_map!()
defp boundary(value), do: value |> JSONCodec.dump() |> Boundary.from_map!()
defp smell(finding) do
raw = normalize(finding)
{location_file, location_line} = smell_location(raw["location"])
source = map_or_empty(raw["source"])
%{
"kind" => raw |> first_present(~w[kind check name]) |> normalize_label(),
"message" => first_present(raw, ~w[message description trigger]),
"file" => first_present([raw["file"], raw["path"], source["file"], location_file]),
"line" =>
first_present([
normalize_line(raw["line"]),
normalize_line(source["line"]),
location_line
]),
"confidence" => normalize_label(raw["confidence"]),
"raw" => raw
}
|> Smell.from_map!()
end
defp map_or_empty(value) when is_map(value), do: value
defp map_or_empty(_value), do: %{}
defp first_present(map, keys) when is_map(map),
do: keys |> Enum.map(&Map.get(map, &1)) |> first_present()
defp first_present(values), do: Enum.find(values, &(not is_nil(&1)))
defp smell_location(location) when is_binary(location) do
case location |> String.split(":") |> List.pop_at(-1) do
{line_text, file_parts} when file_parts != [] ->
case Integer.parse(line_text) do
{line, ""} -> {Enum.join(file_parts, ":"), line}
_invalid -> {location, nil}
end
_invalid ->
{location, nil}
end
end
defp smell_location(_location), do: {nil, nil}
defp normalize_line(line) when is_integer(line), do: line
defp normalize_line(line) when is_binary(line) do
case Integer.parse(line) do
{number, ""} -> number
_invalid -> nil
end
end
defp normalize_line(_line), do: nil
defp normalize_label(":" <> label), do: label
defp normalize_label(label) when is_binary(label), do: label
defp normalize_label(_label), do: nil
defp smell_sort_key(%Smell{} = finding) do
{
if(finding.kind in @high_impact_smells, do: 0, else: 1),
confidence_priority(finding.confidence),
if(is_binary(finding.file), do: 0, else: 1),
finding.file || "",
finding.line || 0,
finding.kind || ""
}
end
defp confidence_priority("high"), do: 0
defp confidence_priority("medium"), do: 1
defp confidence_priority("low"), do: 2
defp confidence_priority(_confidence), do: 3
defp normalize_call_tree(nodes) do
Enum.map(nodes, fn node ->
node
|> Map.update!(:id, fn id -> %{mfa: id, label: mfa_string(id)} end)
|> Map.update(:children, [], &normalize_call_tree/1)
end)
|> normalize()
end
defp clause_labels(func) do
func.children
|> Enum.filter(&(&1.type == :clause))
|> Enum.map(fn clause -> clause.meta[:pattern] || clause.meta[:label] end)
|> Enum.reject(&is_nil/1)
end
defp mfa_string({module, fun, arity}), do: IRHelpers.func_id_to_string({module, fun, arity})
defp mfa_string(other), do: inspect(other)
defp ensure_reach! do
unless available?() do
raise "Reach is not available. Add {:reach, \"~> 2.7\", only: [:dev, :test], runtime: false} to the project or use pi_bridge with Reach included."
end
end
@doc false
def reflection_output(%Reflection{} = reflection, opts \\ []) do
plain = to_plain(reflection)
tree = Pi.Output.tree(plain, opts)
%Pi.Output{
parts: [OutputPart.text(reflection_summary(reflection)) | tree.parts],
text: inspect(plain)
}
end
@doc false
def to_plain(value), do: normalize(value)
defp normalize(value) when is_struct(value), do: value |> Map.from_struct() |> normalize()
defp normalize(value) when is_map(value),
do: Map.new(value, fn {k, v} -> {normalize_key(k), normalize(v)} end)
defp normalize(value) when is_list(value), do: Enum.map(value, &normalize/1)
defp normalize(value) when is_tuple(value), do: inspect(value)
defp normalize(value) when is_atom(value), do: inspect(value)
defp normalize(value) when is_function(value), do: inspect(value)
defp normalize(value) when is_pid(value), do: inspect(value)
defp normalize(value) when is_reference(value), do: inspect(value)
defp normalize(value) when is_port(value), do: inspect(value)
defp normalize(value), do: value
defp normalize_key(key) when is_atom(key), do: Atom.to_string(key)
defp normalize_key(key), do: to_string(key)
end
defimpl Pi.Output.Renderable, for: Pi.CodeMap.Reflection do
def to_output(reflection, opts), do: Pi.CodeMap.reflection_output(reflection, opts)
end
defimpl Pi.Output.Renderable, for: Pi.CodeMap.FunctionRef do
def to_output(function_ref, opts),
do: function_ref |> Pi.CodeMap.to_plain() |> Pi.Output.tree(opts)
end
defimpl Pi.Output.Renderable, for: Pi.CodeMap.Hotspot do
def to_output(hotspot, opts), do: hotspot |> Pi.CodeMap.to_plain() |> Pi.Output.tree(opts)
end
defimpl Pi.Output.Renderable, for: Pi.CodeMap.Boundary do
def to_output(boundary, opts), do: boundary |> Pi.CodeMap.to_plain() |> Pi.Output.tree(opts)
end
defimpl Pi.Output.Renderable, for: Pi.CodeMap.Smell do
def to_output(smell, opts), do: smell |> Pi.CodeMap.to_plain() |> Pi.Output.tree(opts)
end