2021-10-21 11:12:54 -04:00
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Ruby 3 gotchas
This section documents several problems we found while working on [Ruby 3 support ](https://gitlab.com/groups/gitlab-org/-/epics/5149 )
and which led to subtle bugs or test failures that were difficult to understand. We encourage every GitLab contributor
who writes Ruby code on a regular basis to familiarize themselves with these issues.
2021-10-24 23:09:38 -04:00
To find the complete list of changes to the Ruby 3 language and standard library, see
[Ruby Changes ](https://rubyreferences.github.io/rubychanges/3.0.html ).
2021-10-21 11:12:54 -04:00
## `Hash#each` consistently yields a 2-element array to lambdas
Consider the following code snippet:
```ruby
def foo(a, b)
p [a, b]
end
def bar(a, b = 2)
p [a, b]
end
foo_lambda = method(:foo).to_proc
bar_lambda = method(:bar).to_proc
{ a: 1 }.each(& foo_lambda)
{ a: 1 }.each(& bar_lambda)
```
In Ruby 2.7, the output of this program suggests that yielding hash entries to lambdas behaves
differently depending on how many required arguments there are:
```ruby
# Ruby 2.7
{ a: 1 }.each(& foo_lambda) # prints [:a, 1]
{ a: 1 }.each(& bar_lambda) # prints [[:a, 1], 2]
```
Ruby 3 makes this behavior consistent and always attempts to yield hash entries as a single `[key, value]` array:
```ruby
# Ruby 3.0
{ a: 1 }.each(& foo_lambda) # `foo': wrong number of arguments (given 1, expected 2) (ArgumentError)
{ a: 1 }.each(& bar_lambda) # prints [[:a, 1], 2]
```
To write code that works under both 2.7 and 3.0, consider the following options:
- Always pass the lambda body as a block: `{ a: 1 }.each { |a, b| p [a, b] }` .
- Deconstruct the lambda arguments: `{ a: 1 }.each(&->((a, b)) { p [a, b] })` .
We recommend always passing the block explicitly, and prefer two required arguments as block parameters.
2021-10-24 23:09:38 -04:00
To learn more, see [Ruby issue 12706 ](https://bugs.ruby-lang.org/issues/12706 ).
2021-10-21 11:12:54 -04:00
## `Symbol#to_proc` returns signature metadata consistent with lambdas
2021-10-24 23:09:38 -04:00
A common idiom in Ruby is to obtain `Proc` objects using the `&:<symbol>` shorthand and
2021-10-21 11:12:54 -04:00
pass them to higher-order functions:
```ruby
[1, 2, 3].each(& :to_s)
```
2021-10-24 23:09:38 -04:00
Ruby desugars `&:<symbol>` to `Symbol#to_proc` . We can call it with
the method _receiver_ as its first argument (here: `Integer` ), and all method _arguments_
(here: none) as its remaining arguments.
2021-10-21 11:12:54 -04:00
2021-10-24 23:09:38 -04:00
This behaves the same in both Ruby 2.7 and Ruby 3. Where Ruby 3 diverges is when capturing
this `Proc` object and inspecting its call signature.
2021-10-21 11:12:54 -04:00
This is often done when writing DSLs or using other forms of meta-programming:
```ruby
p = :foo.to_proc # This usually happens via a conversion through `&:foo`
# Ruby 2.7: prints [[:rest]] (-1)
# Ruby 3.0: prints [[:req], [:rest]] (-2)
puts "#{p.parameters} (#{p.arity})"
```
2021-10-24 23:09:38 -04:00
Ruby 2.7 reports zero required and one optional parameter for this `Proc` object, while Ruby 3 reports one required
and one optional parameter. Ruby 2.7 is incorrect: the first argument must
always be passed, as it is the receiver of the method the `Proc` object represents, and methods cannot be
2021-10-21 11:12:54 -04:00
called without a receiver.
2021-10-24 23:09:38 -04:00
Ruby 3 corrects this: the code that tests `Proc` object arity or parameter lists might now break and
2021-10-21 11:12:54 -04:00
has to be updated.
2021-10-24 23:09:38 -04:00
To learn more, see [Ruby issue 16260 ](https://bugs.ruby-lang.org/issues/16260 ).
2021-10-21 11:12:54 -04:00
## `OpenStruct` does not evaluate fields lazily
2021-10-24 23:09:38 -04:00
The `OpenStruct` implementation has undergone a partial rewrite in Ruby 3, resulting in
2021-10-21 11:12:54 -04:00
behavioral changes. In Ruby 2.7, `OpenStruct` defines methods lazily, when the method is first accessed.
In Ruby 3.0, it defines these methods eagerly in the initializer, which can break classes that inherit from `OpenStruct`
and override these methods.
Don't inherit from `OpenStruct` for these reasons; ideally, don't use it at all.
2021-10-24 23:09:38 -04:00
`OpenStruct` is [considered problematic ](https://ruby-doc.org/stdlib-3.0.2/libdoc/ostruct/rdoc/OpenStruct.html#class-OpenStruct-label-Caveats ).
When writing new code, prefer a `Struct` instead, which is simpler in implementation, although less flexible.
2021-10-21 11:12:54 -04:00
## `Regexp` and `Range` instances are frozen
2021-10-24 23:09:38 -04:00
It is not necessary anymore to explicitly freeze `Regexp` or `Range` instances because Ruby 3 freezes
2021-10-21 11:12:54 -04:00
them automatically upon creation.
2021-10-24 23:09:38 -04:00
This has a subtle side-effect: Tests that stub method calls on these types now fail with an error because
2021-10-21 11:12:54 -04:00
RSpec cannot stub frozen objects:
```ruby
# Ruby 2.7: works
# Ruby 3.0: error: "can't modify frozen object"
allow(subject.function_returning_range).to receive(:max).and_return(42)
```
Rewrite affected tests by not stubbing method calls on frozen objects. The example above can be rewritten as:
```ruby
# Works with any Ruby version
allow(subject).to receive(:function_returning_range).and_return(1..42)
```
## Table tests fail with Ruby 3.0.2
Ruby 3.0.2 has a known bug that causes [table tests ](testing_guide/best_practices.md#table-based--parameterized-tests )
to fail when table values consist of integer values.
The reasons are documented in [issue 337614 ](https://gitlab.com/gitlab-org/gitlab/-/issues/337614 ).
2021-10-24 23:09:38 -04:00
This problem has been fixed in Ruby and the fix is expected to be included in Ruby 3.0.3.
2021-10-21 11:12:54 -04:00
The problem only affects users who run an unpatched Ruby 3.0.2. This is likely the case when you
installed Ruby manually or via tools like `asdf` . Users of the `gitlab-development-kit (GDK)`
are also affected by this problem.
2021-10-24 23:09:38 -04:00
Build images are not affected because they include the patch set addressing this bug.
2022-06-03 14:08:54 -04:00
## Deprecations are not caught in DeprecationToolkit if the method is stubbed
We rely on `deprecation_toolkit` to fail fast when using functionality that is deprecated in Ruby 2 and removed in Ruby 3.
2022-06-06 14:09:02 -04:00
A common issue caught during the transition from Ruby 2 to Ruby 3 relates to
the [separation of positional and keyword arguments in Ruby 3.0 ](https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/ ).
2022-06-03 14:08:54 -04:00
Unfortunately, if the author has stubbed such methods in tests, deprecations would not be caught.
2022-06-06 14:09:02 -04:00
We run automated detection for this warning in tests via `deprecation_toolkit` ,
but it relies on the fact that `Kernel#warn` emits a warning, so stubbing out this call will effectively remove the call to warn, which means `deprecation_toolkit` will never see the deprecation warnings.
2022-06-03 14:08:54 -04:00
Stubbing out the implementation removes that warning, and we never pick it up, so the build is green.
Please refer to [issue 364099 ](https://gitlab.com/gitlab-org/gitlab/-/issues/364099 ) for more context.
## Testing in `irb` and `rails console`
2022-06-06 14:09:02 -04:00
Another pitfall is that testing in `irb` /`rails c` silences the deprecation warning,
2022-06-03 14:08:54 -04:00
since `irb` in Ruby 2.7.x has a [bug ](https://bugs.ruby-lang.org/issues/17377 ) that prevents deprecation warnings from showing.
When writing code and performing code reviews, pay extra attention to method calls of the form `f({k: v})` .
This is valid in Ruby 2 when `f` takes either a `Hash` or keyword arguments, but Ruby 3 only considers this valid if `f` takes a `Hash` .
For Ruby 3 compliance, this should be changed to one of the following invocations if `f` takes keyword arguments:
- `f(**{k: v})`
- `f(k: v)`
2022-09-06 20:10:34 -04:00
## RSpec `with` argument matcher fails for shorthand Hash syntax
Because keyword arguments ("kwargs") are a first-class concept in Ruby 3, keyword arguments are not
converted into internal `Hash` instances anymore. This leads to RSpec method argument matchers failing
when the receiver takes a positional options hash instead of kwargs:
```ruby
def m(options={}); end
```
```ruby
expect(subject).to receive(:m).with(a: 42)
```
In Ruby 3 this expectations fails with the following error:
```plaintext
Failure/Error:
#< subject > received :m with unexpected arguments
expected: ({:a=>42})
got: ({:a=>42})
```
This happens because RSpec uses a kwargs argument matcher here, but the method takes a hash.
It works in Ruby 2, because `a: 42` is converted to a hash first and RSpec will use a hash argument matcher.
A workaround is to not use the shorthand syntax and pass an actual `Hash` instead whenever we know a method
to take an options hash:
```ruby
# Note the braces around the key-value pair.
expect(subject).to receive(:m).with({ a: 42 })
```
For more information, see [the official issue report for RSpec ](https://github.com/rspec/rspec-mocks/issues/1460 ).