defmodule Mix.Tasks.Compile.LockstepRewrite do
@moduledoc """
Mix compiler that rewrites source files via `Lockstep.Rewriter`
before the standard Elixir compiler runs.
## Setup
In your `mix.exs`:
def project, do: [
...
compilers: compilers(Mix.env()),
elixirc_paths: elixirc_paths(Mix.env()),
lockstep_rewrite: lockstep_rewrite(Mix.env()),
]
defp compilers(:test), do: [:lockstep_rewrite] ++ Mix.compilers()
defp compilers(_), do: Mix.compilers()
defp elixirc_paths(:test) do
["_build/test/lockstep_rewritten/lib", "test/support"]
end
defp elixirc_paths(_), do: ["lib"]
defp lockstep_rewrite(:test) do
%{paths: ["lib/**/*.ex", "src/**/*.erl"]}
end
defp lockstep_rewrite(_), do: nil
In test env this will:
1. Read every `lib/**/*.ex` and `src/**/*.erl` file.
2. Run `.ex` through `Lockstep.Rewriter` and `.erl` through
`Lockstep.ErlangRewriter` -- both rewrite vanilla OTP calls
(`GenServer.call`, `gen_server:call`, `send/2`, `Pid ! Msg`,
bare `receive`, ...) to their `Lockstep.*` controller-aware
equivalents.
3. Write the rewritten copy to `_build/test/lockstep_rewritten/...`.
4. The standard compilers then read from there because
`elixirc_paths(:test)` and `erlc_paths(:test)` are overridden.
In dev / prod, the original sources compile normally; the rewriter
never runs.
## Configuration
The `:lockstep_rewrite` project option takes a map:
* `:paths` (required) -- list of glob patterns to rewrite.
* `:output` (optional) -- output directory. Defaults to
`_build/$ENV/lockstep_rewritten`.
Set the option to `nil` (or omit it) to skip rewriting in that env.
## Limitations
* Comments and source formatting are lost (the rewriter round-
trips through AST).
* Only handles user code matching the configured paths. Dep
libraries (in `deps/`) need a separate workflow because the dep's
compiled BEAM in `_build/$ENV/lib/<dep>/ebin` would conflict with
our rewritten copy. See the README's "Rewriting deps" section.
"""
use Mix.Task.Compiler
@impl Mix.Task.Compiler
def run(_args) do
case Mix.Project.config()[:lockstep_rewrite] do
nil ->
{:ok, []}
config when is_map(config) ->
do_run(config)
end
end
defp do_run(config) do
case Lockstep.MixCompiler.compile(config) do
{:ok, files} ->
if files != [], do: Mix.shell().info("[lockstep] rewrote #{length(files)} file(s)")
{:ok, []}
{:error, {:parse_error, path, meta, message, token}} ->
diagnostic = %Mix.Task.Compiler.Diagnostic{
file: path,
severity: :error,
message: "lockstep_rewrite parse error: #{message} #{inspect(token)}",
position: Keyword.get(meta, :line, 0),
compiler_name: "lockstep_rewrite"
}
{:error, [diagnostic]}
end
end
end