Adze.FindCallers (Adze v0.1.0)

Copy Markdown View Source

Find every project-wide reference to a Module.fun[/arity].

Read-only consumer of Adze.ProjectRewrite's file enumeration — same source loading as rename / extract!, no mutation. Output is a per-file list of call sites, captures, and pipe references.

Scope

  • Qualified callsModule.fun(args), including the pipe-expanded form x |> Module.fun(args) and the bare-pipe form x |> Module.fun.
  • Captures&Module.fun/arity.
  • Aliased referencesalias Foo.Bar then Bar.fun(...), and alias Foo, as: F then F.fun(...), and brace-form alias Foo.{Bar, Baz}.

Not yet handled (intentional v1 limits):

  • Unqualified calls via import Module — bare fun(args) after import is not detected. Imports rarely cross file boundaries in modern Elixir code; if this bites, file-local import scoping can be added later.
  • Dynamic refsapply/3, Module.concat(...), runtime module composition.
  • String-literal mentions — comments, @moduledoc heredocs.

Output shape

%{
  target: %{module: "MyApp.Foo", function: :bar, arity: 2 | :any},
  total: 3,
  files: %{
    "lib/x.ex" => [
      %{line: 10, kind: :call,    arity: 2, snippet: "MyApp.Foo.bar(a, b)",
        in_module: "MyApp.Caller"},
      %{line: 15, kind: :capture, arity: 2, snippet: "&Foo.bar/2",
        in_module: "MyApp.Caller"}
    ]
  }
}

in_module is the innermost-enclosing defmodule name at the ref's position, or nil for top-level refs (scripts, .exs without defmodule).

Usage

iex> Adze.FindCallers.find_callers("MyApp.Foo.bar/2", mix_root: ".")
{:ok, %{target: ..., files: %{...}, total: 3}}

iex> Adze.FindCallers.find_callers({MyApp.Foo, :bar, :any},
...>   files: %{"lib/x.ex" => "..."})
{:ok, %{...}}

Summary

Types

caller()

@type caller() :: %{
  line: pos_integer(),
  kind: :call | :capture,
  arity: non_neg_integer(),
  snippet: String.t(),
  in_module: String.t() | nil
}

result()

@type result() :: %{
  target: target(),
  total: non_neg_integer(),
  files: %{required(Path.t()) => [caller()]}
}

target()

@type target() :: %{
  module: String.t(),
  function: atom(),
  arity: non_neg_integer() | :any
}

target_spec()

@type target_spec() ::
  String.t() | {module(), atom()} | {module(), atom(), non_neg_integer() | :any}

Functions

find_callers(target_spec, opts \\ [])

@spec find_callers(
  target_spec(),
  keyword()
) :: {:ok, result()} | {:error, term()}