Class: RuboCop::Cop::DevDoc::Style::TapBlockIgnoresValue
- Inherits:
-
Base
- Object
- Base
- RuboCop::Cop::DevDoc::Style::TapBlockIgnoresValue
- Defined in:
- lib/rubocop/cop/dev_doc/style/tap_block_ignores_value.rb
Overview
Flag ‘Object#tap` whose block ignores the yielded value.
## Rationale ‘tap` exists to **operate on the yielded receiver and return it** —`obj.tap { |x| configure(x) }`. The yielded value is the whole point of the method.
When the block ignores the yielded value, ‘tap` is no longer doing that job. It’s being used purely as a temp-variable-elision trick for “run a side effect, then return the receiver”:
❌ Block never references the yielded value — `tap` is eliding a temp var
read_value.tap { do_side_effect }
This costs readability for no benefit:
-
**It hides the return value in the receiver position.** ‘tap`’s “returns self” behaviour is its *less obvious* property. A reader has to stop and recall that the expression returns the receiver, not the block’s value. The explicit form puts the return value on its own line where “what does this return” is literal.
-
**Footgun magnet.** Return-value semantics here are subtle — it’s easy to nearby-edit the block into something whose value you think matters, or to confuse it with the broken ‘expr; side_effect` sequence (which returns the side effect’s value, not ‘expr`’s). Code whose contract is “return this value” reads and edits more safely when the return is explicit.
-
**Equal stakes → prefer the duller form.** Both forms are correct and neither is faster. With no functional difference, the more obvious one wins in a shared codebase.
✔️ Explicit intent, return value on its own line result = read_value do_side_effect result
## When ‘tap` IS the right choice Reserve `tap` for its canonical use — when the block references the yielded value:
✔️ Block uses the yielded value — this is what `tap` is for
Model.new.tap { |m| m.role = :admin; m.save }
record.tap { |r| logger.debug(r.to_sql) }
## Exception Genuine intentional uses (e.g. a logging side effect that reads clearer in tap position) go through an inline ‘# rubocop:disable` with a reason. The friction is the feature — it forces the choice to be articulated and reviewed.
NOTE: Numbered-param (‘_1`) and implicit-`it` (Ruby 3.4+) forms are not flagged — by construction, those forms reference the yielded value, which is the canonical use of `tap`. Block-pass (`&:sym`) is likewise not flagged; it isn’t a block node.
NOTE: If a nested block shadows the outer arg name (‘tap { |v| items.each { |v| v.foo } }`), the inner reference is treated as covering the outer — a rare residual false negative. Inline-disable if it ever matters.
Constant Summary collapse
- MSG =
'`tap` is for operating on the yielded value — when the ' \ 'block ignores it, prefer an explicit assign-and-return. ' \ 'Disable with a reason when the side effect reads clearer ' \ 'in `tap` position.'.freeze
Instance Method Summary collapse
Instance Method Details
#on_block(node) ⇒ Object
88 89 90 91 92 93 |
# File 'lib/rubocop/cop/dev_doc/style/tap_block_ignores_value.rb', line 88 def on_block(node) return unless node.send_node.method?(:tap) return if block_uses_yielded_value?(node) add_offense(node.send_node.loc.selector) end |