Skip to content

Commit 10c8ec0

Browse files
Add a new cop RSpec/Output
This is based on the `Rails/Output` cop with three minor changes. 1. Autocorrection is removed as the expectation is that the print statement will be removed by the user. 2. The message is changed. 3. The cop runs only on spec files. Clean up rubocop:disable Co-authored-by: Yudai Takada <t.yudai92@gmail.com> Update comment Apply suggestions from code review Make the code more maintainable and add autocorrect. Co-authored-by: Ryo Nakamura <r7kamura@gmail.com> Update specs Add AutoCorrect: contextual Co-authored-by: Ryo Nakamura <r7kamura@gmail.com> Add @safety comment
1 parent 68b461f commit 10c8ec0

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)