rubocop-fourshark 0.8.4 → 0.8.6
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 +4 -6
- data/config/default.yml +2 -2
- data/lib/rubocop/cop/style/disallow_delegate.rb +43 -4
- 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: 1bdc31121f34ef40cca25af1bc804caafecc8b8f58fd2996457cd01d82b98502
|
|
4
|
+
data.tar.gz: 26bbc30201fe22eef3477a20ce1eaf7016ee020a1ed93f53134429654da4a11d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ce23cde85c85d07932802b8bc89aa7c48f047cd274f59ff7d8c32b5e5e490e4bc46f118bb0e6216796cc774f9dc7c687bc464d33ac59361885a8bab2dd865edb
|
|
7
|
+
data.tar.gz: 616ed09b7f636b5cc4931894d86063a62b9e7357cf35802dee67f6d4a2c77ab8523131519ced29806cb6e1bd862ce8771931401411b3b1e0d59ebe6fabc5b575
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## [0.8.6] - 2026-08-08
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- `Style/DisallowDelegate` — a chained receiver carrying an own state argument is no longer flagged
|
|
6
|
+
|
|
7
|
+
## [0.8.5] - 2026-08-07
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Convention rationale in the README
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- `Style/DisallowDelegate` — a macro aimed at the object's own class is no longer flagged
|
|
16
|
+
|
|
1
17
|
## [0.8.4] - 2026-08-07
|
|
2
18
|
|
|
3
19
|
### Fixed
|
data/README.md
CHANGED
|
@@ -118,20 +118,18 @@ Where a 4Shark cop supersedes or contradicts a stock cop, `config/default.yml` t
|
|
|
118
118
|
|---|---|
|
|
119
119
|
| `FactoryBot/AssociationInFactory` | No associations declared inside a factory — they trigger cascading object creation and callbacks. Set the association manually in the spec. Scoped to `spec/factories`. |
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
This README states the intent each cop enforces. One convention deviates from a safe Rails default and is worth spelling out — see below.
|
|
122
122
|
|
|
123
123
|
### Why `belongs_to` is `optional: true` by default
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
By the time an association is assigned, existence and ownership have already been verified — more strictly than Rails would. Rails' default `belongs_to` (`optional: false`) then adds an existence `SELECT` per record on top of that: redundant work, and at high API throughput a measurable per-request cost.
|
|
125
|
+
Rails' default `belongs_to` (`optional: false`) validates that the associated record exists, which costs an existence `SELECT` per record. That validation is redundant in an application whose own request handling already establishes, before the association is assigned, that the record exists and is reachable by the caller — and under high throughput the redundant query is a measurable per-request cost.
|
|
128
126
|
|
|
129
127
|
So the convention is:
|
|
130
128
|
|
|
131
129
|
- `belongs_to` is always declared `optional: true` (enforced by `Rails/OptionalBelongsTo`), turning off Rails' automatic existence validation.
|
|
132
130
|
- Presence is validated manually with `validates :x_id, presence: true` **where the business rule requires it** — case by case, not globally (which is why no cop enforces the presence side).
|
|
133
131
|
|
|
134
|
-
A deliberate performance trade-off
|
|
132
|
+
A deliberate performance trade-off — not an omission.
|
|
135
133
|
|
|
136
134
|
## Development
|
|
137
135
|
|
|
@@ -147,7 +145,7 @@ Each new cop ships with a spec (`expect_offense` / `expect_no_offenses`) and a `
|
|
|
147
145
|
|
|
148
146
|
## Releasing
|
|
149
147
|
|
|
150
|
-
|
|
148
|
+
Releases are cut from `main`: feature branches merge into `main` via PR, the version is bumped, a `vX.Y.Z` tag is created from `main`, and the gem is published to RubyGems.
|
|
151
149
|
|
|
152
150
|
## License
|
|
153
151
|
|
data/config/default.yml
CHANGED
|
@@ -16,8 +16,8 @@ Layout/MultilineMethodCallIndentation:
|
|
|
16
16
|
|
|
17
17
|
# Stock cop configured (not a 4Shark cop): name the rescued exception variable
|
|
18
18
|
# `exception`. The cop's default is `e`, and a single-letter variable is banned by
|
|
19
|
-
# the 4Shark naming convention — so the two disagree
|
|
20
|
-
#
|
|
19
|
+
# the 4Shark naming convention — so the two disagree wherever a rescue binds its
|
|
20
|
+
# exception. `PreferredName` is the option the cop ships to settle exactly that,
|
|
21
21
|
# and the cop autocorrects, so the rename is mechanical.
|
|
22
22
|
Naming/RescuedExceptionsVariableName:
|
|
23
23
|
PreferredName: exception
|
|
@@ -22,6 +22,13 @@ 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
|
+
# A macro aimed at `:class` reaches the same place and is exempt for the
|
|
26
|
+
# same reason. Remove Middle Man has nothing to prescribe here: there is no
|
|
27
|
+
# third object for the caller to navigate to, so the only alternatives are
|
|
28
|
+
# the macro and a hand-written body that says exactly what the macro says.
|
|
29
|
+
# Between those two the macro is the better spelling, and flagging it while
|
|
30
|
+
# exempting the body would push every author toward the longer one.
|
|
31
|
+
#
|
|
25
32
|
# `each` in a class that includes or prepends `Enumerable` is the module's
|
|
26
33
|
# required contract, not delegation. The class has no interface without it
|
|
27
34
|
# — every method `Enumerable` provides is built on `each` — so the class is
|
|
@@ -79,6 +86,10 @@ module RuboCop
|
|
|
79
86
|
# self.class.lock_key(owner_id: owner_id)
|
|
80
87
|
# end
|
|
81
88
|
#
|
|
89
|
+
# # good — aimed at the object's own class, where there is no third
|
|
90
|
+
# # object to navigate to
|
|
91
|
+
# delegate :model, to: :class
|
|
92
|
+
#
|
|
82
93
|
# # good — `each` is the Enumerable contract this class implements
|
|
83
94
|
# class SearchResult
|
|
84
95
|
# include Enumerable
|
|
@@ -124,8 +135,23 @@ module RuboCop
|
|
|
124
135
|
(send nil? {:include :prepend} (const {nil? cbase} :Enumerable))
|
|
125
136
|
PATTERN
|
|
126
137
|
|
|
138
|
+
# `delegate` names its target in a `to:` option, wherever that option
|
|
139
|
+
# sits among the forwarded method names.
|
|
140
|
+
# @!method to_option(node)
|
|
141
|
+
def_node_matcher :to_option, <<~PATTERN
|
|
142
|
+
(send nil? :delegate ... (hash <(pair (sym :to) $_) ...>))
|
|
143
|
+
PATTERN
|
|
144
|
+
|
|
145
|
+
# The Forwardable macros and `delegate_missing_to` take their target as
|
|
146
|
+
# the first argument. `DelegateClass` names none and never matches.
|
|
147
|
+
# @!method leading_target(node)
|
|
148
|
+
def_node_matcher :leading_target, <<~PATTERN
|
|
149
|
+
(send nil? {:delegate_missing_to :def_delegator :def_delegators} $_ ...)
|
|
150
|
+
PATTERN
|
|
151
|
+
|
|
127
152
|
def on_send(node)
|
|
128
153
|
return unless node.receiver.nil?
|
|
154
|
+
return if own_class_target?(node)
|
|
129
155
|
|
|
130
156
|
add_offense(node, message: MACRO_MSG)
|
|
131
157
|
end
|
|
@@ -140,6 +166,16 @@ module RuboCop
|
|
|
140
166
|
|
|
141
167
|
private
|
|
142
168
|
|
|
169
|
+
# The target is written as a bare name, so a symbol and a string say the
|
|
170
|
+
# same thing and both are read as one.
|
|
171
|
+
def own_class_target?(node)
|
|
172
|
+
target = to_option(node) || leading_target(node)
|
|
173
|
+
return false if target.nil?
|
|
174
|
+
return false unless target.type?(:sym, :str)
|
|
175
|
+
|
|
176
|
+
target.value.to_sym == :class
|
|
177
|
+
end
|
|
178
|
+
|
|
143
179
|
def forwards_own_message?(body, method_name)
|
|
144
180
|
return false if body.nil?
|
|
145
181
|
return false unless body.send_type?
|
|
@@ -176,11 +212,14 @@ module RuboCop
|
|
|
176
212
|
mixes_in_enumerable?(body)
|
|
177
213
|
end
|
|
178
214
|
|
|
179
|
-
#
|
|
180
|
-
#
|
|
215
|
+
# A body that passes the object's own state is composing, not forwarding:
|
|
216
|
+
# it collapses `owner.collaborator.message(owner.state)` into
|
|
217
|
+
# `owner.message`, so the method hides the passing of a parameter the
|
|
218
|
+
# object itself holds. How far the collaborator sits is not part of that
|
|
219
|
+
# judgement — depth changes the path to the collaborator, never who the
|
|
220
|
+
# answer is about. A body that passes nothing, or passes a parameter the
|
|
221
|
+
# caller handed in, composes nothing and stays an offense at any depth.
|
|
181
222
|
def composes_own_state?(body)
|
|
182
|
-
return false if call_with_receiver?(body.receiver)
|
|
183
|
-
|
|
184
223
|
body.arguments.any? { |argument| own_state?(argument, body.receiver) }
|
|
185
224
|
end
|
|
186
225
|
|