Lockstep.MixCompiler (Lockstep v0.1.0)

Copy Markdown View Source

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.

Summary

Functions

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.

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.

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

Types

config()

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

preprocess_fn()

@type preprocess_fn() :: (source :: String.t(), path :: String.t() -> String.t())

Functions

compile(config)

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

@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(source_path, output_dir, preprocessors \\ [])

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