Current section

Files

Jump to
reach lib reach cli commands inspect.ex
Raw

lib/reach/cli/commands/inspect.ex

defmodule Reach.CLI.Commands.Inspect do
@moduledoc """
Explains one function, module, file, or line.
mix reach.inspect Reach.Frontend.Elixir.translate/3
mix reach.inspect lib/reach/frontend/elixir.ex:54
mix reach.inspect TARGET --deps
mix reach.inspect TARGET --impact
mix reach.inspect TARGET --slice
mix reach.inspect TARGET --graph
mix reach.inspect TARGET --call-graph
mix reach.inspect TARGET --data
mix reach.inspect TARGET --context
mix reach.inspect TARGET --candidates
mix reach.inspect TARGET --why OTHER
## Options
* `--format` — output format passed to delegated analyses: `text`, `json`, `oneline`
* `--deps` — callers, callees, and shared state
* `--impact` — direct/transitive change impact
* `--slice` — backward program slice
* `--forward` — use a forward slice with `--slice` or `--data`
* `--graph` — render graph output where supported
* `--call-graph` — render the call graph around the target
* `--data` — target-local data-flow view
* `--context` — agent-readable bundle: deps, impact, data, effects
* `--candidates` — advisory placeholder for graph-backed refactoring candidates
* `--why` — explain the shortest graph-backed relationship to another target
* `--depth` — transitive depth passed to deps/impact
* `--variable` — variable filter passed to slice/data views
* `--limit` — text display limit for truncated context sections
* `--all` — show all text rows in context output
"""
alias Reach.CLI.BoxartGraph
alias Reach.CLI.Commands.Inspect.Deps
alias Reach.CLI.Commands.Inspect.Impact
alias Reach.CLI.Commands.Trace.Slice
alias Reach.CLI.Project
alias Reach.CLI.Render.Inspect, as: InspectRender
alias Reach.Config
alias Reach.Inspect.{Candidates, Context, Data, Why}
alias Reach.IR.Helpers, as: IRHelpers
alias Reach.Project.Query
def run(opts, target_args \\ []) do
target =
List.first(target_args) || Mix.raise("Expected a target. Usage: mix reach.inspect TARGET")
run_action(inspect_action(opts), target, opts)
end
defp inspect_action(opts) do
[
{opts[:context], :context},
{opts[:why] != nil, :why},
{opts[:candidates], :candidates},
{opts[:impact], :impact},
{opts[:deps], :deps},
{opts[:data] == true and opts[:format] == "json", :data},
{opts[:data] == true and opts[:graph] != true, :data},
{opts[:slice] == true or opts[:data] == true, :slice},
{opts[:call_graph], :call_graph},
{opts[:graph], :graph}
]
|> Enum.find_value(:context, fn
{true, action} -> action
{_enabled, _action} -> nil
end)
end
defp run_action(:context, target, opts), do: run_context(target, opts)
defp run_action(:why, target, opts), do: run_why(target, opts)
defp run_action(:candidates, target, opts), do: run_candidates(target, opts)
defp run_action(:impact, target, opts), do: Impact.run_target(target, opts, "reach.inspect")
defp run_action(:deps, target, opts), do: Deps.run_target(target, opts, "reach.inspect")
defp run_action(:call_graph, target, opts),
do: Deps.run_target(target, Keyword.put(opts, :graph, true), "reach.inspect")
defp run_action(:graph, target, opts), do: render_cfg(target, opts)
defp run_action(:data, target, opts), do: run_data(target, opts)
defp run_action(:slice, target, opts),
do: Slice.run_target(target, opts, command: "reach.inspect")
defp run_why(target, opts) do
project = Project.load(quiet: opts[:format] == "json")
result = Why.result(project, target, opts[:why], opts[:depth] || 6)
InspectRender.render_why(result, opts[:format] || "text")
end
defp run_context(target, opts) do
project = load_target_project(target, opts)
{mfa, func} = resolve_function!(project, target)
if opts[:format] == "json" do
context = Context.build(project, mfa, func, opts)
InspectRender.render_context(context, "json")
else
InspectRender.render_context(
context_text_data(project, mfa, func, opts),
"text",
display_limit(opts)
)
end
end
defp context_text_data(project, mfa, func, opts) do
depth = opts[:depth]
%{
mfa: mfa,
func: func,
data: Data.summary(project, func, opts[:variable]),
direct_callers: Query.callers(project, mfa, 1),
transitive_callers: Query.callers(project, mfa, depth || 4),
callees: Query.callees(project, mfa, depth || 3)
}
end
defp display_limit(opts) do
cond do
opts[:all] -> :all
is_integer(opts[:limit]) and opts[:limit] > 0 -> opts[:limit]
true -> 20
end
end
defp run_data(target, opts) do
project = load_target_project(target, opts)
{mfa, func} = resolve_function!(project, target)
summary = Data.summary(project, func, opts[:variable])
InspectRender.render_data(summary, mfa, func, opts[:format] || "text")
end
defp render_cfg(target, opts) do
BoxartGraph.require!()
{{_mod, fun, arity}, func} = resolve_graph_target!(target, opts)
file = func.source_span && func.source_span.file
InspectRender.render_cfg_header(fun, arity)
if file do
BoxartGraph.render_cfg(func, file)
else
InspectRender.render_missing_source()
end
end
defp resolve_graph_target!(target, opts) do
case Query.parse_file_line(target) do
{file, line} ->
project = Project.load(paths: [file], quiet: opts[:format] == "json")
func = Query.find_function_at_location(project, file, line)
if func do
{{func.meta[:module], func.meta[:name], func.meta[:arity]}, func}
else
Mix.raise("Function not found: #{target}")
end
nil ->
project = Project.load(quiet: opts[:format] == "json")
resolve_function!(project, target)
end
end
defp load_target_project(target, opts) do
case Query.parse_file_line(target) do
{file, _line} -> Project.load(paths: [file], quiet: opts[:format] == "json")
nil -> Project.load(quiet: opts[:format] == "json")
end
end
defp resolve_function!(project, raw) do
case Query.parse_file_line(raw) do
{file, line} ->
func =
Query.find_function_at_location(project, file, line) ||
Mix.raise("Function not found at #{raw}")
{{func.meta[:module], func.meta[:name], func.meta[:arity]}, func}
nil ->
mfa = Query.resolve_target(project, raw) || Mix.raise("Function not found: #{raw}")
func =
Query.find_function(project, mfa) ||
Mix.raise("Function definition not found in IR: #{raw}")
{mfa, func}
end
end
defp run_candidates(target, opts) do
project = load_target_project(target, opts)
{mfa, func} = resolve_function!(project, target)
target_string = IRHelpers.func_id_to_string(mfa)
candidate_config = Config.read() |> Config.normalize() |> Map.fetch!(:candidates)
candidates =
Enum.map(
Candidates.find(project, mfa, func, candidate_config),
&Map.put(&1, :target, target_string)
)
result = %{
command: "reach.inspect",
target: target_string,
candidates: candidates,
note: "Candidates are advisory. Prove behavior preservation before editing."
}
InspectRender.render_candidates(result, opts[:format] || "text")
end
end