ExAST 🔬
Search, replace, and diff Elixir code by AST pattern.
Patterns are plain Elixir — variables capture, _ is a wildcard,
structs match partially, pipes are normalized. 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.exWhy
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)"
)
# 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.11", only: [:dev, :test], runtime: false}]
endDocumentation
| Guide | Content |
|---|---|
| Getting Started | Install, first search, first replace |
| Pattern Language | Syntax, wildcards, captures, ellipsis, pipes, recipes |
| Querying | Relationship filters, selectors, capture guards |
| Indexing and Code Intelligence | Structural terms, selector plans, comments, symbols |
| CLI Reference | Command-line flags and usage |
| Diff | Syntax-aware code diffing |
| API Reference | Module documentation |
What you can match
# Function calls (any arity with ...)
Enum.map(_, _)
Logger.info(...)
# Definitions
def handle_call(msg, _, state) do _ end
# 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}
%User{role: :admin}
%{name: name}
# Directives and attributes
use GenServer
@env Application.get_env(_, _)
# Control flow
case _ do _ -> _ end
fn _ -> _ endCode 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.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
-
No function-name wildcards —
def _(_)won't match arbitrary names - Alias expansion is syntax-aware, not semantic — no macro expansion
- Multi-node patterns require contiguous statements
-
Replacement formatting uses
Macro.to_string/1— runmix formatafter