Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1c7a360

Browse files
committed
Add metadata detection to ScatteredSetup cop
RSpec allows hash metadata to be present when no explicit hook scope is given. However, if hook scope is omitted, symbolic metadata is not allowed, and that causes RSpec to raise an error. Fixes rubocop#850
1 parent 1c81a96 commit 1c7a360

File tree

6 files changed

+159
-45
lines changed

6 files changed

+159
-45
lines changed

CHANGELOG.md

+1
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

+2-2
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

+44-11
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

+12
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

+30-5
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

+70-27
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)