rubocop-fourshark 0.8.2 → 0.8.4

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: 4cdbe5b265ec54b100ac589171adad9038c99fee37dac3008cb56bba16e1a002
4
+ data.tar.gz: 8dfc81f74e7af24ad5518bf67ab735e9d75557cf2db03d4401ee46ba1c471f61
5
5
  SHA512:
6
- metadata.gz: edd536903d83b3516f576bdcf318847c5914a9b747d52203f2153a2dc2eaf5af783a7fc9d16d1f730674d51a8bde815b576a29f64619655824a227551a2979b1
7
- data.tar.gz: c063cd068bace0cdccf5e0948e94c40088b0160fc023f14ee315f4177374134bea62eedc49d42655a505579afc0d43a9248d1f2d4a6231344d624154e5b95afe
6
+ metadata.gz: 684f7b5b0f1064064eb545240a7f2f290207c9d0e7f759363e26d2607b2fe0d48fda173e8e48827769fa625f3eefb5ed6591ffdbdc770bf29f63bebbe156c50f
7
+ data.tar.gz: 19a91ad805511e8a3f19d903d18eeb007c5a2c1d4e2329733336e12d6b041cad998466f0fe080556830fbf8a90be6736f568b33eaf1d803b3e2e4c94b0dd8555
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## [0.8.4] - 2026-08-07
2
+
3
+ ### Fixed
4
+
5
+ - `Style/DisallowDelegate` — own state reached through a call on it is no longer flagged
6
+ - `Style/DisallowDelegate` — an argument rooted at the collaborator being forwarded to is flagged again
7
+ - `Style/DisallowDelegate` — own state read through an explicit `self` is no longer flagged
8
+ - `Style/DisallowDelegate` — a call reached through safe navigation counts as the call it is, on both the receiver and the argument side
9
+
10
+ ## [0.8.3] - 2026-08-06
11
+
12
+ ### Fixed
13
+
14
+ - `Style/DisallowDelegate` — own state passed through a splat, a double splat, an array or a block is no longer flagged
15
+ - `Style/DisallowDelegate` — `each` in a class prepending `Enumerable` is no longer flagged
16
+ - `Style/DisallowDelegate` — a chained receiver carrying an own state argument is flagged again
17
+ - `Style/DisallowDelegate` — `each` in a singleton class is flagged again
18
+
1
19
  ## [0.8.2] - 2026-08-06
2
20
 
3
21
  ### 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. |
@@ -141,7 +141,7 @@ module RuboCop
141
141
  #
142
142
  # Example:
143
143
  # "UserAccount" → "user_account"
144
- # "PlanStatementAudit::Row" → "plan_statement_audit/row"
144
+ # "InvoiceLineItem::Row" → "invoice_line_item/row"
145
145
  def camel_to_snake(name)
146
146
  return '' if name.nil?
147
147
 
@@ -127,7 +127,7 @@ module RuboCop
127
127
  match && match[1]
128
128
  end
129
129
 
130
- # "UserAccount" → "user_account"; "Plan::Statement" → "plan/statement"
130
+ # "UserAccount" → "user_account"; "Order::LineItem" → "order/line_item"
131
131
  def camel_to_snake(name)
132
132
  name.gsub('::', '/')
133
133
  .gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
@@ -22,36 +22,61 @@ 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
+ # Own state is an instance variable, `self`, or a receiverless call,
41
+ # reached directly, through a wrapper, or through a call on it — `record`,
42
+ # `record.owner_id` and `[record.owner_id]` all carry the object's own
43
+ # data. A method PARAMETER does not: it arrives as an `lvar`, so a setter
44
+ # handing its argument to a collaborator stays flagged.
45
+ #
46
+ # Two limits keep the exemption from swallowing the rule.
47
+ #
48
+ # The receiver must not itself be a chain. Remove Middle Man on a message
49
+ # chain IS the caller navigating, so composing own state does not excuse
50
+ # it.
51
+ #
52
+ # An argument rooted at the collaborator being forwarded to does not earn
53
+ # the exemption. `author.name(author.locale)` hands the collaborator back
54
+ # its own data, which is the echo this rule forbids, not composition, so
55
+ # with no other argument to carry it the forward stays flagged. Rooted at
56
+ # the object's OWN state the argument may be a chain of any depth — the
57
+ # object still supplied what the collaborator needed, and how far it
58
+ # reached inside itself to build the value is its own business.
59
+ #
60
+ # A forward to a collaborator that passes NO argument is the one that
61
+ # republishes, and it is always flagged. That is the line — supplying data
62
+ # the collaborator needs is composition, echoing back what the collaborator
63
+ # already knows is delegation.
39
64
  #
40
65
  # @example
41
66
  # # bad
42
- # delegate :name, to: :commission
67
+ # delegate :name, to: :author
43
68
  #
44
69
  # # bad — same promise, written by hand
45
70
  # def name
46
- # commission.name
71
+ # author.name
47
72
  # end
48
73
  #
49
74
  # # good — the caller navigates
50
- # statement.commission.name
75
+ # post.author.name
51
76
  #
52
77
  # # good — the object answers about itself, not for a collaborator
53
78
  # def lock_key
54
- # self.class.lock_key(company_id: company_id)
79
+ # self.class.lock_key(owner_id: owner_id)
55
80
  # end
56
81
  #
57
82
  # # good — `each` is the Enumerable contract this class implements
@@ -65,7 +90,22 @@ module RuboCop
65
90
  #
66
91
  # # good — composes its own attribute, so the caller cannot just navigate
67
92
  # def output
68
- # variable.output(value)
93
+ # formatter.output(value)
94
+ # end
95
+ #
96
+ # # good — names an expression built from its own record; not a forward
97
+ # def lock_key
98
+ # Registry.lock_key(owner_id: record.owner_id)
99
+ # end
100
+ #
101
+ # # bad — a chain does not stop being a chain because an argument rode along
102
+ # def starts_at
103
+ # schedule.window.period.starts_at(calendar)
104
+ # end
105
+ #
106
+ # # bad — the argument is the collaborator's own data handed back to it
107
+ # def name
108
+ # author.name(author.locale)
69
109
  # end
70
110
  #
71
111
  class DisallowDelegate < ::RuboCop::Cop::Base
@@ -74,17 +114,22 @@ module RuboCop
74
114
 
75
115
  RESTRICT_ON_SEND = %i[delegate delegate_missing_to def_delegator def_delegators DelegateClass].freeze
76
116
 
117
+ # An argument reaches the body wrapped in one of these when the call site
118
+ # spreads or blocks it, and the wrapper says nothing about whose state it
119
+ # carries.
120
+ ARGUMENT_WRAPPERS = %i[hash pair array splat kwsplat block_pass].freeze
121
+
122
+ # @!method mixes_in_enumerable?(node)
123
+ def_node_matcher :mixes_in_enumerable?, <<~PATTERN
124
+ (send nil? {:include :prepend} (const {nil? cbase} :Enumerable))
125
+ PATTERN
126
+
77
127
  def on_send(node)
78
128
  return unless node.receiver.nil?
79
129
 
80
130
  add_offense(node, message: MACRO_MSG)
81
131
  end
82
132
 
83
- # @!method includes_enumerable?(node)
84
- def_node_matcher :includes_enumerable?, <<~PATTERN
85
- (send nil? :include (const {nil? cbase} :Enumerable))
86
- PATTERN
87
-
88
133
  def on_def(node)
89
134
  return unless forwards_own_message?(node.body, node.method_name)
90
135
  return if enumerable_contract?(node)
@@ -112,35 +157,65 @@ module RuboCop
112
157
  receiver.receiver.self_type?
113
158
  end
114
159
 
160
+ # The nearest enclosing scope decides, and a singleton class never
161
+ # inherits the instance side's mixin.
115
162
  def enumerable_contract?(node)
116
163
  return false unless node.method?(:each)
117
164
 
118
- enclosing = node.each_ancestor(:class, :module).first
165
+ enclosing = node.each_ancestor(:class, :module, :sclass).first
119
166
  return false if enclosing.nil?
167
+ return false if enclosing.sclass_type?
120
168
 
121
169
  enumerable_body?(enclosing.body)
122
170
  end
123
171
 
124
172
  def enumerable_body?(body)
125
173
  return false if body.nil?
126
- return body.children.any? { |child| includes_enumerable?(child) } if body.begin_type?
174
+ return body.children.any? { |child| mixes_in_enumerable?(child) } if body.begin_type?
127
175
 
128
- includes_enumerable?(body)
176
+ mixes_in_enumerable?(body)
129
177
  end
130
178
 
179
+ # Remove Middle Man on a message chain IS the caller navigating, so an
180
+ # argument riding along does not earn the exemption.
131
181
  def composes_own_state?(body)
132
- body.arguments.any? { |argument| own_state?(argument) }
182
+ return false if call_with_receiver?(body.receiver)
183
+
184
+ body.arguments.any? { |argument| own_state?(argument, body.receiver) }
133
185
  end
134
186
 
135
187
  # 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.
137
- def own_state?(argument)
188
+ # variable, `self`, or a receiverless call counts as the object's own
189
+ # state — read directly, through a wrapper, or through a call on it.
190
+ # Anything rooted at the collaborator is that collaborator's own data
191
+ # coming back, which is the echo the rule forbids. The first guard
192
+ # refuses anything that is not a node, which covers both the nil child
193
+ # an anonymous block pass carries and a wrapper whose children are raw
194
+ # Ruby values rather than nodes.
195
+ def own_state?(argument, collaborator)
196
+ return false unless argument.is_a?(::RuboCop::AST::Node)
197
+ return false if argument == collaborator
138
198
  return true if argument.ivar_type?
139
- return true if argument.send_type? && argument.receiver.nil?
140
- return argument.values.any? { |value| own_state?(value) } if argument.hash_type?
199
+ return true if argument.self_type?
200
+ return own_state?(argument.receiver, collaborator) if call_with_receiver?(argument)
201
+ return true if argument.send_type?
202
+ return argument.children.any? { |child| own_state?(child, collaborator) } if wrapper?(argument)
141
203
 
142
204
  false
143
205
  end
206
+
207
+ # `call_type?` is the `send`/`csend` union, so safe navigation counts as
208
+ # the call it is — a `&.` link neither escapes the chain limit nor stops
209
+ # own state from being recognized.
210
+ def call_with_receiver?(node)
211
+ return false unless node.call_type?
212
+
213
+ !node.receiver.nil?
214
+ end
215
+
216
+ def wrapper?(argument)
217
+ ARGUMENT_WRAPPERS.include?(argument.type)
218
+ end
144
219
  end
145
220
  end
146
221
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Fourshark
5
- VERSION = '0.8.2'
5
+ VERSION = '0.8.4'
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.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Ribeiro