extract — cut a definition (plus its private closure) out of a module
into a brand-new module file.
Builds on Adze.LsDeps.ls_extract/3 for the closure and on
Adze.Definition for the logical-definition groups. Produces:
- a new target file containing
defmodule TargetModule do ... endwith the closure definitions in original source order and a filtered subset of the source module'salias/use/import/requiredirectives; - a modified source where the cut definitions are gone and an
alias TargetModuleline is inserted near the top of the source module's body.
Args
Adze.Extract.extract(source,
definition: "bar/2",
module: "MyApp.Bar",
from_module: "MyApp.Source" # optional disambiguator
)Output
{:ok, %{
target_module: "MyApp.Bar",
target_path: "lib/my_app/bar.ex",
target_content: "...",
new_source: "...",
source_diff: "..."
}}Behaviour notes (settled for v1)
- Target path is derived from
--moduleviaMacro.underscore/1and prefixed withlib/. Override withpath:ormix_root:for tests / non-standard projects. - Target file must not exist. Existing file →
{:error, {:target_exists, path}}. Append-into-existing is deliberately deferred — surface the collision to the AI. - Multi-module source files require
from_module:to disambiguate when the named def exists in more than one siblingdefmodule. - Directive policy:
aliasis mechanically filtered — copied only when the binding (last segment or:asname) is referenced in the closure AST.use/import/requireare never copied to the target. The tool can't know without macro expansion which (if any) the closure depends on, so the principled mechanical answer is to drop them. The compiler is louder when we're under-permissive (missing macro → compile error) than when we're over-permissive (extraimport→ silent), so the failure mode favors dropping. Every dropped directive comes back indropped_directivesas%{kind:, line:, text:}so the AI driver can decide which (if any) to add back to the target.
- Cross-file caller updating is out of scope for this session.
External callers of the moved public def will need updating — the
next compile run surfaces them.
find-callers(Session 7) will make this proactive.
extract! writes both files
Adze.Extract.extract!("lib/source.ex",
definition: "bar/2",
module: "MyApp.Bar"
)Returns the same shape as extract/2, plus the side effects of
writing lib/my_app/bar.ex (new) and rewriting lib/source.ex.
Summary
Types
@type dropped_directive() :: %{ kind: :use | :import | :require, line: pos_integer(), text: String.t() }
@type result() :: %{ target_module: String.t(), target_path: Path.t(), target_content: String.t(), new_source: String.t(), source_diff: String.t(), source_module: String.t(), public_closure_keys: [{atom(), non_neg_integer()}], caller_diffs: %{required(Path.t()) => String.t()}, dropped_directives: [dropped_directive()] }