0.11.1

Added

  • piped() selector predicate — matches only when the selected node is a pipe expression (|>). Since ExAST normalizes pipes during matching, there was previously no way to distinguish piped from direct call forms. Use where(piped()) to match only piped calls, or where(not piped()) for direct calls only.

0.11.0

Added

0.10.1

Fixed

  • Restored pipe-aware pattern matching after the 0.10.0 candidate prefilter optimization. Piped calls such as data |> Enum.map(fun) now match direct call patterns like Enum.map(_, _) again.

0.10.0

Added

  • ExAST.Patcher.find_many/3 for running multiple named AST pattern checks in a single traversal where possible, returning matches tagged with :pattern.
  • ExAST.search_many/3 for searching files with multiple named patterns while preserving search/3 options such as :limit.

Changed

  • Optimized repeated single-node pattern matching by compiling patterns once, normalizing candidate nodes once per traversal, and using conservative call signature prefilters for common local, remote, piped, and nested call patterns.

0.9.1

Changed

  • Restructured documentation: short README with topic-based guides (Getting Started, Pattern Language, Querying, CLI Reference, Diff)

0.9.0

Added

  • Capture guardswhere/2 now accepts ^pin syntax to filter on captured values, similar to Ecto's parameter references:

    from("Enum.take(_, count)")
    |> where(match?({:-, _, [_]}, ^count))

    Supports match?/2 for structural checks, plain comparisons (^event == :click), multi-capture expressions (^left == ^right), and composition with existing structural predicates.

  • Source text in match resultsPatcher.find_all/3 now includes a :source field with the matched source snippet. nil for AST/zipper input.

  • Module attribute pattern matching — attribute names are now captureable:

    Patcher.find_all(source, "@name Application.get_env(_, _)")

    Previously @env Application.get_env(...) would not match a pattern with a different attribute name, even as a capture variable.

0.8.1

Fixed

  • CLI commands now handle closed stdout pipes cleanly, so commands like mix ex_ast.search ... | head no longer emit EPIPE writer crash messages.

0.8.0

Added

  • Comment predicates — queries can now filter by associated source comments with comment/1, comment_before/1, comment_after/1, comment_inside/1, and comment_inline/1. Comment matchers accept strings, regexes, and explicit text matchers like prefix/2, suffix/2, and text/2. CLI comment filters detect /.../ and ~r/.../ regex syntax.

0.7.0

Added

  • SQL-like query API via ExAST.Query: from/1, where/2, find/2, find_child/2, contains/1, inside/1, sibling predicates (follows/1, precedes/1, immediately_follows/1, immediately_precedes/1), positional predicates (first/0, last/0, nth/1), and boolean predicate combinators (any/1, all/1).
  • Selector alternatives — query starts can now accept a list of alternative patterns, e.g. from(["def _ do ... end", "defp _ do ... end"]).
  • Search limits and broad-query guardExAST.search/3 and mix ex_ast.search now support limit: n / --limit n and refuse unbounded from("_") searches unless allow_broad: true / --allow-broad is passed.
  • Query-style CLI flagsmix ex_ast.search and mix ex_ast.replace now support --contains, sibling filters (--follows, --precedes, --immediately-follows, --immediately-precedes), and position filters (--first, --last, --nth).

Fixed

  • Selector negation now works Ecto-style without import hackswhere(not ...) is rewritten by the selector DSL, so users no longer need import Kernel, except: [not: 1].
  • Search result rendering no longer fails when the current project formatter config references unavailable import_deps.
  • Selector descendant traversal now walks nested AST shapes reliably, fixing missed matches for remote calls nested inside assignments and control flow.
  • Alias-aware matching expands local alias directives so canonical remote-call patterns like AshPhoenix.Form.for_update(...) match alias-based call sites like Form.for_update(...).
  • Grouped alias handling now supports real-world forms such as alias Phoenix.Socket.{Broadcast, Message, Reply}.
  • Alias collection no longer misclassifies ordinary variables named alias as alias directives.

0.6.0

Added

  • CSS-like AST selectors — build relationship-aware selectors with ExAST.Selector:
    import ExAST.Selector
    
    pattern("defmodule _ do ... end")
    |> descendant("def _ do ... end")
    |> child("IO.inspect(_)")
  • Selector predicates — filter selected nodes with where/2 and Ecto-style not/1: parent/1, ancestor/1, has_child/1, has_descendant/1, and has/1.
  • CLI relationship filters for mix ex_ast.search and mix ex_ast.replace: --parent, --ancestor, --has-child, --has-descendant, --has, and corresponding --not-* flags.

0.5.0

Added

  • Ellipsis (...) — matches zero or more nodes in function args, lists, and block bodies: IO.inspect(...), foo(first, ..., last), def run(_) do ... end
  • ~p sigil — compile-time pattern parsing via import ExAST.Sigil

0.4.0

Added

  • Syntax-aware diffExAST.diff/3, ExAST.diff_files/3, ExAST.apply_diff/1
    • GumTree-inspired AST matching: functions matched by name/arity, nodes by kind/label/signature
    • Edit classification: :insert, :delete, :update, :move
    • Function reorder detection reported as :move edits
    • Child suppression: edits covered by a parent update/insert/delete are rolled up
    • Inline line-level diffs within updated nodes (Myers algorithm via List.myers_difference)
    • Patch application: ExAST.apply_diff/1 produces patched source from a diff result
  • mix ex_ast.diff CLI task
    • Colored output with -/+ markers (red/green, auto-detected)
    • --no-color, --no-moves, --summary, --json flags
    • Human-readable labels (def create/1 instead of {:def, :create, 1})
  • AST and zipper inputPatcher.find_all/3 and Patcher.replace_all/4 now accept source strings, Sourceror.Zipper, or raw AST as input. Source-string variants return strings, AST/zipper variants return AST.
  • Quoted expressions as patterns — patterns and replacements can be strings or quoted expressions:
    Patcher.find_all(source, quote(do: IO.inspect(_)))
    Patcher.replace_all(ast, quote(do: IO.inspect(expr)), quote(do: dbg(expr)))
    inside/not_inside options also accept quoted.
  • Ellipsis (...) — matches zero or more nodes in args, lists, and block bodies. IO.inspect(...) matches any arity, foo(first, ..., last) captures surrounding args, def run(_) do ... end matches any body.
  • ~p sigil — compile-time pattern parsing via import ExAST.Sigil: ~p"IO.inspect(...)" returns parsed AST with no runtime overhead.
  • ex_dna added to CI checks

0.3.0

  • Pipe awareness: data |> Enum.map(f) matches Enum.map(data, f)
  • Where conditions: --inside, --not-inside filters
  • Multi-node patterns: a = Repo.get!(_, _); Repo.delete(a)

0.2.0

  • Initial release with search and replace