Skip to content

Commit 6a5bf09

Browse files
authored
Merge pull request #2129 from kevinrobell-st/add-rspec/output-cop
Add a new cop `RSpec/Output`
2 parents 68b461f + 10c8ec0 commit 6a5bf09

File tree

8 files changed

+350
-0
lines changed

8 files changed

+350
-0
lines changed

.rubocop.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,3 +294,4 @@ Performance/ZipWithoutBlock: {Enabled: true}
294294

295295
RSpec/IncludeExamples: {Enabled: true}
296296
RSpec/LeakyLocalVariable: {Enabled: true}
297+
RSpec/Output: {Enabled: true}

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- Fix detection of nameless doubles with methods in `RSpec/VerifiedDoubles`. ([@ushi-as])
1414
- Improve an offense message for `RSpec/RepeatedExample` cop. ([@ydah])
1515
- Let `RSpec/SpecFilePathFormat` leverage ActiveSupport inflections when configured. ([@corsonknowles], [@bquorning])
16+
- Add new cop `RSpec/Output`. ([@kevinrobell-st])
1617

1718
## 3.7.0 (2025-09-01)
1819

@@ -1025,6 +1026,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
10251026
[@jtannas]: https://github.com/jtannas
10261027
[@k-s-a]: https://github.com/K-S-A
10271028
[@kellysutton]: https://github.com/kellysutton
1029+
[@kevinrobell-st]: https://github.com/kevinrobell-st
10281030
[@koic]: https://github.com/koic
10291031
[@krororo]: https://github.com/krororo
10301032
[@kuahyeow]: https://github.com/kuahyeow

config/default.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,14 @@ RSpec/NotToNot:
758758
VersionAdded: '1.4'
759759
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/NotToNot
760760

761+
RSpec/Output:
762+
Description: Checks for the use of output calls like puts and print in specs.
763+
Enabled: pending
764+
AutoCorrect: contextual
765+
SafeAutoCorrect: false
766+
VersionAdded: "<<next>>"
767+
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Output
768+
761769
RSpec/OverwritingSetup:
762770
Description: Checks if there is a let/subject that overwrites an existing one.
763771
Enabled: true

docs/modules/ROOT/pages/cops.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
* xref:cops_rspec.adoc#rspecnestedgroups[RSpec/NestedGroups]
7878
* xref:cops_rspec.adoc#rspecnoexpectationexample[RSpec/NoExpectationExample]
7979
* xref:cops_rspec.adoc#rspecnottonot[RSpec/NotToNot]
80+
* xref:cops_rspec.adoc#rspecoutput[RSpec/Output]
8081
* xref:cops_rspec.adoc#rspecoverwritingsetup[RSpec/OverwritingSetup]
8182
* xref:cops_rspec.adoc#rspecpending[RSpec/Pending]
8283
* xref:cops_rspec.adoc#rspecpendingwithoutreason[RSpec/PendingWithoutReason]

docs/modules/ROOT/pages/cops_rspec.adoc

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4725,6 +4725,44 @@ end
47254725
47264726
* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/NotToNot
47274727
4728+
[#rspecoutput]
4729+
== RSpec/Output
4730+
4731+
|===
4732+
| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed
4733+
4734+
| Pending
4735+
| Yes
4736+
| Command-line only (Unsafe)
4737+
| <<next>>
4738+
| -
4739+
|===
4740+
4741+
Checks for the use of output calls like puts and print in specs.
4742+
4743+
[#safety-rspecoutput]
4744+
=== Safety
4745+
4746+
This autocorrection is marked as unsafe because, in rare cases, print
4747+
statements can be used on purpose for integration testing and deleting
4748+
them will cause tests to fail.
4749+
4750+
[#examples-rspecoutput]
4751+
=== Examples
4752+
4753+
[source,ruby]
4754+
----
4755+
# bad
4756+
puts 'A debug message'
4757+
pp 'A debug message'
4758+
print 'A debug message'
4759+
----
4760+
4761+
[#references-rspecoutput]
4762+
=== References
4763+
4764+
* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Output
4765+
47284766
[#rspecoverwritingsetup]
47294767
== RSpec/OverwritingSetup
47304768

lib/rubocop/cop/rspec/output.rb

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
# NOTE: Originally based on the `Rails/Output` cop.
6+
module RSpec
7+
# Checks for the use of output calls like puts and print in specs.
8+
#
9+
# @safety
10+
# This autocorrection is marked as unsafe because, in rare cases, print
11+
# statements can be used on purpose for integration testing and deleting
12+
# them will cause tests to fail.
13+
#
14+
# @example
15+
# # bad
16+
# puts 'A debug message'
17+
# pp 'A debug message'
18+
# print 'A debug message'
19+
class Output < Base
20+
extend AutoCorrector
21+
22+
MSG = 'Do not write to stdout in specs.'
23+
24+
KERNEL_METHODS = %i[
25+
ap
26+
p
27+
pp
28+
pretty_print
29+
print
30+
puts
31+
].to_set.freeze
32+
private_constant :KERNEL_METHODS
33+
34+
IO_METHODS = %i[
35+
binwrite
36+
syswrite
37+
write
38+
write_nonblock
39+
].to_set.freeze
40+
private_constant :IO_METHODS
41+
42+
RESTRICT_ON_SEND = (KERNEL_METHODS + IO_METHODS).to_a.freeze
43+
44+
# @!method output?(node)
45+
def_node_matcher :output?, <<~PATTERN
46+
(send nil? KERNEL_METHODS ...)
47+
PATTERN
48+
49+
# @!method io_output?(node)
50+
def_node_matcher :io_output?, <<~PATTERN
51+
(send
52+
{
53+
(gvar #match_gvar?)
54+
(const {nil? cbase} {:STDOUT :STDERR})
55+
}
56+
IO_METHODS
57+
...)
58+
PATTERN
59+
60+
def on_send(node) # rubocop:disable Metrics/CyclomaticComplexity
61+
return if node.parent&.call_type? || node.block_node
62+
return if !output?(node) && !io_output?(node)
63+
return if node.arguments.any? { |arg| arg.type?(:hash, :block_pass) }
64+
65+
add_offense(node) do |corrector|
66+
corrector.remove(node)
67+
end
68+
end
69+
70+
private
71+
72+
def match_gvar?(sym)
73+
%i[$stdout $stderr].include?(sym)
74+
end
75+
end
76+
end
77+
end
78+
end

lib/rubocop/cop/rspec_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
require_relative 'rspec/nested_groups'
7676
require_relative 'rspec/no_expectation_example'
7777
require_relative 'rspec/not_to_not'
78+
require_relative 'rspec/output'
7879
require_relative 'rspec/overwriting_setup'
7980
require_relative 'rspec/pending'
8081
require_relative 'rspec/pending_without_reason'
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::RSpec::Output do
4+
it 'registers an offense for using `p` method without a receiver' do
5+
expect_offense(<<~RUBY)
6+
p "edmond dantes"
7+
^^^^^^^^^^^^^^^^^ Do not write to stdout in specs.
8+
RUBY
9+
10+
expect_correction(<<~RUBY)
11+
12+
RUBY
13+
end
14+
15+
it 'registers an offense for using `puts` method without a receiver' do
16+
expect_offense(<<~RUBY)
17+
puts "sinbad"
18+
^^^^^^^^^^^^^ Do not write to stdout in specs.
19+
RUBY
20+
21+
expect_correction(<<~RUBY)
22+
23+
RUBY
24+
end
25+
26+
it 'registers an offense for using `print` method without a receiver' do
27+
expect_offense(<<~RUBY)
28+
print "abbe busoni"
29+
^^^^^^^^^^^^^^^^^^^ Do not write to stdout in specs.
30+
RUBY
31+
32+
expect_correction(<<~RUBY)
33+
34+
RUBY
35+
end
36+
37+
it 'registers an offense for using `pp` method without a receiver' do
38+
expect_offense(<<~RUBY)
39+
pp "monte cristo"
40+
^^^^^^^^^^^^^^^^^ Do not write to stdout in specs.
41+
RUBY
42+
43+
expect_correction(<<~RUBY)
44+
45+
RUBY
46+
end
47+
48+
it 'registers an offense with `$stdout.write`' do
49+
expect_offense(<<~RUBY)
50+
$stdout.write "lord wilmore"
51+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not write to stdout in specs.
52+
RUBY
53+
54+
expect_correction(<<~RUBY)
55+
56+
RUBY
57+
end
58+
59+
it 'registers an offense with `$stderr.syswrite`' do
60+
expect_offense(<<~RUBY)
61+
$stderr.syswrite "faria"
62+
^^^^^^^^^^^^^^^^^^^^^^^^ Do not write to stdout in specs.
63+
RUBY
64+
65+
expect_correction(<<~RUBY)
66+
67+
RUBY
68+
end
69+
70+
it 'registers an offense with `STDOUT.write`' do
71+
expect_offense(<<~RUBY)
72+
STDOUT.write "bertuccio"
73+
^^^^^^^^^^^^^^^^^^^^^^^^ Do not write to stdout in specs.
74+
RUBY
75+
76+
expect_correction(<<~RUBY)
77+
78+
RUBY
79+
end
80+
81+
it 'registers an offense with `::STDOUT.write`' do
82+
expect_offense(<<~RUBY)
83+
::STDOUT.write "bertuccio"
84+
^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not write to stdout in specs.
85+
RUBY
86+
87+
expect_correction(<<~RUBY)
88+
89+
RUBY
90+
end
91+
92+
it 'registers an offense with `STDERR.write`' do
93+
expect_offense(<<~RUBY)
94+
STDERR.write "bertuccio"
95+
^^^^^^^^^^^^^^^^^^^^^^^^ Do not write to stdout in specs.
96+
RUBY
97+
98+
expect_correction(<<~RUBY)
99+
100+
RUBY
101+
end
102+
103+
it 'registers an offense with `::STDERR.write`' do
104+
expect_offense(<<~RUBY)
105+
::STDERR.write "bertuccio"
106+
^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not write to stdout in specs.
107+
RUBY
108+
109+
expect_correction(<<~RUBY)
110+
111+
RUBY
112+
end
113+
114+
it 'does not record an offense for methods with a receiver' do
115+
expect_no_offenses(<<~RUBY)
116+
obj.print
117+
something.p
118+
nothing.pp
119+
RUBY
120+
end
121+
122+
it 'registers an offense for methods without arguments' do
123+
expect_offense(<<~RUBY)
124+
print
125+
^^^^^ Do not write to stdout in specs.
126+
pp
127+
^^ Do not write to stdout in specs.
128+
puts
129+
^^^^ Do not write to stdout in specs.
130+
$stdout.write
131+
^^^^^^^^^^^^^ Do not write to stdout in specs.
132+
STDERR.write
133+
^^^^^^^^^^^^ Do not write to stdout in specs.
134+
RUBY
135+
136+
expect_correction(<<~RUBY)
137+
138+
139+
140+
141+
142+
RUBY
143+
end
144+
145+
it 'registers an offense when `p` method with positional argument' do
146+
expect_offense(<<~RUBY)
147+
p(do_something)
148+
^^^^^^^^^^^^^^^ Do not write to stdout in specs.
149+
RUBY
150+
151+
expect_correction(<<~RUBY)
152+
153+
RUBY
154+
end
155+
156+
it 'does not register an offense when a method is called ' \
157+
'to a local variable with the same name as a print method' do
158+
expect_no_offenses(<<~RUBY)
159+
p.do_something
160+
RUBY
161+
end
162+
163+
it 'does not register an offense when `p` method with keyword argument' do
164+
expect_no_offenses(<<~RUBY)
165+
p(class: 'this `p` method is a DSL')
166+
RUBY
167+
end
168+
169+
it 'does not register an offense when `p` method with symbol proc' do
170+
expect_no_offenses(<<~RUBY)
171+
p(&:this_p_method_is_a_dsl)
172+
RUBY
173+
end
174+
175+
it 'does not register an offense when the `p` method is called ' \
176+
'with block argument' do
177+
expect_no_offenses(<<~RUBY)
178+
# phlex-rails gem.
179+
div do
180+
p { 'Some text' }
181+
end
182+
RUBY
183+
end
184+
185+
it 'does not register an offense when io method is called ' \
186+
'with block argument' do
187+
expect_no_offenses(<<~RUBY)
188+
obj.write { do_somethig }
189+
RUBY
190+
end
191+
192+
it 'does not register an offense when io method is called ' \
193+
'with numbered block argument' do
194+
expect_no_offenses(<<~RUBY)
195+
obj.write { do_something(_1) }
196+
RUBY
197+
end
198+
199+
it 'does not register an offense when io method is called ' \
200+
'with `it` parameter', :ruby34, unsupported_on: :parser do
201+
expect_no_offenses(<<~RUBY)
202+
obj.write { do_something(it) }
203+
RUBY
204+
end
205+
206+
it 'does not register an offense when a method is safe navigation called ' \
207+
'to a local variable with the same name as a print method' do
208+
expect_no_offenses(<<~RUBY)
209+
p&.do_something
210+
RUBY
211+
end
212+
213+
it 'does not record an offense for comments' do
214+
expect_no_offenses(<<~RUBY)
215+
# print "test"
216+
# p
217+
# $stdout.write
218+
# STDERR.binwrite
219+
RUBY
220+
end
221+
end

0 commit comments

Comments
 (0)