rubocop-factory_bot 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module FactoryBot
6
+ # Checks for create_list usage.
7
+ #
8
+ # This cop can be configured using the `EnforcedStyle` option
9
+ #
10
+ # @example `EnforcedStyle: create_list` (default)
11
+ # # bad
12
+ # 3.times { create :user }
13
+ #
14
+ # # good
15
+ # create_list :user, 3
16
+ #
17
+ # # bad
18
+ # 3.times { create :user, age: 18 }
19
+ #
20
+ # # good - index is used to alter the created models attributes
21
+ # 3.times { |n| create :user, age: n }
22
+ #
23
+ # # good - contains a method call, may return different values
24
+ # 3.times { create :user, age: rand }
25
+ #
26
+ # @example `EnforcedStyle: n_times`
27
+ # # bad
28
+ # create_list :user, 3
29
+ #
30
+ # # good
31
+ # 3.times { create :user }
32
+ #
33
+ class CreateList < ::RuboCop::Cop::Base
34
+ extend AutoCorrector
35
+ include ConfigurableEnforcedStyle
36
+ include RuboCop::FactoryBot::Language
37
+
38
+ MSG_CREATE_LIST = 'Prefer create_list.'
39
+ MSG_N_TIMES = 'Prefer %<number>s.times.'
40
+ RESTRICT_ON_SEND = %i[create_list].freeze
41
+
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
+ )
51
+ PATTERN
52
+
53
+ # @!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
+ )
60
+ PATTERN
61
+
62
+ # @!method arguments_include_method_call?(node)
63
+ def_node_matcher :arguments_include_method_call?, <<-PATTERN
64
+ (send ${nil? #factory_bot?} :create (sym $_) `$(send ...))
65
+ PATTERN
66
+
67
+ # @!method factory_call(node)
68
+ def_node_matcher :factory_call, <<-PATTERN
69
+ (send ${nil? #factory_bot?} :create (sym $_) $...)
70
+ PATTERN
71
+
72
+ # @!method factory_list_call(node)
73
+ def_node_matcher :factory_list_call, <<-PATTERN
74
+ (send {nil? #factory_bot?} :create_list (sym _) (int $_) ...)
75
+ PATTERN
76
+
77
+ def on_block(node) # rubocop:todo InternalAffairs/NumblockHandler
78
+ return unless style == :create_list
79
+
80
+ return unless array_new_or_n_times_block?(node)
81
+ return if block_with_arg_and_used?(node)
82
+ return unless node.body
83
+ return if arguments_include_method_call?(node.body)
84
+ return unless contains_only_factory?(node.body)
85
+
86
+ add_offense(node.send_node, message: MSG_CREATE_LIST) do |corrector|
87
+ CreateListCorrector.new(node.send_node).call(corrector)
88
+ end
89
+ end
90
+
91
+ def on_send(node)
92
+ return unless style == :n_times
93
+
94
+ factory_list_call(node) do |count|
95
+ message = format(MSG_N_TIMES, number: count)
96
+ add_offense(node.loc.selector, message: message) do |corrector|
97
+ TimesCorrector.new(node).call(corrector)
98
+ end
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ def contains_only_factory?(node)
105
+ if node.block_type?
106
+ factory_call(node.send_node)
107
+ else
108
+ factory_call(node)
109
+ end
110
+ end
111
+
112
+ # :nodoc
113
+ module Corrector
114
+ private
115
+
116
+ def build_options_string(options)
117
+ options.map(&:source).join(', ')
118
+ end
119
+
120
+ def format_method_call(node, method, arguments)
121
+ if node.block_type? || node.parenthesized?
122
+ "#{method}(#{arguments})"
123
+ else
124
+ "#{method} #{arguments}"
125
+ end
126
+ end
127
+
128
+ def format_receiver(receiver)
129
+ return '' unless receiver
130
+
131
+ "#{receiver.source}."
132
+ end
133
+ end
134
+
135
+ # :nodoc
136
+ class TimesCorrector
137
+ include Corrector
138
+
139
+ def initialize(node)
140
+ @node = node
141
+ end
142
+
143
+ def call(corrector)
144
+ replacement = generate_n_times_block(node)
145
+ corrector.replace(node.block_node || node, replacement)
146
+ end
147
+
148
+ private
149
+
150
+ attr_reader :node
151
+
152
+ def generate_n_times_block(node)
153
+ factory, count, *options = node.arguments
154
+
155
+ arguments = factory.source
156
+ options = build_options_string(options)
157
+ arguments += ", #{options}" unless options.empty?
158
+
159
+ replacement = format_receiver(node.receiver)
160
+ replacement += format_method_call(node, 'create', arguments)
161
+ replacement += " #{factory_call_block_source}" if node.block_node
162
+ "#{count.source}.times { #{replacement} }"
163
+ end
164
+
165
+ def factory_call_block_source
166
+ node.block_node.location.begin.with(
167
+ end_pos: node.block_node.location.end.end_pos
168
+ ).source
169
+ end
170
+ end
171
+
172
+ # :nodoc:
173
+ class CreateListCorrector
174
+ include Corrector
175
+
176
+ def initialize(node)
177
+ @node = node.parent
178
+ end
179
+
180
+ def call(corrector)
181
+ replacement = if node.body.block_type?
182
+ call_with_block_replacement(node)
183
+ else
184
+ call_replacement(node)
185
+ end
186
+
187
+ corrector.replace(node, replacement)
188
+ end
189
+
190
+ private
191
+
192
+ attr_reader :node
193
+
194
+ def call_with_block_replacement(node)
195
+ block = node.body
196
+ arguments = build_arguments(block, count_from(node))
197
+ replacement = format_receiver(block.receiver)
198
+ replacement += format_method_call(block, 'create_list', arguments)
199
+ replacement += format_block(block)
200
+ replacement
201
+ end
202
+
203
+ def build_arguments(node, count)
204
+ factory, *options = *node.send_node.arguments
205
+
206
+ arguments = ":#{factory.value}, #{count}"
207
+ options = build_options_string(options)
208
+ arguments += ", #{options}" unless options.empty?
209
+ arguments
210
+ end
211
+
212
+ def call_replacement(node)
213
+ block = node.body
214
+ factory, *options = *block.arguments
215
+
216
+ arguments = "#{factory.source}, #{count_from(node)}"
217
+ options = build_options_string(options)
218
+ arguments += ", #{options}" unless options.empty?
219
+
220
+ replacement = format_receiver(block.receiver)
221
+ replacement += format_method_call(block, 'create_list', arguments)
222
+ replacement
223
+ end
224
+
225
+ def count_from(node)
226
+ count_node =
227
+ if node.receiver.int_type?
228
+ node.receiver
229
+ else
230
+ node.send_node.first_argument
231
+ end
232
+ count_node.source
233
+ end
234
+
235
+ def format_block(node)
236
+ if node.body.begin_type?
237
+ format_multiline_block(node)
238
+ else
239
+ format_singleline_block(node)
240
+ end
241
+ end
242
+
243
+ def format_multiline_block(node)
244
+ indent = ' ' * node.body.loc.column
245
+ indent_end = ' ' * node.parent.loc.column
246
+ " do #{node.arguments.source}\n" \
247
+ "#{indent}#{node.body.source}\n" \
248
+ "#{indent_end}end"
249
+ end
250
+
251
+ def format_singleline_block(node)
252
+ " { #{node.arguments.source} #{node.body.source} }"
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module FactoryBot
6
+ # Use string value when setting the class attribute explicitly.
7
+ #
8
+ # This cop would promote faster tests by lazy-loading of
9
+ # application files. Also, this could help you suppress potential bugs
10
+ # in combination with external libraries by avoiding a preload of
11
+ # application files from the factory files.
12
+ #
13
+ # @example
14
+ # # bad
15
+ # factory :foo, class: Foo do
16
+ # end
17
+ #
18
+ # # good
19
+ # factory :foo, class: 'Foo' do
20
+ # end
21
+ #
22
+ class FactoryClassName < ::RuboCop::Cop::Base
23
+ extend AutoCorrector
24
+
25
+ MSG = "Pass '%<class_name>s' string instead of `%<class_name>s` " \
26
+ 'constant.'
27
+ ALLOWED_CONSTANTS = %w[Hash OpenStruct].freeze
28
+ RESTRICT_ON_SEND = %i[factory].freeze
29
+
30
+ # @!method class_name(node)
31
+ def_node_matcher :class_name, <<~PATTERN
32
+ (send _ :factory _ (hash <(pair (sym :class) $(const ...)) ...>))
33
+ PATTERN
34
+
35
+ def on_send(node)
36
+ class_name(node) do |cn|
37
+ next if allowed?(cn.const_name)
38
+
39
+ msg = format(MSG, class_name: cn.const_name)
40
+ add_offense(cn, message: msg) do |corrector|
41
+ corrector.replace(cn, "'#{cn.source}'")
42
+ end
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def allowed?(const_name)
49
+ ALLOWED_CONSTANTS.include?(const_name)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module FactoryBot
6
+ # Checks for name style for argument of FactoryBot::Syntax::Methods.
7
+ #
8
+ # @example EnforcedStyle: symbol (default)
9
+ # # bad
10
+ # create('user')
11
+ # build "user", username: "NAME"
12
+ #
13
+ # # good
14
+ # create(:user)
15
+ # build :user, username: "NAME"
16
+ #
17
+ # @example EnforcedStyle: string
18
+ # # bad
19
+ # create(:user)
20
+ # build :user, username: "NAME"
21
+ #
22
+ # # good
23
+ # create('user')
24
+ # build "user", username: "NAME"
25
+ #
26
+ class FactoryNameStyle < ::RuboCop::Cop::Base
27
+ extend AutoCorrector
28
+ include ConfigurableEnforcedStyle
29
+ include RuboCop::FactoryBot::Language
30
+
31
+ MSG = 'Use %<prefer>s to refer to a factory.'
32
+ FACTORY_CALLS = RuboCop::FactoryBot::Language::METHODS
33
+ RESTRICT_ON_SEND = FACTORY_CALLS
34
+
35
+ # @!method factory_call(node)
36
+ def_node_matcher :factory_call, <<-PATTERN
37
+ (send
38
+ {#factory_bot? nil?} %FACTORY_CALLS
39
+ ${str sym} ...
40
+ )
41
+ PATTERN
42
+
43
+ def on_send(node)
44
+ factory_call(node) do |name|
45
+ if offense_for_symbol_style?(name)
46
+ register_offense(name, name.value.to_sym.inspect)
47
+ elsif offense_for_string_style?(name)
48
+ register_offense(name, name.value.to_s.inspect)
49
+ end
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def offense_for_symbol_style?(name)
56
+ name.str_type? && style == :symbol
57
+ end
58
+
59
+ def offense_for_string_style?(name)
60
+ name.sym_type? && style == :string
61
+ end
62
+
63
+ def register_offense(name, prefer)
64
+ add_offense(name,
65
+ message: format(MSG, prefer: style.to_s)) do |corrector|
66
+ corrector.replace(name, prefer)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module FactoryBot
6
+ # Use shorthands from `FactoryBot::Syntax::Methods` in your specs.
7
+ #
8
+ # @safety
9
+ # The autocorrection is marked as unsafe because the cop
10
+ # cannot verify whether you already include
11
+ # `FactoryBot::Syntax::Methods` in your test suite.
12
+ #
13
+ # If you're using Rails, add the following configuration to
14
+ # `spec/support/factory_bot.rb` and be sure to require that file in
15
+ # `rails_helper.rb`:
16
+ #
17
+ # [source,ruby]
18
+ # ----
19
+ # RSpec.configure do |config|
20
+ # config.include FactoryBot::Syntax::Methods
21
+ # end
22
+ # ----
23
+ #
24
+ # If you're not using Rails:
25
+ #
26
+ # [source,ruby]
27
+ # ----
28
+ # RSpec.configure do |config|
29
+ # config.include FactoryBot::Syntax::Methods
30
+ #
31
+ # config.before(:suite) do
32
+ # FactoryBot.find_definitions
33
+ # end
34
+ # end
35
+ # ----
36
+ #
37
+ # @example
38
+ # # bad
39
+ # FactoryBot.create(:bar)
40
+ # FactoryBot.build(:bar)
41
+ # FactoryBot.attributes_for(:bar)
42
+ #
43
+ # # good
44
+ # create(:bar)
45
+ # build(:bar)
46
+ # attributes_for(:bar)
47
+ #
48
+ class SyntaxMethods < ::RuboCop::Cop::Base
49
+ extend AutoCorrector
50
+ include RangeHelp
51
+ include RuboCop::FactoryBot::Language
52
+
53
+ MSG = 'Use `%<method>s` from `FactoryBot::Syntax::Methods`.'
54
+
55
+ RESTRICT_ON_SEND = RuboCop::FactoryBot::Language::METHODS
56
+
57
+ # @!method spec_group?(node)
58
+ def_node_matcher :spec_group?, <<~PATTERN
59
+ (block
60
+ (send
61
+ {(const {nil? cbase} :RSpec) nil?}
62
+ {
63
+ :describe :context :feature :example_group
64
+ :xdescribe :xcontext :xfeature
65
+ :fdescribe :fcontext :ffeature
66
+ :shared_examples :shared_examples_for
67
+ :shared_context
68
+ }
69
+ ...)
70
+ ...)
71
+ PATTERN
72
+
73
+ def on_send(node)
74
+ return unless factory_bot?(node.receiver)
75
+
76
+ return unless inside_example_group?(node)
77
+
78
+ message = format(MSG, method: node.method_name)
79
+
80
+ add_offense(crime_scene(node), message: message) do |corrector|
81
+ corrector.remove(offense(node))
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def crime_scene(node)
88
+ range_between(
89
+ node.source_range.begin_pos,
90
+ node.loc.selector.end_pos
91
+ )
92
+ end
93
+
94
+ def offense(node)
95
+ range_between(
96
+ node.source_range.begin_pos,
97
+ node.loc.selector.begin_pos
98
+ )
99
+ end
100
+
101
+ def inside_example_group?(node)
102
+ return spec_group?(node) if example_group_root?(node)
103
+
104
+ root = node.ancestors.find { |parent| example_group_root?(parent) }
105
+
106
+ spec_group?(root)
107
+ end
108
+
109
+ def example_group_root?(node)
110
+ node.parent.nil? || example_group_root_with_siblings?(node.parent)
111
+ end
112
+
113
+ def example_group_root_with_siblings?(node)
114
+ node.begin_type? && node.parent.nil?
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'factory_bot/attribute_defined_statically'
4
+ require_relative 'factory_bot/consistent_parentheses_style'
5
+ require_relative 'factory_bot/create_list'
6
+ require_relative 'factory_bot/factory_class_name'
7
+ require_relative 'factory_bot/factory_name_style'
8
+ require_relative 'factory_bot/syntax_methods'
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module RuboCop
6
+ module FactoryBot
7
+ # Builds a YAML config file from two config hashes
8
+ class ConfigFormatter
9
+ EXTENSION_ROOT_DEPARTMENT = %r{^(FactoryBot/)}.freeze
10
+ SUBDEPARTMENTS = [].freeze
11
+ AMENDMENTS = [].freeze
12
+ COP_DOC_BASE_URL = 'https://www.rubydoc.info/gems/rubocop-factory_bot/RuboCop/Cop/'
13
+
14
+ def initialize(config, descriptions)
15
+ @config = config
16
+ @descriptions = descriptions
17
+ end
18
+
19
+ def dump
20
+ YAML.dump(unified_config)
21
+ .gsub(EXTENSION_ROOT_DEPARTMENT, "\n\\1")
22
+ .gsub(/^(\s+)- /, '\1 - ')
23
+ .gsub('"~"', '~')
24
+ end
25
+
26
+ private
27
+
28
+ def unified_config
29
+ cops.each_with_object(config.dup) do |cop, unified|
30
+ next if SUBDEPARTMENTS.include?(cop) || AMENDMENTS.include?(cop)
31
+
32
+ replace_nil(unified[cop])
33
+ unified[cop].merge!(descriptions.fetch(cop))
34
+ unified[cop]['Reference'] = reference(cop)
35
+ end
36
+ end
37
+
38
+ def cops
39
+ (descriptions.keys | config.keys).grep(EXTENSION_ROOT_DEPARTMENT)
40
+ end
41
+
42
+ def replace_nil(config)
43
+ config.each do |key, value|
44
+ config[key] = '~' if value.nil?
45
+ end
46
+ end
47
+
48
+ def reference(cop)
49
+ COP_DOC_BASE_URL + cop
50
+ end
51
+
52
+ attr_reader :config, :descriptions
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module FactoryBot
5
+ # Extracts cop descriptions from YARD docstrings
6
+ class DescriptionExtractor
7
+ def initialize(yardocs)
8
+ @code_objects = yardocs.map(&CodeObject.public_method(:new))
9
+ end
10
+
11
+ def to_h
12
+ code_objects
13
+ .select(&:cop?)
14
+ .map(&:configuration)
15
+ .reduce(:merge)
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :code_objects
21
+
22
+ # Decorator of a YARD code object for working with documented cops
23
+ class CodeObject
24
+ RUBOCOP_COP_CLASS_NAME = 'RuboCop::Cop::Base'
25
+
26
+ def initialize(yardoc)
27
+ @yardoc = yardoc
28
+ end
29
+
30
+ # Test if the YARD code object documents a concrete cop class
31
+ #
32
+ # @return [Boolean]
33
+ def cop?
34
+ cop_subclass? && !abstract?
35
+ end
36
+
37
+ # Configuration for the documented cop that would live in default.yml
38
+ #
39
+ # @return [Hash]
40
+ def configuration
41
+ { cop_name => { 'Description' => description } }
42
+ end
43
+
44
+ private
45
+
46
+ def cop_name
47
+ Object.const_get(documented_constant).cop_name
48
+ end
49
+
50
+ def description
51
+ yardoc.docstring.split("\n\n").first.to_s
52
+ end
53
+
54
+ def documented_constant
55
+ yardoc.to_s
56
+ end
57
+
58
+ def cop_subclass?
59
+ yardoc.superclass.path == RUBOCOP_COP_CLASS_NAME
60
+ end
61
+
62
+ def abstract?
63
+ yardoc.tags.any? { |tag| tag.tag_name.eql?('abstract') }
64
+ end
65
+
66
+ attr_reader :yardoc
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ # RuboCop FactoryBot project namespace
5
+ module FactoryBot
6
+ ATTRIBUTE_DEFINING_METHODS = %i[
7
+ factory
8
+ ignore
9
+ trait
10
+ traits_for_enum
11
+ transient
12
+ ].freeze
13
+
14
+ UNPROXIED_METHODS = %i[
15
+ __send__
16
+ __id__
17
+ nil?
18
+ send
19
+ object_id
20
+ extend
21
+ instance_eval
22
+ initialize
23
+ block_given?
24
+ raise
25
+ caller
26
+ method
27
+ ].freeze
28
+
29
+ DEFINITION_PROXY_METHODS = %i[
30
+ add_attribute
31
+ after
32
+ association
33
+ before
34
+ callback
35
+ ignore
36
+ initialize_with
37
+ sequence
38
+ skip_create
39
+ to_create
40
+ ].freeze
41
+
42
+ RESERVED_METHODS =
43
+ DEFINITION_PROXY_METHODS +
44
+ UNPROXIED_METHODS +
45
+ ATTRIBUTE_DEFINING_METHODS
46
+
47
+ private_constant(
48
+ :ATTRIBUTE_DEFINING_METHODS,
49
+ :UNPROXIED_METHODS,
50
+ :DEFINITION_PROXY_METHODS,
51
+ :RESERVED_METHODS
52
+ )
53
+
54
+ def self.attribute_defining_methods
55
+ ATTRIBUTE_DEFINING_METHODS
56
+ end
57
+
58
+ def self.reserved_methods
59
+ RESERVED_METHODS
60
+ end
61
+ end
62
+ end