Skip to content

Support wildcard callees in call patterns - #11

Merged
dannote merged 2 commits into
elixir-vibe:masterfrom
rodrigues:vr/wildcard
Jul 22, 2026
Merged

Support wildcard callees in call patterns#11
dannote merged 2 commits into
elixir-vibe:masterfrom
rodrigues:vr/wildcard

Conversation

@rodrigues

Copy link
Copy Markdown
Contributor

With this PR _ can stand in for the name of the function being called — the one spot where it didn't already work.

_ already matched arguments, remote-call modules, and definition names; this fills the gap:

Pattern Matches
_(...) any local (unqualified) call
_._(...) any remote (qualified) call
_.section(...) a call to section on any module
Foo._(...) any call on Foo

_(...) means "a function call," not "any node": special forms, operators, and definitions (def, case, %{}, fn, with, …) are excluded, so it won't match map literals, definition heads, or control-flow forms. Remote calls are excluded too — use _._(...) for those.

Before

_ in callee position matched nothing:

$ mix ex_ast.search --count "_(...)" lib
0
$ mix ex_ast.search --count "_._(...)" lib
0
$ mix ex_ast.search "Enum._(...)" --count
0
$ mix ex_ast.search "_(_, _, ...)" --count
0

After

$ mix ex_ast.search "_(...)" lib/ex_ast/cli/json.ex
lib/ex_ast/cli/json.ex:24
  normalize_value(key, item)

lib/ex_ast/cli/json.ex:27
  normalize(value)



22 match(es)
$ mix ex_ast.search --count "_(...)" lib
3210
$ mix ex_ast.search --count "_._(...)" lib
1829
$ mix ex_ast.search "Enum._(...)" --count
407
$ mix ex_ast.search "_(_, _, ...)" --count
1241

Pin the function name and leave the module open with _.name(...):

$ mix ex_ast.search "_.encode!(...)" lib
lib/ex_ast/cli/json.ex:7
  value
      |> normalize()
      |> Jason.encode!(pretty: true)

lib/ex_ast/cli/json.ex:9
  Jason.encode!(pretty: true)

2 match(es)

Comment thread lib/ex_ast/pattern.ex
# function calls, so `_(...)` does not treat them as calls. Symbolic heads
# (`%{}`, `{}`, `.`, operators, `@`, ...) are already excluded by
# `regular_identifier?/1`, so only identifier-shaped forms need listing.
@non_call_heads ExAST.Symbols.definition_forms() ++

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

and, or, not, and in all pass regular_identifier?/1, so _(...) matches them as calls:

Patcher.find_all("left and right", "_(...)")
# one match

That contradicts the operator exclusion described above. Could you filter word operators too and add coverage for them?

`_(...)` matches any local call and `_._(...)` any remote call, filling the
one gap where `_` couldn't be used — the name of the function being called.
`_` already matched arguments, remote-call modules, and definition names.

Special forms, operators, and definitions are excluded so `_(...)` means a
function call, not any node. The exclusion list reuses
`ExAST.Symbols.definition_forms/0` and omits symbolic heads already handled
by identifier checking.
`and`, `or`, `not`, and `in` parse to identifier-shaped heads that pass
`regular_identifier?/1`, so `_(...)` was matching them as local calls —
contradicting the operator exclusion the surrounding comment describes.

Collect the identifier-shaped operators (`and`, `or`, `not`, `in`, `when`)
into `@word_operators` and prepend them to `@non_call_heads` so
`real_call_name?/1` rejects them. Symbolic operators (`&&`, `++`, `^^^`, …)
are still excluded by `regular_identifier?/1`.
@rodrigues
rodrigues requested a review from dannote July 21, 2026 17:32
@dannote
dannote merged commit bbbea4e into elixir-vibe:master Jul 22, 2026
2 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