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 bareimport Mod(andimport Mod, except: [...]) to the module's real exports, somap(a, b)matchesMod.map(_, _). RequiresModto 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)matchesEnum.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/