Adze.ProjectRewrite (Adze v0.1.0)

Copy Markdown View Source

Thin wrapper around Igniter for project-wide write ops.

All cross-file write ops in adze — rename, extract!'s caller rewriting (Session 6.5), find-callers (Session 7) — funnel through this module so the Igniter-shaped plumbing (build the rewrite, run the op, extract per-file diffs, write changes) lives in exactly one place.

Lifecycle

iex> {:ok, rewrite} = Adze.ProjectRewrite.new(mix_root: ".")
iex> {:ok, rewrite, _report} =
...>   Adze.ProjectRewrite.rename_module(rewrite, MyApp.Old, MyApp.New)
iex> {:ok, result} = Adze.ProjectRewrite.result(rewrite)
iex> Adze.ProjectRewrite.write!(rewrite)

result/1 is dry-run shaped; it inspects the Igniter without touching disk. write!/1 flushes the same Igniter to the filesystem.

Working directory

Igniter is built around the assumption that the current working directory is the project root — Igniter.new/0 reads .igniter.exs and the dot formatter from cwd; prepare_for_write/1 walks relative paths. The Adze.ProjectRewrite struct carries the project root so every operation can cd into it before delegating to Igniter and restore the previous cwd afterwards. Callers already in the right cwd (e.g. the mix adze task) pay no cost — the wrapper short-circuits when root == File.cwd!().

Test mode

Adze.ProjectRewrite.new(files: %{"lib/foo.ex" => "..."})

builds an in-memory test Igniter backed by Igniter.Test.test_project/1. No files are read from disk; write!/1 is unsupported (raises). Used by the tests under test/project_rewrite_test.exs and test/rename_test.exs.

Summary

Functions

Add a brand-new file to the project. Wraps Igniter.create_new_file/4 so the eventual write!/1 writes it alongside any other rewrites.

Build a ProjectRewrite rooted at mix_root: (defaults to the current directory). When files: is supplied, returns an in-memory test rewrite instead — see the moduledoc.

Replace the content of an existing tracked source. Used by Adze.Extract to inject its in-file rewrite (cut + caller fix-up + alias insert) into the project before the cross-file pass runs.

Rewrite call sites of {old_module, old_function} to {new_module, new_function} across the project. Wraps Igniter.Refactors.Rename.rename_function/4.

Rename a module globally across the project. Wraps Igniter.Refactors.Rename.rename_module/3 and adds the adze-specific follow-ups documented below.

Extract a dry-run-shaped result from the rewrite without writing.

Flush the rewrite to disk. Refuses to write when the rewrite is in test mode or carries unresolved issues.

Types

opts()

@type opts() :: [
  mix_root: Path.t(),
  files: %{required(Path.t()) => String.t()},
  app_name: atom()
]

ref()

@type ref() :: %{path: Path.t(), line: non_neg_integer(), short: atom()}

rename_report()

@type rename_report() :: %{fixed_refs: [ref()], surviving_refs: [ref()]}

result()

@type result() :: %{
  diffs: %{required(Path.t()) => String.t()},
  moves: %{required(Path.t()) => Path.t()},
  warnings: [String.t()],
  issues: [String.t()],
  notices: [String.t()]
}

t()

@type t() :: %Adze.ProjectRewrite{
  igniter: Igniter.t(),
  root: Path.t() | nil,
  test?: boolean()
}

Functions

create_file(rewrite, path, content)

@spec create_file(t(), Path.t(), String.t()) :: {:ok, t()} | {:error, term()}

Add a brand-new file to the project. Wraps Igniter.create_new_file/4 so the eventual write!/1 writes it alongside any other rewrites.

new(opts \\ [])

@spec new(opts()) :: {:ok, t()} | {:error, term()}

Build a ProjectRewrite rooted at mix_root: (defaults to the current directory). When files: is supplied, returns an in-memory test rewrite instead — see the moduledoc.

put_content(rewrite, path, content)

@spec put_content(t(), Path.t(), String.t()) :: {:ok, t()} | {:error, term()}

Replace the content of an existing tracked source. Used by Adze.Extract to inject its in-file rewrite (cut + caller fix-up + alias insert) into the project before the cross-file pass runs.

rename_function(rewrite, arg1, arg2, opts \\ [])

@spec rename_function(t(), {module(), atom()}, {module(), atom()}, keyword()) ::
  {:ok, t()}

Rewrite call sites of {old_module, old_function} to {new_module, new_function} across the project. Wraps Igniter.Refactors.Rename.rename_function/4.

Pass arity: n (or a list) to narrow to a specific arity; defaults to :any. When the function definition has already been removed from old_module (e.g. by Adze.Extract cutting it before this runs), Igniter's def-move step is a no-op and only the call-site rewrites take effect.

rename_module(rewrite, old_module, new_module)

@spec rename_module(t(), module(), module()) :: {:ok, t(), rename_report()}

Rename a module globally across the project. Wraps Igniter.Refactors.Rename.rename_module/3 and adds the adze-specific follow-ups documented below.

Returns {:ok, rewrite, report} where report describes the short-ref fix-up:

  • fixed_refs — bare OldShort.fun(...) AST nodes that Igniter's same-namespace pass left in place and adze patched on the caller's behalf.
  • surviving_refs — bare-short references the fix-up could not safely rewrite (no qualifying non-as: alias declaration in pre-rewrite content). Callers like Adze.Rename surface these as a warning and refuse to write without force: true.

Follow-up work performed

  1. Test-file move. Igniter rewrites the test module's defmodule but doesn't move its file. We find the rewritten test source and schedule a move to its canonical-for-new-name path so the module name and file path stay in sync.
  2. Short-ref fix-up. Igniter's same-namespace rename has an upstream bug — the string-substitution pass rewrites the alias declaration before the AST pass runs, so bare OldShort.fun(...) call sites are no longer resolvable to the old aliases list and get left un-rewritten. Adze patches them itself in files that had a non-as: alias to the renamed module in their pre-rewrite content. Files without that evidence get left alone and reported in surviving_refs.

result(project_rewrite)

@spec result(t()) :: {:ok, result()}

Extract a dry-run-shaped result from the rewrite without writing.

diffs keys are file paths; values are plain-text unified-ish diffs rendered by Rewrite (no ANSI color). moves keys are pre-move paths, values are post-move paths. Errors/warnings are surfaced separately so the caller can decide whether to refuse to write.

write!(project_rewrite)

@spec write!(t()) :: :ok

Flush the rewrite to disk. Refuses to write when the rewrite is in test mode or carries unresolved issues.