Class: RuboCop::Cop::Style::DisallowDelegate

Inherits:
Base
  • Object
show all
Defined in:
lib/rubocop/cop/style/disallow_delegate.rb

Overview

Forbids delegation — a class answering a question that belongs to a collaborator. The community name for the smell is Fowler's Middle Man, and the prescribed refactoring is Remove Middle Man: delete the forwarder and let the caller navigate to the object that knows.

Two shapes are flagged. The macro family (delegate, delegate_missing_to, def_delegator/def_delegators, DelegateClass) and an instance method whose entire body forwards the same message to a collaborator. The second is the same defect: writing the forwarder by hand changes what grep finds, not what the class claims to be responsible for.

self.class is not a collaborator — it is the object's own class. An instance method that composes its own attributes into a same-named class method answers for a domain it owns, so it is not delegation and is not flagged.

A macro aimed at :class reaches the same place and is exempt for the same reason. Remove Middle Man has nothing to prescribe here: there is no third object for the caller to navigate to, so the only alternatives are the macro and a hand-written body that says exactly what the macro says. Between those two the macro is the better spelling, and flagging it while exempting the body would push every author toward the longer one.

each in a class that includes or prepends Enumerable is the module's required contract, not delegation. The class has no interface without it — every method Enumerable provides is built on each — so the class is implementing its own interface rather than answering for a collaborator. It is not flagged. Any other method in such a class still is, and so is each in a singleton class: an instance-side mixin obliges the instance, never the singleton.

A body that passes the object's own state as an argument to a DIRECT collaborator is not a pass-through. Middle Man is a method that republishes a collaborator's answer verbatim, which the caller could reach by navigating; a method that supplies its own attributes contributes something the caller would otherwise have to reach in and take, and the result is a simpler API on the object that owns the data.

Own state is an instance variable, self, or a receiverless call, reached directly, through a wrapper, or through a call on it — record, record.owner_id and [record.owner_id] all carry the object's own data. A method PARAMETER does not: it arrives as an lvar, so a setter handing its argument to a collaborator stays flagged.

Two limits keep the exemption from swallowing the rule.

The receiver must not itself be a chain. Remove Middle Man on a message chain IS the caller navigating, so composing own state does not excuse it.

An argument rooted at the collaborator being forwarded to does not earn the exemption. author.name(author.locale) hands the collaborator back its own data, which is the echo this rule forbids, not composition, so with no other argument to carry it the forward stays flagged. Rooted at the object's OWN state the argument may be a chain of any depth — the object still supplied what the collaborator needed, and how far it reached inside itself to build the value is its own business.

A forward to a collaborator that passes NO argument is the one that republishes, and it is always flagged. That is the line — supplying data the collaborator needs is composition, echoing back what the collaborator already knows is delegation.

Examples:

# bad
delegate :name, to: :author

# bad — same promise, written by hand
def name
  author.name
end

# good — the caller navigates
post.author.name

# good — the object answers about itself, not for a collaborator
def lock_key
  self.class.lock_key(owner_id: owner_id)
end

# good — aimed at the object's own class, where there is no third
# object to navigate to
delegate :model, to: :class

# good — `each` is the Enumerable contract this class implements
class SearchResult
  include Enumerable

  def each(&)
    results.each(&)
  end
end

# good — composes its own attribute, so the caller cannot just navigate
def output
  formatter.output(value)
end

# good — names an expression built from its own record; not a forward
def lock_key
  Registry.lock_key(owner_id: record.owner_id)
end

# bad — a chain does not stop being a chain because an argument rode along
def starts_at
  schedule.window.period.starts_at(calendar)
end

# bad — the argument is the collaborator's own data handed back to it
def name
  author.name(author.locale)
end

Constant Summary collapse

MACRO_MSG =
'Do not use automatic delegation. Delete the forwarder and let the caller navigate.'
FORWARDER_MSG =
'Do not forward a collaborator\'s message. Delete the forwarder and let the caller navigate.'
RESTRICT_ON_SEND =
%i[delegate delegate_missing_to def_delegator def_delegators DelegateClass].freeze
ARGUMENT_WRAPPERS =

An argument reaches the body wrapped in one of these when the call site spreads or blocks it, and the wrapper says nothing about whose state it carries.

%i[hash pair array splat kwsplat block_pass].freeze

Instance Method Summary collapse

Instance Method Details

#leading_target(node) ⇒ Object

The Forwardable macros and delegate_missing_to take their target as the first argument. DelegateClass names none and never matches.



148
149
150
# File 'lib/rubocop/cop/style/disallow_delegate.rb', line 148

def_node_matcher :leading_target, <<~PATTERN
  (send nil? {:delegate_missing_to :def_delegator :def_delegators} $_ ...)
PATTERN

#mixes_in_enumerable?(node) ⇒ Object



134
135
136
# File 'lib/rubocop/cop/style/disallow_delegate.rb', line 134

def_node_matcher :mixes_in_enumerable?, <<~PATTERN
  (send nil? {:include :prepend} (const {nil? cbase} :Enumerable))
PATTERN

#on_def(node) ⇒ Object



159
160
161
162
163
164
165
# File 'lib/rubocop/cop/style/disallow_delegate.rb', line 159

def on_def(node)
  return unless forwards_own_message?(node.body, node.method_name)
  return if enumerable_contract?(node)
  return if composes_own_state?(node.body)

  add_offense(node.loc.name, message: FORWARDER_MSG)
end

#on_send(node) ⇒ Object



152
153
154
155
156
157
# File 'lib/rubocop/cop/style/disallow_delegate.rb', line 152

def on_send(node)
  return unless node.receiver.nil?
  return if own_class_target?(node)

  add_offense(node, message: MACRO_MSG)
end

#to_option(node) ⇒ Object

delegate names its target in a to: option, wherever that option sits among the forwarded method names.



141
142
143
# File 'lib/rubocop/cop/style/disallow_delegate.rb', line 141

def_node_matcher :to_option, <<~PATTERN
  (send nil? :delegate ... (hash <(pair (sym :to) $_) ...>))
PATTERN