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 +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +1 -1
- data/lib/rubocop/cop/style/disallow_delegate.rb +108 -0
- data/lib/rubocop/fourshark/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dbfa3d2a442ba57bc74ba0171563f0bcadb6f4460b769654963066462ca0b61f
|
|
4
|
+
data.tar.gz: '097de5b926870d292e63aa39f79aacc8342fe1909e34809bf9bb2f066c309a22'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|