rubocop-fourshark 0.8.1 → 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: 31d77a73abb724b68a4c2d187cc780899067d2ef82564e50bcce0d6b09dbffeb
4
- data.tar.gz: fd2eda214bf001b848e18be1f68df32463d70272be3f7ce999ac7cb3b1be8f8f
3
+ metadata.gz: dbfa3d2a442ba57bc74ba0171563f0bcadb6f4460b769654963066462ca0b61f
4
+ data.tar.gz: '097de5b926870d292e63aa39f79aacc8342fe1909e34809bf9bb2f066c309a22'
5
5
  SHA512:
6
- metadata.gz: 61aabc197c8224de85d39c9f150cb0fb1b73916a8882fd35cb3241b4b5ca330eb44f4098a12eff4fc934b918ea2ccf75a81bd150c1eac96193678ef620ae734c
7
- data.tar.gz: b0b7426b78afa3a99f028eb84b4dd78479c153d219997d55611e7e1ab3544ddfe99ead0b323306c24220c9806715ddf413b1ace76bdb19df8ccad9bb43391562
6
+ metadata.gz: 36791e28a8f0c14fa70a4990d953207412390095deda3386d9aa4edb233447ef4576d645b4ea499414c5e3ecac44fe9e4d2845033d83d607453a303655d6e32a
7
+ data.tar.gz: 17bbb761062d3b28d03238fcf94df33bc29cc2f85ba839038469e7cf39d5c7bd0a6ced5402fac62d6660c7eb5d7c57e8971aa118f3ce1283412f7ddd366f223a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
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
+
10
+ ## [0.8.2] - 2026-08-06
11
+
12
+ ### Fixed
13
+
14
+ - `Style/DisallowDelegate` — `each` in a class including `Enumerable` is no longer flagged
15
+ - `Style/DisallowDelegate` — a method composing its own state into the call is no longer flagged
16
+
1
17
  ## [0.8.1] - 2026-08-06
2
18
 
3
19
  ### 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,6 +22,34 @@ 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 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
+ # implementing its own interface rather than answering for a collaborator.
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.
32
+ #
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.
52
+ #
25
53
  # @example
26
54
  # # bad
27
55
  # delegate :name, to: :commission
@@ -39,12 +67,41 @@ module RuboCop
39
67
  # self.class.lock_key(company_id: company_id)
40
68
  # end
41
69
  #
70
+ # # good — `each` is the Enumerable contract this class implements
71
+ # class SearchResult
72
+ # include Enumerable
73
+ #
74
+ # def each(&)
75
+ # results.each(&)
76
+ # end
77
+ # end
78
+ #
79
+ # # good — composes its own attribute, so the caller cannot just navigate
80
+ # def output
81
+ # variable.output(value)
82
+ # end
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
+ #
42
89
  class DisallowDelegate < ::RuboCop::Cop::Base
43
90
  MACRO_MSG = 'Do not use automatic delegation. Delete the forwarder and let the caller navigate.'
44
91
  FORWARDER_MSG = 'Do not forward a collaborator\'s message. Delete the forwarder and let the caller navigate.'
45
92
 
46
93
  RESTRICT_ON_SEND = %i[delegate delegate_missing_to def_delegator def_delegators DelegateClass].freeze
47
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
+
48
105
  def on_send(node)
49
106
  return unless node.receiver.nil?
50
107
 
@@ -53,6 +110,8 @@ module RuboCop
53
110
 
54
111
  def on_def(node)
55
112
  return unless forwards_own_message?(node.body, node.method_name)
113
+ return if enumerable_contract?(node)
114
+ return if composes_own_state?(node.body)
56
115
 
57
116
  add_offense(node.loc.name, message: FORWARDER_MSG)
58
117
  end
@@ -75,6 +134,55 @@ module RuboCop
75
134
 
76
135
  receiver.receiver.self_type?
77
136
  end
137
+
138
+ # The nearest enclosing scope decides, and a singleton class never
139
+ # inherits the instance side's mixin.
140
+ def enumerable_contract?(node)
141
+ return false unless node.method?(:each)
142
+
143
+ enclosing = node.each_ancestor(:class, :module, :sclass).first
144
+ return false if enclosing.nil?
145
+ return false if enclosing.sclass_type?
146
+
147
+ enumerable_body?(enclosing.body)
148
+ end
149
+
150
+ def enumerable_body?(body)
151
+ return false if body.nil?
152
+ return body.children.any? { |child| mixes_in_enumerable?(child) } if body.begin_type?
153
+
154
+ mixes_in_enumerable?(body)
155
+ end
156
+
157
+ def composes_own_state?(body)
158
+ return false if chained_receiver?(body.receiver)
159
+
160
+ body.arguments.any? { |argument| own_state?(argument) }
161
+ end
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
+
171
+ # A method parameter reaches the body as an `lvar`, so only an instance
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.
174
+ def own_state?(argument)
175
+ return false if argument.nil?
176
+ return true if argument.ivar_type?
177
+ return true if argument.send_type? && argument.receiver.nil?
178
+ return argument.children.any? { |child| own_state?(child) } if wrapper?(argument)
179
+
180
+ false
181
+ end
182
+
183
+ def wrapper?(argument)
184
+ ARGUMENT_WRAPPERS.include?(argument.type)
185
+ end
78
186
  end
79
187
  end
80
188
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Fourshark
5
- VERSION = '0.8.1'
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.1
4
+ version: 0.8.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Ribeiro