Skip to content

Commit 2c8aaed

Browse files
committed
Add RSpec/FactoryBot/AssociationStyle cop
1 parent 60f6359 commit 2c8aaed

File tree

8 files changed

+654
-0
lines changed

8 files changed

+654
-0
lines changed

.rubocop.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ RSpec/Capybara/SpecificFinders:
136136
Enabled: true
137137
RSpec/Capybara/SpecificMatcher:
138138
Enabled: true
139+
RSpec/FactoryBot/AssociationStyle:
140+
Enabled: true
139141
RSpec/FactoryBot/SyntaxMethods:
140142
Enabled: true
141143
RSpec/Rails/AvoidSetupHook:

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- Fix `RSpec/NestedGroups` to correctly use `AllowedGroups` config. ([@samrjenkins])
1414
- Remove `Runners` and `HookScopes` RSpec DSL elements from configuration. ([@pirj])
1515
- Add `with default RSpec/Language config` helper to `lib` (under `rubocop/rspec/shared_contexts/default_rspec_language_config_context`), to allow use for downstream cops based on `RuboCop::Cop::RSpec::Base`. ([@smcgivern])
16+
- Add `RSpec/FactoryBot/AssociationStyle` cop. ([@r7kamura])
1617

1718
## 2.14.2 (2022-10-25)
1819

config/default.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,22 @@ RSpec/FactoryBot:
911911
Include: *1
912912
Language: *2
913913

914+
RSpec/FactoryBot/AssociationStyle:
915+
Description: Use consistent style to define associations.
916+
Enabled: pending
917+
Safe: false
918+
Include:
919+
- spec/factories.rb
920+
- spec/factories/**/*.rb
921+
- features/support/factories/**/*.rb
922+
VersionAdded: "<<next>>"
923+
EnforcedStyle: implicit
924+
SupportedStyles:
925+
- explicit
926+
- implicit
927+
NonImplicitAssociationMethodNames: ~
928+
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/FactoryBot/AssociationStyle
929+
914930
RSpec/FactoryBot/AttributeDefinedStatically:
915931
Description: Always declare attribute values as blocks.
916932
Enabled: true

docs/modules/ROOT/pages/cops.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104

105105
=== Department xref:cops_rspec_factorybot.adoc[RSpec/FactoryBot]
106106

107+
* xref:cops_rspec_factorybot.adoc#rspecfactorybot/associationstyle[RSpec/FactoryBot/AssociationStyle]
107108
* xref:cops_rspec_factorybot.adoc#rspecfactorybot/attributedefinedstatically[RSpec/FactoryBot/AttributeDefinedStatically]
108109
* xref:cops_rspec_factorybot.adoc#rspecfactorybot/consistentparenthesesstyle[RSpec/FactoryBot/ConsistentParenthesesStyle]
109110
* xref:cops_rspec_factorybot.adoc#rspecfactorybot/createlist[RSpec/FactoryBot/CreateList]

docs/modules/ROOT/pages/cops_rspec_factorybot.adoc

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,74 @@
11
= RSpec/FactoryBot
22

3+
== RSpec/FactoryBot/AssociationStyle
4+
5+
|===
6+
| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed
7+
8+
| Pending
9+
| No
10+
| Yes (Unsafe)
11+
| <<next>>
12+
| -
13+
|===
14+
15+
Use consistent style to define associations.
16+
17+
=== Safety
18+
19+
This cop may cause false-positives in `EnforcedStyle: explicit`
20+
case. It recognizes any method call that has no arguments as an
21+
implicit association but it might be a user-defined trait call.
22+
23+
=== Examples
24+
25+
==== EnforcedStyle: implicit (default)
26+
27+
[source,ruby]
28+
----
29+
# bad
30+
association :user
31+
32+
# good
33+
user
34+
----
35+
36+
==== EnforcedStyle: explicit
37+
38+
[source,ruby]
39+
----
40+
# bad
41+
user
42+
43+
# good
44+
association :user
45+
46+
# good (defined in NonImplicitAssociationMethodNames)
47+
skip_create
48+
----
49+
50+
=== Configurable attributes
51+
52+
|===
53+
| Name | Default value | Configurable values
54+
55+
| Include
56+
| `spec/factories.rb`, `+spec/factories/**/*.rb+`, `+features/support/factories/**/*.rb+`
57+
| Array
58+
59+
| EnforcedStyle
60+
| `implicit`
61+
| `explicit`, `implicit`
62+
63+
| NonImplicitAssociationMethodNames
64+
| `<none>`
65+
|
66+
|===
67+
68+
=== References
69+
70+
* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/FactoryBot/AssociationStyle
71+
372
== RSpec/FactoryBot/AttributeDefinedStatically
473

574
[separator=¦]
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module RSpec
6+
module FactoryBot
7+
# Use consistent style to define associations.
8+
#
9+
# @safety
10+
# This cop may cause false-positives in `EnforcedStyle: explicit`
11+
# case. It recognizes any method call that has no arguments as an
12+
# implicit association but it might be a user-defined trait call.
13+
#
14+
# @example EnforcedStyle: implicit (default)
15+
# # bad
16+
# association :user
17+
#
18+
# # good
19+
# user
20+
#
21+
# @example EnforcedStyle: explicit
22+
# # bad
23+
# user
24+
#
25+
# # good
26+
# association :user
27+
#
28+
# # good (defined in NonImplicitAssociationMethodNames)
29+
# skip_create
30+
class AssociationStyle < Base # rubocop:disable Metrics/ClassLength
31+
extend AutoCorrector
32+
33+
include ConfigurableEnforcedStyle
34+
35+
DEFAULT_NON_IMPLICIT_ASSOCIATION_METHOD_NAMES = %w[
36+
association
37+
sequence
38+
skip_create
39+
traits_for_enum
40+
].freeze
41+
42+
RESTRICT_ON_SEND = %i[factory trait].freeze
43+
44+
# @param node [RuboCop::AST::SendNode]
45+
# @return [void]
46+
def on_send(node)
47+
bad_associations_in(node).each do |association|
48+
add_offense(
49+
association,
50+
message: "Use #{style} style to define associations."
51+
) do |corrector|
52+
autocorrect(corrector, association)
53+
end
54+
end
55+
end
56+
57+
private
58+
59+
# @!method explicit_association?(node)
60+
# @param node [RuboCop::AST::SendNode]
61+
# @return [Boolean]
62+
def_node_matcher :explicit_association?, <<~PATTERN
63+
(send nil? :association sym ...)
64+
PATTERN
65+
66+
# @!method implicit_association?(node)
67+
# @param node [RuboCop::AST::SendNode]
68+
# @return [Boolean]
69+
def_node_matcher :implicit_association?, <<~PATTERN
70+
(send nil? !#non_implicit_association_method_name? ...)
71+
PATTERN
72+
73+
# @!method factory_option_matcher(node)
74+
# @param node [RuboCop::AST::SendNode]
75+
# @return [Array<Symbol>, Symbol, nil]
76+
def_node_matcher :factory_option_matcher, <<~PATTERN
77+
(send
78+
nil?
79+
:association
80+
...
81+
(hash
82+
<
83+
(pair
84+
(sym :factory)
85+
{
86+
(sym $_) |
87+
(array (sym $_)*)
88+
}
89+
)
90+
...
91+
>
92+
)
93+
)
94+
PATTERN
95+
96+
# @!method trait_arguments_matcher(node)
97+
# @param node [RuboCop::AST::SendNode]
98+
# @return [Array<Symbol>, nil]
99+
def_node_matcher :trait_arguments_matcher, <<~PATTERN
100+
(send nil? :association _ (sym $_)* ...)
101+
PATTERN
102+
103+
# @!method trait_option_matcher(node)
104+
# @param node [RuboCop::AST::SendNode]
105+
# @return [Array<Symbol>, nil]
106+
def_node_matcher :trait_option_matcher, <<~PATTERN
107+
(send
108+
nil?
109+
:association
110+
...
111+
(hash
112+
<
113+
(pair
114+
(sym :traits)
115+
(array (sym $_)*)
116+
)
117+
...
118+
>
119+
)
120+
)
121+
PATTERN
122+
123+
# @param corrector [RuboCop::Cop::Corrector]
124+
# @param node [RuboCop::AST::SendNode]
125+
def autocorrect(corrector, node)
126+
case style
127+
when :explicit
128+
autocorrect_to_explicit_style(corrector, node)
129+
when :implicit
130+
autocorrect_to_implicit_style(corrector, node)
131+
end
132+
end
133+
134+
# @param corrector [RuboCop::Cop::Corrector]
135+
# @param node [RuboCop::AST::SendNode]
136+
# @return [void]
137+
def autocorrect_to_explicit_style(corrector, node)
138+
arguments = [
139+
":#{node.method_name}",
140+
*node.arguments.map(&:source)
141+
]
142+
corrector.replace(node, "association #{arguments.join(', ')}")
143+
end
144+
145+
# @param corrector [RuboCop::Cop::Corrector]
146+
# @param node [RuboCop::AST::SendNode]
147+
# @return [void]
148+
def autocorrect_to_implicit_style(corrector, node)
149+
source = node.first_argument.value.to_s
150+
options = options_for_autocorrect_to_implicit_style(node)
151+
unless options.empty?
152+
rest = options.map { |option| option.join(': ') }.join(', ')
153+
source += " #{rest}"
154+
end
155+
corrector.replace(node, source)
156+
end
157+
158+
# @param node [RuboCop::AST::SendNode]
159+
# @return [Boolean]
160+
def autocorrectable_to_implicit_style?(node)
161+
node.arguments.one?
162+
end
163+
164+
# @param node [RuboCop::AST::SendNode]
165+
# @return [Boolean]
166+
def bad?(node)
167+
case style
168+
when :explicit
169+
implicit_association?(node)
170+
when :implicit
171+
explicit_association?(node)
172+
end
173+
end
174+
175+
# @param node [RuboCop::AST::SendNode]
176+
# @return [Array<RuboCop::AST::SendNode>]
177+
def bad_associations_in(node)
178+
children_of_factory_block(node).select do |child|
179+
bad?(child)
180+
end
181+
end
182+
183+
# @param node [RuboCop::AST::SendNode]
184+
# @return [Array<RuboCop::AST::Node>]
185+
def children_of_factory_block(node)
186+
block = node.parent
187+
return [] unless block
188+
return [] unless block.block_type?
189+
return [] unless block.body
190+
191+
if block.body.begin_type?
192+
block.body.children
193+
else
194+
[block.body]
195+
end
196+
end
197+
198+
# @param node [RuboCop::AST::SendNode]
199+
# @return [Array<Symbol>]
200+
def factory_names_from_explicit(node)
201+
trait_names = trait_names_from_explicit(node)
202+
factory_names = Array(factory_option_matcher(node))
203+
result = factory_names + trait_names
204+
if factory_names.empty? && !trait_names.empty?
205+
result.prepend(node.first_argument.value)
206+
end
207+
result
208+
end
209+
210+
# @param method_name [Symbol]
211+
# @return [Boolean]
212+
def non_implicit_association_method_name?(method_name)
213+
non_implicit_association_method_names.include?(method_name.to_s)
214+
end
215+
216+
# @return [Array<String>]
217+
def non_implicit_association_method_names
218+
DEFAULT_NON_IMPLICIT_ASSOCIATION_METHOD_NAMES +
219+
(cop_config['NonImplicitAssociationMethodNames'] || [])
220+
end
221+
222+
# @param node [RuboCop::AST::SendNode]
223+
# @return [Hash{Symbol => String}]
224+
def options_from_explicit(node)
225+
return {} unless node.last_argument.hash_type?
226+
227+
node.last_argument.pairs.inject({}) do |options, pair|
228+
options.merge(pair.key.value => pair.value.source)
229+
end
230+
end
231+
232+
# @param node [RuboCop::AST::SendNode]
233+
# @return [Hash{Symbol => String}]
234+
def options_for_autocorrect_to_implicit_style(node)
235+
options = options_from_explicit(node)
236+
options.delete(:traits)
237+
factory_names = factory_names_from_explicit(node)
238+
unless factory_names.empty?
239+
options[:factory] = "%i[#{factory_names.join(' ')}]"
240+
end
241+
options
242+
end
243+
244+
# @param node [RuboCop::AST::SendNode]
245+
# @return [Array<Symbol>]
246+
def trait_names_from_explicit(node)
247+
(trait_arguments_matcher(node) || []) +
248+
(trait_option_matcher(node) || [])
249+
end
250+
end
251+
end
252+
end
253+
end
254+
end

lib/rubocop/cop/rspec_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
require_relative 'rspec/capybara/specific_matcher'
99
require_relative 'rspec/capybara/visibility_matcher'
1010

11+
require_relative 'rspec/factory_bot/association_style'
1112
require_relative 'rspec/factory_bot/attribute_defined_statically'
1213
require_relative 'rspec/factory_bot/consistent_parentheses_style'
1314
require_relative 'rspec/factory_bot/create_list'

0 commit comments

Comments
 (0)