Skip to content

Commit 376e30c

Browse files
authored
Merge pull request #851 from pirj/fix-scattered-setup-cop
Add metadata detection to RSpec/ScatteredSetup cop
2 parents 1c81a96 + 1c7a360 commit 376e30c

File tree

6 files changed

+159
-45
lines changed

6 files changed

+159
-45
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Master (Unreleased)
44

55
* Fix `RSpec/InstanceVariable` detection inside custom matchers. ([@pirj][])
6+
* Fix `RSpec/ScatteredSetup` to distinguish hooks with different metadata. ([@pirj][])
67

78
## 1.37.1 (2019-12-16)
89

lib/rubocop/cop/rspec/scattered_setup.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ def on_block(node)
3636
def analyzable_hooks(node)
3737
RuboCop::RSpec::ExampleGroup.new(node)
3838
.hooks
39-
.select { |hook| hook.knowable_scope? && hook.valid_scope? }
40-
.group_by { |hook| [hook.name, hook.scope] }
39+
.select(&:knowable_scope?)
40+
.group_by { |hook| [hook.name, hook.scope, hook.metadata] }
4141
.values
4242
.reject(&:one?)
4343
.flatten

lib/rubocop/rspec/hook.rb

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,72 @@ module RuboCop
44
module RSpec
55
# Wrapper for RSpec hook
66
class Hook < Concept
7-
STANDARDIZED_SCOPES = %i[each context suite].freeze
8-
private_constant(:STANDARDIZED_SCOPES)
7+
def_node_matcher :extract_metadata, <<~PATTERN
8+
(block
9+
{
10+
(send _ _ #valid_scope? $...)
11+
(send _ _ $...)
12+
}
13+
...
14+
)
15+
PATTERN
916

1017
def name
1118
node.method_name
1219
end
1320

1421
def knowable_scope?
15-
return true unless scope_argument
16-
17-
scope_argument.sym_type?
18-
end
19-
20-
def valid_scope?
21-
STANDARDIZED_SCOPES.include?(scope)
22+
scope_argument.nil? ||
23+
scope_argument.sym_type? ||
24+
scope_argument.hash_type?
2225
end
2326

2427
def example?
2528
scope.equal?(:each)
2629
end
2730

2831
def scope
32+
return :each if scope_argument&.hash_type?
33+
2934
case scope_name
3035
when nil, :each, :example then :each
3136
when :context, :all then :context
3237
when :suite then :suite
33-
else
34-
scope_name
3538
end
3639
end
3740

41+
def metadata
42+
(extract_metadata(node) || [])
43+
.map { |meta| transform_metadata(meta) }
44+
.flatten
45+
.inject(&:merge)
46+
end
47+
3848
private
3949

50+
def valid_scope?(node)
51+
node&.sym_type? && Hooks::Scopes::ALL.include?(node.value)
52+
end
53+
54+
def transform_metadata(meta)
55+
if meta.sym_type?
56+
{ meta => true }
57+
else
58+
# This check is to be able to compare those two hooks:
59+
#
60+
# before(:example, :special) { ... }
61+
# before(:example, special: true) { ... }
62+
#
63+
# In the second case it's a node with a pair that has a value
64+
# of a `true_type?`.
65+
meta.pairs.map { |pair| { pair.key => transform_true(pair.value) } }
66+
end
67+
end
68+
69+
def transform_true(node)
70+
node.true_type? ? true : node
71+
end
72+
4073
def scope_name
4174
scope_argument.to_a.first
4275
end

lib/rubocop/rspec/language.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,18 @@ module Hooks
9494
append_after
9595
]
9696
)
97+
98+
module Scopes
99+
ALL = SelectorSet.new(
100+
%i[
101+
each
102+
example
103+
context
104+
all
105+
suite
106+
]
107+
)
108+
end
97109
end
98110

99111
module Helpers

spec/rubocop/cop/rspec/scattered_setup_spec.rb

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
RUBY
3939
end
4040

41-
it 'does not flag different hooks' do
41+
it 'ignores different hooks' do
4242
expect_no_offenses(<<-RUBY)
4343
describe Foo do
4444
before { bar }
@@ -48,7 +48,7 @@
4848
RUBY
4949
end
5050

51-
it 'does not flag different hook types' do
51+
it 'ignores different hook types' do
5252
expect_no_offenses(<<-RUBY)
5353
describe Foo do
5454
before { bar }
@@ -58,7 +58,7 @@
5858
RUBY
5959
end
6060

61-
it 'does not flag hooks in different example groups' do
61+
it 'ignores hooks in different example groups' do
6262
expect_no_offenses(<<-RUBY)
6363
describe Foo do
6464
before { bar }
@@ -70,7 +70,7 @@
7070
RUBY
7171
end
7272

73-
it 'does not flag hooks in different shared contexts' do
73+
it 'ignores hooks in different shared contexts' do
7474
expect_no_offenses(<<-RUBY)
7575
describe Foo do
7676
shared_context 'one' do
@@ -84,7 +84,7 @@
8484
RUBY
8585
end
8686

87-
it 'does not flag similar method names inside of examples' do
87+
it 'ignores similar method names inside of examples' do
8888
expect_no_offenses(<<-RUBY)
8989
describe Foo do
9090
before { bar }
@@ -95,4 +95,29 @@
9595
end
9696
RUBY
9797
end
98+
99+
it 'ignores hooks with different metadata' do
100+
expect_no_offenses(<<-RUBY)
101+
describe Foo do
102+
before(:example) { foo }
103+
before(:example, :special_case) { bar }
104+
end
105+
RUBY
106+
end
107+
108+
it 'flags hooks with similar metadata' do
109+
expect_offense(<<-RUBY)
110+
describe Foo do
111+
before(:each, :special_case) { foo }
112+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not define multiple hooks in the same example group.
113+
before(:example, :special_case) { bar }
114+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not define multiple hooks in the same example group.
115+
before(:example, special_case: true) { bar }
116+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not define multiple hooks in the same example group.
117+
before(special_case: true) { bar }
118+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not define multiple hooks in the same example group.
119+
before(:example, special_case: false) { bar }
120+
end
121+
RUBY
122+
end
98123
end

spec/rubocop/rspec/hook_spec.rb

Lines changed: 70 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,43 +11,86 @@ def hook(source)
1111
expect(hook('around(:each) { }').name).to be(:around)
1212
end
1313

14-
it 'does not break if a hook is not given a symbol literal' do
15-
expect(hook('before(scope) { example_setup }').knowable_scope?).to be(false)
16-
end
14+
describe '#knowable_scope?' do
15+
it 'does not break if a hook is not given a symbol literal' do
16+
expect(hook('before(scope) { example_setup }').knowable_scope?)
17+
.to be(false)
18+
end
1719

18-
it 'knows the scope of a hook with a symbol literal' do
19-
expect(hook('before { example_setup }').knowable_scope?).to be(true)
20-
end
20+
it 'knows the scope of a hook with a symbol literal' do
21+
expect(hook('before(:example) { example_setup }').knowable_scope?)
22+
.to be(true)
23+
end
2124

22-
it 'ignores other arguments to hooks' do
23-
expect(hook('before(:each, :metadata) { example_setup }').scope)
24-
.to be(:each)
25-
end
25+
it 'knows the scope of a hook with no argument' do
26+
expect(hook('before { example_setup }').knowable_scope?)
27+
.to be(true)
28+
end
2629

27-
it 'classifies nonstandard hook arguments as invalid' do
28-
expect(hook('before(:nothing) { example_setup }').valid_scope?).to be(false)
30+
it 'knows the scope of a hook with hash metadata' do
31+
expect(hook('before(special: true) { example_setup }').knowable_scope?)
32+
.to be(true)
33+
end
2934
end
3035

31-
it 'classifies :each as a valid hook argument' do
32-
expect(hook('before(:each) { example_setup }').valid_scope?).to be(true)
33-
end
36+
describe '#scope' do
37+
it 'ignores other arguments to hooks' do
38+
expect(hook('before(:each, :metadata) { example_setup }').scope)
39+
.to be(:each)
40+
end
3441

35-
it 'classifies :each as an example hook' do
36-
expect(hook('before(:each) { }').example?).to be(true)
37-
end
42+
it 'classifies :each as an example hook' do
43+
expect(hook('before(:each) { }').example?).to be(true)
44+
end
45+
46+
it 'defaults to example hook with hash metadata' do
47+
expect(hook('before(special: true) { }').example?).to be(true)
48+
end
3849

39-
shared_examples 'standardizes scope' do |source, scope|
40-
it "interprets #{source} as having scope #{scope}" do
41-
expect(hook(source).scope).to equal(scope)
50+
shared_examples 'standardizes scope' do |source, scope|
51+
it "interprets #{source} as having scope #{scope}" do
52+
expect(hook(source).scope).to equal(scope)
53+
end
4254
end
55+
56+
include_examples 'standardizes scope', 'before(:each) { }', :each
57+
include_examples 'standardizes scope', 'around(:example) { }', :each
58+
include_examples 'standardizes scope', 'after { }', :each
59+
60+
include_examples 'standardizes scope', 'before(:all) { }', :context
61+
include_examples 'standardizes scope', 'around(:context) { }', :context
62+
63+
include_examples 'standardizes scope', 'after(:suite) { }', :suite
4364
end
4465

45-
include_examples 'standardizes scope', 'before(:each) { }', :each
46-
include_examples 'standardizes scope', 'around(:example) { }', :each
47-
include_examples 'standardizes scope', 'after { }', :each
66+
describe '#metadata' do
67+
def metadata(source)
68+
hook(source).metadata.to_s
69+
end
4870

49-
include_examples 'standardizes scope', 'before(:all) { }', :context
50-
include_examples 'standardizes scope', 'around(:context) { }', :context
71+
it 'extracts symbol metadata' do
72+
expect(metadata('before(:example, :special) { foo }'))
73+
.to eq('{s(:sym, :special)=>true}')
74+
end
75+
76+
it 'extracts hash metadata' do
77+
expect(metadata('before(:example, special: true) { foo }'))
78+
.to eq('{s(:sym, :special)=>true}')
79+
end
5180

52-
include_examples 'standardizes scope', 'after(:suite) { }', :suite
81+
it 'combines symbol and hash metadata' do
82+
expect(metadata('before(:example, :symbol, special: true) { foo }'))
83+
.to eq('{s(:sym, :symbol)=>true, s(:sym, :special)=>true}')
84+
end
85+
86+
it 'extracts hash metadata with no scope given' do
87+
expect(metadata('before(special: true) { foo }'))
88+
.to eq('{s(:sym, :special)=>true}')
89+
end
90+
91+
it 'withstands no arguments' do
92+
expect(metadata('before { foo }'))
93+
.to be_empty
94+
end
95+
end
5396
end

0 commit comments

Comments
 (0)