MikaCredoRules.NoMixEnvAtRuntime (MikaCredoRules v0.1.0)

Copy Markdown View Source

Mix.env() and Mix.target() must not be called from compiled code.

Mix is a build tool — it is not part of a release. A Mix.env() call that compiles fine in dev crashes in prod with UndefinedFunctionError. Branch on the environment at compile time via config instead.

# BAD — crashes in a release
defmodule MyApp.Worker do
  def start_link(opts) do
    if Mix.env() === :prod, do: connect(opts), else: :ignore
  end
end

# GOOD — config decides, code reads config
# config/prod.exs
config :my_app, connect_on_start: true

# worker.ex
defmodule MyApp.Worker do
  @connect_on_start Application.compile_env(:my_app, :connect_on_start, false)

  def start_link(opts) do
    if @connect_on_start, do: connect(opts), else: :ignore
  end
end

Script files are exempt: any file ending in .exs (mix.exs, config/*.exs, tests) runs under Mix, where Mix.env() is available and appropriate.

Mix tasks are exempt too — they only ever run under Mix, never in a release. A file is treated as a Mix task when it contains use Mix.Task or when its path contains an entry of :excluded_paths (default ["mix/tasks/"]).

Module attributes are still flagged. @env Mix.env() is evaluated at compile time and does not crash a release, but it bakes the build environment into the code invisibly — use Application.compile_env/3 with per-environment config so the branch is auditable from config/.

test/support/*.ex files are still flagged too. They compile only for tests and never ship in a release, but the stance is the same as for module attributes: the environment branch belongs in config. Add "test/support/" to :excluded_paths to opt out.

Known limitations

The module is matched by spelling — Mix.env() and Elixir.Mix.env() are caught. A renamed alias (alias Mix, as: Build) is not resolved and escapes the check; shadowing (alias MyApp.Mix) is likewise not resolved and would be falsely flagged. Both spellings are pathological enough not to warrant alias tracking here.

use Mix.Task exempts the whole file, not just the enclosing module. Being wrong would require a file to define both a Mix task and an ordinary runtime module — one module per file makes this moot.

Dynamic dispatch escapes the check — apply(Mix, :env, []) crashes a release exactly like Mix.env() but is not a dot-call node and is not matched. This is an evasion vector, not an idiom; no static check catches it without taint analysis.