Skip to content

feat: add Igniter.Code.Pattern for ExAST-powered pattern matching#375

Merged
zachdaniel merged 3 commits intoash-project:mainfrom
dannote:feature/ex-ast-pattern
Apr 21, 2026
Merged

feat: add Igniter.Code.Pattern for ExAST-powered pattern matching#375
zachdaniel merged 3 commits intoash-project:mainfrom
dannote:feature/ex-ast-pattern

Conversation

@dannote
Copy link
Copy Markdown
Contributor

@dannote dannote commented Apr 20, 2026

Adds Igniter.Code.Pattern — pattern-based AST navigation and rewriting powered by ExAST as an optional dependency.

Motivation

Writing zipper predicates for code search/replace is verbose:

Igniter.Code.Common.move_to(zipper, fn zipper ->
  Igniter.Code.Function.function_call?(zipper, :use, 2) &&
    Igniter.Code.Function.argument_equals?(zipper, 0, GenServer)
end)

With Igniter.Code.Pattern:

Pattern.move_to(zipper, "use GenServer")

Particularly useful for upgrade tasks and broad API migrations.

API

Zipper-level (follows existing Igniter.Code.* conventions):

  • matches?(zipper, pattern) — predicate, for use in Common.move_to callbacks
  • move_to(zipper, pattern, opts){:ok, zipper} | :error
  • find_all(zipper, pattern, opts)[zipper]
  • replace(zipper, pattern, replacement, opts){:ok, zipper} | :error
  • replace_all(zipper, pattern, replacement, opts){:ok, zipper} | :error

Project-level:

  • replace_in_file(igniter, path, pattern, replacement, opts)Igniter.t()
  • replace_in_all_files(igniter, pattern, replacement, opts)Igniter.t()

All accept :inside / :not_inside options for ancestor context filtering.

Pattern syntax

Patterns are valid Elixir — strings, quoted expressions, or ~p sigil:

Syntax Meaning
_ or _name Wildcard — matches anything, not captured
expr, name Variable — matches anything, captured by name
... Ellipsis — matches zero or more nodes
Everything else Literal — must match exactly
# String patterns
Pattern.move_to(zipper, "use GenServer")
Pattern.find_all(zipper, "Repo.get!(...)")
Pattern.replace_all(zipper, "Logger.debug(msg, ...)", "Logger.warning(msg, ...)")

# Quoted patterns
Pattern.matches?(zipper, quote(do: Enum.map(_, _)))

# ~p sigil — compile-time parsing, no runtime overhead
import Igniter.Code.Pattern
Pattern.find_all(zipper, ~p"Repo.get!(...)")
Pattern.replace_all(zipper, ~p"Enum.map(list, f)", ~p"Enum.flat_map(list, f)")

Dependency

ExAST is optional — all functions gracefully return :error or [] when not available:

{:ex_ast, "~> 0.5", optional: true}

@zachdaniel
Copy link
Copy Markdown
Contributor

Lets just make it a required dependency 😄 Easier to wrangle since igniter itself is typically an optional dependency used only in dev. This is awesome! ❤️

Comment thread lib/igniter/code/pattern.ex Outdated
end

defp elixir_source?(source) do
path = Rewrite.Source.get(source, :path)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe there is a better way to check the source. There is like a source type or a source implementation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, fixed

@zachdaniel
Copy link
Copy Markdown
Contributor

Okay, only one last small comment, just a nitpick really.

@zachdaniel zachdaniel merged commit 0e433e6 into ash-project:main Apr 21, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants