rubocop-dev_doc 0.3.0 → 0.3.1

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: cd15873d6ee53104760162aa0e5cced9dd69ef1297a70a014dd5643b9404703c
4
- data.tar.gz: 25d0fa51436912d008625752269ff09ef93e78c14d6d1a941295c6afa47795b4
3
+ metadata.gz: 163ac4a232d22f5d731e680d4ae882172570cdcf3085a07be7d6c3195223e365
4
+ data.tar.gz: a3eae8c77dcb707ddc4875be90255bea2fa33bcaeba370c7aff06436cd045661
5
5
  SHA512:
6
- metadata.gz: 516d5742aa468568e3884d779649164761f982391be0c5f7e5df474d64a933cf71148fe1fddb433c5cf1cd577debd7aadcb16bf22893963d721a3661a1f0fff0
7
- data.tar.gz: 69a643cf2335d6c7bb365a4a2f8513e9a66d09a6cddb2bec5e44df861ebcb906de51d17fbbd1b591388c651c2f1cb662de5e5a887df7f11db3c9885e9d6ae597
6
+ metadata.gz: a605681c5f22058138abe3d8bdcd1e12eb94073f74e02fde30bc1f1df0baa538022b69fe33c4f83279cfaed995e60e5c9a4936de43cad7a89f07105c4b6e7a24
7
+ data.tar.gz: 0e845bcf2236245a8c4b0289a02d7731e12c9ac0023732f6135127f02f436c78f349012b98bd18127e89cdb4e70736597c2fc5313229ed3faaf572294e847168
data/config/default.yml CHANGED
@@ -348,12 +348,17 @@ Rails/HasManyOrHasOneDependent:
348
348
  Enabled: true
349
349
 
350
350
  DevDoc/Auth/LoadResourceCurrentUserGuard:
351
- Description: "Require a `return unless current_user` guard before using `current_user` inside `glib_load_resource`, and forbid `current_user&.`."
351
+ Description: "Require a `return unless current_user` (or `assert_current_user_present`) guard before using `current_user` inside `glib_load_resource`, and forbid `current_user&.`."
352
352
  Enabled: true
353
353
  # LoadResourceMethodNames: list of method names where the guard rule applies.
354
354
  # Extend if your project uses a different lifecycle hook name.
355
355
  LoadResourceMethodNames:
356
356
  - glib_load_resource
357
+ # CurrentUserAssertionMethodNames: calls that raise when current_user is nil
358
+ # (e.g. glib's assert_current_user_present). A call to one before the first
359
+ # current_user use satisfies the guard. Add custom assertion helpers here.
360
+ CurrentUserAssertionMethodNames:
361
+ - assert_current_user_present
357
362
 
358
363
  DevDoc/Auth/CurrentUserBranching:
359
364
  Description: "Avoid branching on `current_user` / `user_signed_in?` in page-specific code. Use shared layouts or an inline disable with a reason for genuinely dual-state pages."
@@ -36,6 +36,24 @@ module RuboCop
36
36
  # end
37
37
  # end
38
38
  #
39
+ # glib's `assert_current_user_present` raises when `current_user` is nil,
40
+ # so it guarantees non-nil just as well as the early return — and any
41
+ # `raise` guard works like the `return` form:
42
+ #
43
+ # ✔️
44
+ # def glib_load_resource
45
+ # assert_current_user_present
46
+ #
47
+ # @post = current_user.posts.find(params[:id])
48
+ # end
49
+ #
50
+ # ✔️
51
+ # def glib_load_resource
52
+ # raise UnauthorizedError unless current_user
53
+ #
54
+ # @post = current_user.posts.find(params[:id])
55
+ # end
56
+ #
39
57
  # ❌ Safe navigation — hides the pre-auth nil risk
40
58
  # def glib_load_resource
41
59
  # @post = current_user&.posts&.find(params[:id])
@@ -51,6 +69,13 @@ module RuboCop
51
69
  # method names where the guard rule applies. Projects standardising on a
52
70
  # different lifecycle method can add it here.
53
71
  #
72
+ # `CurrentUserAssertionMethodNames` (default:
73
+ # `[assert_current_user_present]`) — calls that raise when `current_user`
74
+ # is nil. A call to one of these before the first `current_user` use
75
+ # satisfies the guard. Add project-specific assertion helpers here. NOTE:
76
+ # the cop trusts the named method to actually raise on nil — a configured
77
+ # name that doesn't will turn into a false negative.
78
+ #
54
79
  # NOTE: The cop performs structural analysis of the method body and does
55
80
  # not track aliasing. If you assign `current_user` to a local variable
56
81
  # and then call methods on that variable, the cop will not flag it —
@@ -73,16 +98,32 @@ module RuboCop
73
98
  #
74
99
  # @post = current_user.posts.find(params[:id])
75
100
  # end
101
+ #
102
+ # # good — guarded with the glib assertion (raises when nil)
103
+ # def glib_load_resource
104
+ # assert_current_user_present
105
+ #
106
+ # @post = current_user.posts.find(params[:id])
107
+ # end
76
108
  class LoadResourceCurrentUserGuard < Base
77
109
  MSG_SAFE_NAV = 'Avoid `current_user&.` inside `%<method>s` — use ' \
78
110
  '`return unless current_user` then `current_user.` instead.'.freeze
79
111
  MSG_MISSING_GUARD = '`current_user` is used without a prior ' \
80
- '`return unless current_user` guard in `%<method>s`. ' \
112
+ '`return unless current_user` (or `assert_current_user_present`) ' \
113
+ 'guard in `%<method>s`. ' \
81
114
  'This method runs before the policy, so `current_user` may be nil.'.freeze
82
115
 
83
- # Methods that test current_user for nil — not calls that risk NoMethodError.
116
+ # Predicates safe to call on a nil `current_user` a `current_user.nil?`
117
+ # etc. is not itself an unguarded use that risks NoMethodError.
84
118
  NIL_CHECK_METHODS = %i[nil? blank? present? empty?].freeze
85
119
 
120
+ # Predicates split by polarity, so a guard's exit branch can be matched
121
+ # to the path on which `current_user` is nil. `present?` is a PRESENCE
122
+ # check (reversed from `nil?`/`blank?`), so `unless current_user.present?`
123
+ # is a valid guard while `if current_user.present?` is not.
124
+ ABSENCE_CHECK_METHODS = %i[nil? blank? empty?].freeze
125
+ PRESENCE_CHECK_METHODS = %i[present?].freeze
126
+
86
127
  def on_def(node)
87
128
  check_load_resource_method(node)
88
129
  end
@@ -94,6 +135,10 @@ module RuboCop
94
135
  Array(cop_config.fetch('LoadResourceMethodNames', ['glib_load_resource'])).map(&:to_sym)
95
136
  end
96
137
 
138
+ def current_user_assertion_method_names
139
+ Array(cop_config.fetch('CurrentUserAssertionMethodNames', ['assert_current_user_present'])).map(&:to_sym)
140
+ end
141
+
97
142
  def load_resource_method?(method_name)
98
143
  load_resource_method_names.include?(method_name.to_sym)
99
144
  end
@@ -122,42 +167,30 @@ module RuboCop
122
167
 
123
168
  # `current_user&.something` — always an offense regardless of guards.
124
169
  def safe_nav_on_current_user?(node)
125
- node.csend_type? && current_user_receiver?(node)
170
+ node.csend_type? && bare_current_user?(node.receiver)
126
171
  end
127
172
 
128
- # `current_user.something` where `something` is not a nil check and
129
- # no dominating guard protects the call.
173
+ # `current_user.something` where `something` is not a nil check and no
174
+ # dominating guard protects the call — i.e. not inside an `if current_user`
175
+ # then-branch and not preceded by a guard statement in its enclosing `begin`.
130
176
  def unguarded_current_user_call?(node)
131
177
  return false unless node.send_type?
132
- return false unless current_user_receiver?(node)
133
- return false if nil_check_method?(node.method_name)
134
-
135
- !dominated_by_guard?(node)
136
- end
137
-
138
- # Does the direct receiver of `node` resolve to bare `current_user`?
139
- def current_user_receiver?(node)
140
- recv = node.receiver
141
- recv&.send_type? && recv.method_name == :current_user && recv.receiver.nil?
142
- end
178
+ return false unless bare_current_user?(node.receiver)
179
+ return false if NIL_CHECK_METHODS.include?(node.method_name)
143
180
 
144
- def nil_check_method?(method_name)
145
- NIL_CHECK_METHODS.include?(method_name)
181
+ !(inside_current_user_branch?(node) || preceded_by_guard?(node))
146
182
  end
147
183
 
148
- # Returns true if `node` is protected by a `current_user` nil-guard via:
149
- # 1. Being inside an `if current_user` then-branch, OR
150
- # 2. Having a preceding guard-return statement in its nearest enclosing
151
- # `begin` sequence (handles flat sequences AND nested `when` branches).
152
- def dominated_by_guard?(node)
153
- inside_current_user_branch?(node) || preceded_by_guard?(node)
184
+ # True if `node` is a bare `current_user` send (no receiver).
185
+ def bare_current_user?(node)
186
+ node&.send_type? && node.method_name == :current_user && node.receiver.nil?
154
187
  end
155
188
 
156
189
  # Walk ancestor `if` nodes and return true when `node` is inside the
157
190
  # then-branch of an `if current_user` (not the else-branch).
158
191
  def inside_current_user_branch?(node)
159
192
  node.each_ancestor(:if) do |if_node|
160
- next unless current_user_truthy_condition?(if_node.condition)
193
+ next unless bare_current_user?(if_node.condition)
161
194
 
162
195
  if_br = if_node.if_branch
163
196
  return true if if_br && (if_br.equal?(node) || descendant_by_identity?(if_br, node))
@@ -186,42 +219,66 @@ module RuboCop
186
219
  root.each_descendant.any? { |d| d.equal?(descendant) }
187
220
  end
188
221
 
189
- # True if `stmt` is a guard-return on `current_user`:
190
- # return unless current_user — condition: current_user, one branch: (return)
191
- # return if current_user.nil? — condition: current_user.nil?, one branch: (return)
192
- # return if current_user.blank? — condition: current_user.blank?, one branch: (return)
222
+ # True if `stmt` guarantees `current_user` is non-nil for everything
223
+ # that follows it either:
193
224
  #
194
- # The parser gem swaps `if_branch`/`else_branch` for `unless`-modifier
195
- # forms, so we check BOTH branches for a bare `return` rather than
196
- # assuming which side it falls on.
225
+ # 1. a bare call to a configured assertion helper (default
226
+ # `assert_current_user_present`), which raises when nil; or
227
+ # 2. an early-exit guard whose branch returns OR raises:
228
+ # return unless current_user raise ... unless current_user
229
+ # return if current_user.nil? raise ... if current_user.blank?
197
230
  def guard_statement?(stmt)
198
- return false unless stmt.if_type?
199
-
200
- condition = stmt.condition
201
- return false unless current_user_truthy_condition?(condition) ||
202
- current_user_nil_condition?(condition)
231
+ assertion_guard_call?(stmt) || exit_guard_statement?(stmt)
232
+ end
203
233
 
204
- return_node?(stmt.if_branch) || return_node?(stmt.else_branch)
234
+ # A bare call (nil receiver) to a "raise when current_user is nil"
235
+ # helper, e.g. glib's `assert_current_user_present`.
236
+ def assertion_guard_call?(stmt)
237
+ stmt.send_type? && stmt.receiver.nil? &&
238
+ current_user_assertion_method_names.include?(stmt.method_name)
205
239
  end
206
240
 
207
- # Condition is bare `current_user` (truthy check).
208
- def current_user_truthy_condition?(condition)
209
- condition&.send_type? &&
210
- condition.method_name == :current_user &&
211
- condition.receiver.nil?
241
+ # A `return`/`raise` guard whose exit happens on the NIL path — the only
242
+ # polarity that actually protects later `current_user` use:
243
+ # `... unless current_user` exits via the else branch
244
+ # `... if current_user.nil?` exits via the if branch
245
+ # (Ruby models an `if`/`unless` modifier as an `if` node with an empty
246
+ # opposite branch, so we tie the required exit to the condition polarity.
247
+ # This rejects inverted guards like `return if current_user`.)
248
+ # A `return`/`raise` guard is valid only when the branch that runs while
249
+ # `current_user` is nil exits. `if_branch` is the written body regardless
250
+ # of `if`/`unless`, so combine the condition's polarity with the keyword to
251
+ # pick that branch. This rejects inverted guards (`return if current_user`).
252
+ def exit_guard_statement?(stmt)
253
+ return false unless stmt.if_type?
254
+
255
+ polarity = condition_polarity(stmt.condition)
256
+ return false unless polarity
257
+
258
+ body_runs_when_nil = polarity == (stmt.unless? ? :presence : :absence)
259
+ branch_exits?(body_runs_when_nil ? stmt.if_branch : stmt.else_branch)
212
260
  end
213
261
 
214
- # Condition is `current_user.nil?` or `current_user.blank?`.
215
- def current_user_nil_condition?(condition)
216
- return false unless condition&.send_type?
217
- return false unless NIL_CHECK_METHODS.include?(condition.method_name)
262
+ # :presence condition truthy when current_user is present (`current_user`,
263
+ # `current_user.present?`); :absence — truthy when absent (`.nil?`/`.blank?`/
264
+ # `.empty?`); nil not a current_user condition.
265
+ def condition_polarity(condition)
266
+ return :presence if bare_current_user?(condition)
267
+ return unless condition&.send_type? && bare_current_user?(condition.receiver)
268
+ return :presence if PRESENCE_CHECK_METHODS.include?(condition.method_name)
218
269
 
219
- recv = condition.receiver
220
- recv&.send_type? && recv.method_name == :current_user && recv.receiver.nil?
270
+ :absence if ABSENCE_CHECK_METHODS.include?(condition.method_name)
221
271
  end
222
272
 
223
- def return_node?(node)
224
- node&.return_type?
273
+ # True if `branch` terminates on entry via an early `return` or a bare
274
+ # `raise`/`fail`. A multi-statement branch (a `begin`) counts when its
275
+ # LAST statement does.
276
+ def branch_exits?(branch)
277
+ return false unless branch
278
+
279
+ branch = branch.children.last if branch.begin_type?
280
+ branch&.return_type? ||
281
+ (branch&.send_type? && branch.receiver.nil? && %i[raise fail].include?(branch.method_name))
225
282
  end
226
283
  end
227
284
  end
@@ -1,5 +1,5 @@
1
1
  module RuboCop
2
2
  module DevDoc
3
- VERSION = "0.3.0".freeze
3
+ VERSION = "0.3.1".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-dev_doc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - dev-doc contributors