Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,4 @@ Performance/ZipWithoutBlock: {Enabled: true}

RSpec/IncludeExamples: {Enabled: true}
RSpec/LeakyLocalVariable: {Enabled: true}
RSpec/Output: {Enabled: true}
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Fix detection of nameless doubles with methods in `RSpec/VerifiedDoubles`. ([@ushi-as])
- Improve an offense message for `RSpec/RepeatedExample` cop. ([@ydah])
- Let `RSpec/SpecFilePathFormat` leverage ActiveSupport inflections when configured. ([@corsonknowles], [@bquorning])
- Add new cop `RSpec/Output`. ([@kevinrobell-st])

## 3.7.0 (2025-09-01)

Expand Down Expand Up @@ -1021,6 +1022,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
[@jtannas]: https://github.com/jtannas
[@k-s-a]: https://github.com/K-S-A
[@kellysutton]: https://github.com/kellysutton
[@kevinrobell-st]: https://github.com/kevinrobell-st
[@koic]: https://github.com/koic
[@krororo]: https://github.com/krororo
[@kuahyeow]: https://github.com/kuahyeow
Expand Down
6 changes: 6 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,12 @@ RSpec/NotToNot:
VersionAdded: '1.4'
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/NotToNot

RSpec/Output:
Description: Checks for the use of output calls like puts and print in specs.
Enabled: pending
VersionAdded: "<<next>>"
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Output

RSpec/OverwritingSetup:
Description: Checks if there is a let/subject that overwrites an existing one.
Enabled: true
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/cops.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
* xref:cops_rspec.adoc#rspecnestedgroups[RSpec/NestedGroups]
* xref:cops_rspec.adoc#rspecnoexpectationexample[RSpec/NoExpectationExample]
* xref:cops_rspec.adoc#rspecnottonot[RSpec/NotToNot]
* xref:cops_rspec.adoc#rspecoutput[RSpec/Output]
* xref:cops_rspec.adoc#rspecoverwritingsetup[RSpec/OverwritingSetup]
* xref:cops_rspec.adoc#rspecpending[RSpec/Pending]
* xref:cops_rspec.adoc#rspecpendingwithoutreason[RSpec/PendingWithoutReason]
Expand Down
31 changes: 31 additions & 0 deletions docs/modules/ROOT/pages/cops_rspec.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4718,6 +4718,37 @@ end

* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/NotToNot

[#rspecoutput]
== RSpec/Output

|===
| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed

| Pending
| Yes
| No
| <<next>>
| -
|===

Checks for the use of output calls like puts and print in specs.

[#examples-rspecoutput]
=== Examples

[source,ruby]
----
# bad
puts 'A debug message'
pp 'A debug message'
print 'A debug message'
----

[#references-rspecoutput]
=== References

* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Output

[#rspecoverwritingsetup]
== RSpec/OverwritingSetup

Expand Down
66 changes: 66 additions & 0 deletions lib/rubocop/cop/rspec/output.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# frozen_string_literal: true

module RuboCop
module Cop
# NOTE: This is the same as the `Rails/Output` cop with minor changes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please delete this comment? It will diverge significantly from the actual Rails/Output cop in the future.

module RSpec
# Checks for the use of output calls like puts and print in specs.
#
# @example
# # bad
# puts 'A debug message'
# pp 'A debug message'
# print 'A debug message'
class Output < Base
include RangeHelp

MSG = 'Do not write to stdout in specs.'
RESTRICT_ON_SEND = %i[ap p pp pretty_print print puts binwrite syswrite
write write_nonblock].freeze

# @!method output?(node)
def_node_matcher :output?, <<~PATTERN
(send nil? {:ap :p :pp :pretty_print :print :puts} ...)
PATTERN

# @!method io_output?(node)
def_node_matcher :io_output?, <<~PATTERN
(send
{
(gvar #match_gvar?)
(const {nil? cbase} {:STDOUT :STDERR})
}
{:binwrite :syswrite :write :write_nonblock}
...)
PATTERN

# rubocop:disable Metrics/CyclomaticComplexity
def on_send(node)
return if node.parent&.call_type? || node.block_node
return if !output?(node) && !io_output?(node)
return if node.arguments.any? { |arg| arg.type?(:hash, :block_pass) }

range = offense_range(node)

add_offense(range)
end
# rubocop:enable Metrics/CyclomaticComplexity
Comment on lines +37 to +47
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add the directive to the specific line where the offense occurs, instead of using a range?

Suggested change
# rubocop:disable Metrics/CyclomaticComplexity
def on_send(node)
return if node.parent&.call_type? || node.block_node
return if !output?(node) && !io_output?(node)
return if node.arguments.any? { |arg| arg.type?(:hash, :block_pass) }
range = offense_range(node)
add_offense(range)
end
# rubocop:enable Metrics/CyclomaticComplexity
def on_send(node) # rubocop:disable Metrics/CyclomaticComplexity
return if node.parent&.call_type? || node.block_node
return if !output?(node) && !io_output?(node)
return if node.arguments.any? { |arg| arg.type?(:hash, :block_pass) }
range = offense_range(node)
add_offense(range)
end


private

def match_gvar?(sym)
%i[$stdout $stderr].include?(sym)
end

def offense_range(node)
if node.receiver
range_between(node.source_range.begin_pos,
node.loc.selector.end_pos)
else
node.loc.selector
end
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/rubocop/cop/rspec_cops.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
require_relative 'rspec/nested_groups'
require_relative 'rspec/no_expectation_example'
require_relative 'rspec/not_to_not'
require_relative 'rspec/output'
require_relative 'rspec/overwriting_setup'
require_relative 'rspec/pending'
require_relative 'rspec/pending_without_reason'
Expand Down
169 changes: 169 additions & 0 deletions spec/rubocop/cop/rspec/output_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::RSpec::Output do
it 'registers an offense for using `p` method without a receiver' do
expect_offense(<<~RUBY)
p "edmond dantes"
^ Do not write to stdout in specs.
RUBY
end

it 'registers an offense for using `puts` method without a receiver' do
expect_offense(<<~RUBY)
puts "sinbad"
^^^^ Do not write to stdout in specs.
RUBY
end

it 'registers an offense for using `print` method without a receiver' do
expect_offense(<<~RUBY)
print "abbe busoni"
^^^^^ Do not write to stdout in specs.
RUBY
end

it 'registers an offense for using `pp` method without a receiver' do
expect_offense(<<~RUBY)
pp "monte cristo"
^^ Do not write to stdout in specs.
RUBY
end

it 'registers an offense with `$stdout.write`' do
expect_offense(<<~RUBY)
$stdout.write "lord wilmore"
^^^^^^^^^^^^^ Do not write to stdout in specs.
RUBY
end

it 'registers an offense with `$stderr.syswrite`' do
expect_offense(<<~RUBY)
$stderr.syswrite "faria"
^^^^^^^^^^^^^^^^ Do not write to stdout in specs.
RUBY
end

it 'registers an offense with `STDOUT.write`' do
expect_offense(<<~RUBY)
STDOUT.write "bertuccio"
^^^^^^^^^^^^ Do not write to stdout in specs.
RUBY
end

it 'registers an offense with `::STDOUT.write`' do
expect_offense(<<~RUBY)
::STDOUT.write "bertuccio"
^^^^^^^^^^^^^^ Do not write to stdout in specs.
RUBY
end

it 'registers an offense with `STDERR.write`' do
expect_offense(<<~RUBY)
STDERR.write "bertuccio"
^^^^^^^^^^^^ Do not write to stdout in specs.
RUBY
end

it 'registers an offense with `::STDERR.write`' do
expect_offense(<<~RUBY)
::STDERR.write "bertuccio"
^^^^^^^^^^^^^^ Do not write to stdout in specs.
RUBY
end

it 'does not record an offense for methods with a receiver' do
expect_no_offenses(<<~RUBY)
obj.print
something.p
nothing.pp
RUBY
end

it 'registers an offense for methods without arguments' do
expect_offense(<<~RUBY)
print
^^^^^ Do not write to stdout in specs.
pp
^^ Do not write to stdout in specs.
puts
^^^^ Do not write to stdout in specs.
$stdout.write
^^^^^^^^^^^^^ Do not write to stdout in specs.
STDERR.write
^^^^^^^^^^^^ Do not write to stdout in specs.
RUBY
end

it 'registers an offense when `p` method with positional argument' do
expect_offense(<<~RUBY)
p(do_something)
^ Do not write to stdout in specs.
RUBY
end

it 'does not register an offense when a method is called ' \
'to a local variable with the same name as a print method' do
expect_no_offenses(<<~RUBY)
p.do_something
RUBY
end

it 'does not register an offense when `p` method with keyword argument' do
expect_no_offenses(<<~RUBY)
p(class: 'this `p` method is a DSL')
RUBY
end

it 'does not register an offense when `p` method with symbol proc' do
expect_no_offenses(<<~RUBY)
p(&:this_p_method_is_a_dsl)
RUBY
end

it 'does not register an offense when the `p` method is called ' \
'with block argument' do
expect_no_offenses(<<~RUBY)
# phlex-rails gem.
div do
p { 'Some text' }
end
RUBY
end

it 'does not register an offense when io method is called ' \
'with block argument' do
expect_no_offenses(<<~RUBY)
obj.write { do_somethig }
RUBY
end

it 'does not register an offense when io method is called ' \
'with numbered block argument' do
expect_no_offenses(<<~RUBY)
obj.write { do_something(_1) }
RUBY
end

it 'does not register an offense when io method is called ' \
'with `it` parameter', :ruby34, unsupported_on: :parser do
expect_no_offenses(<<~RUBY)
obj.write { do_something(it) }
RUBY
end

it 'does not register an offense when a method is safe navigation called ' \
'to a local variable with the same name as a print method' do
expect_no_offenses(<<~RUBY)
p&.do_something
RUBY
end

it 'does not record an offense for comments' do
expect_no_offenses(<<~RUBY)
# print "test"
# p
# $stdout.write
# STDERR.binwrite
RUBY
end
end