mix ex_ast.search (ExAST v0.13.0)

Copy Markdown View Source

Searches for AST patterns in Elixir source files.

Usage

mix ex_ast.search 'IO.inspect(_)' [path ...]

Options

  • -e, --pattern — add a pattern to a multi-pattern batch (repeatable)
  • --count — only print the number of matches
  • --count-by-file — print per-file match counts, most matches first
  • --limit n — stop after returning this many matches
  • --allow-broad — allow unbounded broad searches like _
  • --expand-imports — resolve bare import Mod (and import Mod, except: [...]) to the module's real exports, so map(a, b) matches Mod.map(_, _). Requires Mod to be compiled and loadable.
  • --inside 'pattern' — only match inside ancestors matching this pattern
  • --not-inside 'pattern' — reject matches inside ancestors matching this pattern
  • --parent 'pattern' / --not-parent 'pattern' — filter by direct semantic parent
  • --ancestor 'pattern' / --not-ancestor 'pattern' — filter by semantic ancestor
  • --has-child 'pattern' / --not-has-child 'pattern' — filter by direct semantic child
  • --contains 'pattern' / --not-contains 'pattern' — filter by semantic descendant
  • --has-descendant 'pattern' / --not-has-descendant 'pattern' — aliases for contains filters
  • --has 'pattern' / --not-has 'pattern' — aliases for contains filters
  • --follows 'pattern' / --not-follows 'pattern' — filter by earlier sibling
  • --precedes 'pattern' / --not-precedes 'pattern' — filter by later sibling
  • --immediately-follows 'pattern' / --not-immediately-follows 'pattern' — filter by previous sibling
  • --immediately-precedes 'pattern' / --not-immediately-precedes 'pattern' — filter by next sibling
  • --first / --not-first, --last / --not-last, --nth n / --not-nth n — filter by sibling position
  • --comment text / --not-comment text — filter by associated comments
  • --comment-before text, --comment-after text, --comment-inside text, --comment-inline text — filter by comment location

Comment values are substrings by default. Use /.../ or ~r/.../ for regexes, including flags like /todo/i.

Pattern syntax

Patterns are valid Elixir expressions:

  • Variables (name, expr) — capture any node
  • _ or _name — wildcard (match, don't capture)
  • Structs/maps — partial match (only listed keys must be present)
  • Pipes are normalized — data |> Enum.map(f) matches Enum.map(data, f)
  • Everything else — literal match

Multiple patterns in one run

Pass a repeatable -e / --pattern flag to search several patterns in a single 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.

Each pattern is tagged in the output by its raw pattern string. Two -e with the same pattern string would collide, so duplicate patterns raise an error.

Per-pattern path scope not expressible

Multi-pattern search uses one shared path list for all patterns, so per-pattern path include/exclude cannot be expressed in a single call — group patterns by shared path scope into separate invocations. Per-pattern selector filters (above) do work, because they live in each pattern's selector.

Examples

mix ex_ast.search 'IO.inspect(_)'
mix ex_ast.search '%Step{id: "subject"}' lib/documents/
mix ex_ast.search '{:error, reason}' lib/ test/
mix ex_ast.search --count 'dbg(_)'
mix ex_ast.search --inside 'def handle_call(_, _, _) do _ end' 'Repo.get!(_)'
mix ex_ast.search --not-inside 'test _ do _ end' 'IO.inspect(_)'
mix ex_ast.search 'IO.inspect(_)' --parent 'def _ do ... end'
mix ex_ast.search 'def name do ... end' --contains 'Repo.transaction(_)' --not-contains 'IO.inspect(...)'
mix ex_ast.search 'Repo.delete(record)' --follows 'record = Repo.get!(_, _)'
mix ex_ast.search 'def name do ... end' --comment-inside TODO
mix ex_ast.search 'def name do ... end' --comment-inside '/TODO|FIXME/'
mix ex_ast.search '_' lib/ --limit 100
mix ex_ast.search 'Enum.map(_, _)' lib/ --expand-imports
mix ex_ast.search -e 'IO.inspect(_)' -e 'dbg(_)' lib/
mix ex_ast.search -e 'App.Repo.get!(_, _)' --inside 'def _ do ... end' -e 'dbg(_)' lib/