rubocop-dev_doc 0.5.0.beta1 → 0.6.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 +74 -43
- data/lib/rubocop/cop/dev_doc/auth/current_user_branching.rb +47 -4
- data/lib/rubocop/cop/dev_doc/i18n/avoid_titleize_humanize.rb +59 -0
- data/lib/rubocop/cop/dev_doc/i18n/localizable_props.rb +109 -0
- data/lib/rubocop/cop/dev_doc/i18n/report_text.rb +8 -44
- data/lib/rubocop/cop/dev_doc/i18n/require_translation.rb +21 -42
- data/lib/rubocop/cop/dev_doc/migration/avoid_column_default.rb +53 -2
- data/lib/rubocop/cop/dev_doc/migration/avoid_vague_column_names.rb +7 -1
- data/lib/rubocop/cop/dev_doc/rails/avoid_ordering_by_id.rb +167 -0
- data/lib/rubocop/cop/dev_doc/rails/avoid_rails_callbacks.rb +14 -4
- data/lib/rubocop/cop/dev_doc/rails/avoid_raw_sql.rb +227 -0
- data/lib/rubocop/cop/dev_doc/style/prefer_public_send.rb +86 -0
- data/lib/rubocop/cop/dev_doc/style/tap_block_ignores_value.rb +123 -0
- data/lib/rubocop/cop/dev_doc/test/avoid_unit_test.rb +24 -0
- data/lib/rubocop/dev_doc/version.rb +1 -1
- metadata +7 -2
- data/lib/rubocop/cop/dev_doc/i18n/unverified_translation.rb +0 -106
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module DevDoc
|
|
4
|
+
module Style
|
|
5
|
+
# Flag `Object#tap` whose block ignores the yielded value.
|
|
6
|
+
#
|
|
7
|
+
# ## Rationale
|
|
8
|
+
# `tap` exists to **operate on the yielded receiver and return it** —
|
|
9
|
+
# `obj.tap { |x| configure(x) }`. The yielded value is the whole
|
|
10
|
+
# point of the method.
|
|
11
|
+
#
|
|
12
|
+
# When the block **ignores** the yielded value, `tap` is no longer
|
|
13
|
+
# doing that job. It's being used purely as a temp-variable-elision
|
|
14
|
+
# trick for "run a side effect, then return the receiver":
|
|
15
|
+
#
|
|
16
|
+
# ❌ Block never references the yielded value — `tap` is eliding a temp var
|
|
17
|
+
# read_value.tap { do_side_effect }
|
|
18
|
+
#
|
|
19
|
+
# This costs readability for no benefit:
|
|
20
|
+
#
|
|
21
|
+
# - **It hides the return value in the receiver position.** `tap`'s
|
|
22
|
+
# "returns self" behaviour is its *less obvious* property. A reader
|
|
23
|
+
# has to stop and recall that the expression returns the receiver,
|
|
24
|
+
# not the block's value. The explicit form puts the return value on
|
|
25
|
+
# its own line where "what does this return" is literal.
|
|
26
|
+
# - **Footgun magnet.** Return-value semantics here are subtle — it's
|
|
27
|
+
# easy to nearby-edit the block into something whose value you think
|
|
28
|
+
# matters, or to confuse it with the broken `expr; side_effect`
|
|
29
|
+
# sequence (which returns the side effect's value, not `expr`'s).
|
|
30
|
+
# Code whose contract is "return this value" reads and edits more
|
|
31
|
+
# safely when the return is explicit.
|
|
32
|
+
# - **Equal stakes → prefer the duller form.** Both forms are correct
|
|
33
|
+
# and neither is faster. With no functional difference, the more
|
|
34
|
+
# obvious one wins in a shared codebase.
|
|
35
|
+
#
|
|
36
|
+
# ✔️ Explicit intent, return value on its own line
|
|
37
|
+
# result = read_value
|
|
38
|
+
# do_side_effect
|
|
39
|
+
# result
|
|
40
|
+
#
|
|
41
|
+
# ## When `tap` IS the right choice
|
|
42
|
+
# Reserve `tap` for its canonical use — when the block references the
|
|
43
|
+
# yielded value:
|
|
44
|
+
#
|
|
45
|
+
# ✔️ Block uses the yielded value — this is what `tap` is for
|
|
46
|
+
# Model.new.tap { |m| m.role = :admin; m.save }
|
|
47
|
+
# record.tap { |r| logger.debug(r.to_sql) }
|
|
48
|
+
#
|
|
49
|
+
# ## Exception
|
|
50
|
+
# Genuine intentional uses (e.g. a logging side effect that reads
|
|
51
|
+
# clearer in tap position) go through an inline `# rubocop:disable`
|
|
52
|
+
# with a reason. The friction is the feature — it forces the choice
|
|
53
|
+
# to be articulated and reviewed.
|
|
54
|
+
#
|
|
55
|
+
# NOTE: Numbered-param (`_1`) and implicit-`it` (Ruby 3.4+) forms are
|
|
56
|
+
# not flagged — by construction, those forms reference the yielded
|
|
57
|
+
# value, which is the canonical use of `tap`. Block-pass (`&:sym`) is
|
|
58
|
+
# likewise not flagged; it isn't a block node.
|
|
59
|
+
#
|
|
60
|
+
# NOTE: If a nested block *shadows* the outer arg name
|
|
61
|
+
# (`tap { |v| items.each { |v| v.foo } }`), the inner reference is
|
|
62
|
+
# treated as covering the outer — a rare residual false negative.
|
|
63
|
+
# Inline-disable if it ever matters.
|
|
64
|
+
#
|
|
65
|
+
# @example
|
|
66
|
+
# # bad — block ignores the yielded value
|
|
67
|
+
# read_value.tap { do_side_effect }
|
|
68
|
+
# record.tap { |r| log_event }
|
|
69
|
+
# value.tap { }
|
|
70
|
+
#
|
|
71
|
+
# # good — block references the yielded value
|
|
72
|
+
# Model.new.tap { |m| m.role = :admin; m.save }
|
|
73
|
+
# record.tap { |r| logger.debug(r.to_sql) }
|
|
74
|
+
#
|
|
75
|
+
# # good — a reference inside a nested block still counts
|
|
76
|
+
# items.tap { |i| [1, 2].each { i << _1 } }
|
|
77
|
+
#
|
|
78
|
+
# # good — preferred explicit form when the value is unused
|
|
79
|
+
# result = read_value
|
|
80
|
+
# do_side_effect
|
|
81
|
+
# result
|
|
82
|
+
class TapBlockIgnoresValue < Base
|
|
83
|
+
MSG = '`tap` is for operating on the yielded value — when the ' \
|
|
84
|
+
'block ignores it, prefer an explicit assign-and-return. ' \
|
|
85
|
+
'Disable with a reason when the side effect reads clearer ' \
|
|
86
|
+
'in `tap` position.'.freeze
|
|
87
|
+
|
|
88
|
+
def on_block(node)
|
|
89
|
+
return unless node.send_node.method?(:tap)
|
|
90
|
+
return if block_uses_yielded_value?(node)
|
|
91
|
+
|
|
92
|
+
add_offense(node.send_node.loc.selector)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def block_uses_yielded_value?(node)
|
|
98
|
+
args = node.arguments
|
|
99
|
+
return false unless args
|
|
100
|
+
return false if args.children.empty? # no param declared → value ignored
|
|
101
|
+
|
|
102
|
+
# `tap` yields one value; multi-arg / destructuring is unusual.
|
|
103
|
+
# Be conservative — don't flag what we can't confidently call
|
|
104
|
+
# "ignoring the value".
|
|
105
|
+
return true if args.children.length >= 2
|
|
106
|
+
|
|
107
|
+
arg = args.children.first
|
|
108
|
+
return true unless arg.respond_to?(:name)
|
|
109
|
+
|
|
110
|
+
return false unless node.body # empty body with declared arg → flag
|
|
111
|
+
|
|
112
|
+
# Walk from the block node (not `body`): `each_descendant`
|
|
113
|
+
# excludes the receiver, so a body that *is* an lvar
|
|
114
|
+
# (`tap { |v| v }`) would be missed otherwise. Nested blocks
|
|
115
|
+
# are walked too, so a reference inside one counts as "used".
|
|
116
|
+
# The shadowing edge case is documented above as a NOTE.
|
|
117
|
+
node.each_descendant(:lvar).any? { |n| n.children.first == arg.name }
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -13,6 +13,18 @@ module RuboCop
|
|
|
13
13
|
# controller test (very rare) — e.g. a search-ranking detail the
|
|
14
14
|
# controller never exposes.
|
|
15
15
|
#
|
|
16
|
+
# The danger a unit test hides: it can stay green while the feature is
|
|
17
|
+
# broken in production. It proves a method works in isolation — NOT that
|
|
18
|
+
# the real request path calls that method, in the right order, inside the
|
|
19
|
+
# transaction it needs. A model method can be flawless in a unit test
|
|
20
|
+
# while the controller calls the wrong method, skips it, or runs it
|
|
21
|
+
# outside its transaction; the unit test stays green and the feature is
|
|
22
|
+
# broken. Controller tests fail when the *wiring* is wrong — which is
|
|
23
|
+
# where regressions actually live. So a passing unit test is not evidence
|
|
24
|
+
# the feature works; it is false confidence about production. Reach for
|
|
25
|
+
# one only when you are sure a controller test genuinely cannot reach the
|
|
26
|
+
# path, not because it is quicker to write.
|
|
27
|
+
#
|
|
16
28
|
# This cop flags only the literal `< ActiveSupport::TestCase`
|
|
17
29
|
# superclass. The blessed blackbox bases —
|
|
18
30
|
# `ActionDispatch::IntegrationTest`, `Glib::IntegrationTest`,
|
|
@@ -20,6 +32,18 @@ module RuboCop
|
|
|
20
32
|
# though they inherit from `ActiveSupport::TestCase` transitively.
|
|
21
33
|
#
|
|
22
34
|
# ## Escape hatch
|
|
35
|
+
# Before reaching for a unit test, assume a controller test IS possible
|
|
36
|
+
# and look harder — that conclusion is almost always premature. Behaviour
|
|
37
|
+
# that feels inherently unit-level is usually reachable end-to-end:
|
|
38
|
+
# - Transaction rollback / "a failure mid-request": inject the failure
|
|
39
|
+
# at a class-method chokepoint the gem/service calls (stub it to
|
|
40
|
+
# raise), drive the real request, and assert the observable rollback
|
|
41
|
+
# (e.g. `assert_no_difference` on the record count). Even atomicity,
|
|
42
|
+
# which feels inherently unit-level, is reachable this way.
|
|
43
|
+
# - "The controller wraps it in a transaction so I can't isolate the
|
|
44
|
+
# model's own": you usually don't need to — assert the *observable*
|
|
45
|
+
# guarantee through the real path; that is what matters in production.
|
|
46
|
+
#
|
|
23
47
|
# When a unit test is genuinely necessary, suppress with a reason that
|
|
24
48
|
# explains why a controller test can't cover the path. That reason IS the
|
|
25
49
|
# required justification — keep it specific and reviewable:
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubocop-dev_doc
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0.beta1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dev-doc contributors
|
|
@@ -91,10 +91,11 @@ files:
|
|
|
91
91
|
- lib/rubocop-dev_doc.rb
|
|
92
92
|
- lib/rubocop/cop/dev_doc/auth/current_user_branching.rb
|
|
93
93
|
- lib/rubocop/cop/dev_doc/auth/load_resource_current_user_guard.rb
|
|
94
|
+
- lib/rubocop/cop/dev_doc/i18n/avoid_titleize_humanize.rb
|
|
95
|
+
- lib/rubocop/cop/dev_doc/i18n/localizable_props.rb
|
|
94
96
|
- lib/rubocop/cop/dev_doc/i18n/report_text.rb
|
|
95
97
|
- lib/rubocop/cop/dev_doc/i18n/require_translation.rb
|
|
96
98
|
- lib/rubocop/cop/dev_doc/i18n/translation_key_prefix.rb
|
|
97
|
-
- lib/rubocop/cop/dev_doc/i18n/unverified_translation.rb
|
|
98
99
|
- lib/rubocop/cop/dev_doc/migration/amount_column_in_cents.rb
|
|
99
100
|
- lib/rubocop/cop/dev_doc/migration/avoid_bypassing_validation.rb
|
|
100
101
|
- lib/rubocop/cop/dev_doc/migration/avoid_column_default.rb
|
|
@@ -109,7 +110,9 @@ files:
|
|
|
109
110
|
- lib/rubocop/cop/dev_doc/migration/require_timestamps.rb
|
|
110
111
|
- lib/rubocop/cop/dev_doc/rails/application_record_transaction.rb
|
|
111
112
|
- lib/rubocop/cop/dev_doc/rails/avoid_lifecycle_method_override.rb
|
|
113
|
+
- lib/rubocop/cop/dev_doc/rails/avoid_ordering_by_id.rb
|
|
112
114
|
- lib/rubocop/cop/dev_doc/rails/avoid_rails_callbacks.rb
|
|
115
|
+
- lib/rubocop/cop/dev_doc/rails/avoid_raw_sql.rb
|
|
113
116
|
- lib/rubocop/cop/dev_doc/rails/bang_save_in_transaction.rb
|
|
114
117
|
- lib/rubocop/cop/dev_doc/rails/enum_column_not_null.rb
|
|
115
118
|
- lib/rubocop/cop/dev_doc/rails/enum_must_be_symbolized.rb
|
|
@@ -125,9 +128,11 @@ files:
|
|
|
125
128
|
- lib/rubocop/cop/dev_doc/style/avoid_send.rb
|
|
126
129
|
- lib/rubocop/cop/dev_doc/style/minimize_variable_scope.rb
|
|
127
130
|
- lib/rubocop/cop/dev_doc/style/no_unscoped_method_definitions.rb
|
|
131
|
+
- lib/rubocop/cop/dev_doc/style/prefer_public_send.rb
|
|
128
132
|
- lib/rubocop/cop/dev_doc/style/repeated_bracket_read.rb
|
|
129
133
|
- lib/rubocop/cop/dev_doc/style/repeated_safe_navigation_receiver.rb
|
|
130
134
|
- lib/rubocop/cop/dev_doc/style/string_symbol_comparison.rb
|
|
135
|
+
- lib/rubocop/cop/dev_doc/style/tap_block_ignores_value.rb
|
|
131
136
|
- lib/rubocop/cop/dev_doc/test/avoid_glib_travel_freeze.rb
|
|
132
137
|
- lib/rubocop/cop/dev_doc/test/avoid_unit_test.rb
|
|
133
138
|
- lib/rubocop/cop/dev_doc/test/response_assert_equal.rb
|
|
@@ -1,106 +0,0 @@
|
|
|
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
|