rubocop-dev_doc 0.16.0 → 0.18.0

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.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +312 -60
  3. data/config/obsoletion.yml +7 -3
  4. data/lib/dev_doc/i18n/tasks.rb +68 -0
  5. data/lib/dev_doc/railtie.rb +19 -0
  6. data/lib/dev_doc/test/lints/cross_tenant_canary_sweep.rb +2 -0
  7. data/lib/dev_doc/test/lints/domain_class_base.rb +138 -0
  8. data/lib/dev_doc/test/lints/http_driven_controller_tests.rb +2 -0
  9. data/lib/dev_doc/test/pseudo_i18n_crawler.rb +5 -0
  10. data/lib/rubocop/cop/dev_doc/i18n/hardcoded_text.rb +243 -0
  11. data/lib/rubocop/cop/dev_doc/i18n/localizable_props.rb +69 -19
  12. data/lib/rubocop/cop/dev_doc/i18n/require_translation.rb +13 -17
  13. data/lib/rubocop/cop/dev_doc/i18n/text_shape.rb +94 -0
  14. data/lib/rubocop/cop/dev_doc/rails/application_model_base.rb +56 -0
  15. data/lib/rubocop/cop/dev_doc/rails/avoid_bypassing_validation.rb +81 -26
  16. data/lib/rubocop/cop/dev_doc/rails/no_errors_add_in_controller.rb +177 -0
  17. data/lib/rubocop/cop/dev_doc/rails/no_form_param_lookup_in_controller.rb +236 -0
  18. data/lib/rubocop/cop/dev_doc/style/in_memory_association_find.rb +162 -0
  19. data/lib/rubocop/cop/dev_doc/style/no_silent_rescue.rb +269 -0
  20. data/lib/rubocop/cop/dev_doc/style/prefer_allowlist_symbol_param.rb +193 -0
  21. data/lib/rubocop/cop/dev_doc/style/use_g_log.rb +133 -0
  22. data/lib/rubocop/cop/dev_doc/test/prefer_find_sole_by.rb +146 -0
  23. data/lib/rubocop/cop/dev_doc/test/test_name_special_characters.rb +108 -0
  24. data/lib/rubocop/cop/dev_doc/view/prefer_prop_over_name.rb +11 -1
  25. data/lib/rubocop/dev_doc/version.rb +1 -1
  26. metadata +16 -3
  27. data/lib/rubocop/cop/dev_doc/i18n/report_text.rb +0 -76
@@ -0,0 +1,133 @@
1
+ module RuboCop
2
+ module Cop
3
+ module DevDoc
4
+ module Style
5
+ # Route logging through `GLog` (glib-web) instead of `Rails.logger` or
6
+ # `Rollbar` directly.
7
+ #
8
+ # ## Rationale
9
+ # `GLog` is the shared logging seam: one place where destination
10
+ # (remote Rollbar vs local-only), level policy, and future redaction
11
+ # live. A call site that names its destination (`GLog.local_info` vs
12
+ # `GLog.error`) keeps that policy changeable; a direct `Rails.logger`
13
+ # or `Rollbar` call locks the destination in at the call site and
14
+ # bypasses any redaction added to the facade later.
15
+ #
16
+ # ## Rules
17
+ #
18
+ # **`Rails.logger.<any method>` is flagged** outside `module GLog`
19
+ # itself, including `::Rails.logger` and safe navigation
20
+ # (`Rails.logger&.info`). Local logging belongs behind
21
+ # `GLog.local_debug/local_info/local_warn/local_error`.
22
+ #
23
+ # **`Rollbar.<reporting method>` is flagged** — `error`, `warning`,
24
+ # `warn`, `info`, `debug`, `critical`, `log`, and the `log_*` family
25
+ # (`log_info`, `log_debug`, `log_warning`, `log_error`), which the
26
+ # rollbar gem delegates alongside the level names (`warn` and
27
+ # `warning` are both defined). A reporting send whose receiver chain
28
+ # roots at the constant — `Rollbar.scope(user).error(e)`,
29
+ # `::Rollbar.error(e)` — is flagged too. Remote reporting belongs
30
+ # behind `GLog.error/warn/info`.
31
+ #
32
+ # **`Rollbar.configure`, bare `Rollbar.scope(...)`, and other
33
+ # non-reporting calls are NOT flagged.** Configuration is setup, not
34
+ # reporting; matching every method on the constant (the local
35
+ # `Custom/UseDLog` cop this replaces did exactly that) made
36
+ # `Rollbar.configure` an offense.
37
+ #
38
+ # Calls inside `module GLog` itself are exempt — the facade routes to
39
+ # `Rails.logger` and `Rollbar` by design.
40
+ #
41
+ # **Requires glib-web >= 7.2.0** (GLog). Projects on an older
42
+ # glib-web disable this cop until they upgrade.
43
+ #
44
+ # @example
45
+ # # bad — destination locked in at the call site
46
+ # Rails.logger.info("something")
47
+ # Rollbar.error(exception)
48
+ # Rollbar.warn("degraded")
49
+ # Rollbar.log_error("message")
50
+ # Rollbar.scope(user: u).error(exception)
51
+ #
52
+ # # good — the facade owns destination and level policy
53
+ # GLog.local_info("something")
54
+ # GLog.error(exception)
55
+ # GLog.warn("degraded")
56
+ #
57
+ # # good — configuration is not reporting
58
+ # Rollbar.configure do |config|
59
+ # config.access_token = token
60
+ # end
61
+ class UseGLog < Base
62
+ MSG_RAILS_LOGGER = 'Use `GLog.local_*` instead of `Rails.logger` directly. ' \
63
+ 'GLog is the seam where destination, level policy, and ' \
64
+ 'redaction live.'.freeze
65
+
66
+ MSG_ROLLBAR = 'Use `GLog.error/warn/info` instead of `Rollbar` directly. ' \
67
+ 'GLog is the seam where destination, level policy, and ' \
68
+ 'redaction live.'.freeze
69
+
70
+ # The rollbar gem's reporting surface: the level names it delegates
71
+ # (`warn` and `warning` are both defined) plus the `log_*` family.
72
+ # `configure`, bare `scope`, and other non-reporting calls stay out —
73
+ # matching every method on the constant would flag configuration.
74
+ REPORTING_METHODS = %i[
75
+ error warning warn info debug critical log
76
+ log_info log_debug log_warning log_error
77
+ ].freeze
78
+
79
+ def on_send(node)
80
+ return if inside_g_log?(node)
81
+
82
+ if rollbar_reporting?(node)
83
+ add_offense(node, message: MSG_ROLLBAR)
84
+ elsif rails_logger?(node)
85
+ add_offense(node, message: MSG_RAILS_LOGGER)
86
+ end
87
+ end
88
+ # `Rails.logger&.info(...)` parses as a csend node.
89
+ alias on_csend on_send
90
+
91
+ private
92
+
93
+ # A reporting-named send whose receiver chain roots at the Rollbar
94
+ # constant: the direct `Rollbar.error(e)`, the cbase-spelled
95
+ # `::Rollbar.error(e)`, and scoped chains like
96
+ # `Rollbar.scope(user).error(e)`.
97
+ def rollbar_reporting?(node)
98
+ REPORTING_METHODS.include?(node.method_name) && rollbar_rooted?(node.receiver)
99
+ end
100
+
101
+ def rollbar_rooted?(node)
102
+ case node
103
+ when RuboCop::AST::ConstNode
104
+ node.const_name.to_s.sub(/\A::/, '') == 'Rollbar'
105
+ when RuboCop::AST::SendNode, RuboCop::AST::CsendNode
106
+ rollbar_rooted?(node.receiver)
107
+ else
108
+ false
109
+ end
110
+ end
111
+
112
+ # `Rails.logger.<method>`, `::Rails.logger.<method>` and the
113
+ # safe-navigated `Rails.logger&.<method>`.
114
+ def rails_logger?(node)
115
+ receiver = node.receiver
116
+ return false unless receiver.is_a?(RuboCop::AST::SendNode) || receiver.is_a?(RuboCop::AST::CsendNode)
117
+ return false unless receiver.method_name == :logger
118
+
119
+ const = receiver.receiver
120
+ const.is_a?(RuboCop::AST::ConstNode) && const.const_name.to_s.sub(/\A::/, '') == 'Rails'
121
+ end
122
+
123
+ # The facade itself routes to Rails.logger and Rollbar by design.
124
+ def inside_g_log?(node)
125
+ node.each_ancestor(:module).any? do |ancestor|
126
+ ancestor.children.first&.const_name == 'GLog'
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,146 @@
1
+ module RuboCop
2
+ module Cop
3
+ module DevDoc
4
+ module Test
5
+ # In test files, a `find_by` / `find_by!` on a bare model class must be
6
+ # `find_sole_by`, so a lookup that matches more than one row fails
7
+ # loudly instead of silently picking one.
8
+ #
9
+ # ## Rationale
10
+ # `Post.find_by(title: 'Hello')` returns the FIRST matching row, and
11
+ # with no `ORDER BY` "first" means whichever row the database returns
12
+ # first — which can differ between runs, between seeds, and between
13
+ # machines. When the attribute is not unique across the whole table,
14
+ # the test binds to a different record on a different day and fails
15
+ # far from the cause: the wrong fixture's association is `nil`, and the
16
+ # symptom is something like
17
+ # `ActionController::UrlGenerationError: ... missing required keys: [:id]`
18
+ # in a test whose diff never touched routing. The failure reproduces
19
+ # only under the seed that happened to expose the ordering, so it reads
20
+ # as a flake.
21
+ #
22
+ # Uniqueness is a property of the DATA, not of the attribute name. An
23
+ # `email` is unique per user but repeats across a tenant-scoped
24
+ # membership table (one row per organization the person belongs to);
25
+ # a `slug` looks like a natural key and still repeats per country. The
26
+ # only way to know is to read the fixture file — and `find_sole_by`
27
+ # does that check for you on every run: it raises
28
+ # `ActiveRecord::SoleRecordExceeded` when more than one row matches
29
+ # (and `RecordNotFound` when none does, like `find_by!`). A test that
30
+ # assumed uniqueness now asserts it.
31
+ #
32
+ # This cop catches the shape that shipped: the bare-class lookup, where
33
+ # the scope is the whole table. Scoping the lookup through the parent
34
+ # association is the other remedy, and it silences the cop because the
35
+ # receiver is no longer a constant — but the assumption only shrinks
36
+ # from "unique in the table" to "unique within the parent";
37
+ # `find_sole_by` on the association asserts that too.
38
+ #
39
+ # ## Remedy
40
+ #
41
+ # ❌ — silently picks one of several matching rows
42
+ # member = Member.find_by(email: 'admin@example.com')
43
+ # member = Member.find_by!(email: 'admin@example.com')
44
+ #
45
+ # ✔️ — raises if the match is not exactly one row
46
+ # member = Member.find_sole_by(email: 'admin@example.com')
47
+ #
48
+ # ✔️ — scoped through the parent (and still asserted unique within it)
49
+ # member = organizations(:acme).members.find_sole_by(email: 'admin@example.com')
50
+ #
51
+ # ✔️ — a fixture accessor is the canonical way to name a known row
52
+ # member = members(:acme_admin)
53
+ #
54
+ # ✔️ — a test that expects NO row asserts absence, not a nil return
55
+ # assert_not Member.exists?(email: 'gone@example.com')
56
+ #
57
+ # ## Autocorrect
58
+ # `find_by!` → `find_sole_by` is autocorrected: both raise when nothing
59
+ # matches, and the only behaviour that changes is the ambiguous case,
60
+ # which now fails instead of passing by luck. A test that starts
61
+ # failing after the correction is the cop finding the bug — fix the
62
+ # lookup (scope it or pick a unique attribute), not the assertion.
63
+ #
64
+ # `find_by` (no bang) is flagged but NOT autocorrected: it returns `nil`
65
+ # when nothing matches, and a test may rely on that
66
+ # (`assert_nil Post.find_by(title: 'deleted')`). Rewrite by hand:
67
+ # `find_sole_by` where the row is expected to exist, `exists?` where it
68
+ # is expected not to.
69
+ #
70
+ # ## What is NOT flagged
71
+ # - Lookups by primary key alone — `Post.find_by(id: some_id)` cannot
72
+ # match twice, and is the idiom for "nil once deleted".
73
+ # - Lookups whose receiver is not the bare class — an association, a
74
+ # fixture accessor, a scope chain (`Post.published.find_by(...)`), a
75
+ # local. Scoping IS the fix this cop pushes toward; the chain case is
76
+ # deliberately out of scope until observed. (`self.class.find_by`
77
+ # inside a test helper IS the bare class, and is flagged.)
78
+ # - Anything outside `test/` and `spec/` (`Include` in the config) —
79
+ # production code returning the first match is a separate concern.
80
+ #
81
+ # NOTE: `find_sole_by` exists from Rails 7.0. On an older consumer the
82
+ # autocorrect produces a `NoMethodError`; disable the cop there.
83
+ #
84
+ # @example
85
+ # # bad
86
+ # member = Member.find_by(email: 'admin@example.com')
87
+ # member = Member.find_by!(email: 'admin@example.com')
88
+ #
89
+ # # good
90
+ # member = Member.find_sole_by(email: 'admin@example.com')
91
+ # member = organizations(:acme).members.find_sole_by(email: 'admin@example.com')
92
+ # member = members(:acme_admin)
93
+ #
94
+ # # good — primary key cannot match twice
95
+ # assert_nil Member.find_by(id: removed_id)
96
+ class PreferFindSoleBy < Base
97
+ extend AutoCorrector
98
+
99
+ MSG_BANG = '`%<receiver>s.find_by!` returns whichever matching row the database yields ' \
100
+ 'first — if the attribute is not unique across the table the test binds to a ' \
101
+ 'different record per run. Use `find_sole_by`, which raises when the match ' \
102
+ 'is not exactly one row.'.freeze
103
+ MSG_PLAIN = '`%<receiver>s.find_by` returns whichever matching row the database yields ' \
104
+ 'first — if the attribute is not unique across the table the test binds to ' \
105
+ 'a different record per run. Use `find_sole_by` (raises unless exactly one ' \
106
+ 'row matches); if the test expects NO row, assert `exists?` instead.'.freeze
107
+
108
+ RESTRICT_ON_SEND = %i[find_by find_by!].freeze
109
+
110
+ # The bare class: a constant, or `self.class` inside a test helper.
111
+ # @!method bare_class?(node)
112
+ def_node_matcher :bare_class?, <<~PATTERN
113
+ {const (send self :class)}
114
+ PATTERN
115
+
116
+ # `Post.find_by(id: x)`: the primary key alone cannot match twice.
117
+ # @!method primary_key_only?(node)
118
+ def_node_matcher :primary_key_only?, <<~PATTERN
119
+ ({send csend} _ _ (hash (pair (sym :id) _)))
120
+ PATTERN
121
+
122
+ def on_send(node)
123
+ return unless node.receiver && bare_class?(node.receiver)
124
+ return if primary_key_only?(node)
125
+
126
+ if node.method?(:find_by!)
127
+ add_offense(node.loc.selector, message: message_for(node, MSG_BANG)) do |corrector|
128
+ corrector.replace(node.loc.selector, 'find_sole_by')
129
+ end
130
+ else
131
+ add_offense(node.loc.selector, message: message_for(node, MSG_PLAIN))
132
+ end
133
+ end
134
+ alias on_csend on_send
135
+
136
+ private
137
+
138
+ def message_for(node, template)
139
+ receiver = node.receiver.const_type? ? node.receiver.const_name : 'self.class'
140
+ format(template, receiver: receiver)
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,108 @@
1
+ module RuboCop
2
+ module Cop
3
+ module DevDoc
4
+ module Test
5
+ # Disallows characters in `test '...'` names that break running that
6
+ # single test by name. Rails derives the method name from the
7
+ # description almost verbatim (only whitespace becomes `_`), so every
8
+ # other character lands in the name a developer or agent must type to
9
+ # run one test:
10
+ #
11
+ # bin/rails test test/models/user_test.rb -n test_creates_(admin)_user
12
+ # bin/rails test test/models/user_test.rb -n /test_creates_[admin]_user/
13
+ #
14
+ # ## Rationale
15
+ # Both spellings of the `-n`/`--name` filter fail on such names, and
16
+ # every character in the default set has a VERIFIED failure in at
17
+ # least one of the two forms (checked against a real minitest
18
+ # single-test run, not reasoned about):
19
+ #
20
+ # 1. **Unquoted, the shell mis-parses the filter before the runner
21
+ # sees it.** `(`, `)`, `;`, `&`, `|`, `<`, `>`, `'`, `"`, `` ` ``,
22
+ # `$`, and `\` are shell grammar: a parenthesised name dies with
23
+ # `syntax error near unexpected token '('`; `<`/`>` silently
24
+ # redirect and pass the runner an empty filter; `$` expands;
25
+ # backtick substitutes. The test is never invoked.
26
+ # 2. **The `/name/` regex filter matches nothing.** The usual workaround
27
+ # for (1) — wrapping the name in slashes — makes the runner treat the
28
+ # string as an unanchored regex, so `[admin]` becomes a character
29
+ # class, `?`/`*`/`+` become quantifiers, `^`/`$` anchor, `(...)`
30
+ # groups. The result is `Nothing ran for filter`, `0 runs, 0
31
+ # failures` — output that reads as a pass while zero tests
32
+ # executed. Verified zero-run for: `[ ] ( ) * + ^ $ \`.
33
+ #
34
+ # The failure hides from review because writing the test and running
35
+ # the whole file both work; only running the test BY NAME breaks —
36
+ # which is exactly what an agent iterating on a failure does.
37
+ #
38
+ # ## Allowed punctuation — and its caveats
39
+ # `# . , - : %` break neither form. Also allowed, with a caveat each:
40
+ #
41
+ # * `?` and `{ }`: verified runnable in BOTH forms on bash (an
42
+ # unanchored regex still matches around them; braces without a
43
+ # comma are literal to both bash and regex). On zsh, an unquoted
44
+ # `?` aborts with `no matches found` — quote the `-n` argument
45
+ # rather than renaming the test.
46
+ # * `/`: verified runnable in both forms — the `/name/` filter pairs
47
+ # the FIRST and LAST slash as delimiters, interior slashes are
48
+ # regex-literal content.
49
+ # * `!`: literal to non-interactive shells and to regex, but
50
+ # INTERACTIVE bash/zsh history-expand it (`!_` becomes the previous
51
+ # command's last argument, silently running a different filter) —
52
+ # quote the argument when running such a test from a terminal.
53
+ #
54
+ # ## Exception
55
+ # Interpolated names (`test "creates #{kind}"`) are not flagged — the
56
+ # matcher only sees string literals, and interpolated test names are
57
+ # their own anti-pattern (the method name varies with runtime data).
58
+ # RSpec `it`/`describe` strings are out of scope: the RSpec runner
59
+ # takes `-e` as a plain (quoted) exact string, which none of these
60
+ # characters break.
61
+ #
62
+ # NOTE: no stock cop covers this — rubocop-minitest's naming cops check
63
+ # the `test_` prefix convention, not the name's content. The matcher
64
+ # recognises implicit-receiver `test` with a string-literal name;
65
+ # explicit receivers (`self.test`, `helper.test`) and non-literal name
66
+ # arguments (`test 'name'.freeze`) are out of reach.
67
+ #
68
+ # @example
69
+ # # bad — shell syntax error unquoted; regex misparse in /name/ form
70
+ # test 'creates (admin) user' do
71
+ # test 'creates [admin] user' do
72
+ # test 'copies a*b of the record' do
73
+ #
74
+ # # good
75
+ # test 'creates admin user via dashboard' do
76
+ # test 'User#name returns full name' do
77
+ class TestNameSpecialCharacters < Base
78
+ MSG = 'Test name contains %<chars>s — these break running the single test by name: unquoted, the ' \
79
+ 'shell mis-parses the `-n` filter; in the `/name/` form, regex-significant ones match nothing ' \
80
+ '(0 runs). Rename using letters, digits, spaces, and allowed punctuation (see cop docs).'.freeze
81
+
82
+ RESTRICT_ON_SEND = %i[test].freeze
83
+
84
+ DEFAULT_FORBIDDEN_CHARACTERS = '()[]*+^$\\&;|<>\'"`'.freeze
85
+
86
+ def_node_matcher :test_name, <<~PATTERN
87
+ (send nil? :test $(str $_) ...)
88
+ PATTERN
89
+
90
+ def on_send(node)
91
+ test_name(node) do |name_node, name|
92
+ forbidden = forbidden_characters.chars.select { |char| name.include?(char) }
93
+ next if forbidden.empty?
94
+
95
+ add_offense(name_node, message: format(MSG, chars: forbidden.map { |c| "`#{c}`" }.join(' ')))
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ # `.to_s` so an explicit `ForbiddenCharacters:` with no value (YAML
102
+ # null) means "nothing forbidden" instead of raising on nil.chars.
103
+ def forbidden_characters = cop_config.fetch('ForbiddenCharacters', DEFAULT_FORBIDDEN_CHARACTERS).to_s
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -36,6 +36,16 @@ module RuboCop
36
36
  # structural array-param key for the group's rows, not a labelled
37
37
  # model attribute — the row template's inner fields are where
38
38
  # `prop:` belongs.
39
+ # - `fields_file` is exempt by default: the (deprecated) File field's
40
+ # `prop:` machinery calls `.attached?` on the bound model value
41
+ # (glib-web fields.rb `File#created` / `#determine_value`), so a
42
+ # bound prop must be an ActiveStorage attachment. A `fields_file`
43
+ # that posts a raw upload the controller parses and discards (CSV
44
+ # imports, server-side processing) has no attachment to bind —
45
+ # converting it to `prop:` raises at render (`nil.attached?` /
46
+ # `field_assert_respond_to`), and the unsafe autocorrect would
47
+ # produce exactly that. Attachment-backed uploads belong on
48
+ # `fields_upload`, which binds `prop:` fine.
39
49
  # ## How to fix — the decision ladder
40
50
  # Work down this ladder before even considering a disable. A full
41
51
  # convert-or-justify sweep of a mature codebase (27 offenses) ended
@@ -153,7 +163,7 @@ module RuboCop
153
163
  end
154
164
 
155
165
  def exempt_field_methods
156
- cop_config.fetch('ExemptFieldMethods', %w[fields_hidden fields_creditCard fields_dynamicGroup])
166
+ cop_config.fetch('ExemptFieldMethods', %w[fields_hidden fields_creditCard fields_dynamicGroup fields_file])
157
167
  end
158
168
  end
159
169
  end
@@ -1,5 +1,5 @@
1
1
  module RuboCop
2
2
  module DevDoc
3
- VERSION = "0.16.0".freeze
3
+ VERSION = "0.18.0".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-dev_doc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - dev-doc contributors
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-28 00:00:00.000000000 Z
11
+ date: 2026-10-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -89,12 +89,15 @@ files:
89
89
  - config/default.yml
90
90
  - config/obsoletion.yml
91
91
  - lib/dev_doc/i18n/pseudo_locale.rb
92
+ - lib/dev_doc/i18n/tasks.rb
93
+ - lib/dev_doc/railtie.rb
92
94
  - lib/dev_doc/test/best_practice_lints.rb
93
95
  - lib/dev_doc/test/lints/cop_drift_check.rb
94
96
  - lib/dev_doc/test/lints/cop_drift_test.rb
95
97
  - lib/dev_doc/test/lints/cron_schedule.rb
96
98
  - lib/dev_doc/test/lints/cross_tenant_canary_check.rb
97
99
  - lib/dev_doc/test/lints/cross_tenant_canary_sweep.rb
100
+ - lib/dev_doc/test/lints/domain_class_base.rb
98
101
  - lib/dev_doc/test/lints/duplicate_snapshot.rb
99
102
  - lib/dev_doc/test/lints/enqueue_disable_naming.rb
100
103
  - lib/dev_doc/test/lints/external_io_boundary.rb
@@ -112,9 +115,10 @@ files:
112
115
  - lib/rubocop/cop/dev_doc/auth/unscoped_find_justification.rb
113
116
  - lib/rubocop/cop/dev_doc/hash_pair_removal.rb
114
117
  - lib/rubocop/cop/dev_doc/i18n/avoid_titleize_humanize.rb
118
+ - lib/rubocop/cop/dev_doc/i18n/hardcoded_text.rb
115
119
  - lib/rubocop/cop/dev_doc/i18n/localizable_props.rb
116
- - lib/rubocop/cop/dev_doc/i18n/report_text.rb
117
120
  - lib/rubocop/cop/dev_doc/i18n/require_translation.rb
121
+ - lib/rubocop/cop/dev_doc/i18n/text_shape.rb
118
122
  - lib/rubocop/cop/dev_doc/i18n/translation_key_prefix.rb
119
123
  - lib/rubocop/cop/dev_doc/migration/amount_column_in_cents.rb
120
124
  - lib/rubocop/cop/dev_doc/migration/avoid_boolean_column.rb
@@ -133,6 +137,7 @@ files:
133
137
  - lib/rubocop/cop/dev_doc/migration/require_primary_key.rb
134
138
  - lib/rubocop/cop/dev_doc/migration/require_reference_foreign_key.rb
135
139
  - lib/rubocop/cop/dev_doc/migration/require_timestamps.rb
140
+ - lib/rubocop/cop/dev_doc/rails/application_model_base.rb
136
141
  - lib/rubocop/cop/dev_doc/rails/application_record_transaction.rb
137
142
  - lib/rubocop/cop/dev_doc/rails/avoid_bypassing_validation.rb
138
143
  - lib/rubocop/cop/dev_doc/rails/avoid_lifecycle_method_override.rb
@@ -145,6 +150,8 @@ files:
145
150
  - lib/rubocop/cop/dev_doc/rails/no_block_predicate_on_relation.rb
146
151
  - lib/rubocop/cop/dev_doc/rails/no_collection_ids_writer_in_controller.rb
147
152
  - lib/rubocop/cop/dev_doc/rails/no_deliver_later_in_transaction.rb
153
+ - lib/rubocop/cop/dev_doc/rails/no_errors_add_in_controller.rb
154
+ - lib/rubocop/cop/dev_doc/rails/no_form_param_lookup_in_controller.rb
148
155
  - lib/rubocop/cop/dev_doc/rails/no_manual_record_invalid.rb
149
156
  - lib/rubocop/cop/dev_doc/rails/no_perform_later_in_model.rb
150
157
  - lib/rubocop/cop/dev_doc/rails/no_persistence_in_service.rb
@@ -159,27 +166,33 @@ files:
159
166
  - lib/rubocop/cop/dev_doc/style/avoid_options_hash.rb
160
167
  - lib/rubocop/cop/dev_doc/style/avoid_symbolizing_boundary_input.rb
161
168
  - lib/rubocop/cop/dev_doc/style/case_else_decision.rb
169
+ - lib/rubocop/cop/dev_doc/style/in_memory_association_find.rb
162
170
  - lib/rubocop/cop/dev_doc/style/literal_operator_in_condition.rb
163
171
  - lib/rubocop/cop/dev_doc/style/literal_or_in_when_clause.rb
164
172
  - lib/rubocop/cop/dev_doc/style/minimize_variable_scope.rb
173
+ - lib/rubocop/cop/dev_doc/style/no_silent_rescue.rb
165
174
  - lib/rubocop/cop/dev_doc/style/no_unscoped_method_definitions.rb
175
+ - lib/rubocop/cop/dev_doc/style/prefer_allowlist_symbol_param.rb
166
176
  - lib/rubocop/cop/dev_doc/style/prefer_public_send.rb
167
177
  - lib/rubocop/cop/dev_doc/style/redundant_guard_after_bang.rb
168
178
  - lib/rubocop/cop/dev_doc/style/repeated_bracket_read.rb
169
179
  - lib/rubocop/cop/dev_doc/style/repeated_safe_navigation_receiver.rb
170
180
  - lib/rubocop/cop/dev_doc/style/string_symbol_comparison.rb
171
181
  - lib/rubocop/cop/dev_doc/style/tap_block_ignores_value.rb
182
+ - lib/rubocop/cop/dev_doc/style/use_g_log.rb
172
183
  - lib/rubocop/cop/dev_doc/test/avoid_glib_travel_freeze.rb
173
184
  - lib/rubocop/cop/dev_doc/test/avoid_unit_test.rb
174
185
  - lib/rubocop/cop/dev_doc/test/justification_header.rb
175
186
  - lib/rubocop/cop/dev_doc/test/no_persistence_in_mailer_previews.rb
176
187
  - lib/rubocop/cop/dev_doc/test/no_unit_idioms_in_integration_tests.rb
188
+ - lib/rubocop/cop/dev_doc/test/prefer_find_sole_by.rb
177
189
  - lib/rubocop/cop/dev_doc/test/require_glib_integration_base.rb
178
190
  - lib/rubocop/cop/dev_doc/test/require_glib_travel.rb
179
191
  - lib/rubocop/cop/dev_doc/test/require_glib_travel_block.rb
180
192
  - lib/rubocop/cop/dev_doc/test/require_unit_test_justification.rb
181
193
  - lib/rubocop/cop/dev_doc/test/response_assert_equal.rb
182
194
  - lib/rubocop/cop/dev_doc/test/test_file_placement.rb
195
+ - lib/rubocop/cop/dev_doc/test/test_name_special_characters.rb
183
196
  - lib/rubocop/cop/dev_doc/view/prefer_prop_over_name.rb
184
197
  - lib/rubocop/dev_doc.rb
185
198
  - lib/rubocop/dev_doc/plugin.rb
@@ -1,76 +0,0 @@
1
- require_relative 'localizable_props'
2
-
3
- module RuboCop
4
- module Cop
5
- module DevDoc
6
- module I18n
7
- # Report every user-facing text in a glib JSON-UI text prop — both
8
- # hardcoded strings and already-localized `t(...)` calls.
9
- #
10
- # ## Rationale
11
- # `DevDoc/I18n/RequireTranslation` flags only *hardcoded* strings; it
12
- # stays silent once a value is localized. This cop is the opposite: it
13
- # fires on **every** text value, localized or not, so you can sweep a
14
- # codebase and collect the full list of user-facing strings (e.g. to
15
- # seed a translation catalog or audit coverage).
16
- #
17
- # It is a tooling aid, not a lint — **disabled by default** and runs at
18
- # `info` severity. Run it during a localization pass; it is not meant
19
- # for every commit.
20
- #
21
- # Both the hardcoded form (`view.p text: 'Welcome'`) and the localized
22
- # form (`view.p text: t('home.welcome')`) are reported. Blank/whitespace
23
- # strings and pure dynamic values (`user.name`) carry no static text and
24
- # are skipped.
25
- #
26
- # The watched method names and localizable keys are configurable via
27
- # `WatchedMethods:` and `LocalizableKeys:`.
28
- #
29
- # 📋 Reported — hardcoded text
30
- # view.p text: 'Welcome'
31
- #
32
- # 📋 Reported — localized text
33
- # view.p text: t('home.welcome')
34
- #
35
- # @example
36
- # # info (hardcoded text)
37
- # view.p text: 'Welcome'
38
- #
39
- # # info (localized text — still reported)
40
- # view.p text: t('home.welcome')
41
- #
42
- # # info (interpolated string)
43
- # view.p text: "Hi #{name}"
44
- #
45
- # # ignored (blank — no text)
46
- # view.fields_text label: ''
47
- #
48
- # # ignored (pure dynamic — no static text)
49
- # view.p text: user.name
50
- class ReportText < Base
51
- include LocalizableProps
52
-
53
- MSG = 'Text for `%<key>s:`: review/collect this for localization.'.freeze
54
-
55
- private
56
-
57
- def inspect_localizable(key, value)
58
- return unless text?(value)
59
-
60
- add_offense(value, message: format(MSG, key: key))
61
- end
62
-
63
- # Any value that carries static user-facing text: a non-blank string
64
- # literal, an interpolated string, or a translation call. Blank strings
65
- # and pure dynamic values (`user.name`) carry no text and are skipped.
66
- def text?(node)
67
- return true if node.dstr_type?
68
- return !node.value.strip.empty? if node.str_type?
69
-
70
- translation_call?(node)
71
- end
72
- end
73
- end
74
- end
75
- end
76
- end