ExAST (ExAST v0.11.0)

Copy Markdown View Source

Search, replace, and diff Elixir code by AST pattern.

Patterns are valid Elixir syntax:

  • Variables (name, expr) capture matched nodes
  • _ and _name are wildcards
  • Structs/maps match partially
  • Pipes are normalized (data |> Enum.map(f) matches Enum.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

diff_result()

@type diff_result() :: ExAST.Diff.Result.t()

match()

@type match() :: %{
  file: String.t(),
  line: pos_integer(),
  source: String.t(),
  captures: ExAST.Pattern.captures()
}

named_pattern()

@type named_pattern() :: ExAST.Patcher.named_pattern()

pattern_name()

@type pattern_name() :: ExAST.Patcher.pattern_name()

tagged_match()

@type tagged_match() :: %{:pattern => pattern_name(), optional(atom()) => term()}

Functions

apply_diff(result)

@spec apply_diff(diff_result()) :: String.t()

Applies a diff result to produce the patched source.

diff(left_source, right_source, opts \\ [])

@spec diff(String.t(), String.t(), keyword()) :: diff_result()

Computes a syntax-aware diff between two Elixir source strings.

diff_files(left_path, right_path, opts \\ [])

@spec diff_files(String.t(), String.t(), keyword()) :: diff_result()

Computes a syntax-aware diff between two Elixir files.

replace(paths, pattern, replacement, opts \\ [])

@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.

search(paths, pattern, opts \\ [])

@spec search(String.t() | [String.t()], String.t() | ExAST.Selector.t(), keyword()) ::
  [match()]

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 like from("_")

search_many(paths, patterns, opts \\ [])

@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.