Structural Elixir refactoring and outline-first code exploration.

Adze is a toolkit built on Sourceror and 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:

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:

Underlying Modules

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

Summary

Functions

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

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

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

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

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

Writes the extraction to disk.

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

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

Writes the def → defp flip to disk.

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

Finds all callers of a function across the project.

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

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

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

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

Dry-run: reorders a definition within a module.

Writes the definition reorder to disk.

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

Returns a structural outline from a source string.

Returns a structural outline of the given source file.

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

Writes the module rename to disk.

Functions

aliases(source, opts \\ [])

@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(path)

@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(source, opts \\ [])

@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(path)

@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(source, opts)

@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!(path, opts)

@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(path, opts)

@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(source, opts)

@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 defdefp 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!(path, opts)

@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(path, opts)

@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(target, opts \\ [])

@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(source, definition, opts \\ [])

@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(path, definition)

@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(source, definition, opts \\ [])

@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(path, definition)

@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(source, opts)

@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!(path, opts)

@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(path, opts)

@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(source, opts \\ [])

@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(path)

@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(opts)

@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 fromto 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!(opts)

@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.