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
|