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 any 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 the warning is missed.
# 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.
Only qualified remote calls (Ash.Query.filter(...)) are
inspected. Unqualified calls like filter(...) after import Ash.Query
are out of scope: if the import is missing, Elixir raises a clean
undefined function filter/2 at compile time, which is obvious enough
to need no lint.
require/import are accepted in any lexical scope visible to the
call - module top, the enclosing def/defp body, an enclosing
if/case/with branch, etc. - matching Elixir's own scoping rules.
A directive in one function does not reach calls in a sibling function.
Each configured module is tracked independently: require Ash.Query
does not cover Ash.Expr.expr(...), and vice versa. A module that
uses macros from both modules needs both directives.
Calls inside quote do ... end blocks are deliberately ignored. 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) is honored by
Ash.Query.filter(...) (or Q.filter(...)) inside a nested defmodule.
This 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; pair with AshCredo.Check.Refactor.DirectiveInFunctionBody
if your team wants to centralise directives at module top.
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 - non-macro calls on the same
module (
Ash.Query.new/1, for example) are never flagged. - New macros added in future Ash releases are automatically picked up without code changes here.
- User-supplied modules in
macro_modulesare handled with the same precision asAsh.Query/Ash.Expr- only their actual macros are flagged, not every qualified call.
Requirements
Your project must be compiled 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 (typical
cause: you added one of your own modules to macro_modules and have
not compiled yet), the check emits a per-module "could not load"
diagnostic and skips that module for the run.
Configuration
macro_modules defaults to [Ash.Query, Ash.Expr]. Extend the list
with additional 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
List of modules whose qualified macro calls the check validates. For each call to <Module>.<macro>/n the check requires a require or import of <Module> to be lexically in scope - module top, the enclosing def body, an enclosing branch, or inherited from an enclosing defmodule. Defaults to [Ash.Query, Ash.Expr]. The exact set of macros on each module is read from compiled-BEAM introspection (module.__info__(:macros)), so only real macros are flagged - regular functions on the same module are ignored.
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.