Adze.Extract (Adze v0.1.0)

Copy Markdown View Source

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 ... end with the closure definitions in original source order and a filtered subset of the source module's alias/use/import/ require directives;
  • a modified source where the cut definitions are gone and an alias TargetModule line 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 --module via Macro.underscore/1 and prefixed with lib/. Override with path: or mix_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 sibling defmodule.
  • Directive policy:
    • alias is mechanically filtered — copied only when the binding (last segment or :as name) is referenced in the closure AST.
    • use / import / require are 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 (extra import → silent), so the failure mode favors dropping. Every dropped directive comes back in dropped_directives as %{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

dropped_directive()

@type dropped_directive() :: %{
  kind: :use | :import | :require,
  line: pos_integer(),
  text: String.t()
}

opts()

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

result()

@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()]
}

Functions

extract(source, opts)

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

extract!(path, opts)

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

extract_file(path, opts)

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