rubocop-factory_bot 2.22.0 → 2.23.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44482485fe839223a23464d6739cbd0160ebaffa5402c9057d2fdc0e3c9b3295
4
- data.tar.gz: 035ac59bb998b1268bf0803bce35b9c2ab75ac6f232a427e28996f0ea857f386
3
+ metadata.gz: 5dd31a57d864549b6a4e86c5e5aaa5e3dd295d0926647fa4d8c6b8c816dd16b9
4
+ data.tar.gz: dd15b4961127fdd387e2460a823d0f5bd71072b9bebcd2749ed5e4fcf7e1c4b5
5
5
  SHA512:
6
- metadata.gz: fd92b4d37f8a352e1acc98ae2bbbad05982248d3ef97dfb6b99edfe5173ba087a4259db3718e1e05c0b7951574652b657b694d2f02a5c34cf162d17fe427303a
7
- data.tar.gz: 10cf9b7446c6fbd09a2ffcfe8629be12c2dc334132d6515acd8015b0f22a7e06e01d704f42316730e096901889f189adcaff8730571df86e401e7767688c07f3
6
+ metadata.gz: 9f55937abd6245e83516aab63b939adb702a370170cc7828b3139e515e1a76221037cf2df8e79f82342694a5825f965a637cfb9cc8640e07d4f03d949c25d54f
7
+ data.tar.gz: e6dbb62382a1d358dde88f26e1488a4a12736c00989ee631b26426e733e20f11dcc9752af06d2f0c145065759157fa1be1ab0eb3a9b2298cd1de75923a9801cd
data/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## Master (Unreleased)
4
4
 
5
+ ## 2.23.0 (2023-05-15)
6
+
7
+ - Add `FactoryBot/FactoryAssociationWithStrategy` cop. ([@morissetcl])
8
+ - Mark `FactoryBot/CreateList` as `SafeAutoCorrect: false`. ([@r7kamura])
9
+ - Change `FactoryBot/CreateList` so that it considers `times.map`. ([@r7kamura])
10
+ - Add `FactoryBot/RedundantFactoryOption` cop. ([@r7kamura])
11
+ - Add `ExplicitOnly` configuration option to `FactoryBot/ConsistentParenthesesStyle`, `FactoryBot/CreateList` and `FactoryBot/FactoryNameStyle`. ([@ydah])
12
+ - Change `FactoryBot/CreateList` so that it checks same factory calls in an Array. ([@r7kamura])
13
+ - Add `FactoryBot/AssociationStyle` cop. ([@r7kamura])
14
+
5
15
  ## 2.22.0 (2023-05-04)
6
16
 
7
17
  - Extracted from `rubocop-rspec` into a separate repository for easier use with Minitest/Cucumber. ([@ydah])
@@ -57,6 +67,7 @@
57
67
  [@jonatas]: https://github.com/jonatas
58
68
  [@leoarnold]: https://github.com/leoarnold
59
69
  [@liberatys]: https://github.com/Liberatys
70
+ [@morissetcl]: https://github.com/morissetcl
60
71
  [@ngouy]: https://github.com/ngouy
61
72
  [@pirj]: https://github.com/pirj
62
73
  [@r7kamura]: https://github.com/r7kamura
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![Gem Version](https://badge.fury.io/rb/rubocop-factory_bot.svg)](https://rubygems.org/gems/rubocop-factory_bot)
5
5
  ![CI](https://github.com/rubocop/rubocop-factory_bot/workflows/CI/badge.svg)
6
6
 
7
- [Factory Bot](https://www.rubydoc.info/gems/factory_bot)-specific analysis for your projects, as an extension to
7
+ [factory_bot](https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md)-specific analysis for your projects, as an extension to
8
8
  [RuboCop](https://github.com/rubocop/rubocop).
9
9
 
10
10
  ## Installation
@@ -17,13 +17,13 @@ gem install rubocop-factory_bot
17
17
 
18
18
  or if you use bundler put this in your `Gemfile`
19
19
 
20
- ```
20
+ ```ruby
21
21
  gem 'rubocop-factory_bot', require: false
22
22
  ```
23
23
 
24
24
  ## Usage
25
25
 
26
- You need to tell RuboCop to load the Factory Bot extension. There are three
26
+ You need to tell RuboCop to load the factory_bot extension. There are three
27
27
  ways to do this:
28
28
 
29
29
  ### RuboCop configuration file
@@ -69,7 +69,7 @@ All cops are located under
69
69
  [`lib/rubocop/cop/factory_bot`](lib/rubocop/cop/factory_bot), and contain
70
70
  examples/documentation.
71
71
 
72
- In your `.rubocop.yml`, you may treat the Factory Bot cops just like any other
72
+ In your `.rubocop.yml`, you may treat the factory_bot cops just like any other
73
73
  cop. For example:
74
74
 
75
75
  ```yaml
data/config/default.yml CHANGED
@@ -8,6 +8,22 @@ FactoryBot:
8
8
  - "**/features/support/factories/**/*.rb"
9
9
  DocumentationBaseURL: https://docs.rubocop.org/rubocop-factory_bot
10
10
 
11
+ FactoryBot/AssociationStyle:
12
+ Description: Use a consistent style to define associations.
13
+ Enabled: pending
14
+ Safe: false
15
+ Include:
16
+ - spec/factories.rb
17
+ - spec/factories/**/*.rb
18
+ - features/support/factories/**/*.rb
19
+ VersionAdded: '2.23'
20
+ EnforcedStyle: implicit
21
+ SupportedStyles:
22
+ - explicit
23
+ - implicit
24
+ NonImplicitAssociationMethodNames: ~
25
+ Reference: https://www.rubydoc.info/gems/rubocop-factory_bot/RuboCop/Cop/FactoryBot/AssociationStyle
26
+
11
27
  FactoryBot/AttributeDefinedStatically:
12
28
  Description: Always declare attribute values as blocks.
13
29
  Enabled: true
@@ -20,13 +36,15 @@ FactoryBot/AttributeDefinedStatically:
20
36
  Reference: https://www.rubydoc.info/gems/rubocop-factory_bot/RuboCop/Cop/FactoryBot/AttributeDefinedStatically
21
37
 
22
38
  FactoryBot/ConsistentParenthesesStyle:
23
- Description: Use a consistent style for parentheses in factory bot calls.
39
+ Description: Use a consistent style for parentheses in factory_bot calls.
24
40
  Enabled: pending
25
41
  EnforcedStyle: require_parentheses
26
42
  SupportedStyles:
27
43
  - require_parentheses
28
44
  - omit_parentheses
45
+ ExplicitOnly: false
29
46
  VersionAdded: '2.14'
47
+ VersionChanged: '2.23'
30
48
  Reference: https://www.rubydoc.info/gems/rubocop-factory_bot/RuboCop/Cop/FactoryBot/ConsistentParenthesesStyle
31
49
 
32
50
  FactoryBot/CreateList:
@@ -42,10 +60,19 @@ FactoryBot/CreateList:
42
60
  SupportedStyles:
43
61
  - create_list
44
62
  - n_times
63
+ ExplicitOnly: false
64
+ SafeAutoCorrect: false
45
65
  VersionAdded: '1.25'
46
- VersionChanged: '2.0'
66
+ VersionChanged: '2.23'
47
67
  Reference: https://www.rubydoc.info/gems/rubocop-factory_bot/RuboCop/Cop/FactoryBot/CreateList
48
68
 
69
+ FactoryBot/FactoryAssociationWithStrategy:
70
+ Description: Use definition in factory association instead of hard coding a strategy.
71
+ Enabled: pending
72
+ VersionAdded: '2.23'
73
+ VersionChanged: '2.23'
74
+ Reference: https://www.rubydoc.info/gems/rubocop-factory_bot/RuboCop/Cop/FactoryBot/FactoryAssociationWithStrategy
75
+
49
76
  FactoryBot/FactoryClassName:
50
77
  Description: Use string value when setting the class attribute explicitly.
51
78
  Enabled: true
@@ -60,13 +87,21 @@ FactoryBot/FactoryClassName:
60
87
  FactoryBot/FactoryNameStyle:
61
88
  Description: Checks for name style for argument of FactoryBot::Syntax::Methods.
62
89
  Enabled: pending
63
- VersionAdded: '2.16'
64
90
  EnforcedStyle: symbol
65
91
  SupportedStyles:
66
92
  - symbol
67
93
  - string
94
+ ExplicitOnly: false
95
+ VersionAdded: '2.16'
96
+ VersionChanged: '2.23'
68
97
  Reference: https://www.rubydoc.info/gems/rubocop-factory_bot/RuboCop/Cop/FactoryBot/FactoryNameStyle
69
98
 
99
+ FactoryBot/RedundantFactoryOption:
100
+ Description: Checks for redundant `factory` option.
101
+ Enabled: pending
102
+ VersionAdded: '2.23'
103
+ Reference: https://www.rubydoc.info/gems/rubocop-factory_bot/RuboCop/Cop/FactoryBot/RedundantFactoryOption
104
+
70
105
  FactoryBot/SyntaxMethods:
71
106
  Description: Use shorthands from `FactoryBot::Syntax::Methods` in your specs.
72
107
  Enabled: pending
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module FactoryBot
6
+ # Use a consistent style to define associations.
7
+ #
8
+ # @safety
9
+ # This cop may cause false-positives in `EnforcedStyle: explicit`
10
+ # case. It recognizes any method call that has no arguments as an
11
+ # implicit association but it might be a user-defined trait call.
12
+ #
13
+ # @example EnforcedStyle: implicit (default)
14
+ # # bad
15
+ # factory :post do
16
+ # association :user
17
+ # end
18
+ #
19
+ # # good
20
+ # factory :post do
21
+ # user
22
+ # end
23
+ #
24
+ # @example EnforcedStyle: explicit
25
+ # # bad
26
+ # factory :post do
27
+ # user
28
+ # end
29
+ #
30
+ # # good
31
+ # factory :post do
32
+ # association :user
33
+ # end
34
+ #
35
+ # # good (NonImplicitAssociationMethodNames: ['email'])
36
+ # sequence :email do |n|
37
+ # "person#{n}@example.com"
38
+ # end
39
+ #
40
+ # factory :user do
41
+ # email
42
+ # end
43
+ class AssociationStyle < ::RuboCop::Cop::Base # rubocop:disable Metrics/ClassLength
44
+ extend AutoCorrector
45
+
46
+ include ConfigurableEnforcedStyle
47
+
48
+ DEFAULT_NON_IMPLICIT_ASSOCIATION_METHOD_NAMES = %w[
49
+ association
50
+ sequence
51
+ skip_create
52
+ traits_for_enum
53
+ ].freeze
54
+
55
+ RESTRICT_ON_SEND = %i[factory trait].freeze
56
+
57
+ def on_send(node)
58
+ bad_associations_in(node).each do |association|
59
+ add_offense(
60
+ association,
61
+ message: "Use #{style} style to define associations."
62
+ ) do |corrector|
63
+ autocorrect(corrector, association)
64
+ end
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ # @!method explicit_association?(node)
71
+ def_node_matcher :explicit_association?, <<~PATTERN
72
+ (send nil? :association sym ...)
73
+ PATTERN
74
+
75
+ # @!method implicit_association?(node)
76
+ def_node_matcher :implicit_association?, <<~PATTERN
77
+ (send nil? !#non_implicit_association_method_name? ...)
78
+ PATTERN
79
+
80
+ # @!method factory_option_matcher(node)
81
+ def_node_matcher :factory_option_matcher, <<~PATTERN
82
+ (send
83
+ nil?
84
+ :association
85
+ ...
86
+ (hash
87
+ <
88
+ (pair
89
+ (sym :factory)
90
+ {
91
+ (sym $_) |
92
+ (array (sym $_)*)
93
+ }
94
+ )
95
+ ...
96
+ >
97
+ )
98
+ )
99
+ PATTERN
100
+
101
+ # @!method trait_names_from_explicit(node)
102
+ def_node_matcher :trait_names_from_explicit, <<~PATTERN
103
+ (send nil? :association _ (sym $_)* ...)
104
+ PATTERN
105
+
106
+ def autocorrect(corrector, node)
107
+ if style == :explicit
108
+ autocorrect_to_explicit_style(corrector, node)
109
+ else
110
+ autocorrect_to_implicit_style(corrector, node)
111
+ end
112
+ end
113
+
114
+ def autocorrect_to_explicit_style(corrector, node)
115
+ arguments = [
116
+ ":#{node.method_name}",
117
+ *node.arguments.map(&:source)
118
+ ]
119
+ corrector.replace(node, "association #{arguments.join(', ')}")
120
+ end
121
+
122
+ def autocorrect_to_implicit_style(corrector, node)
123
+ source = node.first_argument.value.to_s
124
+ options = options_for_autocorrect_to_implicit_style(node)
125
+ unless options.empty?
126
+ rest = options.map { |option| option.join(': ') }.join(', ')
127
+ source += " #{rest}"
128
+ end
129
+ corrector.replace(node, source)
130
+ end
131
+
132
+ def bad?(node)
133
+ if style == :explicit
134
+ implicit_association?(node)
135
+ else
136
+ explicit_association?(node)
137
+ end
138
+ end
139
+
140
+ def bad_associations_in(node)
141
+ children_of_factory_block(node).select do |child|
142
+ bad?(child)
143
+ end
144
+ end
145
+
146
+ def children_of_factory_block(node)
147
+ block = node.parent
148
+ return [] unless block
149
+ return [] unless block.body
150
+
151
+ if block.body.begin_type?
152
+ block.body.children
153
+ else
154
+ [block.body]
155
+ end
156
+ end
157
+
158
+ def factory_names_from_explicit(node)
159
+ trait_names = trait_names_from_explicit(node)
160
+ factory_names = Array(factory_option_matcher(node))
161
+ result = factory_names + trait_names
162
+ if factory_names.empty? && !trait_names.empty?
163
+ result.prepend(node.first_argument.value)
164
+ end
165
+ result
166
+ end
167
+
168
+ def non_implicit_association_method_name?(method_name)
169
+ non_implicit_association_method_names.include?(method_name.to_s)
170
+ end
171
+
172
+ def non_implicit_association_method_names
173
+ DEFAULT_NON_IMPLICIT_ASSOCIATION_METHOD_NAMES +
174
+ (cop_config['NonImplicitAssociationMethodNames'] || [])
175
+ end
176
+
177
+ def options_from_explicit(node)
178
+ return {} unless node.last_argument.hash_type?
179
+
180
+ node.last_argument.pairs.inject({}) do |options, pair|
181
+ options.merge(pair.key.value => pair.value.source)
182
+ end
183
+ end
184
+
185
+ def options_for_autocorrect_to_implicit_style(node)
186
+ options = options_from_explicit(node)
187
+ factory_names = factory_names_from_explicit(node)
188
+ unless factory_names.empty?
189
+ options[:factory] = "%i[#{factory_names.join(' ')}]"
190
+ end
191
+ options
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
@@ -30,13 +30,13 @@ module RuboCop
30
30
  MSG = 'Use a block to declare attribute values.'
31
31
 
32
32
  # @!method value_matcher(node)
33
- def_node_matcher :value_matcher, <<-PATTERN
34
- (send _ !#reserved_method? $...)
33
+ def_node_matcher :value_matcher, <<~PATTERN
34
+ (send _ !#reserved_method? $...)
35
35
  PATTERN
36
36
 
37
37
  # @!method factory_attributes(node)
38
- def_node_matcher :factory_attributes, <<-PATTERN
39
- (block (send _ #attribute_defining_method? ...) _ { (begin $...) $(send ...) } )
38
+ def_node_matcher :factory_attributes, <<~PATTERN
39
+ (block (send _ #attribute_defining_method? ...) _ { (begin $...) $(send ...) } )
40
40
  PATTERN
41
41
 
42
42
  def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
@@ -3,31 +3,27 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module FactoryBot
6
- # Use a consistent style for parentheses in factory bot calls.
6
+ # Use a consistent style for parentheses in factory_bot calls.
7
7
  #
8
- # @example
8
+ # @example `EnforcedStyle: require_parentheses` (default)
9
9
  #
10
10
  # # bad
11
11
  # create :user
12
- # build(:user)
13
- # create(:login)
14
- # create :login
15
- #
16
- # @example `EnforcedStyle: require_parentheses` (default)
12
+ # build :login
17
13
  #
18
14
  # # good
19
15
  # create(:user)
20
- # create(:user)
21
- # create(:login)
22
16
  # build(:login)
23
17
  #
24
18
  # @example `EnforcedStyle: omit_parentheses`
25
19
  #
20
+ # # bad
21
+ # create(:user)
22
+ # build(:login)
23
+ #
26
24
  # # good
27
25
  # create :user
28
- # build :user
29
- # create :login
30
- # create :login
26
+ # build :login
31
27
  #
32
28
  # # also good
33
29
  # # when method name and first argument are not on same line
@@ -39,49 +35,67 @@ module RuboCop
39
35
  # name: 'foo'
40
36
  # )
41
37
  #
38
+ # @example `ExplicitOnly: false` (default)
39
+ #
40
+ # # bad - with `EnforcedStyle: require_parentheses`
41
+ # FactoryBot.create :user
42
+ # build :user
43
+ #
44
+ # # good - with `EnforcedStyle: require_parentheses`
45
+ # FactoryBot.create(:user)
46
+ # build(:user)
47
+ #
48
+ # @example `ExplicitOnly: true`
49
+ #
50
+ # # bad - with `EnforcedStyle: require_parentheses`
51
+ # FactoryBot.create :user
52
+ # FactoryBot.build :user
53
+ #
54
+ # # good - with `EnforcedStyle: require_parentheses`
55
+ # FactoryBot.create(:user)
56
+ # FactoryBot.build(:user)
57
+ # create :user
58
+ # build :user
59
+ #
42
60
  class ConsistentParenthesesStyle < ::RuboCop::Cop::Base
43
61
  extend AutoCorrector
44
62
  include ConfigurableEnforcedStyle
45
- include RuboCop::FactoryBot::Language
46
- include RuboCop::Cop::Util
47
-
48
- def self.autocorrect_incompatible_with
49
- [Style::MethodCallWithArgsParentheses]
50
- end
63
+ include ConfigurableExplicitOnly
51
64
 
52
65
  MSG_REQUIRE_PARENS = 'Prefer method call with parentheses'
53
66
  MSG_OMIT_PARENS = 'Prefer method call without parentheses'
54
-
55
67
  FACTORY_CALLS = RuboCop::FactoryBot::Language::METHODS
56
-
57
68
  RESTRICT_ON_SEND = FACTORY_CALLS
58
69
 
59
70
  # @!method factory_call(node)
60
- def_node_matcher :factory_call, <<-PATTERN
61
- (send
62
- {#factory_bot? nil?} %FACTORY_CALLS
63
- {sym str send lvar} _*
64
- )
71
+ def_node_matcher :factory_call, <<~PATTERN
72
+ (send
73
+ #factory_call? %FACTORY_CALLS
74
+ {sym str send lvar} _*
75
+ )
65
76
  PATTERN
66
77
 
78
+ def self.autocorrect_incompatible_with
79
+ [Style::MethodCallWithArgsParentheses]
80
+ end
81
+
67
82
  def on_send(node)
68
83
  return if ambiguous_without_parentheses?(node)
69
84
 
70
- factory_call(node) do
71
- return if node.method?(:generate) && node.arguments.count > 1
72
-
73
- if node.parenthesized?
74
- process_with_parentheses(node)
75
- else
76
- process_without_parentheses(node)
77
- end
78
- end
85
+ factory_call(node) { register_offense(node) }
79
86
  end
80
87
 
81
88
  private
82
89
 
83
- def process_with_parentheses(node)
84
- return unless style == :omit_parentheses
90
+ def register_offense(node)
91
+ return if node.method?(:generate) && node.arguments.count > 1
92
+
93
+ register_offense_with_parentheses(node)
94
+ register_offense_without_parentheses(node)
95
+ end
96
+
97
+ def register_offense_with_parentheses(node)
98
+ return if style == :require_parentheses || !node.parenthesized?
85
99
  return unless same_line?(node, node.first_argument)
86
100
 
87
101
  add_offense(node.loc.selector,
@@ -90,8 +104,8 @@ module RuboCop
90
104
  end
91
105
  end
92
106
 
93
- def process_without_parentheses(node)
94
- return unless style == :require_parentheses
107
+ def register_offense_without_parentheses(node)
108
+ return if style == :omit_parentheses || node.parenthesized?
95
109
 
96
110
  add_offense(node.loc.selector,
97
111
  message: MSG_REQUIRE_PARENS) do |corrector|
@@ -7,9 +7,16 @@ module RuboCop
7
7
  #
8
8
  # This cop can be configured using the `EnforcedStyle` option
9
9
  #
10
+ # @safety
11
+ # This cop's autocorrection is unsafe because replacing `n.times` to
12
+ # `create_list` changes its returned value.
13
+ #
10
14
  # @example `EnforcedStyle: create_list` (default)
11
15
  # # bad
12
16
  # 3.times { create :user }
17
+ # 3.times.map { create :user }
18
+ # [create(:user), create(:user), create(:user)]
19
+ # Array.new(3) { create :user }
13
20
  #
14
21
  # # good
15
22
  # create_list :user, 3
@@ -26,58 +33,105 @@ module RuboCop
26
33
  # @example `EnforcedStyle: n_times`
27
34
  # # bad
28
35
  # create_list :user, 3
36
+ # [create(:user), create(:user), create(:user)]
29
37
  #
30
38
  # # good
39
+ # 3.times.map { create :user }
40
+ #
41
+ # @example `ExplicitOnly: false` (default)
42
+ #
43
+ # # bad - with `EnforcedStyle: create_list`
44
+ # 3.times { FactoryBot.create :user }
45
+ # 3.times { create :user }
46
+ #
47
+ # # good - with `EnforcedStyle: create_list`
48
+ # FactoryBot.create_list :user, 3
49
+ # create_list :user, 3
50
+ #
51
+ # @example `ExplicitOnly: true`
52
+ #
53
+ # # bad - with `EnforcedStyle: create_list`
54
+ # 3.times { FactoryBot.create :user }
55
+ #
56
+ # # good - with `EnforcedStyle: create_list`
57
+ # FactoryBot.create_list :user, 3
58
+ # create_list :user, 3
31
59
  # 3.times { create :user }
32
60
  #
33
61
  class CreateList < ::RuboCop::Cop::Base
34
62
  extend AutoCorrector
35
63
  include ConfigurableEnforcedStyle
36
64
  include RuboCop::FactoryBot::Language
65
+ include ConfigurableExplicitOnly
37
66
 
38
67
  MSG_CREATE_LIST = 'Prefer create_list.'
39
- MSG_N_TIMES = 'Prefer %<number>s.times.'
68
+ MSG_N_TIMES = 'Prefer %<number>s.times.map.'
40
69
  RESTRICT_ON_SEND = %i[create_list].freeze
41
70
 
42
- # @!method array_new_or_n_times_block?(node)
43
- def_node_matcher :array_new_or_n_times_block?, <<-PATTERN
44
- (block
45
- {
46
- (send (const {nil? | cbase} :Array) :new (int _)) |
47
- (send (int _) :times)
48
- }
49
- ...
50
- )
71
+ # @!method repetition_block?(node)
72
+ def_node_matcher :repetition_block?, <<-PATTERN
73
+ (block {#array_new? #n_times? #n_times_map?} ...)
74
+ PATTERN
75
+
76
+ # @!method array_new?(node)
77
+ def_node_matcher :array_new?, <<-PATTERN
78
+ (send (const {nil? cbase} :Array) :new (int _))
79
+ PATTERN
80
+
81
+ # @!method n_times?(node)
82
+ def_node_matcher :n_times?, <<-PATTERN
83
+ (send (int _) :times)
84
+ PATTERN
85
+
86
+ # @!method n_times_map?(node)
87
+ def_node_matcher :n_times_map?, <<-PATTERN
88
+ (send #n_times? :map)
51
89
  PATTERN
52
90
 
53
91
  # @!method block_with_arg_and_used?(node)
54
- def_node_matcher :block_with_arg_and_used?, <<-PATTERN
55
- (block
56
- _
57
- (args (arg _value))
58
- `_value
59
- )
92
+ def_node_matcher :block_with_arg_and_used?, <<~PATTERN
93
+ (block
94
+ _
95
+ (args (arg _value))
96
+ `_value
97
+ )
60
98
  PATTERN
61
99
 
62
100
  # @!method arguments_include_method_call?(node)
63
- def_node_matcher :arguments_include_method_call?, <<-PATTERN
64
- (send ${nil? #factory_bot?} :create (sym $_) `$(send ...))
101
+ def_node_matcher :arguments_include_method_call?, <<~PATTERN
102
+ (send #factory_call? :create sym `$(send ...))
65
103
  PATTERN
66
104
 
67
105
  # @!method factory_call(node)
68
- def_node_matcher :factory_call, <<-PATTERN
69
- (send ${nil? #factory_bot?} :create (sym $_) $...)
106
+ def_node_matcher :factory_call, <<~PATTERN
107
+ (send #factory_call? :create sym ...)
70
108
  PATTERN
71
109
 
72
110
  # @!method factory_list_call(node)
73
- def_node_matcher :factory_list_call, <<-PATTERN
74
- (send {nil? #factory_bot?} :create_list (sym _) (int $_) ...)
111
+ def_node_matcher :factory_list_call, <<~PATTERN
112
+ (send #factory_call? :create_list (sym _) (int $_) ...)
75
113
  PATTERN
76
114
 
115
+ # @!method factory_calls_in_array?(node)
116
+ def_node_search :factory_calls_in_array?, <<-PATTERN
117
+ (array #factory_call+)
118
+ PATTERN
119
+
120
+ def on_array(node)
121
+ return unless same_factory_calls_in_array?(node)
122
+
123
+ add_offense(
124
+ node,
125
+ message: preferred_message_for_array(node)
126
+ ) do |corrector|
127
+ autocorrect_same_factory_calls_in_array(corrector, node)
128
+ end
129
+ end
130
+
77
131
  def on_block(node) # rubocop:todo InternalAffairs/NumblockHandler
78
132
  return unless style == :create_list
79
133
 
80
- return unless array_new_or_n_times_block?(node)
134
+ return unless repetition_block?(node)
81
135
  return if block_with_arg_and_used?(node)
82
136
  return unless node.body
83
137
  return if arguments_include_method_call?(node.body)
@@ -101,6 +155,20 @@ module RuboCop
101
155
 
102
156
  private
103
157
 
158
+ # For ease of modification, it is replaced with the `n_times` style,
159
+ # but if it is not appropriate for the configured style,
160
+ # it will be replaced in the subsequent autocorrection.
161
+ def autocorrect_same_factory_calls_in_array(corrector, node)
162
+ corrector.replace(
163
+ node,
164
+ format(
165
+ '%<count>s.times.map { %<factory_call>s }',
166
+ count: node.children.count,
167
+ factory_call: node.children.first.source
168
+ )
169
+ )
170
+ end
171
+
104
172
  def contains_only_factory?(node)
105
173
  if node.block_type?
106
174
  factory_call(node.send_node)
@@ -109,6 +177,20 @@ module RuboCop
109
177
  end
110
178
  end
111
179
 
180
+ def preferred_message_for_array(node)
181
+ if style == :create_list &&
182
+ !arguments_include_method_call?(node.children.first)
183
+ MSG_CREATE_LIST
184
+ else
185
+ format(MSG_N_TIMES, number: node.children.count)
186
+ end
187
+ end
188
+
189
+ def same_factory_calls_in_array?(node)
190
+ factory_calls_in_array?(node) &&
191
+ node.children.map(&:source).uniq.one?
192
+ end
193
+
112
194
  # :nodoc
113
195
  module Corrector
114
196
  private
@@ -159,7 +241,7 @@ module RuboCop
159
241
  replacement = format_receiver(node.receiver)
160
242
  replacement += format_method_call(node, 'create', arguments)
161
243
  replacement += " #{factory_call_block_source}" if node.block_node
162
- "#{count.source}.times { #{replacement} }"
244
+ "#{count.source}.times.map { #{replacement} }"
163
245
  end
164
246
 
165
247
  def factory_call_block_source
@@ -224,10 +306,13 @@ module RuboCop
224
306
 
225
307
  def count_from(node)
226
308
  count_node =
227
- if node.receiver.int_type?
228
- node.receiver
229
- else
309
+ case node.method_name
310
+ when :map
311
+ node.receiver.receiver
312
+ when :new
230
313
  node.send_node.first_argument
314
+ when :times
315
+ node.receiver
231
316
  end
232
317
  count_node.source
233
318
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module FactoryBot
6
+ # Use definition in factory association instead of hard coding a strategy.
7
+ #
8
+ # @example
9
+ # # bad - only works for one strategy
10
+ # factory :foo do
11
+ # profile { create(:profile) }
12
+ # end
13
+ #
14
+ # # good - implicit
15
+ # factory :foo do
16
+ # profile
17
+ # end
18
+ #
19
+ # # good - explicit
20
+ # factory :foo do
21
+ # association :profile
22
+ # end
23
+ #
24
+ # # good - inline
25
+ # factory :foo do
26
+ # profile { association :profile }
27
+ # end
28
+ #
29
+ class FactoryAssociationWithStrategy < ::RuboCop::Cop::Base
30
+ MSG = 'Use an implicit, explicit or inline definition instead of ' \
31
+ 'hard coding a strategy for setting association within factory.'
32
+
33
+ HARDCODED = Set.new(%i[create build build_stubbed]).freeze
34
+
35
+ # @!method factory_declaration(node)
36
+ def_node_matcher :factory_declaration, <<~PATTERN
37
+ (block (send nil? {:factory :trait} ...)
38
+ ...
39
+ )
40
+ PATTERN
41
+
42
+ # @!method factory_strategy_association(node)
43
+ def_node_matcher :factory_strategy_association, <<~PATTERN
44
+ (block
45
+ (send nil? _association_name)
46
+ (args)
47
+ < $(send nil? HARDCODED ...) ... >
48
+ )
49
+ PATTERN
50
+
51
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
52
+ factory_declaration(node) do
53
+ node.each_node do |statement|
54
+ factory_strategy_association(statement) do |hardcoded_association|
55
+ add_offense(hardcoded_association)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -23,19 +23,42 @@ module RuboCop
23
23
  # create('user')
24
24
  # build "user", username: "NAME"
25
25
  #
26
+ # @example `ExplicitOnly: false` (default)
27
+ #
28
+ # # bad - with `EnforcedStyle: symbol`
29
+ # FactoryBot.create('user')
30
+ # create('user')
31
+ #
32
+ # # good - with `EnforcedStyle: symbol`
33
+ # FactoryBot.create(:user)
34
+ # create(:user)
35
+ #
36
+ # @example `ExplicitOnly: true`
37
+ #
38
+ # # bad - with `EnforcedStyle: symbol`
39
+ # FactoryBot.create(:user)
40
+ # FactoryBot.build "user", username: "NAME"
41
+ #
42
+ # # good - with `EnforcedStyle: symbol`
43
+ # FactoryBot.create('user')
44
+ # FactoryBot.build "user", username: "NAME"
45
+ # FactoryBot.create(:user)
46
+ # create(:user)
47
+ #
26
48
  class FactoryNameStyle < ::RuboCop::Cop::Base
27
49
  extend AutoCorrector
28
50
  include ConfigurableEnforcedStyle
29
51
  include RuboCop::FactoryBot::Language
52
+ include ConfigurableExplicitOnly
30
53
 
31
54
  MSG = 'Use %<prefer>s to refer to a factory.'
32
55
  FACTORY_CALLS = RuboCop::FactoryBot::Language::METHODS
33
56
  RESTRICT_ON_SEND = FACTORY_CALLS
34
57
 
35
58
  # @!method factory_call(node)
36
- def_node_matcher :factory_call, <<-PATTERN
59
+ def_node_matcher :factory_call, <<~PATTERN
37
60
  (send
38
- {#factory_bot? nil?} %FACTORY_CALLS
61
+ #factory_call? %FACTORY_CALLS
39
62
  ${str sym} ...
40
63
  )
41
64
  PATTERN
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module FactoryBot
6
+ # Handles `ExplicitOnly` configuration parameters.
7
+ module ConfigurableExplicitOnly
8
+ include RuboCop::FactoryBot::Language
9
+
10
+ def factory_call?(node)
11
+ return factory_bot?(node) if explicit_only?
12
+
13
+ factory_bot?(node) || node.nil?
14
+ end
15
+
16
+ def explicit_only?
17
+ cop_config['ExplicitOnly']
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module FactoryBot
6
+ # Checks for redundant `factory` option.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # association :user, factory: :user
11
+ #
12
+ # # good
13
+ # association :user
14
+ class RedundantFactoryOption < ::RuboCop::Cop::Base
15
+ extend AutoCorrector
16
+
17
+ include RangeHelp
18
+
19
+ MSG = 'Remove redundant `factory` option.'
20
+
21
+ RESTRICT_ON_SEND = %i[association].freeze
22
+
23
+ # @!method association_with_a_factory_option(node)
24
+ def_node_matcher :association_with_a_factory_option, <<~PATTERN
25
+ (send nil? :association
26
+ (sym $_association_name)
27
+ ...
28
+ (hash
29
+ <$(pair
30
+ (sym :factory)
31
+ {
32
+ (sym $_factory_name)
33
+ (array (sym $_factory_name))
34
+ }
35
+ )
36
+ ...
37
+ >
38
+ )
39
+ )
40
+ PATTERN
41
+
42
+ def on_send(node)
43
+ association_with_a_factory_option(node) do
44
+ |association_name, factory_option, factory_name|
45
+ next if association_name != factory_name
46
+
47
+ add_offense(factory_option) do |corrector|
48
+ autocorrect(corrector, factory_option)
49
+ end
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def autocorrect(corrector, node)
56
+ corrector.remove(
57
+ range_with_surrounding_comma(
58
+ range_with_surrounding_space(
59
+ node.source_range,
60
+ side: :left
61
+ ),
62
+ :left
63
+ )
64
+ )
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'factory_bot/association_style'
3
4
  require_relative 'factory_bot/attribute_defined_statically'
4
5
  require_relative 'factory_bot/consistent_parentheses_style'
5
6
  require_relative 'factory_bot/create_list'
7
+ require_relative 'factory_bot/factory_association_with_strategy'
6
8
  require_relative 'factory_bot/factory_class_name'
7
9
  require_relative 'factory_bot/factory_name_style'
10
+ require_relative 'factory_bot/redundant_factory_option'
8
11
  require_relative 'factory_bot/syntax_methods'
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RuboCop
4
- # RuboCop FactoryBot project namespace
4
+ # RuboCop factory_bot project namespace
5
5
  module FactoryBot
6
6
  ATTRIBUTE_DEFINING_METHODS = %i[
7
7
  factory
@@ -2,7 +2,7 @@
2
2
 
3
3
  module RuboCop
4
4
  module FactoryBot
5
- # Contains node matchers for common FactoryBot DSL.
5
+ # Contains node matchers for common factory_bot DSL.
6
6
  module Language
7
7
  extend RuboCop::NodePattern::Macros
8
8
 
@@ -2,9 +2,9 @@
2
2
 
3
3
  module RuboCop
4
4
  module FactoryBot
5
- # Version information for the FactoryBot RuboCop plugin.
5
+ # Version information for the factory_bot RuboCop plugin.
6
6
  module Version
7
- STRING = '2.22.0'
7
+ STRING = '2.23.0'
8
8
  end
9
9
  end
10
10
  end
@@ -8,6 +8,8 @@ require 'rubocop'
8
8
  require_relative 'rubocop/factory_bot/factory_bot'
9
9
  require_relative 'rubocop/factory_bot/language'
10
10
 
11
+ require_relative 'rubocop/cop/factory_bot/mixin/configurable_explicit_only'
12
+
11
13
  require_relative 'rubocop/cop/factory_bot_cops'
12
14
 
13
15
  project_root = File.join(__dir__, '..')
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-factory_bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.22.0
4
+ version: 2.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Backus
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2023-05-05 00:00:00.000000000 Z
15
+ date: 2023-05-15 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: rubocop
@@ -28,9 +28,9 @@ dependencies:
28
28
  - - "~>"
29
29
  - !ruby/object:Gem::Version
30
30
  version: '1.33'
31
- description: |2
32
- Code style checking for FactoryBot files.
33
- A plugin for the RuboCop code style enforcing & linting tool.
31
+ description: |
32
+ Code style checking for factory_bot files.
33
+ A plugin for the RuboCop code style enforcing & linting tool.
34
34
  email:
35
35
  executables: []
36
36
  extensions: []
@@ -44,11 +44,15 @@ files:
44
44
  - README.md
45
45
  - config/default.yml
46
46
  - lib/rubocop-factory_bot.rb
47
+ - lib/rubocop/cop/factory_bot/association_style.rb
47
48
  - lib/rubocop/cop/factory_bot/attribute_defined_statically.rb
48
49
  - lib/rubocop/cop/factory_bot/consistent_parentheses_style.rb
49
50
  - lib/rubocop/cop/factory_bot/create_list.rb
51
+ - lib/rubocop/cop/factory_bot/factory_association_with_strategy.rb
50
52
  - lib/rubocop/cop/factory_bot/factory_class_name.rb
51
53
  - lib/rubocop/cop/factory_bot/factory_name_style.rb
54
+ - lib/rubocop/cop/factory_bot/mixin/configurable_explicit_only.rb
55
+ - lib/rubocop/cop/factory_bot/redundant_factory_option.rb
52
56
  - lib/rubocop/cop/factory_bot/syntax_methods.rb
53
57
  - lib/rubocop/cop/factory_bot_cops.rb
54
58
  - lib/rubocop/factory_bot/config_formatter.rb
@@ -78,8 +82,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
78
82
  - !ruby/object:Gem::Version
79
83
  version: '0'
80
84
  requirements: []
81
- rubygems_version: 3.3.3
85
+ rubygems_version: 3.4.5
82
86
  signing_key:
83
87
  specification_version: 4
84
- summary: Code style checking for FactoryBot files
88
+ summary: Code style checking for factory_bot files
85
89
  test_files: []