MikaCredoRules.GenServerRequiresHandleContinue (MikaCredoRules v0.1.0)

Copy Markdown View Source

GenServer init/1 must defer real work to handle_continue/2.

init/1 blocks GenServer.start_link/3 and with it everything else the supervisor still has to start. An init/1 that hits the database, another process or the network delays the whole supervision tree and risks the five second init timeout. Build the initial state, return {:ok, state, {:continue, term}}, and do the work in handle_continue/2.

# BAD — blocks the supervisor while the query runs
defmodule MyApp.Server do
  use GenServer

  def init(opts) do
    rows = MyApp.Repo.all(MyApp.Row)
    {:ok, %{rows: rows, opts: opts}}
  end
end

# GOOD — init/1 only builds state, the query runs in handle_continue/2
defmodule MyApp.Server do
  use GenServer

  def init(opts) do
    {:ok, %{rows: [], opts: opts}, {:continue, :load}}
  end

  def handle_continue(:load, state) do
    {:noreply, %{state | rows: MyApp.Repo.all(MyApp.Row)}}
  end
end

This check is a heuristic. Files without a literal use GenServer are skipped entirely. In files that have one, an init/1 clause is flagged when it contains a remote call not covered by the :allowed_modules list and no {:continue, _} tuple anywhere in the clause. Each clause is judged on its own — a {:continue, _} in one clause does not excuse blocking work in another — and each violating clause produces one issue, anchored at its first disallowed call.

The allow-list mixes whole modules and single functions. A bare module entry (Keyword, Logger) allows every call on it; a {module, function} tuple grants one function surgically. The defaults allow the cheap init idioms Process.flag/2, Process.monitor/1 and Process.send_after/3 without allowing Process wholesale — so a blocking Process.sleep/1 in init/1 stays flagged.

Known approximations, chosen to keep the check cheap and false positives rare:

  • A {:continue, _} tuple anywhere in a clause counts as deferring, even when it is not in return position.
  • Local function calls are always allowed — the check cannot cheaply resolve what a private helper does.
  • Struct literals (%__MODULE__{}, %SomeStruct{}) and calls on __MODULE__ are allowed.
  • use GenServer is detected per file, so every init/1 in a file that uses GenServer anywhere is checked, and a use injected by another macro's __using__ is invisible.
  • Aliases are not resolved — a module is matched by the name it is written as, so alias MyApp.Repo followed by Repo.all/1 is flagged as Repo, not MyApp.Repo.