Search, replace, and diff Elixir code by AST pattern.
Patterns are valid Elixir syntax:
- Variables (
name,expr) capture matched nodes _and_nameare wildcards- Structs/maps match partially
- Pipes are normalized (
data |> Enum.map(f)matchesEnum.map(data, f)) - Everything else matches literally
- CSS-like selectors can be built with
ExAST.Selector
Options
:inside— only match nodes inside an ancestor matching this pattern:not_inside— reject nodes inside an ancestor matching this pattern
Examples
# Find all IO.inspect calls
ExAST.search("lib/**/*.ex", "IO.inspect(_)")
# Find IO.inspect only inside test blocks
ExAST.search("test/", "IO.inspect(_)", inside: "test _ do _ end")
# Replace dbg with the expression itself
ExAST.replace("lib/**/*.ex", "dbg(expr)", "expr")
# Match piped and direct calls interchangeably
ExAST.search("lib/", "Enum.map(_, _)") # also finds `data |> Enum.map(f)`
# Relationship-aware queries
import ExAST.Query
query =
from("def _ do ... end")
|> where(contains("Repo.transaction(_)"))
|> where(not contains("IO.inspect(...)"))
ExAST.search("lib/", query)
# Capture guards — filter on captured values with ^pin
import ExAST.Query
query =
from("Enum.take(_, count)")
|> where(match?({:-, _, [_]}, ^count))
ExAST.search("lib/", query)
# Syntax-aware diff
result = ExAST.diff(old_source, new_source)
result.edits #=> [%ExAST.Diff.Edit{op: :update, kind: :function, ...}]
ExAST.diff_files("lib/old.ex", "lib/new.ex")
Summary
Functions
Applies a diff result to produce the patched source.
Computes a syntax-aware diff between two Elixir source strings.
Computes a syntax-aware diff between two Elixir files.
Replaces AST pattern matches in files.
Searches files for AST pattern matches.
Searches files for multiple named AST patterns.
Types
@type diff_result() :: ExAST.Diff.Result.t()
@type match() :: %{ file: String.t(), line: pos_integer(), source: String.t(), captures: ExAST.Pattern.captures() }
@type named_pattern() :: ExAST.Patcher.named_pattern()
@type pattern_name() :: ExAST.Patcher.pattern_name()
@type tagged_match() :: %{:pattern => pattern_name(), optional(atom()) => term()}
Functions
@spec apply_diff(diff_result()) :: String.t()
Applies a diff result to produce the patched source.
@spec diff(String.t(), String.t(), keyword()) :: diff_result()
Computes a syntax-aware diff between two Elixir source strings.
@spec diff_files(String.t(), String.t(), keyword()) :: diff_result()
Computes a syntax-aware diff between two Elixir files.
@spec replace( String.t() | [String.t()], String.t() | ExAST.Selector.t(), String.t(), keyword() ) :: [ {String.t(), pos_integer()} ]
Replaces AST pattern matches in files.
Options:
:dry_run— return changes without writing (default:false):inside— only replace inside ancestors matching this pattern:not_inside— skip replacements inside ancestors matching this pattern
Returns a list of {file, count} tuples for modified files.
Searches files for AST pattern matches.
Returns a list of match maps with :file, :line, :source, and :captures.
Accepts :inside and :not_inside options to filter by context.
Options:
:limit— stop after returning this many matches:allow_broad— allow unbounded broad searches likefrom("_")
@spec search_many(String.t() | [String.t()], [named_pattern()] | map(), keyword()) :: [tagged_match()]
Searches files for multiple named AST patterns.
patterns may be a keyword list or a map. Returned matches include a
:pattern field with the matching pattern name. This is more efficient than
calling search/3 repeatedly for analyzers that run many checks over the same
files.
Options are the same as search/3.