rubocop-dev_doc 0.3.0 → 0.5.0.beta1
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/config/default.yml +153 -1
- data/lib/rubocop/cop/dev_doc/auth/load_resource_current_user_guard.rb +108 -51
- data/lib/rubocop/cop/dev_doc/i18n/report_text.rb +112 -0
- data/lib/rubocop/cop/dev_doc/i18n/require_translation.rb +116 -0
- data/lib/rubocop/cop/dev_doc/i18n/translation_key_prefix.rb +89 -0
- data/lib/rubocop/cop/dev_doc/i18n/unverified_translation.rb +106 -0
- data/lib/rubocop/cop/dev_doc/rails/avoid_lifecycle_method_override.rb +107 -0
- data/lib/rubocop/cop/dev_doc/rails/strong_parameters_expect.rb +9 -3
- data/lib/rubocop/dev_doc/version.rb +1 -1
- metadata +8 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e13da0de0c2a17314d72cbe4bbeb9882a0ad1f93e4976d2e0548210af1684073
|
|
4
|
+
data.tar.gz: 57bf1fb471573aaa8417f3a86f9c4eba84ebc1d71d38d397c7cac2d05607d058
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c302ad93dcb52860f23365fa361df3c1f593d618c6948b7b113eac124e640687345c2e953eca523777377eb27d7f75b4c177c2029dc5ceb71e7c30e0d52d5fab
|
|
7
|
+
data.tar.gz: bf88a2042d7dce6169ec7b274c1249b4c291518076f1d26c5d5b9f8c5a95699bcd1e343d27970e8809f7ad91fafe67d1206a553b1e5e162f14c59458cf3b0416
|
data/config/default.yml
CHANGED
|
@@ -7,6 +7,10 @@ DevDoc/Auth:
|
|
|
7
7
|
DocumentationBaseURL: https://github.com/hgani/dev-doc/blob/main/docs/cops
|
|
8
8
|
DocumentationExtension: ".md"
|
|
9
9
|
|
|
10
|
+
DevDoc/I18n:
|
|
11
|
+
DocumentationBaseURL: https://github.com/hgani/dev-doc/blob/main/docs/cops
|
|
12
|
+
DocumentationExtension: ".md"
|
|
13
|
+
|
|
10
14
|
DevDoc/Migration:
|
|
11
15
|
DocumentationBaseURL: https://github.com/hgani/dev-doc/blob/main/docs/cops
|
|
12
16
|
DocumentationExtension: ".md"
|
|
@@ -247,6 +251,17 @@ DevDoc/Rails/AvoidRailsCallbacks:
|
|
|
247
251
|
Include:
|
|
248
252
|
- "app/models/**/*.rb"
|
|
249
253
|
|
|
254
|
+
DevDoc/Rails/AvoidLifecycleMethodOverride:
|
|
255
|
+
Description: "Avoid overriding Rails validation/persistence lifecycle methods (run_validations!, valid?, etc.) to dodge AvoidRailsCallbacks."
|
|
256
|
+
Enabled: true
|
|
257
|
+
Include:
|
|
258
|
+
- "app/models/**/*.rb"
|
|
259
|
+
Methods:
|
|
260
|
+
- run_validations!
|
|
261
|
+
- valid?
|
|
262
|
+
- invalid?
|
|
263
|
+
- perform_validations
|
|
264
|
+
|
|
250
265
|
DevDoc/Rails/ApplicationRecordTransaction:
|
|
251
266
|
Description: "Use `ApplicationRecord.transaction` instead of `SomeModel.transaction` outside model files."
|
|
252
267
|
Enabled: true
|
|
@@ -348,12 +363,17 @@ Rails/HasManyOrHasOneDependent:
|
|
|
348
363
|
Enabled: true
|
|
349
364
|
|
|
350
365
|
DevDoc/Auth/LoadResourceCurrentUserGuard:
|
|
351
|
-
Description: "Require a `return unless current_user` guard before using `current_user` inside `glib_load_resource`, and forbid `current_user&.`."
|
|
366
|
+
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
367
|
Enabled: true
|
|
353
368
|
# LoadResourceMethodNames: list of method names where the guard rule applies.
|
|
354
369
|
# Extend if your project uses a different lifecycle hook name.
|
|
355
370
|
LoadResourceMethodNames:
|
|
356
371
|
- glib_load_resource
|
|
372
|
+
# CurrentUserAssertionMethodNames: calls that raise when current_user is nil
|
|
373
|
+
# (e.g. glib's assert_current_user_present). A call to one before the first
|
|
374
|
+
# current_user use satisfies the guard. Add custom assertion helpers here.
|
|
375
|
+
CurrentUserAssertionMethodNames:
|
|
376
|
+
- assert_current_user_present
|
|
357
377
|
|
|
358
378
|
DevDoc/Auth/CurrentUserBranching:
|
|
359
379
|
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."
|
|
@@ -364,3 +384,135 @@ DevDoc/Auth/CurrentUserBranching:
|
|
|
364
384
|
- "app/controllers/concerns/**/*.rb"
|
|
365
385
|
- "app/views/layouts/**/*"
|
|
366
386
|
- "app/controllers/application_controller.rb"
|
|
387
|
+
|
|
388
|
+
DevDoc/I18n/RequireTranslation:
|
|
389
|
+
Description: "Localize user-facing strings in glib JSON-UI props; pass `t(...)` instead of a hardcoded string."
|
|
390
|
+
Enabled: false
|
|
391
|
+
# WatchedMethods: glib component builder methods whose hash props carry
|
|
392
|
+
# user-facing text. LocalizableKeys: the prop names checked on those calls.
|
|
393
|
+
# Extend both if your project adds custom components or text props.
|
|
394
|
+
WatchedMethods:
|
|
395
|
+
- h1
|
|
396
|
+
- h2
|
|
397
|
+
- h3
|
|
398
|
+
- h4
|
|
399
|
+
- h5
|
|
400
|
+
- p
|
|
401
|
+
- label
|
|
402
|
+
- markdown
|
|
403
|
+
- fields_text
|
|
404
|
+
- fields_number
|
|
405
|
+
- fields_select
|
|
406
|
+
- fields_password
|
|
407
|
+
- fields_textarea
|
|
408
|
+
- fields_check
|
|
409
|
+
- fields_checkGroup
|
|
410
|
+
- fields_chipGroup
|
|
411
|
+
- fields_timeZone
|
|
412
|
+
- fields_radioGroup
|
|
413
|
+
- fields_date
|
|
414
|
+
- fields_datetime
|
|
415
|
+
LocalizableKeys:
|
|
416
|
+
- title
|
|
417
|
+
- subtitle
|
|
418
|
+
- subsubtitle
|
|
419
|
+
- label
|
|
420
|
+
- placeholder
|
|
421
|
+
- text
|
|
422
|
+
Include:
|
|
423
|
+
- "app/views/**/*.jbuilder"
|
|
424
|
+
- "app/views/**/*.rb"
|
|
425
|
+
|
|
426
|
+
DevDoc/I18n/TranslationKeyPrefix:
|
|
427
|
+
Description: "Translation keys must start with an allowed namespace prefix (e.g. `hotel.`, `general.`)."
|
|
428
|
+
Enabled: false
|
|
429
|
+
# AllowedPrefixes is project-specific, so it defaults to empty and the cop is
|
|
430
|
+
# a no-op until configured. Matches `t`, `translate`, and `I18n.t`. Only
|
|
431
|
+
# statically-literal keys are checked; dynamic/interpolated keys are skipped.
|
|
432
|
+
AllowedPrefixes: []
|
|
433
|
+
Include:
|
|
434
|
+
- "app/views/**/*.jbuilder"
|
|
435
|
+
- "app/views/**/*.rb"
|
|
436
|
+
- "app/controllers/**/*.rb"
|
|
437
|
+
- "app/mailers/**/*.rb"
|
|
438
|
+
- "app/helpers/**/*.rb"
|
|
439
|
+
|
|
440
|
+
DevDoc/I18n/UnverifiedTranslation:
|
|
441
|
+
Description: "Warn when a glib text prop is set from a non-literal value that isn't a `t(...)` call — it may be an unlocalized string."
|
|
442
|
+
# A review aid, not a clean lint: it can't tell a stashed literal from
|
|
443
|
+
# dynamic data (`user.name`) or a translation reached via a variable. Off by
|
|
444
|
+
# default and `info` severity — run it during a localization pass. Mirrors
|
|
445
|
+
# RequireTranslation's WatchedMethods/LocalizableKeys.
|
|
446
|
+
Enabled: false
|
|
447
|
+
Severity: info
|
|
448
|
+
WatchedMethods:
|
|
449
|
+
- h1
|
|
450
|
+
- h2
|
|
451
|
+
- h3
|
|
452
|
+
- h4
|
|
453
|
+
- h5
|
|
454
|
+
- p
|
|
455
|
+
- label
|
|
456
|
+
- markdown
|
|
457
|
+
- fields_text
|
|
458
|
+
- fields_number
|
|
459
|
+
- fields_select
|
|
460
|
+
- fields_password
|
|
461
|
+
- fields_textarea
|
|
462
|
+
- fields_check
|
|
463
|
+
- fields_checkGroup
|
|
464
|
+
- fields_chipGroup
|
|
465
|
+
- fields_timeZone
|
|
466
|
+
- fields_radioGroup
|
|
467
|
+
- fields_date
|
|
468
|
+
- fields_datetime
|
|
469
|
+
LocalizableKeys:
|
|
470
|
+
- title
|
|
471
|
+
- subtitle
|
|
472
|
+
- subsubtitle
|
|
473
|
+
- label
|
|
474
|
+
- placeholder
|
|
475
|
+
- text
|
|
476
|
+
Include:
|
|
477
|
+
- "app/views/**/*.jbuilder"
|
|
478
|
+
- "app/views/**/*.rb"
|
|
479
|
+
|
|
480
|
+
DevDoc/I18n/ReportText:
|
|
481
|
+
Description: "Report every user-facing glib text prop — hardcoded and already-localized — to collect all possible texts."
|
|
482
|
+
# A tooling aid, not a lint: unlike RequireTranslation it fires on *every*
|
|
483
|
+
# text value (including `t(...)` calls) so you can sweep the codebase and
|
|
484
|
+
# collect the full list of user-facing strings. Off by default and `info`
|
|
485
|
+
# severity. Mirrors RequireTranslation's WatchedMethods/LocalizableKeys.
|
|
486
|
+
Enabled: false
|
|
487
|
+
Severity: info
|
|
488
|
+
WatchedMethods:
|
|
489
|
+
- h1
|
|
490
|
+
- h2
|
|
491
|
+
- h3
|
|
492
|
+
- h4
|
|
493
|
+
- h5
|
|
494
|
+
- p
|
|
495
|
+
- label
|
|
496
|
+
- markdown
|
|
497
|
+
- fields_text
|
|
498
|
+
- fields_number
|
|
499
|
+
- fields_select
|
|
500
|
+
- fields_password
|
|
501
|
+
- fields_textarea
|
|
502
|
+
- fields_check
|
|
503
|
+
- fields_checkGroup
|
|
504
|
+
- fields_chipGroup
|
|
505
|
+
- fields_timeZone
|
|
506
|
+
- fields_radioGroup
|
|
507
|
+
- fields_date
|
|
508
|
+
- fields_datetime
|
|
509
|
+
LocalizableKeys:
|
|
510
|
+
- title
|
|
511
|
+
- subtitle
|
|
512
|
+
- subsubtitle
|
|
513
|
+
- label
|
|
514
|
+
- placeholder
|
|
515
|
+
- text
|
|
516
|
+
Include:
|
|
517
|
+
- "app/views/**/*.jbuilder"
|
|
518
|
+
- "app/views/**/*.rb"
|
|
@@ -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`
|
|
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
|
-
#
|
|
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? &&
|
|
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
|
-
#
|
|
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
|
|
133
|
-
return false if
|
|
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
|
-
|
|
145
|
-
NIL_CHECK_METHODS.include?(method_name)
|
|
181
|
+
!(inside_current_user_branch?(node) || preceded_by_guard?(node))
|
|
146
182
|
end
|
|
147
183
|
|
|
148
|
-
#
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
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
|
|
190
|
-
#
|
|
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
|
-
#
|
|
195
|
-
#
|
|
196
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
#
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module DevDoc
|
|
4
|
+
module I18n
|
|
5
|
+
# Report every user-facing text in a glib JSON-UI text prop — both
|
|
6
|
+
# hardcoded strings and already-localized `t(...)` calls.
|
|
7
|
+
#
|
|
8
|
+
# ## Rationale
|
|
9
|
+
# `DevDoc/I18n/RequireTranslation` flags only *hardcoded* strings; it
|
|
10
|
+
# stays silent once a value is localized. This cop is the opposite: it
|
|
11
|
+
# fires on **every** text value, localized or not, so you can sweep a
|
|
12
|
+
# codebase and collect the full list of user-facing strings (e.g. to
|
|
13
|
+
# seed a translation catalog or audit coverage).
|
|
14
|
+
#
|
|
15
|
+
# It is a tooling aid, not a lint — **disabled by default** and runs at
|
|
16
|
+
# `info` severity. Run it during a localization pass; it is not meant
|
|
17
|
+
# for every commit.
|
|
18
|
+
#
|
|
19
|
+
# Both the hardcoded form (`view.p text: 'Welcome'`) and the localized
|
|
20
|
+
# form (`view.p text: t('home.welcome')`) are reported. Blank/whitespace
|
|
21
|
+
# strings and pure dynamic values (`user.name`) carry no static text and
|
|
22
|
+
# are skipped — see `DevDoc/I18n/UnverifiedTranslation` for those.
|
|
23
|
+
#
|
|
24
|
+
# The watched method names and localizable keys are configurable via
|
|
25
|
+
# `WatchedMethods:` and `LocalizableKeys:`.
|
|
26
|
+
#
|
|
27
|
+
# 📋 Reported — hardcoded text
|
|
28
|
+
# view.p text: 'Welcome'
|
|
29
|
+
#
|
|
30
|
+
# 📋 Reported — localized text
|
|
31
|
+
# view.p text: t('home.welcome')
|
|
32
|
+
#
|
|
33
|
+
# @example
|
|
34
|
+
# # info (hardcoded text)
|
|
35
|
+
# view.p text: 'Welcome'
|
|
36
|
+
#
|
|
37
|
+
# # info (localized text — still reported)
|
|
38
|
+
# view.p text: t('home.welcome')
|
|
39
|
+
#
|
|
40
|
+
# # info (interpolated string)
|
|
41
|
+
# view.p text: "Hi #{name}"
|
|
42
|
+
#
|
|
43
|
+
# # ignored (blank — no text)
|
|
44
|
+
# view.fields_text label: ''
|
|
45
|
+
#
|
|
46
|
+
# # ignored (pure dynamic — no static text)
|
|
47
|
+
# view.p text: user.name
|
|
48
|
+
class ReportText < Base
|
|
49
|
+
MSG = 'Text for `%<key>s:`: review/collect this for localization.'.freeze
|
|
50
|
+
|
|
51
|
+
DEFAULT_WATCHED_METHODS = %w[
|
|
52
|
+
h1 h2 h3 h4 h5 p label markdown
|
|
53
|
+
fields_text fields_number fields_select fields_password
|
|
54
|
+
fields_textarea fields_check fields_checkGroup fields_chipGroup
|
|
55
|
+
fields_timeZone fields_radioGroup fields_date fields_datetime
|
|
56
|
+
].freeze
|
|
57
|
+
|
|
58
|
+
DEFAULT_LOCALIZABLE_KEYS = %w[
|
|
59
|
+
title subtitle subsubtitle label placeholder text
|
|
60
|
+
].freeze
|
|
61
|
+
|
|
62
|
+
TRANSLATION_METHODS = %i[t translate].freeze
|
|
63
|
+
|
|
64
|
+
def on_send(node)
|
|
65
|
+
return unless watched_methods.include?(node.method_name.to_s)
|
|
66
|
+
|
|
67
|
+
node.arguments.each do |arg|
|
|
68
|
+
next unless arg.hash_type?
|
|
69
|
+
|
|
70
|
+
arg.pairs.each { |pair| check_pair(pair) }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
alias on_csend on_send
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def check_pair(pair)
|
|
78
|
+
key = pair.key
|
|
79
|
+
return unless key.sym_type?
|
|
80
|
+
return unless localizable_keys.include?(key.value.to_s)
|
|
81
|
+
return unless text?(pair.value)
|
|
82
|
+
|
|
83
|
+
add_offense(pair.value, message: format(MSG, key: key.value))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Any value that carries static user-facing text: a non-blank string
|
|
87
|
+
# literal, an interpolated string, or a translation call. Blank strings
|
|
88
|
+
# and pure dynamic values (`user.name`) carry no text and are skipped.
|
|
89
|
+
def text?(node)
|
|
90
|
+
return true if node.dstr_type?
|
|
91
|
+
return !node.value.strip.empty? if node.str_type?
|
|
92
|
+
|
|
93
|
+
translation_call?(node)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def translation_call?(node)
|
|
97
|
+
(node.send_type? || node.csend_type?) &&
|
|
98
|
+
TRANSLATION_METHODS.include?(node.method_name)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def watched_methods
|
|
102
|
+
cop_config.fetch('WatchedMethods', DEFAULT_WATCHED_METHODS)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def localizable_keys
|
|
106
|
+
cop_config.fetch('LocalizableKeys', DEFAULT_LOCALIZABLE_KEYS)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module DevDoc
|
|
4
|
+
module I18n
|
|
5
|
+
# Flag hardcoded user-facing strings in glib JSON-UI component props.
|
|
6
|
+
#
|
|
7
|
+
# ## Rationale
|
|
8
|
+
# Glib components render text from props like `text:`, `label:`, and
|
|
9
|
+
# `title:`. Passing a string literal ships untranslatable copy — it can
|
|
10
|
+
# never be localized and bypasses the I18n catalog. Pass `t('...')` (or
|
|
11
|
+
# any non-literal) so the text resolves through the locale files.
|
|
12
|
+
#
|
|
13
|
+
# Only the hash form is checked (`view.p text: '...'`). Empty/whitespace
|
|
14
|
+
# strings are ignored — they carry no user-facing text. An interpolated
|
|
15
|
+
# string (`"Hi #{name}"`) is flagged because its literal portions are
|
|
16
|
+
# still hardcoded copy — unless it interpolates a `t(...)` (or
|
|
17
|
+
# `translate`/`I18n.t`) call, which is treated as positive localization
|
|
18
|
+
# and left alone.
|
|
19
|
+
#
|
|
20
|
+
# The watched method names and localizable keys are configurable via
|
|
21
|
+
# `WatchedMethods:` and `LocalizableKeys:`.
|
|
22
|
+
#
|
|
23
|
+
# ❌ Hardcoded — can't be translated
|
|
24
|
+
# view.p text: 'Welcome'
|
|
25
|
+
# view.fields_text label: 'Email'
|
|
26
|
+
#
|
|
27
|
+
# ✔️ Resolved through I18n
|
|
28
|
+
# view.p text: t('home.welcome')
|
|
29
|
+
# view.fields_text label: t('user.email')
|
|
30
|
+
# view.p text: "#{t('home.welcome')}, #{user.name}"
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# # bad
|
|
34
|
+
# view.p text: 'Welcome'
|
|
35
|
+
#
|
|
36
|
+
# # bad
|
|
37
|
+
# view.fields_text label: 'Email'
|
|
38
|
+
#
|
|
39
|
+
# # bad (interpolation, but no translation call)
|
|
40
|
+
# view.p text: "Hi #{name}"
|
|
41
|
+
#
|
|
42
|
+
# # good
|
|
43
|
+
# view.p text: t('home.welcome')
|
|
44
|
+
#
|
|
45
|
+
# # good (interpolates a translation call)
|
|
46
|
+
# view.p text: "#{t('home.greeting')} #{user.name}"
|
|
47
|
+
#
|
|
48
|
+
# # good (non-literal — not flagged)
|
|
49
|
+
# view.p text: user.name
|
|
50
|
+
class RequireTranslation < Base
|
|
51
|
+
MSG = 'Localize this string: pass `t(...)` instead of a hardcoded ' \
|
|
52
|
+
'string for `%<key>s:`.'.freeze
|
|
53
|
+
|
|
54
|
+
DEFAULT_WATCHED_METHODS = %w[
|
|
55
|
+
h1 h2 h3 h4 h5 p label markdown
|
|
56
|
+
fields_text fields_number fields_select fields_password
|
|
57
|
+
fields_textarea fields_check fields_checkGroup fields_chipGroup
|
|
58
|
+
fields_timeZone fields_radioGroup fields_date fields_datetime
|
|
59
|
+
].freeze
|
|
60
|
+
|
|
61
|
+
DEFAULT_LOCALIZABLE_KEYS = %w[
|
|
62
|
+
title subtitle subsubtitle label placeholder text
|
|
63
|
+
].freeze
|
|
64
|
+
|
|
65
|
+
TRANSLATION_METHODS = %i[t translate].freeze
|
|
66
|
+
|
|
67
|
+
def on_send(node)
|
|
68
|
+
return unless watched_methods.include?(node.method_name.to_s)
|
|
69
|
+
|
|
70
|
+
node.arguments.each do |arg|
|
|
71
|
+
next unless arg.hash_type?
|
|
72
|
+
|
|
73
|
+
arg.pairs.each { |pair| check_pair(pair) }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
alias on_csend on_send
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def check_pair(pair)
|
|
81
|
+
key = pair.key
|
|
82
|
+
return unless key.sym_type?
|
|
83
|
+
return unless localizable_keys.include?(key.value.to_s)
|
|
84
|
+
return unless hardcoded_string?(pair.value)
|
|
85
|
+
|
|
86
|
+
add_offense(pair.value, message: format(MSG, key: key.value))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# A plain string literal is hardcoded copy unless it's blank. A `dstr`
|
|
90
|
+
# (interpolated string) is hardcoded copy too — unless it interpolates
|
|
91
|
+
# a translation call, which counts as positive localization.
|
|
92
|
+
def hardcoded_string?(node)
|
|
93
|
+
return !localized_interpolation?(node) if node.dstr_type?
|
|
94
|
+
return false unless node.str_type?
|
|
95
|
+
|
|
96
|
+
!node.value.strip.empty?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def localized_interpolation?(node)
|
|
100
|
+
node.each_descendant(:send, :csend).any? do |call|
|
|
101
|
+
TRANSLATION_METHODS.include?(call.method_name)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def watched_methods
|
|
106
|
+
cop_config.fetch('WatchedMethods', DEFAULT_WATCHED_METHODS)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def localizable_keys
|
|
110
|
+
cop_config.fetch('LocalizableKeys', DEFAULT_LOCALIZABLE_KEYS)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module DevDoc
|
|
4
|
+
module I18n
|
|
5
|
+
# Require translation keys to start with an allowed namespace prefix.
|
|
6
|
+
#
|
|
7
|
+
# ## Rationale
|
|
8
|
+
# A flat translation catalog drifts into collisions and dead keys.
|
|
9
|
+
# Requiring every `t(...)` key to start with an agreed namespace (e.g.
|
|
10
|
+
# `hotel.`, `general.`) keeps the locale files organized and makes it
|
|
11
|
+
# obvious which feature owns a key.
|
|
12
|
+
#
|
|
13
|
+
# The allowed prefixes are project-specific, so `AllowedPrefixes:`
|
|
14
|
+
# defaults to empty and the cop does nothing until a project configures
|
|
15
|
+
# it. Matches `t`, `translate`, and `I18n.t` (the receiver is ignored).
|
|
16
|
+
#
|
|
17
|
+
# Only statically-literal keys are checked. A key built from
|
|
18
|
+
# interpolation (`t("#{prefix}.foo")`) or a variable (`t(key)`) can't be
|
|
19
|
+
# verified and is skipped. A key that begins with a literal segment
|
|
20
|
+
# (`t("hotel.#{id}")`) is checked against that leading segment.
|
|
21
|
+
#
|
|
22
|
+
# ❌ No recognized prefix
|
|
23
|
+
# t('welcome.title')
|
|
24
|
+
#
|
|
25
|
+
# ✔️ Namespaced
|
|
26
|
+
# t('general.welcome.title')
|
|
27
|
+
# t('hotel.rooms.heading')
|
|
28
|
+
#
|
|
29
|
+
# @example AllowedPrefixes: ['hotel.', 'general.']
|
|
30
|
+
# # bad
|
|
31
|
+
# t('welcome.title')
|
|
32
|
+
#
|
|
33
|
+
# # bad (lazy key has no namespace)
|
|
34
|
+
# t('.title')
|
|
35
|
+
#
|
|
36
|
+
# # good
|
|
37
|
+
# t('hotel.rooms.heading')
|
|
38
|
+
#
|
|
39
|
+
# # good (variable suffix, literal prefix is checked)
|
|
40
|
+
# t("general.#{key}")
|
|
41
|
+
#
|
|
42
|
+
# # not checked (fully dynamic key)
|
|
43
|
+
# t("#{prefix}.title")
|
|
44
|
+
class TranslationKeyPrefix < Base
|
|
45
|
+
MSG = 'Translation key `%<key>s` must start with an allowed ' \
|
|
46
|
+
'prefix: %<prefixes>s.'.freeze
|
|
47
|
+
|
|
48
|
+
RESTRICT_ON_SEND = %i[t translate].freeze
|
|
49
|
+
|
|
50
|
+
def on_send(node)
|
|
51
|
+
prefixes = allowed_prefixes
|
|
52
|
+
return if prefixes.empty?
|
|
53
|
+
|
|
54
|
+
key_node = node.first_argument
|
|
55
|
+
return unless key_node
|
|
56
|
+
|
|
57
|
+
leading = leading_literal(key_node)
|
|
58
|
+
return if leading.nil?
|
|
59
|
+
return if prefixes.any? { |prefix| leading.start_with?(prefix) }
|
|
60
|
+
|
|
61
|
+
add_offense(key_node, message: format(MSG, key: key_node.source, prefixes: prefixes_display(prefixes)))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# The statically-known leading portion of the key, or nil when the key
|
|
67
|
+
# is dynamic (a variable, or a string that begins with interpolation).
|
|
68
|
+
def leading_literal(node)
|
|
69
|
+
case node.type
|
|
70
|
+
when :str then node.value
|
|
71
|
+
when :sym then node.value.to_s
|
|
72
|
+
when :dstr
|
|
73
|
+
first = node.children.first
|
|
74
|
+
first&.str_type? ? first.value : nil
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def allowed_prefixes
|
|
79
|
+
Array(cop_config['AllowedPrefixes'])
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def prefixes_display(prefixes)
|
|
83
|
+
prefixes.map { |prefix| "`#{prefix}`" }.join(', ')
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module DevDoc
|
|
4
|
+
module I18n
|
|
5
|
+
# Warn when a glib text prop is set from a non-literal value that isn't
|
|
6
|
+
# a `t(...)` call — it may be an unlocalized string.
|
|
7
|
+
#
|
|
8
|
+
# ## Rationale
|
|
9
|
+
# `DevDoc/I18n/RequireTranslation` only catches string literals written
|
|
10
|
+
# at the call site. A value passed through a variable or method
|
|
11
|
+
# (`label: label_text`) escapes it — the literal could be one hop away.
|
|
12
|
+
# This cop flags those values so a human can confirm they resolve
|
|
13
|
+
# through I18n.
|
|
14
|
+
#
|
|
15
|
+
# It can't tell a stashed literal from genuinely dynamic data
|
|
16
|
+
# (`user.name`) or a translation reached via a variable, so it's a
|
|
17
|
+
# review aid, not a clean lint: it's **disabled by default** and runs at
|
|
18
|
+
# `info` severity. Run it during a localization pass, not on every
|
|
19
|
+
# commit. Values that are obviously translated (`t(...)`, `translate`,
|
|
20
|
+
# `I18n.t`) are excluded; literals are left to `RequireTranslation`.
|
|
21
|
+
#
|
|
22
|
+
# ⚠️ Can't be verified — confirm it's localized
|
|
23
|
+
# view.fields_text label: label_text
|
|
24
|
+
# view.p text: user.name
|
|
25
|
+
#
|
|
26
|
+
# ✔️ Obviously localized — not flagged
|
|
27
|
+
# view.p text: t('home.welcome')
|
|
28
|
+
#
|
|
29
|
+
# @example
|
|
30
|
+
# # warning (non-literal — may be unlocalized)
|
|
31
|
+
# view.fields_text label: label_text
|
|
32
|
+
#
|
|
33
|
+
# # warning (attribute — may be unlocalized)
|
|
34
|
+
# view.p text: user.name
|
|
35
|
+
#
|
|
36
|
+
# # good (obviously localized — not flagged)
|
|
37
|
+
# view.p text: t('home.welcome')
|
|
38
|
+
#
|
|
39
|
+
# # ignored (literal — handled by RequireTranslation)
|
|
40
|
+
# view.p text: 'Welcome'
|
|
41
|
+
class UnverifiedTranslation < Base
|
|
42
|
+
MSG = 'Possibly unlocalized: `%<key>s:` is set from a non-literal. ' \
|
|
43
|
+
'Use `t(...)` if this is user-facing text.'.freeze
|
|
44
|
+
|
|
45
|
+
DYNAMIC_TYPES = %i[lvar ivar send csend].freeze
|
|
46
|
+
TRANSLATION_METHODS = %i[t translate].freeze
|
|
47
|
+
|
|
48
|
+
DEFAULT_WATCHED_METHODS = %w[
|
|
49
|
+
h1 h2 h3 h4 h5 p label markdown
|
|
50
|
+
fields_text fields_number fields_select fields_password
|
|
51
|
+
fields_textarea fields_check fields_checkGroup fields_chipGroup
|
|
52
|
+
fields_timeZone fields_radioGroup fields_date fields_datetime
|
|
53
|
+
].freeze
|
|
54
|
+
|
|
55
|
+
DEFAULT_LOCALIZABLE_KEYS = %w[
|
|
56
|
+
title subtitle subsubtitle label placeholder text
|
|
57
|
+
].freeze
|
|
58
|
+
|
|
59
|
+
def on_send(node)
|
|
60
|
+
return unless watched_methods.include?(node.method_name.to_s)
|
|
61
|
+
|
|
62
|
+
node.arguments.each do |arg|
|
|
63
|
+
next unless arg.hash_type?
|
|
64
|
+
|
|
65
|
+
arg.pairs.each { |pair| check_pair(pair) }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
alias on_csend on_send
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def check_pair(pair)
|
|
73
|
+
key = pair.key
|
|
74
|
+
return unless key.sym_type?
|
|
75
|
+
return unless localizable_keys.include?(key.value.to_s)
|
|
76
|
+
return unless unverified?(pair.value)
|
|
77
|
+
|
|
78
|
+
add_offense(pair.value, message: format(MSG, key: key.value))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A value we can neither confirm nor deny is localized: a variable or
|
|
82
|
+
# method result (but not an obvious `t(...)`/`translate` call). String
|
|
83
|
+
# and other literals are excluded — those are RequireTranslation's job.
|
|
84
|
+
def unverified?(node)
|
|
85
|
+
return false unless DYNAMIC_TYPES.include?(node.type)
|
|
86
|
+
|
|
87
|
+
!translation_call?(node)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def translation_call?(node)
|
|
91
|
+
(node.send_type? || node.csend_type?) &&
|
|
92
|
+
TRANSLATION_METHODS.include?(node.method_name)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def watched_methods
|
|
96
|
+
cop_config.fetch('WatchedMethods', DEFAULT_WATCHED_METHODS)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def localizable_keys
|
|
100
|
+
cop_config.fetch('LocalizableKeys', DEFAULT_LOCALIZABLE_KEYS)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module DevDoc
|
|
4
|
+
module Rails
|
|
5
|
+
# Avoid overriding Rails validation/persistence *lifecycle* methods in
|
|
6
|
+
# model files (`run_validations!`, `valid?`, etc.).
|
|
7
|
+
#
|
|
8
|
+
# ## Rationale
|
|
9
|
+
# `DevDoc/Rails/AvoidRailsCallbacks` bans the callback DSL so lifecycle
|
|
10
|
+
# behaviour stays visible at explicit call sites. Overriding a lifecycle
|
|
11
|
+
# method is a loophole — it reproduces the same hidden control flow
|
|
12
|
+
# without tripping that cop:
|
|
13
|
+
#
|
|
14
|
+
# # Flagged by AvoidRailsCallbacks:
|
|
15
|
+
# before_validation :do_something
|
|
16
|
+
#
|
|
17
|
+
# # NOT flagged by it — but functionally identical, and worse:
|
|
18
|
+
# def run_validations!
|
|
19
|
+
# do_something
|
|
20
|
+
# super
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# `def run_validations!; do_something; super; end` is a
|
|
24
|
+
# `before_validation :do_something` in disguise, and it's strictly worse:
|
|
25
|
+
#
|
|
26
|
+
# - **Silent on typo.** Misspell the method name (drop the `!`, or
|
|
27
|
+
# `run_validatons!`) and it's just a never-called method — no load-time
|
|
28
|
+
# error, no override in effect, behaviour silently reverts to stock
|
|
29
|
+
# Rails. The callback DSL fails loudly (`NoMethodError`) on a mistyped
|
|
30
|
+
# macro or symbol, caught by the first test that validates.
|
|
31
|
+
# - **More obscure.** Overriding an internal Rails method hides the
|
|
32
|
+
# lifecycle hook even more than the DSL it's avoiding.
|
|
33
|
+
#
|
|
34
|
+
# This cop pairs with (it does not replace) `AvoidRailsCallbacks`: the two
|
|
35
|
+
# cover the two ways to inject lifecycle behaviour — the DSL, and the
|
|
36
|
+
# override.
|
|
37
|
+
#
|
|
38
|
+
# ## What to do instead
|
|
39
|
+
# 1. Prefer an explicit method at the call site (e.g. `save_with_*`) that
|
|
40
|
+
# makes the behaviour visible, or a plain `validate :some_check` where
|
|
41
|
+
# a validation-time check is all you need.
|
|
42
|
+
# 2. If an override is genuinely required (e.g. a model whose validators
|
|
43
|
+
# read access-gated attributes and must wrap the *entire* validation
|
|
44
|
+
# run — something `validate` can't express), disable this cop inline
|
|
45
|
+
# with a written justification:
|
|
46
|
+
#
|
|
47
|
+
# def run_validations! # rubocop:disable DevDoc/Rails/AvoidLifecycleMethodOverride
|
|
48
|
+
# # Reason: <explanation>
|
|
49
|
+
# with_access { super }
|
|
50
|
+
# end
|
|
51
|
+
#
|
|
52
|
+
# ## Not airtight
|
|
53
|
+
# A determined dodge can still `prepend` a module, `alias_method`, or use
|
|
54
|
+
# `method_missing`. This cop raises the bar and makes the documented path
|
|
55
|
+
# (inline disable + justification) the easy one; it does not seal every
|
|
56
|
+
# hole.
|
|
57
|
+
#
|
|
58
|
+
# ## Configuration
|
|
59
|
+
# The flagged methods are configurable via `Methods`. The default is the
|
|
60
|
+
# validation-lifecycle set — the cleanest 1:1 substitute for the
|
|
61
|
+
# validation callbacks `AvoidRailsCallbacks` bans, with the least
|
|
62
|
+
# false-positive noise. Projects that also want to guard persistence
|
|
63
|
+
# verbs can add `save`/`save!`/`create`/`update`/`destroy` etc.
|
|
64
|
+
#
|
|
65
|
+
# @example
|
|
66
|
+
# # bad
|
|
67
|
+
# def run_validations!
|
|
68
|
+
# normalize
|
|
69
|
+
# super
|
|
70
|
+
# end
|
|
71
|
+
#
|
|
72
|
+
# # bad
|
|
73
|
+
# def valid?(context = nil)
|
|
74
|
+
# toggle_access { super }
|
|
75
|
+
# end
|
|
76
|
+
#
|
|
77
|
+
# # good — an explicit method / a plain validation declaration
|
|
78
|
+
# validate :assert_consistent
|
|
79
|
+
#
|
|
80
|
+
# # good — a non-lifecycle override is fine
|
|
81
|
+
# def to_param
|
|
82
|
+
# slug
|
|
83
|
+
# end
|
|
84
|
+
class AvoidLifecycleMethodOverride < Base
|
|
85
|
+
MSG = 'Avoid overriding the Rails lifecycle method `%<method>s` — it hides ' \
|
|
86
|
+
'callback-like control flow and silently no-ops if mistyped. Prefer an ' \
|
|
87
|
+
'explicit method or a `validate` declaration; if an override is genuinely ' \
|
|
88
|
+
'required, disable this cop inline with a written justification.'.freeze
|
|
89
|
+
|
|
90
|
+
DEFAULT_METHODS = %w[run_validations! valid? invalid? perform_validations].freeze
|
|
91
|
+
|
|
92
|
+
def on_def(node)
|
|
93
|
+
return unless flagged_methods.include?(node.method_name)
|
|
94
|
+
|
|
95
|
+
add_offense(node.loc.name, message: format(MSG, method: node.method_name))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def flagged_methods
|
|
101
|
+
@flagged_methods ||= Array(cop_config['Methods'] || DEFAULT_METHODS).map(&:to_sym)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -120,15 +120,21 @@ module RuboCop
|
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
# Build the replacement for `params.require(:key).permit(*args)`.
|
|
123
|
-
# Symbol args stay as-is; hash args
|
|
124
|
-
#
|
|
123
|
+
# Symbol args stay as-is; hash args need explicit braces when placed
|
|
124
|
+
# inside the `expect` array literal.
|
|
125
125
|
def build_require_permit_replacement(params_node, key, permit_args)
|
|
126
126
|
inner = permit_args.map { |arg| permit_arg_source(arg) }.join(', ')
|
|
127
127
|
"#{params_node.source}.expect(#{key}: [#{inner}])"
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
+
# A hash arg written WITHOUT braces (the trailing-kwargs form,
|
|
131
|
+
# `permit(:a, b: [1])`) has a brace-less source, so add them. A hash
|
|
132
|
+
# arg written WITH explicit braces (`permit(:a, { b: [1] })`) already
|
|
133
|
+
# includes them in its source — wrapping again would emit `{ {...} }`.
|
|
130
134
|
def permit_arg_source(arg)
|
|
131
|
-
|
|
135
|
+
return arg.source unless arg.hash_type?
|
|
136
|
+
|
|
137
|
+
arg.braces? ? arg.source : "{ #{arg.source} }"
|
|
132
138
|
end
|
|
133
139
|
end
|
|
134
140
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubocop-dev_doc
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0.beta1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dev-doc contributors
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activesupport
|
|
@@ -80,8 +79,6 @@ dependencies:
|
|
|
80
79
|
- - ">="
|
|
81
80
|
- !ruby/object:Gem::Version
|
|
82
81
|
version: '2.0'
|
|
83
|
-
description:
|
|
84
|
-
email:
|
|
85
82
|
executables: []
|
|
86
83
|
extensions: []
|
|
87
84
|
extra_rdoc_files: []
|
|
@@ -94,6 +91,10 @@ files:
|
|
|
94
91
|
- lib/rubocop-dev_doc.rb
|
|
95
92
|
- lib/rubocop/cop/dev_doc/auth/current_user_branching.rb
|
|
96
93
|
- lib/rubocop/cop/dev_doc/auth/load_resource_current_user_guard.rb
|
|
94
|
+
- lib/rubocop/cop/dev_doc/i18n/report_text.rb
|
|
95
|
+
- lib/rubocop/cop/dev_doc/i18n/require_translation.rb
|
|
96
|
+
- lib/rubocop/cop/dev_doc/i18n/translation_key_prefix.rb
|
|
97
|
+
- lib/rubocop/cop/dev_doc/i18n/unverified_translation.rb
|
|
97
98
|
- lib/rubocop/cop/dev_doc/migration/amount_column_in_cents.rb
|
|
98
99
|
- lib/rubocop/cop/dev_doc/migration/avoid_bypassing_validation.rb
|
|
99
100
|
- lib/rubocop/cop/dev_doc/migration/avoid_column_default.rb
|
|
@@ -107,6 +108,7 @@ files:
|
|
|
107
108
|
- lib/rubocop/cop/dev_doc/migration/require_primary_key.rb
|
|
108
109
|
- lib/rubocop/cop/dev_doc/migration/require_timestamps.rb
|
|
109
110
|
- lib/rubocop/cop/dev_doc/rails/application_record_transaction.rb
|
|
111
|
+
- lib/rubocop/cop/dev_doc/rails/avoid_lifecycle_method_override.rb
|
|
110
112
|
- lib/rubocop/cop/dev_doc/rails/avoid_rails_callbacks.rb
|
|
111
113
|
- lib/rubocop/cop/dev_doc/rails/bang_save_in_transaction.rb
|
|
112
114
|
- lib/rubocop/cop/dev_doc/rails/enum_column_not_null.rb
|
|
@@ -132,12 +134,10 @@ files:
|
|
|
132
134
|
- lib/rubocop/dev_doc.rb
|
|
133
135
|
- lib/rubocop/dev_doc/plugin.rb
|
|
134
136
|
- lib/rubocop/dev_doc/version.rb
|
|
135
|
-
homepage:
|
|
136
137
|
licenses: []
|
|
137
138
|
metadata:
|
|
138
139
|
default_lint_roller_plugin: RuboCop::DevDoc::Plugin
|
|
139
140
|
rubygems_mfa_required: 'true'
|
|
140
|
-
post_install_message:
|
|
141
141
|
rdoc_options: []
|
|
142
142
|
require_paths:
|
|
143
143
|
- lib
|
|
@@ -152,8 +152,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
152
152
|
- !ruby/object:Gem::Version
|
|
153
153
|
version: '0'
|
|
154
154
|
requirements: []
|
|
155
|
-
rubygems_version:
|
|
156
|
-
signing_key:
|
|
155
|
+
rubygems_version: 4.0.6
|
|
157
156
|
specification_version: 4
|
|
158
157
|
summary: RuboCop cops enforcing dev-doc best practices
|
|
159
158
|
test_files: []
|