Current section

Files

Jump to
reach lib reach cli commands trace slice.ex
Raw

lib/reach/cli/commands/trace/slice.ex

defmodule Reach.CLI.Commands.Trace.Slice do
@moduledoc """
Program slicing — finds the minimum set of statements that affect a value.
mix reach.trace lib/my_app/user_controller.ex:18
mix reach.trace MyApp.UserService.create/1 --variable changeset
mix reach.trace --forward lib/my_app/user_service.ex:30 --variable user
mix reach.trace lib/my_app_web/controllers/user_controller.ex:18 --format json
## Options
* `--forward` — forward slice (where does this value flow to?)
* `--variable` — trace a specific variable name within the slice
* `--format` — output format: `text` (default), `json`, `oneline`
"""
@switches [format: :string, forward: :boolean, variable: :string, graph: :boolean]
@aliases [f: :format]
alias Reach.CLI.BoxartGraph
alias Reach.CLI.Options
alias Reach.CLI.Project
alias Reach.CLI.Render.Trace.Slice, as: SliceRender
alias Reach.Project.Query
alias Reach.Trace.Slice
@default_statement_limit 30
def run(args, cli_opts \\ []) do
{opts, target_args} = Options.parse(args, @switches, @aliases)
raw_target =
List.first(target_args) ||
Mix.raise(
"Expected a target. Usage:\n" <>
" mix reach.trace lib/foo.ex:42\n" <>
" mix reach.trace Module.function/arity"
)
run_target(raw_target, opts, cli_opts)
end
def run_target(raw_target, opts, cli_opts \\ []) do
format = opts[:format] || "text"
project = Project.load(quiet: format == "json")
forward? = Keyword.get(opts, :forward, false)
var_name = opts[:variable]
{node, target} = resolve_slice_target(project, raw_target)
result =
Slice.compute(project, node,
forward: forward?,
variable: var_name,
limit: statement_limit(opts)
)
if opts[:graph] do
BoxartGraph.require!()
BoxartGraph.render_slice_graph(project, node.id, forward?)
else
SliceRender.render(result, target, format, command(cli_opts))
end
end
defp resolve_slice_target(project, raw) do
case Project.parse_file_line(raw) do
{file, line} ->
node = Slice.find_node_at_location(project, file, line)
unless node, do: Mix.raise("No node found at #{file}:#{line}")
{node, %{file: file, line: line}}
nil ->
mfa = Query.resolve_target(project, raw)
unless mfa, do: Mix.raise("Function not found: #{raw}")
func_node = Query.find_function(project, mfa)
unless func_node, do: Mix.raise("Function definition not found in IR: #{raw}")
span = func_node.source_span
{func_node, %{file: span[:file], line: span[:start_line]}}
end
end
defp command(cli_opts), do: Keyword.get(cli_opts, :command, "reach.trace")
defp statement_limit(opts), do: Keyword.get(opts, :statement_limit, @default_statement_limit)
end