Skip to content

dannote/ex_ast

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ExAST 🔬

Search and replace Elixir code by AST pattern.

Patterns are plain Elixir — variables capture, _ is a wildcard, structs match partially. No regex, no custom DSL.

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

Installation

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

Pattern syntax

Patterns are valid Elixir expressions parsed by Code.string_to_quoted!/1. Three rules:

Syntax Meaning
_ or _name Wildcard — matches any node, not captured
name, expr, x Capture — matches any node, bound by name
Everything else Literal — must match exactly

Structs and maps match partially — only the keys you write must be present. %User{role: r} matches any User with a role field, regardless of other fields.

Repeated variable names require the same value at every position: Enum.map(a, a) only matches calls where both arguments are identical.

Examples

Search

# Find all IO.inspect calls (any arity)
mix ex_ast.search 'IO.inspect(_)'
mix ex_ast.search 'IO.inspect(_, _)'

# Find structs by field value
mix ex_ast.search '%Step{id: "subject"}' lib/documents/

# Find {:error, _} tuples
mix ex_ast.search '{:error, _}' lib/ test/

# Find GenServer callbacks
mix ex_ast.search 'def handle_call(_, _, _) do _ end'

# Count matches
mix ex_ast.search --count 'dbg(_)'

Replace

# Remove debug calls
mix ex_ast.replace 'dbg(expr)' 'expr'
mix ex_ast.replace 'IO.inspect(expr, _)' 'expr'

# Replace struct with function call
mix ex_ast.replace '%Step{id: "subject"}' 'SharedSteps.subject_step(@opts)' lib/types/

# Migrate API
mix ex_ast.replace 'Repo.get!(mod, id)' 'Repo.get!(mod, id) || raise NotFoundError'

# Preview without writing
mix ex_ast.replace --dry-run 'use Mix.Config' 'import Config'

Captures from the pattern are substituted into the replacement by name.

Programmatic API

# Search
ExAST.search("lib/", "IO.inspect(_)")
#=> [%{file: "lib/worker.ex", line: 12, source: "IO.inspect(data)", captures: %{}}]

# Replace
ExAST.replace("lib/", "dbg(expr)", "expr")
#=> [{"lib/worker.ex", 2}]

# Low-level: single string
ExAST.Patcher.find_all(source_code, "IO.inspect(_)")
ExAST.Patcher.replace_all(source_code, "dbg(expr)", "expr")

What you can match

# Function calls
Enum.map(_, _)
Logger.info(_)
Repo.all(_)

# Definitions
def handle_call(msg, _, state) do _ end
def mount(_, _, _) do _ end

# Pipes
_ |> Repo.all()
_ |> Enum.map(_) |> Enum.filter(_)

# Tuples
{:ok, result}
{:error, reason}
{:noreply, state}

# Structs (partial match)
%User{role: :admin}
%Changeset{valid?: false}

# Maps (partial match)
%{name: name}

# Directives
use GenServer
import Ecto.Query
alias MyApp.Accounts.User

# Module attributes
@impl true
@behaviour mod

# Ecto
from(_ in _, _)
cast(_, _)
validate_required(_)

# Control flow
case _ do _ -> _ end
with {:ok, _} <- _ do _ end
fn _ -> _ end
&_/1

# Misc
raise _
dbg(_)

Limitations

  • No function-name wildcardsdef _(_) do _ end won't match arbitrary function names because _ in that position parses as a call, not a wildcard. Use the actual name or match the do block.
  • Exact list length[a, b] only matches two-element lists. No rest/splat syntax.
  • No multi-expression wildcards — can't match "any number of statements" inside a do block.
  • Replacement formatting — the replacement string is rendered by Macro.to_string/1. Run mix format afterward for consistent style.

How it works

  1. Source files are parsed with Sourceror, preserving source positions and comments
  2. The pattern string is parsed with Code.string_to_quoted!/1
  3. Both ASTs are normalized (metadata stripped, __block__ wrappers removed)
  4. The pattern is matched against every node via depth-first traversal using Sourceror.Zipper
  5. Variables in the pattern bind to the matched subtrees (captures)
  6. For replacements, captures are substituted into the replacement template and the result is patched into the original source using Sourceror.patch_string/2, preserving formatting of unchanged code

License

MIT

About

Search and replace Elixir code by AST pattern

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages