Skip to content

Commit c394abf

Browse files
petehamiltonkoic
authored andcommitted
In Rails >= 5, use optional: true, not required: false (#6596)
The `required` option on belongs_to associations has been deprecated, instead the recommended (and default) approach is to set this to true by default and disable it using `optional: true`. The developer can override this default globally, however, so we can't say that things like `optional: false` are superfluous, or indeed that `required: true` isn't necessary. This change therefore only makes the known-to-be-valid correction from `required: false` to `optional: true` and registers an offence for any instances of `required` See: https://guides.rubyonrails.org/5_0_release_notes.html
1 parent 04cd016 commit c394abf

File tree

4 files changed

+187
-0
lines changed

4 files changed

+187
-0
lines changed

lib/rubocop/cop/rails/belongs_to.rb

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Rails
6+
# This cop looks for belongs_to associations where we control whether the
7+
# association is required via the deprecated `required` option instead.
8+
#
9+
# Since Rails 5, belongs_to associations are required by default and this
10+
# can be controlled through the use of `optional: true`.
11+
#
12+
# From the release notes:
13+
#
14+
# belongs_to will now trigger a validation error by default if the
15+
# association is not present. You can turn this off on a
16+
# per-association basis with optional: true. Also deprecate required
17+
# option in favor of optional for belongs_to. (Pull Request)
18+
#
19+
# In the case that the developer is doing `required: false`, we
20+
# definitely want to autocorrect to `optional: true`.
21+
#
22+
# However, without knowing whether they've set overriden the default
23+
# value of `config.active_record.belongs_to_required_by_default`, we
24+
# can't say whether it's safe to remove `required: true` or replace it
25+
# with `optional: false` (or, similarly, remove a superfluous `optional:
26+
# false`). Therefore, in the cases we're using `required: true`, we'll
27+
# highlight that `required` is deprecated but otherwise do nothing.
28+
#
29+
# @example
30+
# # bad
31+
# class Post < ApplicationRecord
32+
# belongs_to :blog, required: false
33+
# end
34+
#
35+
# # good
36+
# class Post < ApplicationRecord
37+
# belongs_to :blog, optional: true
38+
# end
39+
#
40+
# @see https://guides.rubyonrails.org/5_0_release_notes.html
41+
# @see https://github.com/rails/rails/pull/18937
42+
class BelongsTo < Cop
43+
extend TargetRailsVersion
44+
45+
minimum_target_rails_version 5.0
46+
47+
DEPRECATED_REQUIRE =
48+
'The use of `required` on belongs_to associations was deprecated ' \
49+
'in Rails 5. Please use the `optional` flag instead'.freeze
50+
51+
SUPERFLOUS_REQUIRE_MSG =
52+
'You specified `required: false`, in Rails > 5.0 the requires ' \
53+
'option is deprecated and you want to use `optional: true`.'.freeze
54+
55+
def_node_matcher :match_belongs_to_with_options, <<-PATTERN
56+
(send $_ :belongs_to _ (hash $...))
57+
PATTERN
58+
59+
def_node_matcher :match_requires_false?, <<-PATTERN
60+
(pair (sym :required) false)
61+
PATTERN
62+
63+
def_node_matcher :match_requires_any?, <<-PATTERN
64+
(pair (sym :required) $_)
65+
PATTERN
66+
67+
def on_send(node)
68+
_, opts = match_belongs_to_with_options(node)
69+
if opts && opts.any? { |opt| match_requires_false?(opt) }
70+
add_offense(
71+
node,
72+
message: SUPERFLOUS_REQUIRE_MSG,
73+
location: :selector
74+
)
75+
elsif opts && opts.any? { |opt| match_requires_any?(opt) }
76+
add_offense(
77+
node,
78+
message: DEPRECATED_REQUIRE,
79+
location: :selector
80+
)
81+
end
82+
end
83+
84+
def autocorrect(node)
85+
_, opts = match_belongs_to_with_options(node)
86+
return nil if opts && opts.none? { |opt| match_requires_false?(opt) }
87+
88+
lambda do |corrector|
89+
requires_expression =
90+
node.children[3].children.find { |c| match_requires_false?(c) }
91+
92+
corrector.replace(
93+
requires_expression.loc.expression,
94+
'optional: true'
95+
)
96+
end
97+
end
98+
end
99+
end
100+
end
101+
end

lib/rubocop/cop/rails_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module Cop
1414
require_relative 'rails/application_job'
1515
require_relative 'rails/application_record'
1616
require_relative 'rails/assert_not'
17+
require_relative 'rails/belongs_to'
1718
require_relative 'rails/blank'
1819
require_relative 'rails/bulk_change_table'
1920
require_relative 'rails/create_table_with_timestamps'

manual/cops_rails.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,49 @@ Name | Default value | Configurable values
162162
--- | --- | ---
163163
Include | `**/test/**/*` | Array
164164

165+
## Rails/BelongsTo
166+
167+
Enabled by default | Supports autocorrection
168+
--- | ---
169+
Enabled | Yes
170+
171+
This cop looks for belongs_to associations where we control whether the
172+
association is required via the deprecated `required` option instead.
173+
174+
Since Rails 5, belongs_to associations are required by default and this
175+
can be controlled through the use of `optional: true`.
176+
177+
From the release notes:
178+
179+
belongs_to will now trigger a validation error by default if the
180+
association is not present. You can turn this off on a
181+
per-association basis with optional: true. Also deprecate required
182+
option in favor of optional for belongs_to. (Pull Request)
183+
184+
In the case that the developer is doing `required: false`, we
185+
definitely want to autocorrect to `optional: true`.
186+
187+
However, without knowing whether they've set overriden the default
188+
value of `config.active_record.belongs_to_required_by_default`, we
189+
can't say whether it's safe to remove `required: true` or replace it
190+
with `optional: false` (or, similarly, remove a superfluous `optional:
191+
false`). Therefore, in the cases we're using `required: true`, we'll
192+
highlight that `required` is deprecated but otherwise do nothing.
193+
194+
### Examples
195+
196+
```ruby
197+
# bad
198+
class Post < ApplicationRecord
199+
belongs_to :blog, required: false
200+
end
201+
202+
# good
203+
class Post < ApplicationRecord
204+
belongs_to :blog, optional: true
205+
end
206+
```
207+
165208
## Rails/Blank
166209

167210
Enabled by default | Supports autocorrection
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::Rails::BelongsTo do
4+
subject(:cop) { described_class.new(config) }
5+
6+
let(:config) { RuboCop::Config.new }
7+
8+
it 'registers an offense when specifying `required: false`' do
9+
expect_offense(<<-RUBY.strip_indent)
10+
belongs_to :foo, required: false
11+
^^^^^^^^^^ You specified `required: false`, in Rails > 5.0 the requires option is deprecated and you want to use `optional: true`.
12+
RUBY
13+
end
14+
15+
it 'registers an offense when specifying `required: true`' do
16+
expect_offense(<<-RUBY.strip_indent)
17+
belongs_to :foo, required: true
18+
^^^^^^^^^^ The use of `required` on belongs_to associations was deprecated in Rails 5. Please use the `optional` flag instead
19+
RUBY
20+
end
21+
22+
it 'auto-corrects `required: false` to `optional: true`' do
23+
expect(autocorrect_source('belongs_to :foo, required: false'))
24+
.to eq('belongs_to :foo, optional: true')
25+
expect(cop.offenses.last.status).to eq(:corrected)
26+
end
27+
28+
it 'does not auto-correct `required: true`' do
29+
code = 'belongs_to :foo, required: true'
30+
expect(autocorrect_source(code)).to eq(code)
31+
expect(cop.offenses.last.status).to eq(:uncorrected)
32+
end
33+
34+
it 'registers no offense when setting `optional: true`' do
35+
expect_no_offenses('belongs_to :foo, optional: true')
36+
end
37+
38+
it 'registers no offense when requires: false is not set' do
39+
expect_no_offenses('belongs_to :foo')
40+
expect_no_offenses('belongs_to :foo, polymorphic: true')
41+
end
42+
end

0 commit comments

Comments
 (0)