rubocop-factory_bot 2.22.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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