Skip to content

Commit f6db226

Browse files
committed
Improve an offense message for RSpec/RepeatedExamplecop
Fixes: #1932
1 parent 9b5dd15 commit f6db226

File tree

3 files changed

+200
-19
lines changed

3 files changed

+200
-19
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Fix a false positive for `RSpec/LetSetup` when `let!` used in outer scope. ([@ydah])
88
- Fix a false positive for `RSpec/ReceiveNever` cop when `allow(...).to receive(...).never`. ([@ydah])
99
- Fix detection of nameless doubles with methods in `RSpec/VerifiedDoubles`. ([@ushi-as])
10+
- Improve an offense message for `RSpec/RepeatedExample` cop. ([@ydah])
1011

1112
## 3.7.0 (2025-09-01)
1213

lib/rubocop/cop/rspec/repeated_example.rb

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,36 +16,55 @@ module RSpec
1616
# end
1717
#
1818
class RepeatedExample < Base
19-
MSG = "Don't repeat examples within an example group."
19+
MSG = "Don't repeat examples within an example group. " \
20+
'Repeated on line(s) %<lines>s.'
2021

2122
def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
2223
return unless example_group?(node)
2324

24-
repeated_examples(node).each do |repeated_example|
25-
add_offense(repeated_example)
25+
find_repeated_examples(node).each do |repeated_examples|
26+
add_offenses_for_repeated_group(repeated_examples)
2627
end
2728
end
2829

2930
private
3031

31-
def repeated_examples(node)
32-
RuboCop::RSpec::ExampleGroup.new(node)
33-
.examples
34-
.group_by { |example| example_signature(example) }
32+
def find_repeated_examples(node)
33+
examples = RuboCop::RSpec::ExampleGroup.new(node).examples
34+
35+
examples
36+
.group_by { |example| build_example_signature(example) }
3537
.values
36-
.reject(&:one?)
37-
.flatten
38-
.map(&:to_node)
38+
.select { |group| group.size > 1 }
3939
end
4040

41-
def example_signature(example)
42-
key_parts = [example.metadata, example.implementation]
43-
41+
def build_example_signature(example)
42+
signature = [example.metadata, example.implementation]
4443
if example.definition.method?(:its)
45-
key_parts << example.definition.arguments
44+
signature << example.definition.arguments
45+
end
46+
signature
47+
end
48+
49+
def add_offenses_for_repeated_group(repeated_examples)
50+
repeated_examples.each do |example|
51+
other_lines = extract_other_lines(repeated_examples, example)
52+
add_offense(example.to_node, message: message(other_lines))
4653
end
54+
end
55+
56+
def extract_other_lines(examples_group, current_example)
57+
current_node = current_example.to_node
58+
59+
examples_group
60+
.reject { |ex| ex.to_node.equal?(current_node) }
61+
.map { |ex| ex.to_node.first_line }
62+
.uniq
63+
.sort
64+
end
4765

48-
key_parts
66+
def message(other_lines)
67+
format(MSG, lines: other_lines.join(', '))
4968
end
5069
end
5170
end

spec/rubocop/cop/rspec/repeated_example_spec.rb

Lines changed: 165 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
expect_offense(<<~RUBY)
66
describe 'doing x' do
77
it "does x" do
8-
^^^^^^^^^^^^^^ Don't repeat examples within an example group.
8+
^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 6.
99
expect(foo).to be(bar)
1010
end
1111
1212
it "does y" do
13-
^^^^^^^^^^^^^^ Don't repeat examples within an example group.
13+
^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 2.
1414
expect(foo).to be(bar)
1515
end
1616
end
@@ -49,9 +49,9 @@
4949
expect_offense(<<~RUBY)
5050
describe 'doing x' do
5151
its(:x) { is_expected.to be_present }
52-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't repeat examples within an example group.
52+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 3.
5353
its(:x) { is_expected.to be_present }
54-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't repeat examples within an example group.
54+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 2.
5555
end
5656
RUBY
5757
end
@@ -89,4 +89,165 @@
8989
end
9090
RUBY
9191
end
92+
93+
it 'shows all duplicate line numbers when there are three duplicates' do
94+
expect_offense(<<~RUBY)
95+
describe 'doing x' do
96+
it "does x" do
97+
^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 6, 10.
98+
expect(foo).to be(bar)
99+
end
100+
101+
it "does y" do
102+
^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 2, 10.
103+
expect(foo).to be(bar)
104+
end
105+
106+
it "does z" do
107+
^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 2, 6.
108+
expect(foo).to be(bar)
109+
end
110+
end
111+
RUBY
112+
end
113+
114+
it 'shows all duplicate line numbers when there are four duplicates' do
115+
expect_offense(<<~RUBY)
116+
describe 'doing x' do
117+
it "first" do
118+
^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 5, 8, 11.
119+
expect(foo).to be(bar)
120+
end
121+
it "second" do
122+
^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 2, 8, 11.
123+
expect(foo).to be(bar)
124+
end
125+
it "third" do
126+
^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 2, 5, 11.
127+
expect(foo).to be(bar)
128+
end
129+
it "fourth" do
130+
^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 2, 5, 8.
131+
expect(foo).to be(bar)
132+
end
133+
end
134+
RUBY
135+
end
136+
137+
it 'correctly reports duplicates with string interpolation' do
138+
expect_offense(<<~RUBY)
139+
describe 'doing x' do
140+
let(:date) { '2024-06-25' }
141+
142+
it { expect(subject).to be_urgent(order, now: T("\#{date}T12:00")) }
143+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 5.
144+
it { expect(subject).to be_urgent(order, now: T("\#{date}T12:00")) }
145+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 4.
146+
end
147+
RUBY
148+
end
149+
150+
it 'does not flag examples with different string interpolation values' do
151+
expect_no_offenses(<<~RUBY)
152+
describe 'doing x' do
153+
let(:date) { '2024-06-25' }
154+
155+
it { expect(subject).to be_urgent(order, now: T("\#{date}T12:00")) }
156+
it { expect(subject).to be_urgent(order, now: T("\#{date}T17:00")) }
157+
end
158+
RUBY
159+
end
160+
161+
it 'handles one-liner examples with duplicates' do
162+
expect_offense(<<~RUBY)
163+
describe 'doing x' do
164+
it { is_expected.to be_valid }
165+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 3, 4.
166+
it { is_expected.to be_valid }
167+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 2, 4.
168+
it { is_expected.to be_valid }
169+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 2, 3.
170+
end
171+
RUBY
172+
end
173+
174+
it 'shows line numbers for examples formatted differently but with ' \
175+
'same AST' do
176+
expect_offense(<<~RUBY)
177+
describe 'doing x' do
178+
it "does x" do
179+
^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 5.
180+
expect(foo).to be(bar)
181+
end
182+
it "does y" do expect(foo).to be(bar); end
183+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 2.
184+
end
185+
RUBY
186+
end
187+
188+
it 'correctly identifies repeated examples across mixed formatting' do
189+
expect_offense(<<~RUBY)
190+
describe 'doing x' do
191+
it "multiline" do
192+
^^^^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 5.
193+
expect(foo).to eq(bar)
194+
end
195+
it("single line") { expect(foo).to eq(bar) }
196+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 2.
197+
end
198+
RUBY
199+
end
200+
201+
it 'handles multiple examples on same line correctly' do
202+
expect_offense(<<~RUBY)
203+
describe 'doing x' do
204+
it "first" do
205+
^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 5.
206+
expect(foo).to eq(bar)
207+
end
208+
it("second") { expect(foo).to eq(bar) }; it("third") { expect(foo).to eq(bar) }
209+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 2, 5.
210+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 2, 5.
211+
end
212+
RUBY
213+
end
214+
215+
it 'does not confuse examples in nested contexts with same implementation' do
216+
expect_no_offenses(<<~RUBY)
217+
describe 'doing x' do
218+
context 'context A' do
219+
it "does x" do
220+
expect(foo).to be(bar)
221+
end
222+
end
223+
224+
context 'context B' do
225+
it "does x" do
226+
expect(foo).to be(bar)
227+
end
228+
end
229+
end
230+
RUBY
231+
end
232+
233+
it 'flags duplicates only within the same example group' do
234+
expect_offense(<<~RUBY)
235+
describe 'doing x' do
236+
it "first" do
237+
^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 5.
238+
expect(foo).to be(bar)
239+
end
240+
it "second" do
241+
^^^^^^^^^^^^^^ Don't repeat examples within an example group. Repeated on line(s) 2.
242+
expect(foo).to be(bar)
243+
end
244+
245+
context 'different scope' do
246+
it "third" do
247+
expect(foo).to be(bar)
248+
end
249+
end
250+
end
251+
RUBY
252+
end
92253
end

0 commit comments

Comments
 (0)