Skip to content

Commit a00e7d7

Browse files
committed
[fix] MatchData#deconstruct_keys: include non-participating captures
JRuby skipped named capture groups that did not participate in the match (value is nil). With nil argument, such keys were omitted entirely. With explicit keys, iteration stopped at the first non-participating capture. CRuby includes non-participating captures with nil values in all cases. This is important for pattern matching where the absence of a key means "does not match" while nil means "matched nothing".
1 parent 4fd8090 commit a00e7d7

2 files changed

Lines changed: 34 additions & 3 deletions

File tree

core/src/main/java/org/jruby/RubyMatchData.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -951,7 +951,7 @@ public IRubyObject deconstruct_keys(ThreadContext context, IRubyObject what) {
951951

952952
for (int b : entry.getBackRefs()) {
953953
IRubyObject value = RubyRegexp.nth_match(context, b, this);
954-
if (value.isTrue()) hash.op_aset(context, key, value);
954+
hash.op_aset(context, key, value);
955955
}
956956
});
957957
} else if (what instanceof RubyArray arr) {
@@ -967,8 +967,6 @@ public IRubyObject deconstruct_keys(ThreadContext context, IRubyObject what) {
967967
if (index == -1) break;
968968

969969
IRubyObject value = RubyRegexp.nth_match(context, index, this);
970-
if (!value.isTrue()) break;
971-
972970
hash.op_aset(context, requestedKey, value);
973971
}
974972
} else {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
require 'rspec'
2+
3+
# Regression test for MatchData#deconstruct_keys with non-participating
4+
# named captures.
5+
#
6+
# When a named capture group doesn't participate in the match (e.g.
7+
# an optional group like (?<b>world)?), deconstruct_keys should include
8+
# the key with a nil value, not omit it entirely.
9+
10+
describe "MatchData#deconstruct_keys with non-participating captures" do
11+
let(:match) { "hello".match(/(?<a>hello)(?<b>world)?/) }
12+
13+
it "includes non-participating captures as nil for nil argument" do
14+
result = match.deconstruct_keys(nil)
15+
expect(result).to eq({ a: "hello", b: nil })
16+
end
17+
18+
it "includes non-participating captures as nil for explicit keys" do
19+
result = match.deconstruct_keys([:a, :b])
20+
expect(result).to eq({ a: "hello", b: nil })
21+
end
22+
23+
it "does not stop iterating at non-participating captures" do
24+
m = "hello!".match(/(?<a>hello)(?<b>world)?(?<c>!)/)
25+
result = m.deconstruct_keys([:b, :c, :a])
26+
expect(result).to eq({ b: nil, c: "!", a: "hello" })
27+
end
28+
29+
it "preserves key order from argument" do
30+
result = match.deconstruct_keys([:b, :a])
31+
expect(result.keys).to eq([:b, :a])
32+
end
33+
end

0 commit comments

Comments
 (0)