Search, replace, and diff Elixir code by AST pattern
Find a file
2026-06-11 12:20:54 +03:00
.github/workflows Use shared Elixir CI workflow 2026-06-06 21:48:41 +03:00
guides Prepare v0.12.0 release 2026-05-15 11:58:42 +03:00
lib Support Elixir 1.20 type checks 2026-06-06 16:59:40 +03:00
test Add JSON output and rewrite planning 2026-05-14 23:45:16 +03:00
tools/reach_runner Add JSON output and rewrite planning 2026-05-14 23:45:16 +03:00
.credo.exs Add JSON output and rewrite planning 2026-05-14 23:45:16 +03:00
.dialyzer_ignore.exs Syntax-aware diff engine with CLI, patch application 2026-04-20 14:51:47 +03:00
.formatter.exs Initial implementation: AST pattern search and replace 2026-03-07 12:11:04 +03:00
.gitignore Add JSON output and rewrite planning 2026-05-14 23:45:16 +03:00
.tool-versions Support Elixir 1.20 type checks 2026-06-06 16:59:40 +03:00
AGENTS.md Move project to Elixir Vibe 2026-04-30 11:13:22 +03:00
CHANGELOG.md Document compiled pattern APIs 2026-05-15 12:12:31 +03:00
LICENSE Fix author name in LICENSE 2026-03-07 22:47:29 +03:00
mix.exs Support Elixir 1.20 type checks 2026-06-06 16:59:40 +03:00
mix.lock Add JSON output and rewrite planning 2026-05-14 23:45:16 +03:00
README.md README: document capture forms; add ecosystem footer 2026-06-11 12:20:54 +03:00

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. ... captures variable arity, ^name matches a literal variable name, name/fun/function can capture function names in definitions, and fun/function can capture call names. 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)"
)

# 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.12", only: [:dev, :test], runtime: false}]
end

Documentation

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 and function-name captures
def handle_call(msg, _, state) do _ end
def name(_, _) do ... end      # captures the definition name
Repo.fun(changeset)            # captures remote call names like insert/update
fun(changeset)                 # captures local call names

# 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}
%User{role: :admin}
%{name: name}

# 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

  • Alias/import expansion is syntax-aware, not full semantic macro expansion
  • Function-name placeholders are intentionally limited: definitions use name, fun, or function; call heads use fun or function
  • Multi-node patterns require contiguous statements
  • Replacement formatting uses Macro.to_string/1; pass format: true or run mix format after

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