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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4cdbe5b265ec54b100ac589171adad9038c99fee37dac3008cb56bba16e1a002
|
|
4
|
+
data.tar.gz: 8dfc81f74e7af24ad5518bf67ab735e9d75557cf2db03d4401ee46ba1c471f61
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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. |
|
|
@@ -127,7 +127,7 @@ module RuboCop
|
|
|
127
127
|
match && match[1]
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
-
# "UserAccount" → "user_account"; "
|
|
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
|
|
26
|
-
# contract, not delegation. The class has no interface without it
|
|
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
|
|
32
|
-
# pass-through. Middle Man is a method that
|
|
33
|
-
# answer verbatim, which the caller could
|
|
34
|
-
# that supplies its own attributes
|
|
35
|
-
# otherwise have to reach in and
|
|
36
|
-
# the
|
|
37
|
-
#
|
|
38
|
-
#
|
|
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: :
|
|
67
|
+
# delegate :name, to: :author
|
|
43
68
|
#
|
|
44
69
|
# # bad — same promise, written by hand
|
|
45
70
|
# def name
|
|
46
|
-
#
|
|
71
|
+
# author.name
|
|
47
72
|
# end
|
|
48
73
|
#
|
|
49
74
|
# # good — the caller navigates
|
|
50
|
-
#
|
|
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(
|
|
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
|
-
#
|
|
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|
|
|
174
|
+
return body.children.any? { |child| mixes_in_enumerable?(child) } if body.begin_type?
|
|
127
175
|
|
|
128
|
-
|
|
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
|
-
|
|
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
|
|
137
|
-
|
|
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.
|
|
140
|
-
return argument.
|
|
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
|