Skip to content

Commit 8521d9d

Browse files
committed
[Fix #647] Add OrderedHooks cop
1 parent d58a043 commit 8521d9d

File tree

7 files changed

+349
-0
lines changed

7 files changed

+349
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
* Fix `HooksBeforeExamples`, `LeadingSubject`, `LetBeforeExamples` and `ScatteredLet` autocorrection to take into account inline comments and comments immediately before the moved node. ([@Darhazer][])
66
* Improve rubocop-rspec performance. ([@Darhazer][])
7+
* Add `RSpec/OrderdHooks` cop. ([@Darhazer][])
78

89
## 2.1.0 (2020-12-17)
910

config/default.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,12 @@ RSpec/NotToNot:
518518
VersionAdded: '1.4'
519519
StyleGuide: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/NotToNot
520520

521+
RSpec/OrderedHooks:
522+
Description: Checks that before/around/after hooks are defined in the correct order.
523+
Enabled: pending
524+
VersionAdded: '2.2'
525+
StyleGuide: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/OrderedHooks
526+
521527
RSpec/OverwritingSetup:
522528
Description: Checks if there is a let/subject that overwrites an existing one.
523529
Enabled: true

docs/modules/ROOT/pages/cops.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
* xref:cops_rspec.adoc#rspecnamedsubject[RSpec/NamedSubject]
5858
* xref:cops_rspec.adoc#rspecnestedgroups[RSpec/NestedGroups]
5959
* xref:cops_rspec.adoc#rspecnottonot[RSpec/NotToNot]
60+
* xref:cops_rspec.adoc#rspecorderedhooks[RSpec/OrderedHooks]
6061
* xref:cops_rspec.adoc#rspecoverwritingsetup[RSpec/OverwritingSetup]
6162
* xref:cops_rspec.adoc#rspecpending[RSpec/Pending]
6263
* xref:cops_rspec.adoc#rspecpredicatematcher[RSpec/PredicateMatcher]

docs/modules/ROOT/pages/cops_rspec.adoc

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2973,6 +2973,47 @@ end
29732973

29742974
* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/NotToNot
29752975

2976+
== RSpec/OrderedHooks
2977+
2978+
|===
2979+
| Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
2980+
2981+
| Pending
2982+
| Yes
2983+
| Yes
2984+
| 2.2
2985+
| -
2986+
|===
2987+
2988+
Checks that before/around/after hooks are defined in the correct order.
2989+
2990+
If multiple hooks are defined in example group, they should appear in
2991+
the following order:
2992+
before :suite
2993+
before :context
2994+
before :example
2995+
around :example
2996+
after :example
2997+
after :context
2998+
after :suite
2999+
3000+
=== Examples
3001+
3002+
[source,ruby]
3003+
----
3004+
# bad
3005+
after { run_cleanup }
3006+
before { run_setup }
3007+
3008+
# good
3009+
before { run_setup }
3010+
after { run_cleanup }
3011+
----
3012+
3013+
=== References
3014+
3015+
* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/OrderedHooks
3016+
29763017
== RSpec/OverwritingSetup
29773018

29783019
|===
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module RSpec
6+
# Checks that before/around/after hooks are defined in the correct order.
7+
#
8+
# If multiple hooks are defined in example group, they should appear in
9+
# the following order:
10+
# before :suite
11+
# before :context
12+
# before :example
13+
# around :example
14+
# after :example
15+
# after :context
16+
# after :suite
17+
#
18+
# @example
19+
# # bad
20+
# after { run_cleanup }
21+
# before { run_setup }
22+
#
23+
# # good
24+
# before { run_setup }
25+
# after { run_cleanup }
26+
#
27+
class OrderedHooks < Base
28+
extend AutoCorrector
29+
30+
MSG = '`%<hook>s` is supposed to appear before `%<previous>s`' \
31+
' at line %<line>d.'
32+
33+
EXPECTED_HOOK_ORDER = %i[before around after].freeze
34+
EXPECTED_SCOPE_ORDER = %i[suite context each].freeze
35+
36+
def on_block(node)
37+
return unless example_group_with_body?(node)
38+
39+
RuboCop::RSpec::ExampleGroup.new(node)
40+
.hooks
41+
.each_cons(2) { |previous, current| check_order(previous, current) }
42+
end
43+
44+
private
45+
46+
def check_order(previous, current)
47+
previous_idx = EXPECTED_HOOK_ORDER.index(previous.name)
48+
current_idx = EXPECTED_HOOK_ORDER.index(current.name)
49+
50+
if previous_idx == current_idx
51+
check_scope_order(previous, current)
52+
elsif previous_idx > current_idx
53+
order_violation(previous, current)
54+
end
55+
end
56+
57+
def check_scope_order(previous, current)
58+
previous_idx = EXPECTED_SCOPE_ORDER.index(previous.scope)
59+
current_idx = EXPECTED_SCOPE_ORDER.index(current.scope)
60+
61+
if current.name == :after # for after we expect reversed order
62+
order_violation(previous, current) if previous_idx < current_idx
63+
elsif previous_idx > current_idx
64+
order_violation(previous, current)
65+
end
66+
end
67+
68+
def order_violation(previous, current)
69+
message = format(MSG,
70+
hook: format_hook(current),
71+
previous: format_hook(previous),
72+
line: previous.to_node.loc.line)
73+
74+
add_offense(current.to_node.send_node, message: message)
75+
end
76+
77+
def format_hook(hook)
78+
msg = hook.name.to_s
79+
raw_scope_name = hook.send(:scope_name)
80+
msg += "(:#{raw_scope_name})" if raw_scope_name
81+
msg
82+
end
83+
end
84+
end
85+
end
86+
end

lib/rubocop/cop/rspec_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
require_relative 'rspec/named_subject'
7070
require_relative 'rspec/nested_groups'
7171
require_relative 'rspec/not_to_not'
72+
require_relative 'rspec/ordered_hooks'
7273
require_relative 'rspec/overwriting_setup'
7374
require_relative 'rspec/pending'
7475
require_relative 'rspec/predicate_matcher'
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::RSpec::OrderedHooks do
4+
it 'detects `before` after `around`' do
5+
expect_offense(<<~RUBY)
6+
describe "hooks order" do
7+
around do |example|
8+
example.call
9+
end
10+
11+
before do
12+
^^^^^^ `before` is supposed to appear before `around` at line 2.
13+
run_setup
14+
end
15+
end
16+
RUBY
17+
end
18+
19+
it 'detects `before` after `after`' do
20+
expect_offense(<<~RUBY)
21+
describe "hooks order" do
22+
after do
23+
run_teardown
24+
end
25+
26+
before do
27+
^^^^^^ `before` is supposed to appear before `after` at line 2.
28+
run_setup
29+
end
30+
end
31+
RUBY
32+
end
33+
34+
it 'detects `around` after `after`' do
35+
expect_offense(<<~RUBY)
36+
describe "hooks order" do
37+
after do
38+
run_teardown
39+
end
40+
41+
around do |example|
42+
^^^^^^ `around` is supposed to appear before `after` at line 2.
43+
example.run
44+
end
45+
end
46+
RUBY
47+
end
48+
49+
context 'with multiple hooks' do
50+
it 'reports each violation independently' do
51+
expect_offense(<<~RUBY)
52+
describe "hooks order" do
53+
after { cleanup }
54+
55+
around do |example|
56+
^^^^^^ `around` is supposed to appear before `after` at line 2.
57+
example.call
58+
end
59+
60+
before { some_preparation }
61+
^^^^^^ `before` is supposed to appear before `around` at line 4.
62+
63+
before { some_other_preparation }
64+
65+
after { more_cleanup }
66+
67+
before { more_preparation }
68+
^^^^^^ `before` is supposed to appear before `after` at line 12.
69+
end
70+
RUBY
71+
end
72+
end
73+
74+
context 'with scoped hooks' do
75+
context 'when hook is before' do
76+
it 'detects `suite` after `context`' do
77+
expect_offense(<<~RUBY)
78+
describe "hooks order" do
79+
before(:context) { init_factories }
80+
before(:suite) { global_setup }
81+
^^^^^^^^^^^^^^ `before(:suite)` is supposed to appear before `before(:context)` at line 2.
82+
end
83+
RUBY
84+
end
85+
86+
it 'detects `suite` after `each`' do
87+
expect_offense(<<~RUBY)
88+
describe "hooks order" do
89+
before(:each) { init_data }
90+
before(:suite) { global_setup }
91+
^^^^^^^^^^^^^^ `before(:suite)` is supposed to appear before `before(:each)` at line 2.
92+
end
93+
RUBY
94+
end
95+
96+
it 'detects `context` after `each`' do
97+
expect_offense(<<~RUBY)
98+
describe "hooks order" do
99+
before(:each) { init_data }
100+
before(:context) { setup }
101+
^^^^^^^^^^^^^^^^ `before(:context)` is supposed to appear before `before(:each)` at line 2.
102+
end
103+
RUBY
104+
end
105+
106+
it 'accepts `example` and `each`' do
107+
expect_no_offenses(<<~RUBY)
108+
describe "hooks order" do
109+
before { setup1 }
110+
before(:each) { setup2 }
111+
before(:example) { setup3 }
112+
end
113+
RUBY
114+
end
115+
116+
it 'detects `context` after `example`' do
117+
expect_offense(<<~RUBY)
118+
describe "hooks order" do
119+
before(:example) { init_data }
120+
before(:context) { setup }
121+
^^^^^^^^^^^^^^^^ `before(:context)` is supposed to appear before `before(:example)` at line 2.
122+
end
123+
RUBY
124+
end
125+
end
126+
127+
context 'when hook is after' do
128+
it 'detects `context` after `suite`' do
129+
expect_offense(<<~RUBY)
130+
describe "hooks order" do
131+
after(:suite) { global_teardown }
132+
after(:context) { teardown }
133+
^^^^^^^^^^^^^^^ `after(:context)` is supposed to appear before `after(:suite)` at line 2.
134+
end
135+
RUBY
136+
end
137+
138+
it 'detects `each` after `suite`' do
139+
expect_offense(<<~RUBY)
140+
describe "hooks order" do
141+
after(:suite) { global_teardown }
142+
after(:each) { teardown }
143+
^^^^^^^^^^^^ `after(:each)` is supposed to appear before `after(:suite)` at line 2.
144+
end
145+
RUBY
146+
end
147+
148+
it 'detects `each` after `context`' do
149+
expect_offense(<<~RUBY)
150+
describe "hooks order" do
151+
after(:context) { teardown }
152+
after(:each) { cleanup }
153+
^^^^^^^^^^^^ `after(:each)` is supposed to appear before `after(:context)` at line 2.
154+
end
155+
RUBY
156+
end
157+
158+
it 'accepts `example` and `each`' do
159+
expect_no_offenses(<<~RUBY)
160+
describe "hooks order" do
161+
after { setup1 }
162+
after(:each) { setup2 }
163+
after(:example) { setup3 }
164+
end
165+
RUBY
166+
end
167+
168+
it 'detects `example` after `context`' do
169+
expect_offense(<<~RUBY)
170+
describe "hooks order" do
171+
after(:context) { cleanup }
172+
after(:example) { teardown }
173+
^^^^^^^^^^^^^^^ `after(:example)` is supposed to appear before `after(:context)` at line 2.
174+
end
175+
RUBY
176+
end
177+
end
178+
end
179+
180+
it 'accepts hooks in order' do
181+
expect_no_offenses(<<~RUBY)
182+
desribe "correctly ordered hooks" do
183+
before(:suite) do
184+
run_global_setup
185+
end
186+
187+
before(:context) do
188+
run_context_setup
189+
end
190+
191+
before(:example) do
192+
run_setup
193+
end
194+
195+
around(:example) do |example|
196+
example.run
197+
end
198+
199+
after(:example) do
200+
run_teardown
201+
end
202+
203+
after(:context) do
204+
run_context_teardown
205+
end
206+
207+
after(:suite) do
208+
run_global_teardown
209+
end
210+
end
211+
RUBY
212+
end
213+
end

0 commit comments

Comments
 (0)