Guard Restriction Example
This example demonstrates compile-time validation of guard usage by walking
the AST with Macro.prewalk/3 and using __before_compile__.
Use Case
You're building a library that restricts certain guards and want to:
- Ban specific guards in a module
- Walk the AST to find violations
- Provide clear error messages with alternatives
Example Error Output

Usage
defmodule MyApp.StrictModule do
use Pentiment.Examples.GuardRestriction, ban: [:is_atom, :is_binary]
# This will compile fine
def process(x) when is_integer(x), do: x * 2
# This will fail: is_atom is banned
def handle(x) when is_atom(x), do: Atom.to_string(x)
endImplementation
The full implementation is in test/support/examples/guard_restriction.ex.
Key Points
1. Override def to capture AST
defmacro __using__(opts) do
banned = Keyword.get(opts, :ban, [])
quote do
@banned_guards unquote(banned)
@before_compile Pentiment.Examples.GuardRestriction
Module.register_attribute(__MODULE__, :guard_function_defs, accumulate: true)
import Kernel, except: [def: 2]
import Pentiment.Examples.GuardRestriction, only: [def: 2]
end
end
defmacro def(call, do: body) do
caller = __CALLER__
quote do
@guard_function_defs {
unquote(Macro.escape(call)),
unquote(Macro.escape(body)),
unquote(caller.file),
unquote(caller.line)
}
Kernel.def(unquote(call), do: unquote(body))
end
end2. Walk guards with Macro.prewalk/3
defp find_banned_guard_calls(nil, _banned), do: []
defp find_banned_guard_calls(ast, banned_guards) do
{_, violations} =
Macro.prewalk(ast, [], fn
{guard_name, meta, args} = node, acc when is_atom(guard_name) and is_list(args) ->
if guard_name in banned_guards do
{node, [{guard_name, meta} | acc]}
else
{node, acc}
end
node, acc ->
{node, acc}
end)
Enum.reverse(violations)
end3. Extract function info including guards
defp extract_function_info({:when, _, [call, guards]}) do
{name, args} = extract_name_and_args(call)
{name, length(args), guards}
end
defp extract_function_info(call) do
{name, args} = extract_name_and_args(call)
{name, length(args), nil}
end4. Build violation errors
defp build_guard_violation_error(guard_name, guard_meta, banned_guards, file) do
span =
if guard_meta != [] do
Pentiment.Elixir.span_from_meta(guard_meta)
else
Pentiment.Span.position(1, 1)
end
Pentiment.Report.error("Use of banned guard `#{guard_name}/1`")
|> Pentiment.Report.with_code("GUARD001")
|> Pentiment.Report.with_source(file)
|> Pentiment.Report.with_label(Pentiment.Label.primary(span, "banned guard"))
|> Pentiment.Report.with_note("this module bans guards: #{Enum.join(banned_guards, ", ")}")
|> Pentiment.Report.with_help("remove the `#{guard_name}` guard")
endKey Techniques
- Override
def: Capture function definitions with their AST Macro.escape/1: Store quoted expressions in module attributesMacro.prewalk/3: Walk the AST to find specific patterns- Guard clause AST: Functions with guards use
{:when, meta, [call, guards]}
Testing
test "banned guard raises CompileError" do
code = """
defmodule TestModule do
use Pentiment.Examples.GuardRestriction, ban: [:is_atom]
def handle(x) when is_atom(x), do: x
end
"""
error = assert_raise CompileError, fn -> Code.compile_string(code) end
assert error.description =~ "Use of banned guard `is_atom/1`"
assert error.description =~ "banned guard"
end
test "allowed guards compile normally" do
code = """
defmodule TestModule do
use Pentiment.Examples.GuardRestriction, ban: [:is_atom]
def process(x) when is_integer(x), do: x * 2
end
"""
assert [{module, _}] = Code.compile_string(code)
assert module.process(5) == 10
end