# `Adze`
[🔗](https://github.com/matthewlehner/adze/blob/v0.1.0/lib/adze.ex#L1)

Structural Elixir refactoring and outline-first code exploration.

Adze is a toolkit built on [Sourceror](https://hex.pm/packages/sourceror) and
[Igniter](https://hex.pm/packages/igniter) that gives AI coding agents (and humans)
fast, token-efficient ways to understand and transform Elixir source code.

Single-file analysis (outline, deps, aliases) uses raw Sourceror for AST parsing.
Project-wide operations (rename, extract caller rewriting, find-callers) use Igniter
for safe, multi-file rewrites with formatting preservation.

## Installation

Add `adze` to your list of dependencies in `mix.exs`:

    def deps do
      [
        {:adze, "~> 0.1.0"}
      ]
    end

## Design Philosophy

Adze splits its operations into two categories:

### Read-Only Analysis

These functions parse source code and return structured data without modifying
anything on disk. They come in two flavours:

  * **`*_file/1`** — reads a file path and returns the analysis.
  * **`*/2` or `*/3`** — accepts a source string directly (useful for editors,
    tests, or piping sources around).

Read-only operations include:

  * `outline/2` / `outline_file/1` — structural outline (modules, defs, attributes, directives)
  * `deps/2` / `deps_file/1` — intra-module call graph
  * `ls_deps/3` / `ls_deps_file/2` — transitive dependency tree from a definition
  * `ls_extract/3` / `ls_extract_file/2` — private closure exclusively reachable from a definition
  * `aliases/2` / `aliases_file/1` — alias/import/require/use directives per module
  * `find_callers/2` — project-wide caller search for a qualified function

### Write Operations (Refactoring)

These functions compute a transformation and can optionally write to disk.
They follow a three-tier naming convention:

  * **`op/2`** — dry-run on a source string, returns `{:ok, result}` with diffs/new source.
  * **`op_file/2`** — dry-run reading from a file path.
  * **`op!/2`** — writes the result to disk (destructive).

Write operations include:

  * `mv/2` / `mv_file/2` / `mv!/2` — reorder a definition within a module
  * `extract_private/2` / `extract_private_file/2` / `extract_private!/2` — flip `def` → `defp`
  * `extract/2` / `extract_file/2` / `extract!/2` — extract a def + closure into a new module
  * `rename/1` / `rename!/1` — rename a module across the entire project

## Underlying Modules

Each operation is implemented in its own module with full documentation:

  * `Adze.Outline`
  * `Adze.Deps`
  * `Adze.LsDeps`
  * `Adze.Aliases`
  * `Adze.FindCallers`
  * `Adze.ExtractPrivate`
  * `Adze.Move`
  * `Adze.Extract`
  * `Adze.Rename`

# `aliases`

```elixir
@spec aliases(
  String.t(),
  keyword()
) :: {:ok, map()} | {:error, term()}
```

Returns all alias/import/require/use directives per module from a source string.

Same as `aliases_file/1` but operates on source code passed directly.

## Options

  * `:file` — an optional filename label.

## Examples

    {:ok, directives} = Adze.aliases(source)

See `Adze.Aliases` for full details on the returned structure.

# `aliases_file`

```elixir
@spec aliases_file(Path.t()) :: {:ok, map()} | {:error, term()}
```

Returns all alias/import/require/use directives per module in the given file.

Group-form aliases (e.g. `alias MyApp.{Foo, Bar}`) are expanded into their
individual forms so every directive is represented as a single entry.

## Examples

    {:ok, directives} = Adze.aliases_file("lib/my_app/context.ex")

See `Adze.Aliases` for full details on the returned structure.

# `deps`

```elixir
@spec deps(
  String.t(),
  keyword()
) :: {:ok, map()} | {:error, term()}
```

Returns the intra-module call graph from a source string.

Same as `deps_file/1` but operates on source code passed directly as a string.

## Options

  * `:file` — an optional filename label.

## Examples

    {:ok, graph} = Adze.deps(source)

See `Adze.Deps` for full details on the returned structure.

# `deps_file`

```elixir
@spec deps_file(Path.t()) :: {:ok, map()} | {:error, term()}
```

Returns the intra-module call graph for the given file.

Reads the file at `path` and builds a map of caller → callees relationships
*within the same module*. Pipe chains and function captures (`&fun/arity`) are
resolved so every internal call edge is captured.

## Examples

    {:ok, graph} = Adze.deps_file("lib/my_app/parser.ex")

See `Adze.Deps` for full details on the returned structure.

# `extract`

```elixir
@spec extract(
  String.t(),
  keyword()
) :: {:ok, map()} | {:error, term()}
```

Dry-run: extracts a function and its private closure into a new module.

Computes the full extraction — the target definition plus all exclusively-reachable
private helpers are moved to a new module. Callers across the project are identified
and their references are rewritten.

## Options

  * `:definition` (required) — the function to extract, e.g. `"parse/1"`.
  * `:module` (required) — the target module name, e.g. `"MyApp.NewParser"`.
  * `:from_module` — disambiguator if source contains multiple modules.
  * `:path` — override the target file path (otherwise inferred from module name).
  * `:mix_root` — path to the Mix project root.
  * `:files` — pre-loaded source map.
  * `:include_attrs` — list of attribute atoms to extract (e.g. `[:doc, :spec]`).
  * `:app_name` — the OTP app name (used for path inference).

## Return Value

Returns `{:ok, result}` where result is:

    %{
      target_module: "MyApp.NewParser",
      target_path: "lib/my_app/new_parser.ex",
      target_content: "defmodule MyApp.NewParser do\n  ...\nend",
      new_source: "...",
      source_diff: "...",
      source_module: "MyApp.Parser",
      public_closure_keys: [parse: 1],
      caller_diffs: %{"lib/other.ex" => "..."},
      dropped_directives: [%{kind: :import, line: 3, text: "import Helpers"}]
    }

## Examples

    {:ok, result} = Adze.extract(source, definition: "parse/1", module: "MyApp.NewParser")

See `Adze.Extract` for full details.

# `extract!`

```elixir
@spec extract!(
  Path.t(),
  keyword()
) :: {:ok, map()} | {:error, term()}
```

Writes the extraction to disk.

Performs the extraction and writes both the new module file and the updated
source file. Also rewrites callers across the project to reference the new
module.

## Examples

    {:ok, result} = Adze.extract!("lib/my_app/parser.ex", definition: "parse/1", module: "MyApp.NewParser")

See `Adze.Extract` for full details.

# `extract_file`

```elixir
@spec extract_file(
  Path.t(),
  keyword()
) :: {:ok, map()} | {:error, term()}
```

Dry-run: reads a file and extracts a function and its private closure into a new module.

Same as `extract/2` but reads source from the given file path.

## Examples

    {:ok, result} = Adze.extract_file("lib/my_app/parser.ex", definition: "parse/1", module: "MyApp.NewParser")

See `Adze.Extract` for full details.

# `extract_private`

```elixir
@spec extract_private(
  String.t(),
  keyword()
) :: {:ok, map()} | {:error, term()}
```

Dry-run: determines if a public function can be safely made private.

Searches the project for external callers of the specified definition. If none
are found, returns the diff that would flip `def` → `defp` along with the
transformed source.

## Options

  * `:definition` (required) — the function spec, e.g. `"helper/2"`.
  * `:mix_root` — path to the Mix project root.
  * `:files` — pre-loaded source map.
  * `:include_attrs` — list of attribute atoms to include with the definition
    (e.g. `[:doc, :spec]`).

## Return Value

On success returns `{:ok, result}` where result is:

    %{
      diff: "...",
      new_source: "...",
      module: "MyApp.Helpers",
      name: :helper,
      arity: 2,
      from_kind: :def,
      to_kind: :defp
    }

Returns `{:error, {:external_callers, [...]}}` if external callers exist.

## Examples

    {:ok, result} = Adze.extract_private(source, definition: "helper/2")

See `Adze.ExtractPrivate` for full details.

# `extract_private!`

```elixir
@spec extract_private!(
  Path.t(),
  keyword()
) :: {:ok, map()} | {:error, term()}
```

Writes the def → defp flip to disk.

Same as `extract_private_file/2` but actually writes the transformed file.
Raises or returns an error if external callers are found.

## Options

  * `:definition` (required) — the function spec, e.g. `"helper/2"`.
  * `:mix_root` — path to the Mix project root.
  * `:files` — pre-loaded source map.
  * `:include_attrs` — list of attribute atoms to include with the definition.

## Examples

    {:ok, result} = Adze.extract_private!("lib/my_app/helpers.ex", definition: "helper/2")

See `Adze.ExtractPrivate` for full details.

# `extract_private_file`

```elixir
@spec extract_private_file(
  Path.t(),
  keyword()
) :: {:ok, map()} | {:error, term()}
```

Dry-run: reads a file and determines if a public function can be made private.

Same as `extract_private/2` but reads source from the given file path.

## Examples

    {:ok, result} = Adze.extract_private_file("lib/my_app/helpers.ex", definition: "helper/2")

See `Adze.ExtractPrivate` for full details.

# `find_callers`

```elixir
@spec find_callers(
  String.t()
  | {module(), atom()}
  | {module(), atom(), non_neg_integer() | :any},
  keyword()
) :: {:ok, map()} | {:error, term()}
```

Finds all callers of a function across the project.

Walks all project source files looking for qualified calls, pipe-chain calls,
and function captures (`&Mod.fun/arity`) that reference the given target.

## Target Formats

The `target` argument accepts several formats:

  * A string: `"MyApp.Accounts.create_user/2"`
  * A tuple with arity: `{MyApp.Accounts, :create_user, 2}`
  * A tuple matching any arity: `{MyApp.Accounts, :create_user}`

## Options

  * `:mix_root` — path to the Mix project root (defaults to current directory).
  * `:files` — a pre-loaded map of `%{path => source_string}` to search instead
    of reading from disk.

## Return Value

Returns `{:ok, result}` where result is a map:

    %{
      target: %{module: "MyApp.Accounts", function: :create_user, arity: 2},
      total: 5,
      files: %{
        "lib/my_app_web/controllers/user_controller.ex" => [
          %{line: 14, kind: :call, arity: 2, snippet: "...", in_module: "MyAppWeb.UserController"}
        ]
      }
    }

## Examples

    {:ok, result} = Adze.find_callers("MyApp.Accounts.create_user/2")
    {:ok, result} = Adze.find_callers({MyApp.Accounts, :create_user, 2}, mix_root: "/path/to/project")

See `Adze.FindCallers` for full details.

# `ls_deps`

```elixir
@spec ls_deps(String.t(), {atom(), non_neg_integer()}, keyword()) ::
  {:ok, map()} | {:error, term()}
```

Returns the transitive dependency tree rooted at `definition` from a source string.

Same as `ls_deps_file/2` but operates on source code passed directly.

## Options

  * `:file` — an optional filename label.

## Examples

    {:ok, tree} = Adze.ls_deps(source, {:parse, 1})

See `Adze.LsDeps` for full details.

# `ls_deps_file`

```elixir
@spec ls_deps_file(
  Path.t(),
  {atom(), non_neg_integer()}
) :: {:ok, map()} | {:error, term()}
```

Returns the transitive dependency tree rooted at `definition` in the given file.

Performs a depth-first traversal from the specified definition, showing all
functions it transitively calls within the same module. Cycles are detected
and marked with `repeat: true` rather than infinitely recursing.

`definition` is a `{name, arity}` tuple, e.g. `{:process, 2}`.

## Examples

    {:ok, tree} = Adze.ls_deps_file("lib/my_app/parser.ex", {:parse, 1})

See `Adze.LsDeps` for full details.

# `ls_extract`

```elixir
@spec ls_extract(String.t(), {atom(), non_neg_integer()}, keyword()) ::
  {:ok, map()} | {:error, term()}
```

Returns the flat extraction closure for `definition` from a source string.

Same as `ls_extract_file/2` but operates on source code passed directly.

## Options

  * `:file` — an optional filename label.

## Examples

    {:ok, closure} = Adze.ls_extract(source, {:parse, 1})

See `Adze.LsDeps` for full details.

# `ls_extract_file`

```elixir
@spec ls_extract_file(
  Path.t(),
  {atom(), non_neg_integer()}
) :: {:ok, map()} | {:error, term()}
```

Returns the flat extraction closure for `definition` in the given file.

Computes the set of private helper functions that are *exclusively* reachable
from the target definition — i.e., the private functions that could safely be
extracted along with it without breaking anything else in the module.

`definition` is a `{name, arity}` tuple, e.g. `{:process, 2}`.

## Examples

    {:ok, closure} = Adze.ls_extract_file("lib/my_app/parser.ex", {:parse, 1})

See `Adze.LsDeps` for full details.

# `mv`

```elixir
@spec mv(
  String.t(),
  keyword()
) :: {:ok, map()} | {:error, term()}
```

Dry-run: reorders a definition within a module.

Moves the specified definition to just before the anchor definition. Returns
the unified diff and the new source with the move applied.

## Options

  * `:definition` (required) — the function to move, e.g. `"process/2"`.
  * `:before` (required) — the anchor function to move before, e.g. `"handle_call/3"`.
  * `:include_attrs` — list of attribute atoms to move along with the definition
    (e.g. `[:doc, :spec]`).

## Return Value

Returns `{:ok, %{diff: String.t(), new_source: String.t()}}`.

## Examples

    {:ok, result} = Adze.mv(source, definition: "process/2", before: "handle_call/3")

See `Adze.Move` for full details.

# `mv!`

```elixir
@spec mv!(
  Path.t(),
  keyword()
) :: {:ok, map()} | {:error, term()}
```

Writes the definition reorder to disk.

Same as `mv_file/2` but actually writes the transformed file.

## Examples

    {:ok, result} = Adze.mv!("lib/my_app/server.ex", definition: "process/2", before: "handle_call/3")

See `Adze.Move` for full details.

# `mv_file`

```elixir
@spec mv_file(
  Path.t(),
  keyword()
) :: {:ok, map()} | {:error, term()}
```

Dry-run: reads a file and reorders a definition within a module.

Same as `mv/2` but reads source from the given file path.

## Examples

    {:ok, result} = Adze.mv_file("lib/my_app/server.ex", definition: "process/2", before: "handle_call/3")

See `Adze.Move` for full details.

# `outline`

```elixir
@spec outline(
  String.t(),
  keyword()
) :: {:ok, map()} | {:error, term()}
```

Returns a structural outline from a source string.

Same as `outline_file/1` but operates on source code passed directly as a string.

## Options

  * `:file` — an optional filename label to include in the result (useful for
    display purposes when the source didn't come from disk).

## Examples

    source = File.read!("lib/my_app/accounts.ex")
    {:ok, outline} = Adze.outline(source, file: "accounts.ex")

See `Adze.Outline` for full details on the returned structure.

# `outline_file`

```elixir
@spec outline_file(Path.t()) :: {:ok, map()} | {:error, term()}
```

Returns a structural outline of the given source file.

Parses the file at `path` and returns every top-level definition — `defmodule`,
`def`, `defp`, `defmacro`, `defmacrop`, `defguard`, `defstruct`, module
attributes (`@moduledoc`, `@doc`, custom attrs), and directives
(`alias`, `import`, `require`, `use`) — annotated with line ranges.

This is designed so an AI coding agent can map a 2000-line file in ~50 tokens.

## Examples

    {:ok, outline} = Adze.outline_file("lib/my_app/accounts.ex")

See `Adze.Outline` for full details on the returned structure.

# `rename`

```elixir
@spec rename(keyword()) :: {:ok, map()} | {:error, term()}
```

Dry-run: renames a module across the entire project.

Scans all project sources and computes the diffs that would rename `from` → `to`
in module definitions, qualified calls, aliases, atoms, and file paths.

## Options

  * `:from` (required) — the current module name (string or atom), e.g. `"MyApp.OldName"`.
  * `:to` (required) — the new module name (string or atom), e.g. `"MyApp.NewName"`.
  * `:mix_root` — path to the Mix project root.
  * `:files` — pre-loaded source map.
  * `:app_name` — the OTP app name (used for path inference).
  * `:force` — if `true`, allow rename even with surviving references (default: `false`).

## Return Value

Returns `{:ok, result}` where result is:

    %{
      from: MyApp.OldName,
      to: MyApp.NewName,
      diffs: %{"lib/my_app/old_name.ex" => "...", ...},
      moves: %{"lib/my_app/old_name.ex" => "lib/my_app/new_name.ex"},
      warnings: [...],
      notices: [...]
    }

## Examples

    {:ok, result} = Adze.rename(from: "MyApp.OldName", to: "MyApp.NewName")

See `Adze.Rename` for full details.

# `rename!`

```elixir
@spec rename!(keyword()) :: {:ok, map()} | {:error, term()}
```

Writes the module rename to disk.

Same as `rename/1` but actually writes all diffs and performs file moves.
Refuses to proceed if there are surviving references that couldn't be
automatically rewritten — unless `force: true` is passed.

## Examples

    {:ok, result} = Adze.rename!(from: "MyApp.OldName", to: "MyApp.NewName")
    {:ok, result} = Adze.rename!(from: "MyApp.OldName", to: "MyApp.NewName", force: true)

See `Adze.Rename` for full details.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
