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.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)"
)

# 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}]
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(...)

# 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 _ -> _ end

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.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 — run mix format after

License

MIT