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
def→defp,defmacro→defmacrop,defguard→defguardp.defdelegatehas 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@specon adefpis allowed by the compiler; an@implon a private callable is rejected, but flipping a function with@implto private is almost certainly wrong anyway —find-callerswould 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
@type external_ref() :: %{ path: Path.t(), line: pos_integer(), kind: :call | :capture, arity: non_neg_integer(), snippet: String.t(), in_module: String.t() | nil }