summaryrefslogtreecommitdiff
path: root/rubocop/cop/rspec/factory_bot/inline_association.rb
blob: ccc6364fb738d441c50af69ca6d667d9c447260d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# 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::Base
          extend RuboCop::Cop::AutoCorrector

          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)) 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