Skip to content

Commit 4084523

Browse files
committed
Inspect inline style for FactoryBot/AssociationStyle
1 parent 84f797f commit 4084523

File tree

4 files changed

+404
-104
lines changed

4 files changed

+404
-104
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Master (Unreleased)
44

55
- Add autocorrect for `FactoryBot/FactoryAssociationWithStrategy` cop. ([@r7kamura])
6+
- Inspect inline style for `FactoryBot/AssociationStyle`. ([@r7kamura])
67

78
## 2.28.0 (2025-11-12)
89

docs/modules/ROOT/pages/cops_factorybot.adoc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ end
5555
factory :post do
5656
user factory: %i[user author]
5757
end
58+
59+
# bad
60+
factory :post do
61+
user { association :user }
62+
end
63+
64+
# good
65+
factory :post do
66+
user
67+
end
68+
69+
# good (NonImplicitAssociationMethodNames: ['foo'])
70+
factory :post do
71+
foo { association :foo }
72+
end
5873
----
5974
6075
[#_enforcedstyle_-explicit_-factorybotassociationstyle]
@@ -82,6 +97,16 @@ factory :post do
8297
association :user, :author
8398
end
8499
100+
# bad
101+
factory :post do
102+
user { association :user }
103+
end
104+
105+
# good
106+
factory :post do
107+
association :user
108+
end
109+
85110
# good (NonImplicitAssociationMethodNames: ['email'])
86111
sequence :email do |n|
87112
"person#{n}@example.com"

lib/rubocop/cop/factory_bot/association_style.rb

Lines changed: 174 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,21 @@ module FactoryBot
3131
# user factory: %i[user author]
3232
# end
3333
#
34+
# # bad
35+
# factory :post do
36+
# user { association :user }
37+
# end
38+
#
39+
# # good
40+
# factory :post do
41+
# user
42+
# end
43+
#
44+
# # good (NonImplicitAssociationMethodNames: ['foo'])
45+
# factory :post do
46+
# foo { association :foo }
47+
# end
48+
#
3449
# @example `EnforcedStyle: explicit`
3550
# # bad
3651
# factory :post do
@@ -52,6 +67,16 @@ module FactoryBot
5267
# association :user, :author
5368
# end
5469
#
70+
# # bad
71+
# factory :post do
72+
# user { association :user }
73+
# end
74+
#
75+
# # good
76+
# factory :post do
77+
# association :user
78+
# end
79+
#
5580
# # good (NonImplicitAssociationMethodNames: ['email'])
5681
# sequence :email do |n|
5782
# "person#{n}@example.com"
@@ -66,19 +91,22 @@ class AssociationStyle < ::RuboCop::Cop::Base # rubocop:disable Metrics/ClassLen
6691
include ConfigurableEnforcedStyle
6792

6893
RESTRICT_ON_SEND = %i[factory trait].freeze
94+
6995
KEYWORDS = %i[alias and begin break case class def defined? do
7096
else elsif end ensure false for if in module
7197
next nil not or redo rescue retry return self
7298
super then true undef unless until when while
73-
yield __FILE__ __LINE__ __ENCODING__].freeze
99+
yield __FILE__ __LINE__ __ENCODING__].to_set.freeze
74100

75101
def on_send(node)
76-
bad_associations_in(node).each do |association|
102+
body_nodes_from(node).each do |maybe_association|
103+
next unless correctable_to_enforced_style?(maybe_association)
104+
77105
add_offense(
78-
association,
106+
maybe_association,
79107
message: "Use #{style} style to define associations."
80108
) do |corrector|
81-
autocorrect(corrector, association)
109+
autocorrect(corrector, maybe_association)
82110
end
83111
end
84112
end
@@ -90,110 +118,118 @@ def on_send(node)
90118
(send nil? :association sym ...)
91119
PATTERN
92120

93-
# @!method with_strategy_build_option?(node)
94-
def_node_matcher :with_strategy_build_option?, <<~PATTERN
95-
(send nil? :association sym ...
96-
(hash <(pair (sym :strategy) (sym :build)) ...>)
97-
)
121+
# @!method with_strategy_option?(node)
122+
def_node_matcher :with_strategy_option?, <<~PATTERN
123+
(send nil? ... (hash <(pair (sym :strategy) _) ...>))
98124
PATTERN
99125

100-
# @!method implicit_association?(node)
101-
def_node_matcher :implicit_association?, <<~PATTERN
102-
(send nil? !#non_implicit_association_method_name? ...)
126+
# @!method receiverless_method_call?(node)
127+
def_node_matcher :receiverless_method_call?, <<~PATTERN
128+
(send nil? ...)
103129
PATTERN
104130

105131
# @!method factory_option_matcher(node)
106132
def_node_matcher :factory_option_matcher, <<~PATTERN
107-
(send
108-
nil?
109-
:association
110-
...
111-
(hash
112-
<
113-
(pair
114-
(sym :factory)
115-
{
116-
(sym $_) |
117-
(array (sym $_)*)
118-
}
119-
)
120-
...
121-
>
122-
)
123-
)
133+
(send nil? ... (hash <(pair (sym :factory) {(sym $_) (array (sym $_)*)}) ...>))
124134
PATTERN
125135

126136
# @!method trait_names_from_explicit(node)
127137
def_node_matcher :trait_names_from_explicit, <<~PATTERN
128138
(send nil? :association _ (sym $_)* ...)
129139
PATTERN
130140

131-
# @!method association_names(node)
132-
def_node_search :association_names, <<~PATTERN
133-
(send nil? :association $...)
141+
# @!method search_defined_trait_names_in(node)
142+
def_node_search :search_defined_trait_names_in, <<~PATTERN
143+
(send nil? :trait (sym $_) )
144+
PATTERN
145+
146+
# @!method factory_definition_node?(node)
147+
def_node_matcher :factory_definition_node?, <<~PATTERN
148+
(block (send nil? :factory ...) ...)
134149
PATTERN
135150

136-
# @!method trait_name(node)
137-
def_node_search :trait_name, <<~PATTERN
138-
(send nil? :trait (sym $_) )
151+
# @!method inline_associationish?(node)
152+
def_node_matcher :inline_associationish?, <<~PATTERN
153+
(block
154+
(send nil? _)
155+
(args)
156+
(send nil? :association ...)
157+
)
139158
PATTERN
140159

141-
def autocorrect(corrector, node)
142-
if style == :explicit
143-
autocorrect_to_explicit_style(corrector, node)
144-
else
145-
autocorrect_to_implicit_style(corrector, node)
160+
def correctable_to_enforced_style?(node)
161+
case style
162+
when :explicit
163+
correctable_to_explicit_style?(node)
164+
when :implicit
165+
correctable_to_implicit_style?(node)
146166
end
147167
end
148168

149-
def autocorrect_to_explicit_style(corrector, node)
150-
arguments = [
151-
":#{node.method_name}",
152-
*node.arguments.map(&:source)
153-
]
154-
corrector.replace(node, "association #{arguments.join(', ')}")
169+
def correctable_to_explicit_style?(node)
170+
if explicit_association?(node)
171+
false
172+
elsif implicit_association?(node)
173+
true
174+
elsif inline_association?(node)
175+
true
176+
end
155177
end
156178

157-
def autocorrect_to_implicit_style(corrector, node)
158-
source = node.first_argument.value.to_s
159-
options = options_for_autocorrect_to_implicit_style(node)
160-
unless options.empty?
161-
rest = options.map { |option| option.join(': ') }.join(', ')
162-
source += " #{rest}"
179+
def correctable_to_implicit_style?(node)
180+
if explicit_association?(node)
181+
!with_strategy_option?(node) &&
182+
!keyword_explicit_association_name?(node)
183+
elsif implicit_association?(node)
184+
false
185+
elsif inline_association?(node)
186+
!with_strategy_option?(node.body)
163187
end
164-
corrector.replace(node, source)
165188
end
166189

167-
def bad?(node)
168-
if style == :explicit
169-
implicit_association?(node) &&
170-
(factory_node = trait_factory_node(node)) && !trait_within_trait?(
171-
node, factory_node
172-
)
173-
else
174-
explicit_association?(node) &&
175-
!with_strategy_build_option?(node) &&
176-
!keyword?(node)
177-
end
190+
def inline_association?(node)
191+
inline_associationish?(node) &&
192+
implicit_association_method?(node)
178193
end
179194

180-
def keyword?(node)
181-
association_names(node).any? do |associations|
182-
associations.any? do |association|
183-
next unless association.sym_type?
195+
def implicit_association?(node)
196+
receiverless_method_call?(node) &&
197+
implicit_association_method?(node)
198+
end
184199

185-
KEYWORDS.include?(association.value)
186-
end
187-
end
200+
def implicit_association_method?(node)
201+
!non_implicit_association_method_names_for(node)
202+
.include?(node.method_name)
188203
end
189204

190-
def bad_associations_in(node)
191-
children_of_factory_block(node).select do |child|
192-
bad?(child)
205+
def non_implicit_association_method_names_for(node)
206+
RuboCop::FactoryBot.reserved_methods +
207+
configured_non_implicit_association_method_names +
208+
search_defined_trait_names_in_same_factory(node)
209+
end
210+
211+
def configured_non_implicit_association_method_names
212+
(cop_config['NonImplicitAssociationMethodNames'] || []).map(&:to_sym)
213+
end
214+
215+
def search_defined_trait_names_in_same_factory(node)
216+
factory_definition_node = find_factory_definition_node_from(node)
217+
return [] unless factory_definition_node
218+
219+
search_defined_trait_names_in(factory_definition_node).to_a
220+
end
221+
222+
def find_factory_definition_node_from(node)
223+
node.ancestors.reverse.find do |ancestor|
224+
factory_definition_node?(ancestor)
193225
end
194226
end
195227

196-
def children_of_factory_block(node)
228+
def keyword_explicit_association_name?(node)
229+
KEYWORDS.include?(node.first_argument.value)
230+
end
231+
232+
def body_nodes_from(node)
197233
block = node.block_node
198234
return [] unless block
199235
return [] unless block.body
@@ -219,9 +255,69 @@ def non_implicit_association_method_name?(method_name)
219255
non_implicit_association_method_names.include?(method_name.to_s)
220256
end
221257

222-
def non_implicit_association_method_names
223-
RuboCop::FactoryBot.reserved_methods.map(&:to_s) +
224-
(cop_config['NonImplicitAssociationMethodNames'] || [])
258+
def autocorrect(corrector, node)
259+
case style
260+
when :explicit
261+
autocorrect_to_explicit_style(corrector, node)
262+
when :implicit
263+
autocorrect_to_implicit_style(corrector, node)
264+
end
265+
end
266+
267+
def autocorrect_to_explicit_style(corrector, node)
268+
if implicit_association?(node)
269+
autocorrect_from_implicit_to_explicit_style(corrector, node)
270+
else
271+
autocorrect_from_inline_to_explicit_style(corrector, node)
272+
end
273+
end
274+
275+
def autocorrect_to_implicit_style(corrector, node)
276+
if explicit_association?(node)
277+
autocorrect_from_explicit_to_implicit_style(corrector, node)
278+
else
279+
autocorrect_from_inline_to_implicit_style(corrector, node)
280+
end
281+
end
282+
283+
def autocorrect_from_explicit_to_implicit_style(corrector, node)
284+
source = node.first_argument.value.to_s
285+
options = options_for_autocorrect_to_implicit_style(node)
286+
unless options.empty?
287+
rest = options.map { |option| option.join(': ') }.join(', ')
288+
source += " #{rest}"
289+
end
290+
corrector.replace(node, source)
291+
end
292+
293+
def autocorrect_from_implicit_to_explicit_style(corrector, node)
294+
arguments = [
295+
":#{node.method_name}",
296+
*node.arguments.map(&:source)
297+
]
298+
corrector.replace(node, "association #{arguments.join(', ')}")
299+
end
300+
301+
def autocorrect_from_inline_to_explicit_style(corrector, node)
302+
return unless autocorrectable_inline_association?(node)
303+
304+
corrector.replace(
305+
node,
306+
format(
307+
'association %<association_name>s, factory: [%<factory_name>s]',
308+
association_name: node.method_name.inspect,
309+
factory_name: node.body.first_argument.source
310+
)
311+
)
312+
end
313+
314+
# Autocorrect to implicit style with the next trial.
315+
alias autocorrect_from_inline_to_implicit_style \
316+
autocorrect_from_inline_to_explicit_style
317+
318+
# Parsing of options is too complex, so it is not currently supported.
319+
def autocorrectable_inline_association?(node)
320+
node.body.arguments.size == 1
225321
end
226322

227323
def options_from_explicit(node)
@@ -240,16 +336,6 @@ def options_for_autocorrect_to_implicit_style(node)
240336
end
241337
options
242338
end
243-
244-
def trait_within_trait?(node, factory_node)
245-
trait_name(factory_node).include?(node.method_name)
246-
end
247-
248-
def trait_factory_node(node)
249-
node.ancestors.reverse.find do |ancestor|
250-
ancestor.method?(:factory) if ancestor.block_type?
251-
end
252-
end
253339
end
254340
end
255341
end

0 commit comments

Comments
 (0)