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)

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:

PredicateMeaning
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.Patcher

When 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" only

Specific 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 only

Structural 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.map

Any Elixir expression works inside wherematch?/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(_)")