diff options
Diffstat (limited to 'rubocop/cop/rspec/factory_bot/inline_association.rb')
-rw-r--r-- | rubocop/cop/rspec/factory_bot/inline_association.rb | 109 |
1 files changed, 109 insertions, 0 deletions
diff --git a/rubocop/cop/rspec/factory_bot/inline_association.rb b/rubocop/cop/rspec/factory_bot/inline_association.rb new file mode 100644 index 00000000000..1c2b8b55b46 --- /dev/null +++ b/rubocop/cop/rspec/factory_bot/inline_association.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module RSpec + module FactoryBot + # This cop encourages the use of inline associations in FactoryBot. + # The explicit use of `create` and `build` is discouraged. + # + # See https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#inline-definition + # + # @example + # + # Context: + # + # Factory.define do + # factory :project, class: 'Project' + # # EXAMPLE below + # end + # end + # + # # bad + # creator { create(:user) } + # creator { create(:user, :admin) } + # creator { build(:user) } + # creator { FactoryBot.build(:user) } + # creator { ::FactoryBot.build(:user) } + # add_attribute(:creator) { build(:user) } + # + # # good + # creator { association(:user) } + # creator { association(:user, :admin) } + # add_attribute(:creator) { association(:user) } + # + # # Accepted + # after(:build) do |instance| + # instance.creator = create(:user) + # end + # + # initialize_with do + # create(:project) + # end + # + # creator_id { create(:user).id } + # + class InlineAssociation < RuboCop::Cop::Cop + MSG = 'Prefer inline `association` over `%{type}`. ' \ + 'See https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#factories' + + REPLACEMENT = 'association' + + def_node_matcher :create_or_build, <<~PATTERN + ( + send + ${ nil? (const { nil? (cbase) } :FactoryBot) } + ${ :create :build } + (sym _) + ... + ) + PATTERN + + def_node_matcher :association_definition, <<~PATTERN + (block + { + (send nil? $_) + (send nil? :add_attribute (sym $_)) + } + ... + ) + PATTERN + + def_node_matcher :chained_call?, <<~PATTERN + (send _ _) + PATTERN + + SKIP_NAMES = %i[initialize_with].to_set.freeze + + def on_send(node) + _receiver, type = create_or_build(node) + return unless type + return if chained_call?(node.parent) + return unless inside_assocation_definition?(node) + + add_offense(node, message: format(MSG, type: type)) + end + + def autocorrect(node) + lambda do |corrector| + receiver, type = create_or_build(node) + receiver = "#{receiver.source}." if receiver + expression = "#{receiver}#{type}" + replacement = node.source.sub(expression, REPLACEMENT) + corrector.replace(node.source_range, replacement) + end + end + + private + + def inside_assocation_definition?(node) + node.each_ancestor(:block).any? do |parent| + name = association_definition(parent) + name && !SKIP_NAMES.include?(name) + end + end + end + end + end + end +end |