mix ex_ast.search 'PATTERN' PATH [OPTIONS]

Search files for AST pattern matches. PATH can be a file, directory, or glob.

Options

FlagMeaning
-e, --pattern PATTERNAdd a pattern to a multi-pattern batch (repeatable). See Multiple patterns
--countPrint match count only
--count-by-filePrint per-file match counts, most matches first
--limit NStop after N matches
--allow-broadAllow patterns like _ that match everything
--expand-importsResolve import Mod to Mod's real exports, scoped per module, so map(a, b) matches Mod.map(_, _). Requires Mod to be loadable
--format json / --jsonPrint structured JSON output
--inside PATTERNOnly match inside ancestors matching pattern
--not-inside PATTERNReject matches inside ancestors matching pattern
--parent PATTERNDirect semantic parent matches pattern
--not-parent PATTERNDirect semantic parent does not match pattern
--ancestor PATTERNAny semantic ancestor matches pattern
--not-ancestor PATTERNNo ancestor matches pattern
--has-child PATTERNHas a direct child matching pattern
--not-has-child PATTERNNo direct child matches pattern
--contains PATTERNHas a descendant matching pattern
--not-contains PATTERNNo descendant matches pattern
--follows PATTERNPrevious sibling matches pattern
--not-follows PATTERNNo previous sibling matches pattern
--precedes PATTERNFollowing sibling matches pattern
--not-precedes PATTERNNo following sibling matches pattern
--immediately-follows PATTERNImmediately previous sibling matches pattern
--immediately-precedes PATTERNImmediately following sibling matches pattern
--firstFirst sibling in parent
--not-firstNot first sibling
--lastLast sibling in parent
--not-lastNot last sibling
--nth NNth sibling (1-based)
--not-nth NNot nth sibling
--comment TEXTAssociated comments contain TEXT
--not-comment TEXTNo associated comments contain TEXT
--comment-before TEXTComment immediately before contains TEXT
--comment-after TEXTComment immediately after contains TEXT
--comment-inside TEXTComment inside range contains TEXT
--comment-inline TEXTInline comment on start line contains TEXT

Comment values are substring matches. Use /.../ for regex:

mix ex_ast.search 'def _ do ... end' --comment-inside '/TODO|FIXME/'

Examples

# Find all IO.inspect calls
mix ex_ast.search 'IO.inspect(_)' lib/

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

# Only inside private functions
mix ex_ast.search --inside 'defp _ do _ end' 'Repo.get!(_, _)'

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

# Per-file match counts, most matches first
mix ex_ast.search 'IO.inspect(_)' lib/ --count-by-file

# Resolve a bare `import Enum` to its real exports
mix ex_ast.search 'Enum.map(_, _)' lib/ --expand-imports

Multiple patterns

Pass a repeatable -e / --pattern flag to search several patterns in one invocation. Each file is read and parsed once for the whole batch, avoiding BEAM startup and per-file re-parsing per pattern — useful for analyzers that run many checks over the same tree.

mix ex_ast.search -e 'IO.inspect(_)' -e 'dbg(_)' lib/

In this mode there is no positional pattern; remaining positional args are paths. Combining a positional pattern with -e is an error.

Per-pattern selector filters

Selector-scoping flags (--inside, --not-inside, --parent, --contains, etc.) are per-pattern: a filter binds to the most recent preceding -e, mirroring grep -e. Filters do not bleed across patterns.

mix ex_ast.search \
  -e 'App.Repo.get!(_, _)' --inside 'def handle_call(_, _, _) do _ end' \
  -e 'IO.inspect(_)' --not-inside 'test _ do _ end' \
  lib/ test/

Global flags (--count, --json, --expand-imports, --limit, --allow-broad, paths) apply to the whole batch.

Output

Each match is tagged by its pattern string:

[IO.inspect(_)] lib/foo.ex:12
  IO.inspect(result)

[dbg(_)] lib/bar.ex:88
  dbg(value)

2 pattern(s), 2 match(es)

--count prints a per-pattern tally plus a total; --json includes the pattern field on each match. Duplicate -e patterns raise an error, and --count-by-file is not supported with -e.

Multi-pattern search uses one shared path list for all patterns, so per-pattern path include/exclude is not expressible in a single call — group patterns by shared path scope into separate invocations. Per-pattern selector filters do work, since they live in each pattern's selector.

Replace

mix ex_ast.replace 'PATTERN' 'REPLACEMENT' PATH [OPTIONS]

Replace AST pattern matches in files. Captures from the pattern are substituted into the replacement by name.

Options

Same relationship filters as search. Additional:

FlagMeaning
--dry-runPreview changes without writing files
--format json / --jsonPrint structured JSON summary
--format-outputRun the Elixir formatter on modified files

Examples

# Remove debug calls
mix ex_ast.replace 'dbg(expr)' 'expr' lib/

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

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

# Preview as JSON
mix ex_ast.replace --dry-run --format json 'dbg(expr)' 'expr' lib/

# Format changed files
mix ex_ast.replace --format-output 'dbg(expr)' 'expr' lib/

# Only outside tests
mix ex_ast.replace --not-inside 'test _ do _ end' 'IO.inspect(expr)' 'expr' lib/

Diff

mix ex_ast.diff FILE1 FILE2 [OPTIONS]

Syntax-aware diff between two Elixir files.

Options

FlagMeaning
--summaryPrint summary lines only
--no-movesDisable move detection
--no-colorDisable colored output
--json / --format jsonPrint edits as JSON

Example output

lib/old.ex  lib/new.ex

L2 UPDATE updated function def first/0
  - def first, do: 1
  + def first, do: 10

L5 INSERT inserted function def fourth/0
  + def fourth, do: 4

2 edit(s)