-
-
Notifications
You must be signed in to change notification settings - Fork 270
/
repeated_subject_call.rb
125 lines (107 loc) · 3.47 KB
/
repeated_subject_call.rb
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
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks for repeated calls to subject missing that it is memoized.
#
# @example
# # bad
# it do
# subject
# expect { subject }.to not_change { A.count }
# end
#
# it do
# expect { subject }.to change { A.count }
# expect { subject }.to not_change { A.count }
# end
#
# # good
# it do
# expect { my_method }.to change { A.count }
# expect { my_method }.to not_change { A.count }
# end
#
# # also good
# it do
# expect { subject.a }.to change { A.count }
# expect { subject.b }.to not_change { A.count }
# end
#
class RepeatedSubjectCall < Base
include TopLevelGroup
MSG = 'Calls to subject are memoized, this block is misleading'
# @!method subject?(node)
# Find a named or unnamed subject definition
#
# @example anonymous subject
# subject?(parse('subject { foo }').ast) do |name|
# name # => :subject
# end
#
# @example named subject
# subject?(parse('subject(:thing) { foo }').ast) do |name|
# name # => :thing
# end
#
# @param node [RuboCop::AST::Node]
#
# @yield [Symbol] subject name
def_node_matcher :subject?, <<-PATTERN
(block
(send nil?
{ #Subjects.all (sym $_) | $#Subjects.all }
) args ...)
PATTERN
# @!method subject_calls(node, method_name)
def_node_search :subject_calls, <<~PATTERN
(send nil? %)
PATTERN
def on_top_level_group(node)
@subjects_by_node = detect_subjects_in_scope(node)
detect_offenses_in_block(node)
end
private
def detect_offense(subject_node)
return if subject_node.chained?
return if subject_node.parent.send_type?
return unless (block_node = expect_block(subject_node))
add_offense(block_node)
end
def expect_block(node)
node.each_ancestor(:block).find { |block| block.method?(:expect) }
end
def detect_offenses_in_block(node, subject_names = [])
subject_names = [*subject_names, *@subjects_by_node[node]]
if example?(node)
return detect_offenses_in_example(node, subject_names)
end
node.each_child_node(:send, :def, :block, :begin) do |child|
detect_offenses_in_block(child, subject_names)
end
end
def detect_offenses_in_example(node, subject_names)
return unless node.body
subjects_used = Hash.new(false)
subject_calls(node.body, Set[*subject_names, :subject]).each do |call|
if subjects_used[call.method_name]
detect_offense(call)
else
subjects_used[call.method_name] = true
end
end
end
def detect_subjects_in_scope(node)
node.each_descendant(:block).with_object({}) do |child, h|
subject?(child) do |name|
outer_example_group = child.each_ancestor(:block).find do |a|
example_group?(a)
end
(h[outer_example_group] ||= []) << name
end
end
end
end
end
end
end