Conditional import fails to compile

I’m trying to import a helper module in my application that is only available in under :dev and :test mix environments.

I’ve got the following in my mix.exs to prevent the helper module from being compiled under :prod:

  defp elixirc_paths(:test), do: ["lib", "test/fixtures", "test/support"]
  defp elixirc_paths(:dev), do: ["lib", "test/fixtures"]
  defp elixirc_paths(_), do: ["lib"]

and then the following in the body of one of my modules:

def MyAppWeb.ProxyController do
  ...
  # Insert controller actions to serve fixture data in development and testing
  if Mix.env() in [:dev, :test] do
    import MyApp.Fixtures, only: [mock_proxy: 1]

    mock_proxy(:the_thing)
  end
  ...
end

The mock_proxy/1 macro injects some function clauses to the module. This works great under :dev or :test environments, but fails in :prod with the following error?

== Compilation error in file lib/my_app_web/controllers/proxy_controller.ex ==
** (CompileError) lib/my_app_web/controllers/proxy_controller.ex:18: module MyApp.Fixtures is not loaded and could not be found
    (stdlib 3.14.2) lists.erl:1358: :lists.mapfoldl/3
    (stdlib 3.14.2) lists.erl:1359: :lists.mapfoldl/3
    (elixir 1.12.1) expanding macro: Kernel.if/2

I thought this wouldn’t happen because of the if statement at the module level, but clearly something else is going on. I can “fix” this by compiling this path in all environments, but I’d like to avoid that if I can.

Any thoughts on what I’m missing here?

The code needs to be put in a macro which can return something depending on a condition. I was able to make the following work:

defmodule Compile do
  defmacro only_in(envs, body) do
    if Mix.env() == envs or Mix.env() in envs do
      body
    end
  end
end

def MyAppWeb.ProxyController do
  require Compile

  Compile.only_in [:dev, :test] do
    import MyApp.Fixtures, only: [mock_proxy: 1]
    mock_proxy(:the_thing)
  end
end

This is just a suggestion. Maybe there’s a better way but this should help you to make progress for now. :+1:

2 Likes

Correct! The code in “if” won’t be executed but it still has to be expanded, hence the failure.

2 Likes

Here is an improved version that allows an optional else-body:

defmodule Compile do
  defmacro only_in(envs, body) do
    if Mix.env() in List.wrap(envs) do
      body[:do]
    else
      body[:else]
    end
  end
end

def MyAppWeb.ProxyController do
  require Compile

  Compile.only_in [:dev, :test] do
    import MyApp.Fixtures, only: [mock_proxy: 1]
    mock_proxy(:the_thing)
  else
    IO.puts("No mock for prod.")
  end
end
3 Likes

Thanks aziz and José.

Just so I really cement my understanding, the body block containing the import in the only_in/2 macro is never unquoted in the module during compilation (or a preprocessing stage?), whereas mine fails because the import is directly in the module body. Is this correct?

Is there a good place to read up on the compilation process, or should i start digging through the Elixir source tree?