Adze.ExtractPrivate (Adze v0.1.0)

Copy Markdown View Source

Flip a public def (or defmacro / defguard) to its private form when find-callers reports zero external callers.

Bookkeeping op — the analysis is "does anything outside this module call this function?" and the mechanical edit is a one-keyword swap per clause line. Dry-run by default; extract_private!/2 writes.

Scope

  • defdefp, defmacrodefmacrop, defguarddefguardp.
  • defdelegate has no private form — returns {:error, :cannot_be_private}.
  • Multi-clause defs flip every clause line.
  • Attached @spec / @doc / @impl / etc. are left in place. A @spec on a defp is allowed by the compiler; an @impl on a private callable is rejected, but flipping a function with @impl to private is almost certainly wrong anyway — find-callers would normally surface the behaviour-callback callers and refuse the flip. We let the compiler complain if the user forces it via a future override.

Safety

External = a caller in any file other than the source file, OR a caller in the source file whose enclosing defmodule is not the target's module. The latter catches the case where a file holds multiple modules and a sibling calls the target via its fully-qualified name. We rely on Adze.FindCallers to find the refs, then re-parse each affected file to determine each ref's enclosing module for classification.

Limitations inherited from find-callers: unqualified calls via import aren't detected, nor dynamic apply/3, nor string-literal mentions. If the codebase uses these against the target, this op will give a green light incorrectly. The compiler will catch the break on the next build, but the failure mode is loud — the user reads the error and reverts. Document it; don't hide it.

Output shape

{:ok, %{
  diff: "...",                 # unified-style line diff
  new_source: "...",
  module: "MyApp.Foo",
  name: :helper,
  arity: 2,
  from_kind: :def,
  to_kind: :defp
}}

{:error, {:external_callers, [
  %{path: "lib/x.ex", line: 10, kind: :call, arity: 2, snippet: "...",
    in_module: "MyApp.Bar"},
  ...
]}}

Usage

iex> Adze.ExtractPrivate.extract_private_file(
...>   "lib/foo.ex", definition: "helper/2")
{:ok, %{diff: "...", from_kind: :def, to_kind: :defp}}

Adze.ExtractPrivate.extract_private!("lib/foo.ex",
  definition: "helper/2")
# → writes lib/foo.ex with `def helper` flipped to `defp helper`

Summary

Types

external_ref()

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

opts()

@type opts() :: [
  definition: Adze.Definition.definition_spec(),
  path: Path.t(),
  mix_root: Path.t(),
  files: %{required(Path.t()) => String.t()},
  include_attrs: [atom()]
]

result()

@type result() :: %{
  diff: String.t(),
  new_source: String.t(),
  module: String.t(),
  name: atom(),
  arity: non_neg_integer(),
  from_kind: atom(),
  to_kind: atom()
}

Functions

extract_private(source, opts)

@spec extract_private(String.t(), opts()) :: {:ok, result()} | {:error, term()}

extract_private!(path, opts)

@spec extract_private!(Path.t(), opts()) :: {:ok, result()} | {:error, term()}

extract_private_file(path, opts)

@spec extract_private_file(Path.t(), opts()) :: {:ok, result()} | {:error, term()}