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.
- checksums.yaml +4 -4
- data/config/default.yml +312 -60
- data/config/obsoletion.yml +7 -3
- data/lib/dev_doc/i18n/tasks.rb +68 -0
- data/lib/dev_doc/railtie.rb +19 -0
- data/lib/dev_doc/test/lints/cross_tenant_canary_sweep.rb +2 -0
- data/lib/dev_doc/test/lints/domain_class_base.rb +138 -0
- data/lib/dev_doc/test/lints/http_driven_controller_tests.rb +2 -0
- data/lib/dev_doc/test/pseudo_i18n_crawler.rb +5 -0
- data/lib/rubocop/cop/dev_doc/i18n/hardcoded_text.rb +243 -0
- data/lib/rubocop/cop/dev_doc/i18n/localizable_props.rb +69 -19
- data/lib/rubocop/cop/dev_doc/i18n/require_translation.rb +13 -17
- data/lib/rubocop/cop/dev_doc/i18n/text_shape.rb +94 -0
- data/lib/rubocop/cop/dev_doc/rails/application_model_base.rb +56 -0
- data/lib/rubocop/cop/dev_doc/rails/avoid_bypassing_validation.rb +81 -26
- data/lib/rubocop/cop/dev_doc/rails/no_errors_add_in_controller.rb +177 -0
- data/lib/rubocop/cop/dev_doc/rails/no_form_param_lookup_in_controller.rb +236 -0
- data/lib/rubocop/cop/dev_doc/style/in_memory_association_find.rb +162 -0
- data/lib/rubocop/cop/dev_doc/style/no_silent_rescue.rb +269 -0
- data/lib/rubocop/cop/dev_doc/style/prefer_allowlist_symbol_param.rb +193 -0
- data/lib/rubocop/cop/dev_doc/style/use_g_log.rb +133 -0
- data/lib/rubocop/cop/dev_doc/test/prefer_find_sole_by.rb +146 -0
- data/lib/rubocop/cop/dev_doc/test/test_name_special_characters.rb +108 -0
- data/lib/rubocop/cop/dev_doc/view/prefer_prop_over_name.rb +11 -1
- data/lib/rubocop/dev_doc/version.rb +1 -1
- metadata +16 -3
- 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
|
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.
|
|
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-
|
|
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
|