Skip to content

Support ... in map and struct patterns#7

Open
rodrigues wants to merge 1 commit into
elixir-vibe:masterfrom
rodrigues:vr/fix_map
Open

Support ... in map and struct patterns#7
rodrigues wants to merge 1 commit into
elixir-vibe:masterfrom
rodrigues:vr/fix_map

Conversation

@rodrigues

@rodrigues rodrigues commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

A literal ... in a map or struct pattern crashed — match_subset/3 had no clause for the ... node — so %{...}, %{..., k: v} and %Struct{...} all raised FunctionClauseError.

The candidate filter already recognized these as any-arity maps/structs, so the feature was half-wired: patterns passed the filter and then blew up in subset matching.

What you can search for now with mix ex_ast.search:

Pattern Matches
%{role: :admin} maps with exactly that key set
%{..., role: :admin} any map with at least role: :admin
%{..., status: s} any map with a status key, capturing its value
%{...} any map, including empty
%Struct{...} any struct of that type

Plain map patterns (%{k: v}) are unchanged — still an exact key set. Structs already matched by subset of fields; %Struct{...} just drops the field constraint entirely.

Before

$ mix ex_ast.search "%{...}" lib/
** (FunctionClauseError) no function clause matching in ExAST.Pattern.match_subset/3

After

$ mix ex_ast.search "%{...}" lib/
%{admin: grant(:admin), user: :ok}
1 match(es)
$ mix ex_ast.search "%{..., start: _, end: _}" lib/
lib/ex_ast.ex:337
  %{start: start, end: end_}

lib/ex_ast/comments.ex:43
  %{start: start, end: end_}

lib/ex_ast/diff/patcher.ex:47
  %{start: point, end: point}

lib/ex_ast/diff/patcher.ex:106
  %{start: start_pos, end: end_pos}

lib/ex_ast/diff/patcher.ex:128
  %{start: s, end: e}

lib/ex_ast/diff/patcher.ex:129
  %{start: [line: s[:line], column: 1], end: [line: e[:line] + 1, column: 1]}

lib/ex_ast/json.ex:2
  {start: start, end: end_}

lib/ex_ast/json.ex:3
  %{start: position(start), end: position(end_)}

lib/ex_ast/patcher.ex:518
  {
          start: Sourceror.get_range(first).start,
          end: Sourceror.get_range(last).end
        }

lib/ex_ast/patcher.ex:768
  %{start: start, end: end_}

lib/ex_ast/patcher.ex:1029
  %{start: start, end: end_}


11 match(es)

`%{...}`, `%{..., k: v}` and `%Struct{...}` raised FunctionClauseError:
match_subset/3 had no clause for the `...` entry. Skip `...` during subset
matching so it imposes no key constraint.

The candidate filter already treated a `...` map/struct as any-arity, so
this completes the feature end to end: `...` matches any map/struct
(including empty), and `%{..., k: v}` matches any map with at least that
entry. Plain `%{k: v}` is unchanged — still an exact key set.

@dannote dannote left a comment

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.

The crash fix looks good, but this also codifies an existing inconsistency in map matching.

describe "map and struct subset matching via search" do
test "a plain map pattern matches only the exact key set" do
src = "one = %{a: 1}\ntwo = %{a: 1, b: 2}\n"
assert [%{}] = ExAST.Patcher.find_all(src, "%{a: 1}")

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.

This test locks in behavior that doesn't match the rest of the matcher. Maps are documented as partial, and nested maps already behave that way:

Patcher.find_all("x = %{a: 1, b: 2}", "_ = %{a: 1}")
# one match

The top-level pattern only looks exact because candidate filtering checks the number of entries. We should fix that inconsistency rather than document it as the intended behavior. The ellipsis crash fix itself looks good.

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