rubocop-dev_doc 0.3.1 → 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 +147 -0
- 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
|
|
@@ -369,3 +384,135 @@ DevDoc/Auth/CurrentUserBranching:
|
|
|
369
384
|
- "app/controllers/concerns/**/*.rb"
|
|
370
385
|
- "app/views/layouts/**/*"
|
|
371
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"
|
|
@@ -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: []
|