ExAST 🔬

Search, replace, and diff Elixir code by AST pattern.

Patterns are plain Elixir — variables capture, _ is a wildcard, maps and structs match partially, pipes are normalized. ... matches variable-length portions of calls, lists, tuples, and blocks, and can explicitly mark the rest of a map or struct. ^name matches a literal variable name, name/fun/function can capture function names in definitions, and fun/function can capture call names. _(...) and _._(...) match any local or remote call. No regex, no custom DSL.

mix ex_ast.search 'IO.inspect(_)'
mix ex_ast.replace 'IO.inspect(expr, _)' 'Logger.debug(inspect(expr))' lib/
mix ex_ast.diff lib/old.ex lib/new.ex

Why

Regex can't tell IO.inspect(data) from IO.inspect(data, label: "debug"). Text diff doesn't know a function moved vs changed. ExAST works on the AST — patterns match structure, not strings.

Quick examples

# Negative literals — flag potential bugs
ExAST.Patcher.find_all(source, "Enum.take(_, -_)")
# Always-true comparisons
ExAST.Patcher.find_all(source, "{a, a}")
# Compile-time config reads
ExAST.Patcher.find_all(source, "@name Application.get_env(_, _)")
# Batch analyzer checks in one scan
ExAST.Patcher.find_many(source,
get_env: "@_ Application.get_env(_, _)",
dbg_call: "dbg(expr)"
)
# same batch from the CLI — one boot, one parse per file:
# mix ex_ast.search -e "@_ Application.get_env(_, _)" -e "dbg(_)" lib/
# Preview rewrites before applying patches
ExAST.rewrite_plan(source, "dbg(expr)", "expr")
#=> %ExAST.Rewriter.Plan{replacements: [...], conflicts: []}
# Specific atom values
import ExAST.Query
from("def handle_event(event, _, _) do ... end")
|> where(^event == :click or ^event == :keydown)
# Functions with transaction but no debug output
from("def _ do ... end")
|> where(contains("Repo.transaction(_)"))
|> where(not contains("IO.inspect(...)"))

Installation

def deps do
[{:ex_ast, "~> 0.13", only: [:dev, :test], runtime: false}]
end

Documentation

GuideContent
Getting StartedInstall, first search, first replace
Pattern LanguageSyntax, wildcards, captures, ellipsis, pipes, recipes
QueryingRelationship filters, selectors, capture guards
Indexing and Code IntelligenceStructural terms, selector plans, comments, symbols
CLI ReferenceCommand-line flags and usage
DiffSyntax-aware code diffing
API ReferenceModule documentation

What you can match

# Function calls (any arity with ...)
Enum.map(_, _)
Logger.info(...)
# Ellipsis inside lists
[...] # any list, including empty
[x, y, z, ...] # capture leading elements; ... absorbs the rest
[..., y, z] # capture trailing elements; ... absorbs the rest
def _ do [...] end # functions whose body is a list literal
# Definitions and function-name captures
def handle_call(msg, _, state) do _ end
def name(_, _) do ... end # captures the definition name
def name/2 do ... end # capture names of two-argument definitions
defp _/_ do ... end # any private definition, any arity
Repo.fun(changeset) # captures remote call names like insert/update
fun(changeset) # captures local call names
# Wildcard callees — match any call
_(...) # any local call
_._(...) # any remote call
_.section(...) # any-module call to `section`
# Literal variable names
{:reply, ^state, ^state}
# Pipes (matches both forms)
Enum.map(data, f) # also matches: data |> Enum.map(f)
# Multi-node sequences
a = Repo.get!(_, _); Repo.delete(a)
# Tuples, structs, maps
{:ok, result}
{:ok, ...} # tuples beginning with :ok, at any remaining arity
{first, ..., last} # capture both tuple edges
%User{role: :admin} # struct with at least this field
%Struct{...} # any struct of that type
%{name: name} # map with at least this key
%{..., name: name} # same subset match, with explicit rest
%{...} # any map, including empty
# Directives and attributes
use GenServer
@env Application.get_env(_, _)
# Control flow and standalone clauses
case _ do _ -> _ end
fn _ -> _ end
{:error, e} -> raise e

Code intelligence APIs

ExAST can expose advisory metadata for external indexes while remaining the semantic verifier:

import ExAST.Query
selector =
from("def _ do ... end")
|> where(contains("Repo.transaction(_)"))
ExAST.Index.plan(selector)
#=> %ExAST.Index.Plan{required_terms: ..., requires_source?: false}
ExAST.Symbols.definitions(source)
ExAST.Symbols.references(source)
ExAST.Comments.extract(source)
ExAST.Comments.associated(source, range, :before)
ExAST.Symbols.qualified_name({Enum, :map, 2})
#=> "Enum.map/2"

Symbols keep stable string names for indexing and expose optional mfa tuples when a BEAM module can be safely resolved.

Use these terms and facts to retrieve candidates, then verify with ExAST.Selector.find_all/3 or ExAST.Selector.match?/3.

Limitations

Part of Elixir Vibe

ExAST searches and rewrites Elixir by AST pattern — the structural editing layer the rest of the stack builds on.

It is one building block of a larger stack — tools that make AI-generated software checkable: structural search, dependence analysis, duplication and slop detection, session replay, and ecosystem-wide code search. See the Elixir Vibe organization for the rest, and Building Blocks for the Future Web for the thesis, architecture, and roadmap that tie them together.

License

MIT