# `Lockstep.MixCompiler`
[🔗](https://github.com/b-erdem/lockstep/blob/v0.1.0/lib/lockstep/mix_compiler.ex#L1)

Core file-rewriting logic for the `:lockstep_rewrite` Mix compiler.
Reads source files, runs them through `Lockstep.Rewriter`, and writes
the rewritten output to a build directory.

The Mix integration lives in `Mix.Tasks.Compile.LockstepRewrite`.
This module is also useful directly from tests when you want to
exercise the rewriting pipeline without going through Mix.

# `config`

```elixir
@type config() :: %{
  :paths =&gt; [String.t()],
  optional(:output) =&gt; Path.t(),
  optional(:preprocess) =&gt; preprocess_fn() | [preprocess_fn()],
  optional(:skip) =&gt; [String.t()]
}
```

# `preprocess_fn`

```elixir
@type preprocess_fn() :: (source :: String.t(), path :: String.t() -&gt; String.t())
```

# `compile`

```elixir
@spec compile(config()) :: {:ok, [Path.t()]} | {:error, term()}
```

Apply the rewrite pipeline to every file matching `config.paths`,
writing the result to `config.output`. Returns `{:ok, [paths]}` on
success or `{:error, reason}` on the first parse failure.

Files whose source mtime is older than the corresponding output file
are skipped (incremental compilation).

## Options

  * `:paths` (required) — list of file path patterns (wildcards
    supported via `Path.wildcard/1`).
  * `:output` — output directory. Defaults to
    `_build/<env>/lockstep_rewritten`.
  * `:skip` — list of glob patterns. Source files matching ANY of
    these patterns are skipped entirely (not rewritten, not copied
    to the output). Use this to omit Lite/SQLite engine sources
    when stubbing Postgres, or other modules you know aren't
    needed for the system under test. Patterns are checked against
    the absolute source path with `String.match?(path, ~r/<pattern>/)`.

  * `:preprocess` — single function or list of functions
    `(source, path -> source)` applied to each file's contents
    *before* AST parsing. Use this to strip or rewrite constructs
    the rewriter can't handle directly. Built-in helpers in
    `Lockstep.MixCompiler.Preprocessors` cover the common cases:

      * `strip_compile_time_external_reads/2` — replaces
        `@moduledoc File.read!(...) |> ...` blocks (used by
        nimble_pool, highlander) with a literal string. The original
        breaks because the rewritten file lives in a different cwd.

      * `strip_code_ensure_loaded/2` — comments out
        `Code.ensure_loaded!(SomeMod)` lines (used by Hammer's
        `__before_compile__`) where the module isn't actually
        available during the rewrite test run.

    Multiple preprocessors are applied in order. See the
    `Preprocessors` module for examples.

## Example

    Lockstep.MixCompiler.compile(%{
      paths: ["/tmp/nimble_pool/lib/**/*.ex"],
      output: "/tmp/nimble_pool_lockstep",
      preprocess: [
        &Lockstep.MixCompiler.Preprocessors.strip_compile_time_external_reads/2
      ]
    })

# `elixirc_paths_for`

```elixir
@spec elixirc_paths_for([String.t()]) :: [Path.t()]
```

Helper for assembling `elixirc_paths` when using the
`:lockstep_rewrite` Mix compiler. Returns the list of paths your
project should compile from in the rewriting env (typically
`:test`), given the source paths you want rewritten.

## Example

    defp elixirc_paths(:test) do
      Lockstep.MixCompiler.elixirc_paths_for(["lib/**/*.ex"]) ++
        ["test/support"]
    end

    defp elixirc_paths(_), do: ["lib"]

Returns the rewritten output dir prepended to the *parent
directories* of the source paths, deduplicated.

# `rewrite_file`

```elixir
@spec rewrite_file(Path.t(), Path.t(), [preprocess_fn()]) ::
  {:ok, Path.t()} | :skipped | {:error, term()}
```

Rewrite a single file from `source_path` into `output_dir`. The
output preserves the source's relative path under `output_dir`.

  * Returns `{:ok, output_path}` on a successful rewrite.
  * Returns `:skipped` if the source mtime is not newer than the
    output mtime.
  * Returns `{:error, {parse_error, source_path, line, message}}`
    if the source doesn't parse.

---

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