Skip to content

Commit b205673

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.
1 parent 3f31059 commit b205673

File tree

8 files changed

+281
-4
lines changed

8 files changed

+281
-4
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
@@ -9,6 +9,7 @@
99
- Fix detection of nameless doubles with methods in `RSpec/VerifiedDoubles`. ([@ushi-as])
1010
- Improve an offense message for `RSpec/RepeatedExample` cop. ([@ydah])
1111
- Let `RSpec/SpecFilePathFormat` leverage ActiveSupport inflections when configured. ([@corsonknowles], [@bquorning])
12+
- Add new cop `RSpec/Output`. ([@kevinrobell-st])
1213

1314
## 3.7.0 (2025-09-01)
1415

@@ -1021,6 +1022,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
10211022
[@jtannas]: https://github.com/jtannas
10221023
[@k-s-a]: https://github.com/K-S-A
10231024
[@kellysutton]: https://github.com/kellysutton
1025+
[@kevinrobell-st]: https://github.com/kevinrobell-st
10241026
[@koic]: https://github.com/koic
10251027
[@krororo]: https://github.com/krororo
10261028
[@kuahyeow]: https://github.com/kuahyeow

config/default.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,12 @@ 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+
VersionAdded: "<<next>>"
765+
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Output
766+
761767
RSpec/OverwritingSetup:
762768
Description: Checks if there is a let/subject that overwrites an existing one.
763769
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: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -870,7 +870,7 @@ end
870870
| Array
871871
872872
| IgnoredMetadata
873-
| `{"type" => ["channel", "controller", "helper", "job", "mailer", "model", "request", "routing", "view", "feature", "system", "mailbox", "aruba", "task"]}`
873+
| `{"type"=>["channel", "controller", "helper", "job", "mailer", "model", "request", "routing", "view", "feature", "system", "mailbox", "aruba", "task"]}`
874874
|
875875
|===
876876
@@ -2015,7 +2015,7 @@ end
20152015
| Name | Default value | Configurable values
20162016
20172017
| CustomTransform
2018-
| `{"be" => "is", "BE" => "IS", "have" => "has", "HAVE" => "HAS"}`
2018+
| `{"be"=>"is", "BE"=>"IS", "have"=>"has", "HAVE"=>"HAS"}`
20192019
|
20202020
20212021
| IgnoredWords
@@ -4718,6 +4718,37 @@ end
47184718
47194719
* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/NotToNot
47204720
4721+
[#rspecoutput]
4722+
== RSpec/Output
4723+
4724+
|===
4725+
| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed
4726+
4727+
| Pending
4728+
| Yes
4729+
| No
4730+
| <<next>>
4731+
| -
4732+
|===
4733+
4734+
Checks for the use of output calls like puts and print in specs.
4735+
4736+
[#examples-rspecoutput]
4737+
=== Examples
4738+
4739+
[source,ruby]
4740+
----
4741+
# bad
4742+
puts 'A debug message'
4743+
pp 'A debug message'
4744+
print 'A debug message'
4745+
----
4746+
4747+
[#references-rspecoutput]
4748+
=== References
4749+
4750+
* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Output
4751+
47214752
[#rspecoverwritingsetup]
47224753
== RSpec/OverwritingSetup
47234754
@@ -6098,15 +6129,15 @@ whatever_spec.rb # describe MyClass, type: :routing do; end
60986129
| Array
60996130
61006131
| CustomTransform
6101-
| `{"RuboCop" => "rubocop", "RSpec" => "rspec"}`
6132+
| `{"RuboCop"=>"rubocop", "RSpec"=>"rspec"}`
61026133
|
61036134
61046135
| IgnoreMethods
61056136
| `false`
61066137
| Boolean
61076138
61086139
| IgnoreMetadata
6109-
| `{"type" => "routing"}`
6140+
| `{"type"=>"routing"}`
61106141
|
61116142
61126143
| InflectorPath

lib/rubocop/cop/rspec/output.rb

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
# NOTE: This is the same as the `Rails/Output` cop with minor changes.
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+
include RangeHelp
16+
17+
MSG = 'Do not write to stdout in specs.'
18+
RESTRICT_ON_SEND = %i[ap p pp pretty_print print puts binwrite syswrite
19+
write write_nonblock].freeze
20+
21+
# @!method output?(node)
22+
def_node_matcher :output?, <<~PATTERN
23+
(send nil? {:ap :p :pp :pretty_print :print :puts} ...)
24+
PATTERN
25+
26+
# @!method io_output?(node)
27+
def_node_matcher :io_output?, <<~PATTERN
28+
(send
29+
{
30+
(gvar #match_gvar?)
31+
(const {nil? cbase} {:STDOUT :STDERR})
32+
}
33+
{:binwrite :syswrite :write :write_nonblock}
34+
...)
35+
PATTERN
36+
37+
# rubocop:disable Metrics/CyclomaticComplexity
38+
def on_send(node)
39+
return if node.parent&.call_type? || node.block_node
40+
return if !output?(node) && !io_output?(node)
41+
return if node.arguments.any? { |arg| arg.type?(:hash, :block_pass) }
42+
43+
range = offense_range(node)
44+
45+
add_offense(range)
46+
end
47+
# rubocop:enable Metrics/CyclomaticComplexity
48+
49+
private
50+
51+
def match_gvar?(sym)
52+
%i[$stdout $stderr].include?(sym)
53+
end
54+
55+
def offense_range(node)
56+
if node.receiver
57+
range_between(node.source_range.begin_pos,
58+
node.loc.selector.end_pos)
59+
else
60+
node.loc.selector
61+
end
62+
end
63+
end
64+
end
65+
end
66+
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: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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+
end
10+
11+
it 'registers an offense for using `puts` method without a receiver' do
12+
expect_offense(<<~RUBY)
13+
puts "sinbad"
14+
^^^^ Do not write to stdout in specs.
15+
RUBY
16+
end
17+
18+
it 'registers an offense for using `print` method without a receiver' do
19+
expect_offense(<<~RUBY)
20+
print "abbe busoni"
21+
^^^^^ Do not write to stdout in specs.
22+
RUBY
23+
end
24+
25+
it 'registers an offense for using `pp` method without a receiver' do
26+
expect_offense(<<~RUBY)
27+
pp "monte cristo"
28+
^^ Do not write to stdout in specs.
29+
RUBY
30+
end
31+
32+
it 'registers an offense with `$stdout.write`' do
33+
expect_offense(<<~RUBY)
34+
$stdout.write "lord wilmore"
35+
^^^^^^^^^^^^^ Do not write to stdout in specs.
36+
RUBY
37+
end
38+
39+
it 'registers an offense with `$stderr.syswrite`' do
40+
expect_offense(<<~RUBY)
41+
$stderr.syswrite "faria"
42+
^^^^^^^^^^^^^^^^ Do not write to stdout in specs.
43+
RUBY
44+
end
45+
46+
it 'registers an offense with `STDOUT.write`' do
47+
expect_offense(<<~RUBY)
48+
STDOUT.write "bertuccio"
49+
^^^^^^^^^^^^ Do not write to stdout in specs.
50+
RUBY
51+
end
52+
53+
it 'registers an offense with `::STDOUT.write`' do
54+
expect_offense(<<~RUBY)
55+
::STDOUT.write "bertuccio"
56+
^^^^^^^^^^^^^^ Do not write to stdout in specs.
57+
RUBY
58+
end
59+
60+
it 'registers an offense with `STDERR.write`' do
61+
expect_offense(<<~RUBY)
62+
STDERR.write "bertuccio"
63+
^^^^^^^^^^^^ Do not write to stdout in specs.
64+
RUBY
65+
end
66+
67+
it 'registers an offense with `::STDERR.write`' do
68+
expect_offense(<<~RUBY)
69+
::STDERR.write "bertuccio"
70+
^^^^^^^^^^^^^^ Do not write to stdout in specs.
71+
RUBY
72+
end
73+
74+
it 'does not record an offense for methods with a receiver' do
75+
expect_no_offenses(<<~RUBY)
76+
obj.print
77+
something.p
78+
nothing.pp
79+
RUBY
80+
end
81+
82+
it 'registers an offense for methods without arguments' do
83+
expect_offense(<<~RUBY)
84+
print
85+
^^^^^ Do not write to stdout in specs.
86+
pp
87+
^^ Do not write to stdout in specs.
88+
puts
89+
^^^^ Do not write to stdout in specs.
90+
$stdout.write
91+
^^^^^^^^^^^^^ Do not write to stdout in specs.
92+
STDERR.write
93+
^^^^^^^^^^^^ Do not write to stdout in specs.
94+
RUBY
95+
end
96+
97+
it 'registers an offense when `p` method with positional argument' do
98+
expect_offense(<<~RUBY)
99+
p(do_something)
100+
^ Do not write to stdout in specs.
101+
RUBY
102+
end
103+
104+
it 'does not register an offense when a method is called ' \
105+
'to a local variable with the same name as a print method' do
106+
expect_no_offenses(<<~RUBY)
107+
p.do_something
108+
RUBY
109+
end
110+
111+
it 'does not register an offense when `p` method with keyword argument' do
112+
expect_no_offenses(<<~RUBY)
113+
p(class: 'this `p` method is a DSL')
114+
RUBY
115+
end
116+
117+
it 'does not register an offense when `p` method with symbol proc' do
118+
expect_no_offenses(<<~RUBY)
119+
p(&:this_p_method_is_a_dsl)
120+
RUBY
121+
end
122+
123+
it 'does not register an offense when the `p` method is called ' \
124+
'with block argument' do
125+
expect_no_offenses(<<~RUBY)
126+
# phlex-rails gem.
127+
div do
128+
p { 'Some text' }
129+
end
130+
RUBY
131+
end
132+
133+
it 'does not register an offense when io method is called ' \
134+
'with block argument' do
135+
expect_no_offenses(<<~RUBY)
136+
obj.write { do_somethig }
137+
RUBY
138+
end
139+
140+
it 'does not register an offense when io method is called ' \
141+
'with numbered block argument' do
142+
expect_no_offenses(<<~RUBY)
143+
obj.write { do_something(_1) }
144+
RUBY
145+
end
146+
147+
it 'does not register an offense when io method is called ' \
148+
'with `it` parameter', :ruby34, unsupported_on: :parser do
149+
expect_no_offenses(<<~RUBY)
150+
obj.write { do_something(it) }
151+
RUBY
152+
end
153+
154+
it 'does not register an offense when a method is safe navigation called ' \
155+
'to a local variable with the same name as a print method' do
156+
expect_no_offenses(<<~RUBY)
157+
p&.do_something
158+
RUBY
159+
end
160+
161+
it 'does not record an offense for comments' do
162+
expect_no_offenses(<<~RUBY)
163+
# print "test"
164+
# p
165+
# $stdout.write
166+
# STDERR.binwrite
167+
RUBY
168+
end
169+
end

0 commit comments

Comments
 (0)