Installation
Add ExAST as a dev dependency:
def deps do
[{:ex_ast, "~> 0.12", only: [:dev, :test], runtime: false}]
endFirst search
Find every IO.inspect call in your project:
mix ex_ast.search 'IO.inspect(_)' lib/
Patterns are plain Elixir syntax — _ is a wildcard, variables capture:
# Any arity
mix ex_ast.search 'IO.inspect(...)' lib/
# Capture the argument
mix ex_ast.search 'IO.inspect(expr)' lib/
# Specific two-arg calls
mix ex_ast.search 'IO.inspect(_, _)' lib/
First replace
Remove all dbg calls — the captured expr substitutes into the replacement:
mix ex_ast.replace 'dbg(expr)' 'expr' lib/
Preview without writing files:
mix ex_ast.replace --dry-run 'use Mix.Config' 'import Config' lib/
Emit structured JSON for tools and agents:
mix ex_ast.search 'IO.inspect(expr)' lib/ --format json
mix ex_ast.replace --dry-run 'dbg(expr)' 'expr' lib/ --format json
Programmatic API
Everything the CLI does is available as functions:
# Search files
ExAST.search("lib/", "IO.inspect(_)")
#=> [%{file: "lib/worker.ex", line: 12, source: "IO.inspect(data)", captures: %{}}]
# Replace in files
ExAST.replace("lib/", "dbg(expr)", "expr")
#=> [{"lib/worker.ex", 2}]
# Plan replacements without applying them
plan = ExAST.rewrite_plan(source_code, "dbg(expr)", "expr")
#=> %ExAST.Rewriter.Plan{replacements: [...], conflicts: []}
# Low-level: work with source strings
ExAST.Patcher.find_all(source_code, "IO.inspect(_)")
#=> [%{node: ..., range: ..., captures: %{}, source: "IO.inspect(data)"}]
ExAST.Patcher.replace_all(source_code, "dbg(expr)", "expr")
#=> "source with dbg calls removed"For large project searches, ExAST uses conservative text prefilters and scans
files in parallel by default. Pass concurrency: n to tune file-level search
parallelism.
What's next
- Pattern Language — full syntax reference
- 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