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 privateCase #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
endrequire 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_modulesget the same precision asAsh.QueryandAsh.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.