AshCredo.Check.Warning.MissingMacroDirective (ash_credo v0.17.1)

Copy Markdown View Source

Basics

This check is disabled by default.

Learn how to enable it via .credo.exs.

This check has a base priority of high and works with any version of Elixir.

Explanation

Flags qualified calls to macros on configured modules (default Ash.Query and Ash.Expr) when no matching require or import of the macro module is lexically in scope at the call site.

Several Ash.Query and Ash.Expr functions are actually macros: Ash.Query.filter/2, equivalent_to/2, superset_of/2, subset_of/2 and their ? variants, and Ash.Expr.expr/1, where/2, or_where/2, calc/1..2. Calling one of them without a matching require in scope has three different failure modes, depending on the shape of the argument:

# 1. Literal expression -> compile error with a misleading message
Ash.Query.filter(Post, state == :published)
# ** (CompileError) undefined variable "state"

# 2. Pinned variable -> compile error about the pin operator
Ash.Query.filter(Post, ^pre_built)
# ** (CompileError) misplaced operator ^pre_built

# 3. Bare variable holding a runtime value -> compiles with an
# easy-to-miss warning, then fails at RUNTIME with
# UndefinedFunctionError when the function is actually called.
def foo(f), do: Ash.Query.filter(Post, f)
# warning: Ash.Query.filter/2 is undefined or private...
# ...later at runtime:
# ** (UndefinedFunctionError) function Ash.Query.filter/2 is
#    undefined or private

Case #3 is the important one for a linter: the other two fail loudly at compile time, but this one ships to production if you miss the warning.

# Flagged
defmodule MyApp.PostQueries do
  def published do
    MyApp.Post
    |> Ash.Query.filter(state == :published)
    |> Ash.read!()
  end
end

# Preferred
defmodule MyApp.PostQueries do
  require Ash.Query

  def published do
    MyApp.Post
    |> Ash.Query.filter(state == :published)
    |> Ash.read!()
  end
end

require and import both satisfy the check: import <Module> implies require <Module> in Elixir, so qualified macro calls work after either directive.

The check only inspects qualified remote calls (Ash.Query.filter(...)). Unqualified calls like filter(...) after import Ash.Query are out of scope: if the import is missing, Elixir raises a clear undefined function filter/2 error at compile time, which is obvious enough to need no lint.

The check accepts require and import in any lexical scope visible to the call: the module top, the enclosing def or defp body, or an enclosing if, case, or with branch. This matches Elixir's own scoping rules, so a directive in one function does not reach calls in a sibling function.

The check tracks each configured module independently: require Ash.Query does not cover Ash.Expr.expr(...), and vice versa. A module that uses macros from both modules needs both directives.

The check deliberately ignores calls inside quote do ... end blocks. A macro author who writes quote do Ash.Query.filter(...) end is injecting the call into the caller's site, not emitting it from their own module, so flagging it would be a false positive.

Nested defmodule blocks inherit require, import, and alias from the enclosing module the same way Elixir does, so an outer require Ash.Query (or alias Ash.Query, as: Q) applies to Ash.Query.filter(...) (or Q.filter(...)) inside a nested defmodule.

The check is a correctness backstop: for projects without --warnings-as-errors, it converts the easy-to-miss runtime case (#3 above) into a lint issue. Style rules about where directives live are out of scope; if your team wants all directives at the module top, pair this check with AshCredo.Check.Refactor.DirectiveInFunctionBody.

Precision

The check uses compiled-BEAM introspection (module.__info__(:macros)) to learn which functions on each configured module are actually macros. This means:

  • It only flags real macro calls; it never flags a non-macro call on the same module (Ash.Query.new/1, for example).
  • New macros in future Ash releases are covered automatically, without code changes here.
  • User-supplied modules in macro_modules get the same precision as Ash.Query and Ash.Expr: only their real macros are flagged, not every qualified call.

Requirements

Compile your project before running mix credo. If Ash is not available in the VM running Credo, the check is a no-op and emits a single diagnostic. If a configured module cannot be loaded, the check emits a "could not load" diagnostic for that module and skips it for the run. The usual cause is adding one of your own modules to macro_modules without compiling first.

Configuration

macro_modules defaults to [Ash.Query, Ash.Expr]. Extend the list with any other macro modules your team uses:

{AshCredo.Check.Warning.MissingMacroDirective,
 [macro_modules: [Ash.Query, Ash.Expr, MyApp.QueryMacros]]}

Check-Specific Parameters

Use the following parameters to configure this check:

:macro_modules

Modules whose qualified macro calls the check validates. For each call to <Module>.<macro>/n, the check requires a require or import of <Module> lexically in scope: the module top, the enclosing def body, an enclosing branch, or a directive inherited from an enclosing defmodule. Defaults to [Ash.Query, Ash.Expr]. The exact set of macros on each module comes from compiled-BEAM introspection (module.__info__(:macros)), so the check only flags real macros and ignores regular functions on the same module.

This parameter defaults to [Ash.Query, Ash.Expr].

General Parameters

Like with all checks, general params can be applied.

Parameters can be configured via the .credo.exs config file.