Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RecursiveOpenStruct#freeze does not play nice with Ruby 3.1 #74

Closed
Richard-Degenne opened this issue Mar 2, 2023 · 0 comments · Fixed by #75
Closed

RecursiveOpenStruct#freeze does not play nice with Ruby 3.1 #74

Richard-Degenne opened this issue Mar 2, 2023 · 0 comments · Fixed by #75

Comments

@Richard-Degenne
Copy link
Contributor

Summary

Freezing an instance of RecursiveOpenStruct in Ruby 3.1 prevents all access to the object, even read access.

Reproducing the issue

o = RecursiveOpenStruct.new({ a: 42 })
o.freeze

puts o.a

Expected behavior

# Ruby 2.6
42

# Ruby 3.1
42

Actual behavior

# Ruby 2.6
42

# Ruby 3.1
.bundle/gems/recursive-open-struct-1.1.3/lib/recursive_open_struct.rb:142:in `define_method': can't modify frozen object: #<RecursiveOpenStruct a=42> (FrozenError)
	from .bundle/gems/recursive-open-struct-1.1.3/lib/recursive_open_struct.rb:142:in `block in new_ostruct_member'
	from .bundle/gems/recursive-open-struct-1.1.3/lib/recursive_open_struct.rb:140:in `class_eval'
	from .bundle/gems/recursive-open-struct-1.1.3/lib/recursive_open_struct.rb:140:in `new_ostruct_member'
	from .bundle/gems/recursive-open-struct-1.1.3/lib/recursive_open_struct.rb:125:in `method_missing'
	from (irb):2:in `<main>'

Preliminary investigation

The issue seems to be caused by a change in behavior in the underlying OpenStruct class introduced somewhere between Ruby 2.7 and Ruby 3.0.

The gist of the problem is that, once an object is frozen, it becomes impossible to define new methods on it.

Ruby 2.7 and prior

Up unitl Ruby 2.7, OpenStruct used #method_missing to lazily define reader and writer methods on attributes.

OpenStruct#freeze has a custom implementation that then defines all methods (using OpenStruct#new_ostruct_member!) before freezing the object. That way, we make sure that the methods are defined and accessible, since creating new methods on a frozen object is forbidden.

Since RecursiveOpenStruct does not override #freeze, it conserves a similar behavior, and all is well.

Ruby 3.0 and later

Starting with Ruby 3.0, OpenStruct changed strategies when it comes to defining methods. All the methods are defined eagerly during initialization (see OpenStruct#initialize). That means that the custom implementation of #freeze is no longer required, since all methods already exist whenever the object gets frozen.

However, RecursiveOpenStruct overrides #initialize and does not call super. That, in turn, means that when an instance of RecursiveOpenStruct receives #freeze, the methods are never defined before freezing. And, when attempting to access an attribute, the attempt to lazily define the methods fails with FrozenError.

Fix proposal

I see two ways to circumvent the problem.

  1. Calling super during initialization, in order to benefit from the OpenStruct current initialization strategy.
  2. Override #freeze to perform the definition of all attributes before freezing the object.

Solution 1 sounds better to me, since it would make it so that future evolutions of the OpenStruct implementation would automatically get carried over to RecursiveOpenStruct. I will prepare a PR that goes that way.

In general, a good rule of thumb is to always call super when overriding a method, unless we absolutely want to get rid of the original implementation.

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 a pull request may close this issue.

1 participant