Skip to content

Commit 7ad5ba6

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
1 parent 68b461f commit 7ad5ba6

File tree

8 files changed

+337
-0
lines changed

8 files changed

+337
-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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,13 @@ 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+
SafeAutoCorrect: false
765+
VersionAdded: "<<next>>"
766+
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Output
767+
761768
RSpec/OverwritingSetup:
762769
Description: Checks if there is a let/subject that overwrites an existing one.
763770
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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4725,6 +4725,37 @@ 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+
| Always (Unsafe)
4737+
| <<next>>
4738+
| -
4739+
|===
4740+
4741+
Checks for the use of output calls like puts and print in specs.
4742+
4743+
[#examples-rspecoutput]
4744+
=== Examples
4745+
4746+
[source,ruby]
4747+
----
4748+
# bad
4749+
puts 'A debug message'
4750+
pp 'A debug message'
4751+
print 'A debug message'
4752+
----
4753+
4754+
[#references-rspecoutput]
4755+
=== References
4756+
4757+
* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Output
4758+
47284759
[#rspecoverwritingsetup]
47294760
== RSpec/OverwritingSetup
47304761

lib/rubocop/cop/rspec/output.rb

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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+
# @example
10+
# # bad
11+
# puts 'A debug message'
12+
# pp 'A debug message'
13+
# print 'A debug message'
14+
class Output < Base
15+
extend AutoCorrector
16+
17+
MSG = 'Do not write to stdout in specs.'
18+
19+
KERNEL_METHODS = %i[
20+
ap
21+
p
22+
pp
23+
pretty_print
24+
print
25+
puts
26+
].to_set.freeze
27+
private_constant :KERNEL_METHODS
28+
29+
IO_METHODS = %i[
30+
binwrite
31+
syswrite
32+
write
33+
write_nonblock
34+
].to_set.freeze
35+
private_constant :IO_METHODS
36+
37+
RESTRICT_ON_SEND = (KERNEL_METHODS + IO_METHODS).to_a.freeze
38+
39+
# @!method output?(node)
40+
def_node_matcher :output?, <<~PATTERN
41+
(send nil? KERNEL_METHODS ...)
42+
PATTERN
43+
44+
# @!method io_output?(node)
45+
def_node_matcher :io_output?, <<~PATTERN
46+
(send
47+
{
48+
(gvar #match_gvar?)
49+
(const {nil? cbase} {:STDOUT :STDERR})
50+
}
51+
IO_METHODS
52+
...)
53+
PATTERN
54+
55+
def on_send(node) # rubocop:disable Metrics/CyclomaticComplexity
56+
return if node.parent&.call_type? || node.block_node
57+
return if !output?(node) && !io_output?(node)
58+
return if node.arguments.any? { |arg| arg.type?(:hash, :block_pass) }
59+
60+
add_offense(node) do |corrector|
61+
corrector.remove(node)
62+
end
63+
end
64+
65+
private
66+
67+
def match_gvar?(sym)
68+
%i[$stdout $stderr].include?(sym)
69+
end
70+
end
71+
end
72+
end
73+
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)