Patterns match shape. Queries add context — "find X only when it's inside Y" or "find X but only if the captured value is a specific atom."
Relationship filters
Filter matches by their surrounding context:
# Only inside private functions
ExAST.search("lib/", "Repo.get!(_, _)", inside: "defp _ do _ end")
# Exclude test blocks
ExAST.search("lib/", "IO.inspect(_)", not_inside: "test _ do _ end")Available options: :inside, :not_inside. Also available as CLI flags:
mix ex_ast.search --inside 'defp _ do _ end' 'Repo.get!(_, _)' lib/
mix ex_ast.search --not-inside 'test _ do _ end' 'IO.inspect(_)' lib/
Query API
Use ExAST.Query when a match depends on AST relationships:
import ExAST.Query
# Find functions that have a transaction but no debug output
query =
from("def _ do ... end")
|> where(contains("Repo.transaction(_)"))
|> where(not contains("IO.inspect(...)"))
ExAST.search("lib/", query)Navigation
Move through the tree with find/2 (descendants) and find_child/2 (direct children):
# Find IO.inspect calls inside any module
from("defmodule _ do ... end")
|> find("IO.inspect(_)")
# Find direct function definitions (not nested ones)
from("defmodule _ do ... end")
|> find_child("def _ do ... end")Predicates
Filter the current selection without changing it:
| Predicate | Meaning |
|---|---|
contains(pattern) | Has a descendant matching pattern |
has_child(pattern) | Has a direct child matching pattern |
inside(pattern) | Is inside an ancestor matching pattern |
parent(pattern) | Has a direct parent matching pattern |
follows(pattern) | Has a previous sibling matching pattern |
precedes(pattern) | Has a following sibling matching pattern |
immediately_follows(pattern) | Immediately after a matching sibling |
immediately_precedes(pattern) | Immediately before a matching sibling |
first() | First sibling in its parent |
last() | Last sibling in its parent |
nth(n) | nth sibling (1-based) |
any([...]) | Any nested predicate matches |
all([...]) | All nested predicates match |
Combine with not, and, or:
from("IO.inspect(value)")
|> where(inside("def _ do ... end"))
|> where(not parent("if _ do ... end"))Alternative patterns
Pass a list to match multiple shapes:
from(["def _ do ... end", "defp _ do ... end"])Capture guards
Use ^ inside where/2 to filter on captured values — similar to Ecto's pin syntax:
import ExAST.Query
alias ExAST.PatcherWhen to use capture guards
Most filtering can be done with patterns alone (see Pattern Language). Reach for capture guards when you need to:
- Compare two captures to each other
- Filter by specific atom or literal values
- Check the structural type of a captured node
Multi-capture comparison
source = """
x == x
x == y
"""
query = from("left == right") |> where(^left == ^right)
Patcher.find_all(source, query)
#=> matches "x == x" onlySpecific atom values
source = """
def handle(:click, socket), do: socket
def handle(:keydown, socket), do: socket
def handle(:submit, socket), do: socket
"""
query =
from("def handle(event, _) do ... end")
|> where(^event == :click or ^event == :keydown)
Patcher.find_all(source, query)
#=> matches :click and :keydown onlyStructural type checks
source = """
Enum.map(users, fn u -> u.name end) |> Enum.filter(fn u -> u.active? end)
Enum.filter(users, fn u -> u.active? end)
"""
# Find Enum.filter where the first arg is itself a pipe expression
query =
from("Enum.filter(expr, _)")
|> where(match?({{:., _, [{:__aliases__, _, [:Enum]}, :map]}, _, _}, ^expr))
Patcher.find_all(source, query)
#=> matches line 1 only — the one fed by Enum.mapAny Elixir expression works inside where — match?/2, is_atom/1, comparisons,
function calls. The ^name references are replaced with the corresponding
captured AST node at match time.
Selector introspection and verification
Selectors expose source/comment requirements and direct verification helpers:
selector =
from("def _ do ... end")
|> where(comment_before("public API"))
ExAST.Selector.requires_source?(selector)
#=> true
ExAST.Selector.requires_comments?(selector)
#=> true
ExAST.Selector.find_all(source, selector)
ExAST.Selector.match?(source, selector)For candidate indexing and code intelligence metadata, see Indexing and Code Intelligence.
Broad queries
from("_") matches every AST node. Project-wide searches refuse those
unless you pass a limit or opt in explicitly:
ExAST.search("lib/", from("_"), limit: 100)
ExAST.search("lib/", from("_"), allow_broad: true)Lower-level API
ExAST.Selector provides the same functionality with CSS-like naming:
import ExAST.Selector
pattern("defmodule _ do ... end")
|> descendant("def _ do ... end")
|> child("IO.inspect(_)")