File: named_subject.rb

package info (click to toggle)
ruby-rubocop-rspec 2.16.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 1,892 kB
  • sloc: ruby: 22,283; makefile: 4
file content (151 lines) | stat: -rw-r--r-- 4,315 bytes parent folder | download
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# frozen_string_literal: true

module RuboCop
  module Cop
    module RSpec
      # Checks for explicitly referenced test subjects.
      #
      # RSpec lets you declare an "implicit subject" using `subject { ... }`
      # which allows for tests like `it { is_expected.to be_valid }`.
      # If you need to reference your test subject you should explicitly
      # name it using `subject(:your_subject_name) { ... }`. Your test subjects
      # should be the most important object in your tests so they deserve
      # a descriptive name.
      #
      # This cop can be configured in your configuration using `EnforcedStyle`,
      # and `IgnoreSharedExamples` which will not report offenses for implicit
      # subjects in shared example groups.
      #
      # @example `EnforcedStyle: always` (default)
      #   # bad
      #   RSpec.describe User do
      #     subject { described_class.new }
      #
      #     it 'is valid' do
      #       expect(subject.valid?).to be(true)
      #     end
      #   end
      #
      #   # good
      #   RSpec.describe User do
      #     subject(:user) { described_class.new }
      #
      #     it 'is valid' do
      #       expect(user.valid?).to be(true)
      #     end
      #   end
      #
      #   # also good
      #   RSpec.describe User do
      #     subject(:user) { described_class.new }
      #
      #     it { is_expected.to be_valid }
      #   end
      #
      # @example `EnforcedStyle: named_only`
      #   # bad
      #   RSpec.describe User do
      #     subject(:user) { described_class.new }
      #
      #     it 'is valid' do
      #       expect(subject.valid?).to be(true)
      #     end
      #   end
      #
      #   # good
      #   RSpec.describe User do
      #     subject(:user) { described_class.new }
      #
      #     it 'is valid' do
      #       expect(user.valid?).to be(true)
      #     end
      #   end
      #
      #   # also good
      #   RSpec.describe User do
      #     subject { described_class.new }
      #
      #     it { is_expected.to be_valid }
      #   end
      #
      #   # acceptable
      #   RSpec.describe User do
      #     subject { described_class.new }
      #
      #     it 'is valid' do
      #       expect(subject.valid?).to be(true)
      #     end
      #   end
      class NamedSubject < Base
        include ConfigurableEnforcedStyle

        MSG = 'Name your test subject if you need to reference it explicitly.'

        # @!method example_or_hook_block?(node)
        def_node_matcher :example_or_hook_block?,
                         block_pattern('{#Examples.all #Hooks.all}')

        # @!method shared_example?(node)
        def_node_matcher :shared_example?,
                         block_pattern('#SharedGroups.examples')

        # @!method subject_usage(node)
        def_node_search :subject_usage, '$(send nil? :subject)'

        def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
          if !example_or_hook_block?(node) || ignored_shared_example?(node)
            return
          end

          subject_usage(node) do |subject_node|
            check_explicit_subject(subject_node)
          end
        end

        private

        def ignored_shared_example?(node)
          cop_config['IgnoreSharedExamples'] &&
            node.each_ancestor(:block).any?(&method(:shared_example?))
        end

        def check_explicit_subject(node)
          return if allow_explicit_subject?(node)

          add_offense(node.loc.selector)
        end

        def allow_explicit_subject?(node)
          !always? && !named_only?(node)
        end

        def always?
          style == :always
        end

        def named_only?(node)
          style == :named_only &&
            subject_definition_is_named?(node)
        end

        def subject_definition_is_named?(node)
          subject = nearest_subject(node)

          subject&.send_node&.arguments?
        end

        def nearest_subject(node)
          node
            .each_ancestor(:block)
            .lazy
            .map { |block_node| find_subject(block_node) }
            .find(&:itself)
        end

        def find_subject(block_node)
          block_node.body.child_nodes.find { |send_node| subject?(send_node) }
        end
      end
    end
  end
end