Skip to content

Commit 351499f

Browse files
Add cop Rails/AcceptsNestedAttributesForUpdateOnly
1 parent ffa1464 commit 351499f

File tree

7 files changed

+260
-0
lines changed

7 files changed

+260
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
## master (unreleased)
1111

12+
* [#1035](https://github.com/rubocop/rubocop-rails/pull/1035): Add cop Rails/AcceptsNestedAttributesForUpdateOnly. ([@olivier-thatch][])
13+
1214
## 2.21.2 (2023-09-30)
1315

1416
### Bug fixes
@@ -952,3 +954,4 @@
952954
[@nipe0324]: https://github.com/nipe0324
953955
[@marocchino]: https://github.com/marocchino
954956
[@jamiemccarthy]: https://github.com/jamiemccarthy
957+
[@olivier-thatch]: https://github.com/olivier-thatch

config/default.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ Rails:
7777
Enabled: true
7878
DocumentationBaseURL: https://docs.rubocop.org/rubocop-rails
7979

80+
Rails/AcceptsNestedAttributesForUpdateOnly:
81+
Description: 'Define the update_only option to the accepts_nested_attributes_for attributes writers.'
82+
StyleGuide: 'https://rails.rubystyle.guide#accepts_nested_attributes_for-update_only-option'
83+
Enabled: false
84+
VersionAdded: '2.21'
85+
Include:
86+
- app/models/**/*.rb
87+
8088
Rails/ActionControllerFlashBeforeRender:
8189
Description: 'Use `flash.now` instead of `flash` before `render`.'
8290
Enabled: 'pending'

docs/modules/ROOT/pages/cops.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ based on the https://rails.rubystyle.guide/[Rails Style Guide].
1616

1717
=== Department xref:cops_rails.adoc[Rails]
1818

19+
* xref:cops_rails.adoc#railsacceptsnestedattributesforupdateonly[Rails/AcceptsNestedAttributesForUpdateOnly]
1920
* xref:cops_rails.adoc#railsactioncontrollerflashbeforerender[Rails/ActionControllerFlashBeforeRender]
2021
* xref:cops_rails.adoc#railsactioncontrollertestcase[Rails/ActionControllerTestCase]
2122
* xref:cops_rails.adoc#railsactionfilter[Rails/ActionFilter]

docs/modules/ROOT/pages/cops_rails.adoc

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,51 @@
11
= Rails
22

3+
== Rails/AcceptsNestedAttributesForUpdateOnly
4+
5+
|===
6+
| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed
7+
8+
| Disabled
9+
| Yes
10+
| No
11+
| 2.21
12+
| -
13+
|===
14+
15+
Looks for `accepts_nested_attributes_for` attributes writers that don't
16+
specify an `:update_only` option.
17+
18+
=== Examples
19+
20+
[source,ruby]
21+
----
22+
# bad
23+
class Member < ActiveRecord::Base
24+
has_one :avatar
25+
accepts_nested_attributes_for :avatar
26+
end
27+
28+
# good
29+
class Member < ActiveRecord::Base
30+
has_one :avatar
31+
accepts_nested_attributes_for :avatar, update_only: true
32+
end
33+
----
34+
35+
=== Configurable attributes
36+
37+
|===
38+
| Name | Default value | Configurable values
39+
40+
| Include
41+
| `+app/models/**/*.rb+`
42+
| Array
43+
|===
44+
45+
=== References
46+
47+
* https://rails.rubystyle.guide#accepts_nested_attributes_for-update_only-option
48+
349
== Rails/ActionControllerFlashBeforeRender
450

551
|===
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Rails
6+
# Looks for `accepts_nested_attributes_for` attributes writers that don't
7+
# specify an `:update_only` option.
8+
#
9+
# @example
10+
# # bad
11+
# class Member < ActiveRecord::Base
12+
# has_one :avatar
13+
# accepts_nested_attributes_for :avatar
14+
# end
15+
#
16+
# # good
17+
# class Member < ActiveRecord::Base
18+
# has_one :avatar
19+
# accepts_nested_attributes_for :avatar, update_only: true
20+
# end
21+
class AcceptsNestedAttributesForUpdateOnly < Base
22+
MSG = 'Specify a `:update_only` option.'
23+
RESTRICT_ON_SEND = %i[accepts_nested_attributes_for].freeze
24+
25+
def_node_search :active_resource_class?, <<~PATTERN
26+
(const (const {nil? cbase} :ActiveResource) :Base)
27+
PATTERN
28+
29+
def_node_matcher :accepts_nested_attributes_for_without_options?, <<~PATTERN
30+
(send _ {:accepts_nested_attributes_for} _)
31+
PATTERN
32+
33+
def_node_matcher :accepts_nested_attributes_for_with_options?, <<~PATTERN
34+
(send _ {:accepts_nested_attributes_for} ... (hash $...))
35+
PATTERN
36+
37+
def_node_matcher :update_only_option?, <<~PATTERN
38+
(pair (sym :update_only) {!nil (nil)})
39+
PATTERN
40+
41+
def_node_matcher :with_options_block, <<~PATTERN
42+
(block
43+
(send nil? :with_options
44+
(hash $...))
45+
(args) ...)
46+
PATTERN
47+
48+
def_node_matcher :accepts_nested_attributes_for_extension_block?, <<~PATTERN
49+
(block
50+
(send nil? :accepts_nested_attributes_for _)
51+
(args) ...)
52+
PATTERN
53+
54+
def on_send(node)
55+
return if active_resource?(node.parent)
56+
return if !accepts_nested_attributes_for_without_options?(node) && \
57+
valid_options?(accepts_nested_attributes_for_with_options?(node))
58+
return if valid_options_in_with_options_block?(node)
59+
60+
add_offense(node.loc.selector)
61+
end
62+
63+
private
64+
65+
def valid_options_in_with_options_block?(node)
66+
return true unless node.parent
67+
68+
n = node.parent.begin_type?
69+
n ||= accepts_nested_attributes_for_extension_block?(node.parent) ? node.parent.parent : node.parent
70+
71+
contain_valid_options_in_with_options_block?(n)
72+
end
73+
74+
def contain_valid_options_in_with_options_block?(node)
75+
if (options = with_options_block(node))
76+
return true if valid_options?(options)
77+
78+
return false unless node.parent
79+
80+
return true if contain_valid_options_in_with_options_block?(node.parent.parent)
81+
end
82+
83+
false
84+
end
85+
86+
def valid_options?(options)
87+
return false if options.nil?
88+
89+
options = extract_option_if_kwsplat(options)
90+
91+
return true unless options
92+
return true if options.any? do |o|
93+
update_only_option?(o)
94+
end
95+
96+
false
97+
end
98+
99+
def extract_option_if_kwsplat(options)
100+
if options.first.kwsplat_type? && options.first.children.first.hash_type?
101+
return options.first.children.first.pairs
102+
end
103+
104+
options
105+
end
106+
107+
def active_resource?(node)
108+
return false if node.nil?
109+
110+
active_resource_class?(node)
111+
end
112+
end
113+
end
114+
end
115+
end

lib/rubocop/cop/rails_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
require_relative 'mixin/migrations_helper'
1010
require_relative 'mixin/target_rails_version'
1111

12+
require_relative 'rails/accepts_nested_attributes_for_update_only'
1213
require_relative 'rails/action_controller_flash_before_render'
1314
require_relative 'rails/action_controller_test_case'
1415
require_relative 'rails/action_filter'
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::Rails::AcceptsNestedAttributesForUpdateOnly, :config do
4+
context 'accepts_nested_attributes_for' do
5+
it 'registers an offense when not specifying any options' do
6+
expect_offense(<<~RUBY)
7+
class Member < ApplicationRecord
8+
has_one :avatar
9+
accepts_nested_attributes_for :avatar
10+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Specify a `:update_only` option.
11+
end
12+
RUBY
13+
end
14+
15+
it 'registers an offense when missing an explicit `:update_only` flag' do
16+
expect_offense(<<~RUBY)
17+
class Member < ApplicationRecord
18+
has_one :avatar
19+
accepts_nested_attributes_for :avatar, reject_if: :all_blank
20+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Specify a `:update_only` option.
21+
end
22+
RUBY
23+
end
24+
25+
it 'does not register an offense when specifying `:update_only` flag' do
26+
expect_no_offenses(<<~RUBY)
27+
class Member < ApplicationRecord
28+
has_one :avatar
29+
accepts_nested_attributes_for :avatar, update_only: true
30+
end
31+
RUBY
32+
end
33+
34+
it 'does not register an offense when specifying `:update_only` flag with double splat' do
35+
expect_no_offenses(<<~RUBY)
36+
class Member < ApplicationRecord
37+
has_one :avatar
38+
accepts_nested_attributes_for :avatar, **{update_only: true}
39+
end
40+
RUBY
41+
end
42+
43+
it 'registers an offense when a variable passed with double splat' do
44+
expect_offense(<<~RUBY)
45+
class Member < ApplicationRecord
46+
has_one :avatar
47+
accepts_nested_attributes_for :avatar, **bar
48+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Specify a `:update_only` option.
49+
end
50+
RUBY
51+
end
52+
53+
context 'with_options update_only: true' do
54+
it 'does not register an offense' do
55+
expect_no_offenses(<<~RUBY)
56+
class Member < ApplicationRecord
57+
has_one :avatar
58+
with_options update_only: true do
59+
accepts_nested_attributes_for :avatar
60+
end
61+
end
62+
RUBY
63+
end
64+
65+
it 'does not register an offense for using `reject_if` option' do
66+
expect_no_offenses(<<~RUBY)
67+
class Member < ApplicationRecord
68+
has_one :avatar
69+
with_options update_only: true do
70+
accepts_nested_attributes_for :avatar, reject_if: :all_blank
71+
end
72+
end
73+
RUBY
74+
end
75+
end
76+
end
77+
78+
context 'when an Active Record model does not have any associations' do
79+
it 'does not register an offense' do
80+
expect_no_offenses(<<~RUBY)
81+
class Member < ApplicationRecord
82+
end
83+
RUBY
84+
end
85+
end
86+
end

0 commit comments

Comments
 (0)