/
multiple_expectations.rb
123 lines (103 loc) · 3.36 KB
/
multiple_expectations.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
# frozen_string_literal: true
module RuboCop
module Cop
module RSpec
# Checks if examples contain too many `expect` calls.
#
# @see http://betterspecs.org/#single Single expectation test
#
# This cop is configurable using the `Max` option
# and works with `--auto-gen-config`.
#
# @example
#
# # bad
# describe UserCreator do
# it 'builds a user' do
# expect(user.name).to eq("John")
# expect(user.age).to eq(22)
# end
# end
#
# # good
# describe UserCreator do
# it 'sets the users name' do
# expect(user.name).to eq("John")
# end
#
# it 'sets the users age' do
# expect(user.age).to eq(22)
# end
# end
#
# @example configuration
#
# # .rubocop.yml
# # RSpec/MultipleExpectations:
# # Max: 2
#
# # not flagged by rubocop
# describe UserCreator do
# it 'builds a user' do
# expect(user.name).to eq("John")
# expect(user.age).to eq(22)
# end
# end
#
class MultipleExpectations < Cop
include ConfigurableMax
MSG = 'Example has too many expectations [%<total>d/%<max>d].'
ANYTHING = ->(_) { true }
def_node_matcher :aggregate_failures?, <<-PATTERN
(block {
(send _ _ <(sym :aggregate_failures) ...>)
(send _ _ ... (hash <(pair (sym :aggregate_failures) %1) ...>))
} ...)
PATTERN
def_node_matcher :expect?, Expectations::ALL.send_pattern
def_node_matcher :aggregate_failures_block?, <<-PATTERN
(block (send nil? :aggregate_failures ...) ...)
PATTERN
def on_block(node)
return unless example?(node)
return if example_with_aggregate_failures?(node)
expectations_count = to_enum(:find_expectation, node).count
return if expectations_count <= max_expectations
self.max = expectations_count
flag_example(node, expectation_count: expectations_count)
end
private
def example_with_aggregate_failures?(example_node)
node_with_aggregate_failures = find_aggregate_failures(example_node)
return false unless node_with_aggregate_failures
aggregate_failures?(node_with_aggregate_failures, :true_type?.to_proc)
end
def find_aggregate_failures(example_node)
example_node.send_node.each_ancestor(:block)
.find { |block_node| aggregate_failures?(block_node, ANYTHING) }
end
def find_expectation(node, &block)
yield if expect?(node) || aggregate_failures_block?(node)
# do not search inside of aggregate_failures block
return if aggregate_failures_block?(node)
node.each_child_node do |child|
find_expectation(child, &block)
end
end
def flag_example(node, expectation_count:)
add_offense(
node.send_node,
message: format(
MSG,
total: expectation_count,
max: max_expectations
)
)
end
def max_expectations
Integer(cop_config.fetch('Max', 1))
end
end
end
end
end