Skip to content

Commit a2ef166

Browse files
authored
Merge pull request #863 from mockdeep/rf-no_let
Add MultipleMemoizedHelpers cop
2 parents e1b688e + 986537f commit a2ef166

File tree

9 files changed

+427
-1
lines changed

9 files changed

+427
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* Improve `RSpec/NestedGroups`, `RSpec/FilePath`, `RSpec/DescribeMethod`, `RSpec/MultipleDescribes`, `RSpec/DescribeClass`'s top-level example group detection. ([@pirj][])
1212
* Add detection of `let!` with a block-pass or a string literal to `RSpec/LetSetup`. ([@pirj][])
1313
* Add `IgnoredPatterns` configuration option to `RSpec/VariableName`. ([@jtannas][])
14+
* Add `RSpec/MultipleMemoizedHelpers` cop. ([@mockdeep][])
1415

1516
## 1.42.0 (2020-07-09)
1617

@@ -542,3 +543,4 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
542543
[@elliterate]: https://github.com/elliterate
543544
[@mlarraz]: https://github.com/mlarraz
544545
[@jtannas]: https://github.com/jtannas
546+
[@mockdeep]: https://github.com/mockdeep

config/default.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,13 @@ RSpec/MultipleExpectations:
394394
VersionChanged: '1.21'
395395
StyleGuide: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/MultipleExpectations
396396

397+
RSpec/MultipleMemoizedHelpers:
398+
Description: Checks if example groups contain too many `let` and `subject` calls.
399+
Enabled: true
400+
AllowSubject: true
401+
Max: 5
402+
StyleGuide: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/MultipleMemoizedHelpers
403+
397404
RSpec/MultipleSubjects:
398405
Description: Checks if an example group defines `subject` multiple times.
399406
Enabled: true
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module RSpec
6+
# Checks if example groups contain too many `let` and `subject` calls.
7+
#
8+
# This cop is configurable using the `Max` option and the `AllowSubject`
9+
# which will configure the cop to only register offenses on calls to
10+
# `let` and not calls to `subject`.
11+
#
12+
# @example
13+
# # bad
14+
# describe MyClass do
15+
# let(:foo) { [] }
16+
# let(:bar) { [] }
17+
# let!(:baz) { [] }
18+
# let(:qux) { [] }
19+
# let(:quux) { [] }
20+
# let(:quuz) { {} }
21+
# end
22+
#
23+
# describe MyClass do
24+
# let(:foo) { [] }
25+
# let(:bar) { [] }
26+
# let!(:baz) { [] }
27+
#
28+
# context 'when stuff' do
29+
# let(:qux) { [] }
30+
# let(:quux) { [] }
31+
# let(:quuz) { {} }
32+
# end
33+
# end
34+
#
35+
# # good
36+
# describe MyClass do
37+
# let(:bar) { [] }
38+
# let!(:baz) { [] }
39+
# let(:qux) { [] }
40+
# let(:quux) { [] }
41+
# let(:quuz) { {} }
42+
# end
43+
#
44+
# describe MyClass do
45+
# context 'when stuff' do
46+
# let(:foo) { [] }
47+
# let(:bar) { [] }
48+
# let!(:booger) { [] }
49+
# end
50+
#
51+
# context 'when other stuff' do
52+
# let(:qux) { [] }
53+
# let(:quux) { [] }
54+
# let(:quuz) { {} }
55+
# end
56+
# end
57+
#
58+
# @example when disabling AllowSubject configuration
59+
#
60+
# # rubocop.yml
61+
# # RSpec/MultipleMemoizedHelpers:
62+
# # AllowSubject: false
63+
#
64+
# # bad - `subject` counts towards memoized helpers
65+
# describe MyClass do
66+
# subject { {} }
67+
# let(:foo) { [] }
68+
# let(:bar) { [] }
69+
# let!(:baz) { [] }
70+
# let(:qux) { [] }
71+
# let(:quux) { [] }
72+
# end
73+
#
74+
# @example with Max configuration
75+
#
76+
# # rubocop.yml
77+
# # RSpec/MultipleMemoizedHelpers:
78+
# # Max: 1
79+
#
80+
# # bad
81+
# describe MyClass do
82+
# let(:foo) { [] }
83+
# let(:bar) { [] }
84+
# end
85+
#
86+
class MultipleMemoizedHelpers < Base
87+
include ConfigurableMax
88+
include RuboCop::RSpec::Variable
89+
90+
MSG = 'Example group has too many memoized helpers [%<count>d/%<max>d]'
91+
92+
def on_block(node)
93+
return unless spec_group?(node)
94+
95+
count = all_helpers(node).uniq.count
96+
97+
return if count <= max
98+
99+
self.max = count
100+
add_offense(node, message: format(MSG, count: count, max: max))
101+
end
102+
103+
def on_new_investigation
104+
@example_group_memoized_helpers = {}
105+
end
106+
107+
private
108+
109+
attr_reader :example_group_memoized_helpers
110+
111+
def all_helpers(node)
112+
[
113+
*helpers(node),
114+
*node.each_ancestor(:block).flat_map(&method(:helpers))
115+
]
116+
end
117+
118+
def helpers(node)
119+
@example_group_memoized_helpers[node] ||=
120+
variable_nodes(node).map do |variable_node|
121+
if variable_node.block_type?
122+
variable_definition?(variable_node.send_node)
123+
else # block-pass (`let(:foo, &bar)`)
124+
variable_definition?(variable_node)
125+
end
126+
end
127+
end
128+
129+
def variable_nodes(node)
130+
example_group = RuboCop::RSpec::ExampleGroup.new(node)
131+
if allow_subject?
132+
example_group.lets
133+
else
134+
example_group.lets + example_group.subjects
135+
end
136+
end
137+
138+
def max
139+
cop_config['Max']
140+
end
141+
142+
def allow_subject?
143+
cop_config['AllowSubject']
144+
end
145+
end
146+
end
147+
end
148+
end

lib/rubocop/cop/rspec_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
require_relative 'rspec/missing_example_group_argument'
6666
require_relative 'rspec/multiple_describes'
6767
require_relative 'rspec/multiple_expectations'
68+
require_relative 'rspec/multiple_memoized_helpers'
6869
require_relative 'rspec/multiple_subjects'
6970
require_relative 'rspec/named_subject'
7071
require_relative 'rspec/nested_groups'

lib/rubocop/rspec/variable.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ module Variable
88
extend RuboCop::NodePattern::Macros
99

1010
def_node_matcher :variable_definition?, <<~PATTERN
11-
(send #rspec? #{(Helpers::ALL + Subject::ALL).node_pattern_union}
11+
(send nil? #{(Helpers::ALL + Subject::ALL).node_pattern_union}
1212
$({sym str dsym dstr} ...) ...)
1313
PATTERN
1414
end

manual/cops.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
* [RSpec/MissingExampleGroupArgument](cops_rspec.md#rspecmissingexamplegroupargument)
6565
* [RSpec/MultipleDescribes](cops_rspec.md#rspecmultipledescribes)
6666
* [RSpec/MultipleExpectations](cops_rspec.md#rspecmultipleexpectations)
67+
* [RSpec/MultipleMemoizedHelpers](cops_rspec.md#rspecmultiplememoizedhelpers)
6768
* [RSpec/MultipleSubjects](cops_rspec.md#rspecmultiplesubjects)
6869
* [RSpec/NamedSubject](cops_rspec.md#rspecnamedsubject)
6970
* [RSpec/NestedGroups](cops_rspec.md#rspecnestedgroups)

manual/cops_rspec.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2119,6 +2119,108 @@ Max | `1` | Integer
21192119

21202120
* [https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/MultipleExpectations](https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/MultipleExpectations)
21212121

2122+
## RSpec/MultipleMemoizedHelpers
2123+
2124+
Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
2125+
--- | --- | --- | --- | ---
2126+
Enabled | Yes | No | - | -
2127+
2128+
Checks if example groups contain too many `let` and `subject` calls.
2129+
2130+
This cop is configurable using the `Max` option and the `AllowSubject`
2131+
which will configure the cop to only register offenses on calls to
2132+
`let` and not calls to `subject`.
2133+
2134+
### Examples
2135+
2136+
```ruby
2137+
# bad
2138+
describe MyClass do
2139+
let(:foo) { [] }
2140+
let(:bar) { [] }
2141+
let!(:baz) { [] }
2142+
let(:qux) { [] }
2143+
let(:quux) { [] }
2144+
let(:quuz) { {} }
2145+
end
2146+
2147+
describe MyClass do
2148+
let(:foo) { [] }
2149+
let(:bar) { [] }
2150+
let!(:baz) { [] }
2151+
2152+
context 'when stuff' do
2153+
let(:qux) { [] }
2154+
let(:quux) { [] }
2155+
let(:quuz) { {} }
2156+
end
2157+
end
2158+
2159+
# good
2160+
describe MyClass do
2161+
let(:bar) { [] }
2162+
let!(:baz) { [] }
2163+
let(:qux) { [] }
2164+
let(:quux) { [] }
2165+
let(:quuz) { {} }
2166+
end
2167+
2168+
describe MyClass do
2169+
context 'when stuff' do
2170+
let(:foo) { [] }
2171+
let(:bar) { [] }
2172+
let!(:booger) { [] }
2173+
end
2174+
2175+
context 'when other stuff' do
2176+
let(:qux) { [] }
2177+
let(:quux) { [] }
2178+
let(:quuz) { {} }
2179+
end
2180+
end
2181+
```
2182+
#### when disabling AllowSubject configuration
2183+
2184+
```ruby
2185+
# rubocop.yml
2186+
# RSpec/MultipleMemoizedHelpers:
2187+
# AllowSubject: false
2188+
2189+
# bad - `subject` counts towards memoized helpers
2190+
describe MyClass do
2191+
subject { {} }
2192+
let(:foo) { [] }
2193+
let(:bar) { [] }
2194+
let!(:baz) { [] }
2195+
let(:qux) { [] }
2196+
let(:quux) { [] }
2197+
end
2198+
```
2199+
#### with Max configuration
2200+
2201+
```ruby
2202+
# rubocop.yml
2203+
# RSpec/MultipleMemoizedHelpers:
2204+
# Max: 1
2205+
2206+
# bad
2207+
describe MyClass do
2208+
let(:foo) { [] }
2209+
let(:bar) { [] }
2210+
end
2211+
```
2212+
2213+
### Configurable attributes
2214+
2215+
Name | Default value | Configurable values
2216+
--- | --- | ---
2217+
AllowSubject | `true` | Boolean
2218+
Max | `5` | Integer
2219+
2220+
### References
2221+
2222+
* [https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/MultipleMemoizedHelpers](https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/MultipleMemoizedHelpers)
2223+
21222224
## RSpec/MultipleSubjects
21232225

21242226
Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged

0 commit comments

Comments
 (0)