rubocop-fourshark 0.8.2 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9358c7dd0bcd6717781203b1f4ccdd6e282851da1e52838db50acf058ce39414
4
- data.tar.gz: f219f75c7fdaea1dff73f43ed9a1c16478e97b44f45f5242d9f21044e57cd19c
3
+ metadata.gz: dbfa3d2a442ba57bc74ba0171563f0bcadb6f4460b769654963066462ca0b61f
4
+ data.tar.gz: '097de5b926870d292e63aa39f79aacc8342fe1909e34809bf9bb2f066c309a22'
5
5
  SHA512:
6
- metadata.gz: edd536903d83b3516f576bdcf318847c5914a9b747d52203f2153a2dc2eaf5af783a7fc9d16d1f730674d51a8bde815b576a29f64619655824a227551a2979b1
7
- data.tar.gz: c063cd068bace0cdccf5e0948e94c40088b0160fc023f14ee315f4177374134bea62eedc49d42655a505579afc0d43a9248d1f2d4a6231344d624154e5b95afe
6
+ metadata.gz: 36791e28a8f0c14fa70a4990d953207412390095deda3386d9aa4edb233447ef4576d645b4ea499414c5e3ecac44fe9e4d2845033d83d607453a303655d6e32a
7
+ data.tar.gz: 17bbb761062d3b28d03238fcf94df33bc29cc2f85ba839038469e7cf39d5c7bd0a6ced5402fac62d6660c7eb5d7c57e8971aa118f3ce1283412f7ddd366f223a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## [0.8.3] - 2026-08-06
2
+
3
+ ### Fixed
4
+
5
+ - `Style/DisallowDelegate` — own state passed through a splat, a double splat, an array or a block is no longer flagged
6
+ - `Style/DisallowDelegate` — `each` in a class prepending `Enumerable` is no longer flagged
7
+ - `Style/DisallowDelegate` — a chained receiver carrying an own state argument is flagged again
8
+ - `Style/DisallowDelegate` — `each` in a singleton class is flagged again
9
+
1
10
  ## [0.8.2] - 2026-08-06
2
11
 
3
12
  ### Fixed
data/README.md CHANGED
@@ -76,7 +76,7 @@ Where a 4Shark cop supersedes or contradicts a stock cop, `config/default.yml` t
76
76
 
77
77
  | Cop | Intent |
78
78
  |---|---|
79
- | `Style/DisallowDelegate` | Flags automatic delegation (`delegate`, `delegate_missing_to`, `def_delegator(s)`, `DelegateClass`). A `delegate` line is a macro call, not a definition, so `grep 'def foo'` and jump-to-definition find nothing — the community name for the smell is Fowler's Middle Man. Write the method, or let the caller ask the object that knows. |
79
+ | `Style/DisallowDelegate` | Flags automatic delegation (`delegate`, `delegate_missing_to`, `def_delegator(s)`, `DelegateClass`) and a hand-written method whose whole body forwards its own name to a collaborator. A `delegate` line is a macro call, not a definition, so `grep 'def foo'` and jump-to-definition find nothing — the community name for the smell is Fowler's Middle Man. Not flagged: a method answering through its own `self.class`, `each` in a class that includes or prepends `Enumerable`, and a body composing the object's own state into a call on a direct collaborator. Let the caller ask the object that knows. |
80
80
  | `Style/DisallowSafeNavigation` | Flags safe navigation (`&.`). 4Shark rejects it — a `&.` chain silently swallows a `nil` that usually signals a real bug. Use an explicit conditional so the `nil` case is handled on purpose. |
81
81
  | `Style/DisallowTernary` | Flags the ternary conditional (`cond ? a : b`). `?` and `:` are punctuation that already mean other things in Ruby (`valid?`, `:symbol`), and the ternary hides a branch inside an expression where skimming misses it. Use an explicit `if`/`else`. |
82
82
  | `Style/DisallowTry` | Flags `try` / `try!`. Same rationale — it hides the `nil`/missing-method case instead of handling it explicitly. |
@@ -22,20 +22,33 @@ module RuboCop
22
22
  # method answers for a domain it owns, so it is not delegation and is not
23
23
  # flagged.
24
24
  #
25
- # `each` in a class that includes `Enumerable` is the module's required
26
- # contract, not delegation. The class has no interface without it — every
27
- # method `Enumerable` provides is built on `each` — so the class is
25
+ # `each` in a class that includes or prepends `Enumerable` is the module's
26
+ # required contract, not delegation. The class has no interface without it
27
+ # — every method `Enumerable` provides is built on `each` — so the class is
28
28
  # implementing its own interface rather than answering for a collaborator.
29
- # It is not flagged. Any other method in such a class still is.
29
+ # It is not flagged. Any other method in such a class still is, and so is
30
+ # `each` in a singleton class: an instance-side mixin obliges the instance,
31
+ # never the singleton.
30
32
  #
31
- # A body that passes the object's own state as an argument is not a
32
- # pass-through. Middle Man is a method that republishes a collaborator's
33
- # answer verbatim, which the caller could reach by navigating; a method
34
- # that supplies its own attributes contributes something the caller would
35
- # otherwise have to reach in and take, and the result is a simpler API on
36
- # the object that owns the data. Own state is an instance variable or a
37
- # receiverless call — a method parameter forwarded through is not, so a
38
- # setter passing its argument along stays flagged.
33
+ # A body that passes the object's own state as an argument to a DIRECT
34
+ # collaborator is not a pass-through. Middle Man is a method that
35
+ # republishes a collaborator's answer verbatim, which the caller could
36
+ # reach by navigating; a method that supplies its own attributes
37
+ # contributes something the caller would otherwise have to reach in and
38
+ # take, and the result is a simpler API on the object that owns the data.
39
+ #
40
+ # Three limits keep that exemption from swallowing the rule. The receiver
41
+ # must not itself be a chain — Remove Middle Man on a message chain IS the
42
+ # caller navigating, so composing own state does not excuse it. Own state
43
+ # is an instance variable or a receiverless call, so a method parameter
44
+ # forwarded through does not qualify and a setter stays flagged. And own
45
+ # state is read through a splat, a double splat, an array or a block pass,
46
+ # because the wrapper does not change whose state the argument carries.
47
+ #
48
+ # An argument derived from own state through another call —
49
+ # `variable.label(value.to_s)` — is still flagged. Recursing into an
50
+ # argument's receiver would also exempt `Lock.lock_key(company_id:
51
+ # user.company_id)`, which is delegation, so the conservative reading wins.
39
52
  #
40
53
  # @example
41
54
  # # bad
@@ -68,23 +81,33 @@ module RuboCop
68
81
  # variable.output(value)
69
82
  # end
70
83
  #
84
+ # # bad — a chain does not stop being a chain because an argument rode along
85
+ # def starts_at
86
+ # commission.plan.period.starts_at(calendar)
87
+ # end
88
+ #
71
89
  class DisallowDelegate < ::RuboCop::Cop::Base
72
90
  MACRO_MSG = 'Do not use automatic delegation. Delete the forwarder and let the caller navigate.'
73
91
  FORWARDER_MSG = 'Do not forward a collaborator\'s message. Delete the forwarder and let the caller navigate.'
74
92
 
75
93
  RESTRICT_ON_SEND = %i[delegate delegate_missing_to def_delegator def_delegators DelegateClass].freeze
76
94
 
95
+ # An argument reaches the body wrapped in one of these when the call site
96
+ # spreads or blocks it, and the wrapper says nothing about whose state it
97
+ # carries.
98
+ ARGUMENT_WRAPPERS = %i[hash pair array splat kwsplat block_pass].freeze
99
+
100
+ # @!method mixes_in_enumerable?(node)
101
+ def_node_matcher :mixes_in_enumerable?, <<~PATTERN
102
+ (send nil? {:include :prepend} (const {nil? cbase} :Enumerable))
103
+ PATTERN
104
+
77
105
  def on_send(node)
78
106
  return unless node.receiver.nil?
79
107
 
80
108
  add_offense(node, message: MACRO_MSG)
81
109
  end
82
110
 
83
- # @!method includes_enumerable?(node)
84
- def_node_matcher :includes_enumerable?, <<~PATTERN
85
- (send nil? :include (const {nil? cbase} :Enumerable))
86
- PATTERN
87
-
88
111
  def on_def(node)
89
112
  return unless forwards_own_message?(node.body, node.method_name)
90
113
  return if enumerable_contract?(node)
@@ -112,35 +135,54 @@ module RuboCop
112
135
  receiver.receiver.self_type?
113
136
  end
114
137
 
138
+ # The nearest enclosing scope decides, and a singleton class never
139
+ # inherits the instance side's mixin.
115
140
  def enumerable_contract?(node)
116
141
  return false unless node.method?(:each)
117
142
 
118
- enclosing = node.each_ancestor(:class, :module).first
143
+ enclosing = node.each_ancestor(:class, :module, :sclass).first
119
144
  return false if enclosing.nil?
145
+ return false if enclosing.sclass_type?
120
146
 
121
147
  enumerable_body?(enclosing.body)
122
148
  end
123
149
 
124
150
  def enumerable_body?(body)
125
151
  return false if body.nil?
126
- return body.children.any? { |child| includes_enumerable?(child) } if body.begin_type?
152
+ return body.children.any? { |child| mixes_in_enumerable?(child) } if body.begin_type?
127
153
 
128
- includes_enumerable?(body)
154
+ mixes_in_enumerable?(body)
129
155
  end
130
156
 
131
157
  def composes_own_state?(body)
158
+ return false if chained_receiver?(body.receiver)
159
+
132
160
  body.arguments.any? { |argument| own_state?(argument) }
133
161
  end
134
162
 
163
+ # Remove Middle Man on a message chain IS the caller navigating, so an
164
+ # argument riding along does not earn the exemption.
165
+ def chained_receiver?(receiver)
166
+ return false unless receiver.send_type?
167
+
168
+ !receiver.receiver.nil?
169
+ end
170
+
135
171
  # A method parameter reaches the body as an `lvar`, so only an instance
136
- # variable or a receiverless call counts as the object's own state.
172
+ # variable or a receiverless call counts as the object's own state. An
173
+ # anonymous block pass carries a nil child, hence the first guard.
137
174
  def own_state?(argument)
175
+ return false if argument.nil?
138
176
  return true if argument.ivar_type?
139
177
  return true if argument.send_type? && argument.receiver.nil?
140
- return argument.values.any? { |value| own_state?(value) } if argument.hash_type?
178
+ return argument.children.any? { |child| own_state?(child) } if wrapper?(argument)
141
179
 
142
180
  false
143
181
  end
182
+
183
+ def wrapper?(argument)
184
+ ARGUMENT_WRAPPERS.include?(argument.type)
185
+ end
144
186
  end
145
187
  end
146
188
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Fourshark
5
- VERSION = '0.8.2'
5
+ VERSION = '0.8.3'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-fourshark
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 0.8.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Ribeiro