-
-
Notifications
You must be signed in to change notification settings - Fork 939
Description
Environment Information
- JRuby 10.0.4.0 (Ruby 3.4.5 compat)
- macOS arm64-darwin / also reproduced on Linux x86_64 in GitHub Actions CI
- No special flags, gems, or frameworks required — reproducible with a standalone script
- Requires # frozen_string_literal: true pragma (or --enable-frozen-string-literal)
Expected Behavior
When a module is included in a Data.define block and the resulting class is subclassed, field accessors on subclass instances should return the values they were initialized with — the same as CRuby.
Given the script jruby_data_include_bug.rb:
#!/usr/bin/env ruby
# frozen_string_literal: true
def check(obj, expected)
expected.each { |k, v| puts " #{k}=#{obj.public_send(k).inspect}" }
end
mod = Module.new { def greet = "hello" }
base = Data.define(:x, :y) { include mod }
sub = Class.new(base)
check(sub.new(x: 1, y: 2), {x: 1, y: 2})
with CRuby:
$ ruby jruby_data_include_bug.rb
x=1
y=2
On JRuby 10.0.4.0, the accessors return nil:
$ jruby jruby_data_include_bug.rb
x=nil
y=nil
The bug requires all of the following conditions to be present simultaneously:
- A module is included inside the Data.define block
- The resulting Data class is subclassed
- The subclass instance is constructed inline as a method argument alongside a hash with the same keys as the Data members
- public_send is called inside the method with keys from that hash
Removing any one condition makes the bug disappear:
# frozen_string_literal: true
def check(obj, expected)
expected.each { |k, v| puts " #{k}=#{obj.public_send(k).inspect}" }
end
mod = Module.new { def greet = "hello" }
base = Data.define(:x, :y) { include mod }
sub = Class.new(base)
# FAILS: all four conditions present
check(sub.new(x: 1, y: 2), {x: 1, y: 2})
# => x=nil, y=nil
# PASSES: no include
base2 = Data.define(:x, :y)
sub2 = Class.new(base2)
check(sub2.new(x: 1, y: 2), {x: 1, y: 2})
# => x=1, y=2
# PASSES: no subclass
check(base.new(x: 1, y: 2), {x: 1, y: 2})
# => x=1, y=2
# PASSES: pre-assigned to variable (not inline)
obj = sub.new(x: 1, y: 2)
puts " x=#{obj.x.inspect}" # => x=1
This appears to be a kwargs dispatch interaction — the hash {x: 1, y: 2} passed as a positional argument interferes with the Data constructor's x: 1, y: 2 keyword arguments when public_send is later called with the same
symbol keys inside the method body.
This causes real-world breakage in any codebase that uses the pattern class Config < Data.define(...) { include SomeMixin } — which is a common pattern for adding shared behavior to Data value classes.