rubocop-dev_doc 0.11.0 → 0.12.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 +28 -0
- data/lib/dev_doc/i18n/pseudo_locale.rb +23 -19
- data/lib/dev_doc/test/lints/cop_drift_check.rb +43 -0
- data/lib/dev_doc/test/lints/cop_drift_test.rb +40 -0
- data/lib/dev_doc/test/lints/cron_schedule.rb +137 -115
- data/lib/dev_doc/test/lints/duplicate_snapshot.rb +5 -2
- data/lib/dev_doc/test/lints/http_driven_controller_tests.rb +5 -0
- data/lib/dev_doc/test/lints/no_file_excludes.rb +32 -21
- data/lib/dev_doc/test/pseudo_i18n_crawler.rb +96 -60
- data/lib/rubocop/cop/dev_doc/auth/load_resource_current_user_guard.rb +3 -3
- data/lib/rubocop/cop/dev_doc/i18n/localizable_props.rb +12 -11
- data/lib/rubocop/cop/dev_doc/i18n/translation_key_prefix.rb +3 -0
- data/lib/rubocop/cop/dev_doc/migration/date_column_naming.rb +3 -0
- data/lib/rubocop/cop/dev_doc/rails/avoid_bypassing_validation.rb +3 -2
- data/lib/rubocop/cop/dev_doc/rails/avoid_lifecycle_method_override.rb +2 -2
- data/lib/rubocop/cop/dev_doc/rails/avoid_rails_callbacks.rb +3 -3
- data/lib/rubocop/cop/dev_doc/rails/enum_column_not_null.rb +3 -1
- data/lib/rubocop/cop/dev_doc/route/no_custom_actions.rb +4 -1
- data/lib/rubocop/cop/dev_doc/route/resource_name_number.rb +5 -4
- data/lib/rubocop/cop/dev_doc/route/resources_require_only.rb +3 -4
- data/lib/rubocop/cop/dev_doc/style/avoid_symbolizing_boundary_input.rb +116 -0
- data/lib/rubocop/cop/dev_doc/style/case_else_decision.rb +2 -1
- data/lib/rubocop/cop/dev_doc/style/minimize_variable_scope.rb +23 -13
- data/lib/rubocop/cop/dev_doc/style/no_unscoped_method_definitions.rb +21 -0
- data/lib/rubocop/cop/dev_doc/style/repeated_bracket_read.rb +13 -9
- data/lib/rubocop/cop/dev_doc/style/repeated_safe_navigation_receiver.rb +13 -10
- data/lib/rubocop/cop/dev_doc/style/string_symbol_comparison.rb +15 -7
- data/lib/rubocop/cop/dev_doc/test/avoid_glib_travel_freeze.rb +2 -2
- data/lib/rubocop/cop/dev_doc/test/no_unit_idioms_in_integration_tests.rb +134 -0
- data/lib/rubocop/dev_doc/plugin.rb +2 -0
- data/lib/rubocop/dev_doc/version.rb +1 -1
- metadata +6 -2
|
@@ -102,17 +102,21 @@ module RuboCop
|
|
|
102
102
|
|
|
103
103
|
seen = {}
|
|
104
104
|
body.each_descendant(:send) do |send_node|
|
|
105
|
-
|
|
105
|
+
track_bracket_read(send_node, seen)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def track_bracket_read(send_node, seen)
|
|
110
|
+
return unless bracket_read?(send_node)
|
|
106
111
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
112
|
+
receiver = send_node.receiver
|
|
113
|
+
key = send_node.first_argument
|
|
114
|
+
composite = composite_key(receiver, key)
|
|
110
115
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
end
|
|
116
|
+
if seen[composite]
|
|
117
|
+
add_offense(send_node.loc.selector, message: format(MSG, receiver: receiver.source, key: key.source))
|
|
118
|
+
else
|
|
119
|
+
seen[composite] = true
|
|
116
120
|
end
|
|
117
121
|
end
|
|
118
122
|
|
|
@@ -85,7 +85,8 @@ module RuboCop
|
|
|
85
85
|
# current_user&.full_name
|
|
86
86
|
# end
|
|
87
87
|
class RepeatedSafeNavigationReceiver < Base
|
|
88
|
-
MSG = "Receiver `%<receiver>s` already used with `&.` earlier in this method —
|
|
88
|
+
MSG = "Receiver `%<receiver>s` already used with `&.` earlier in this method — " \
|
|
89
|
+
"assign once and use `.` after.".freeze
|
|
89
90
|
|
|
90
91
|
def on_def(node)
|
|
91
92
|
check_method(node)
|
|
@@ -99,16 +100,18 @@ module RuboCop
|
|
|
99
100
|
return unless body
|
|
100
101
|
|
|
101
102
|
seen = {}
|
|
102
|
-
body.each_descendant(:csend)
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
body.each_descendant(:csend) { |csend| track_receiver(csend, seen) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def track_receiver(csend, seen)
|
|
107
|
+
receiver = csend.receiver
|
|
108
|
+
return unless receiver
|
|
105
109
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
end
|
|
110
|
+
key = receiver.source
|
|
111
|
+
if seen[key]
|
|
112
|
+
add_offense(csend.loc.dot, message: format(MSG, receiver: key))
|
|
113
|
+
else
|
|
114
|
+
seen[key] = true
|
|
112
115
|
end
|
|
113
116
|
end
|
|
114
117
|
end
|
|
@@ -11,16 +11,23 @@ module RuboCop
|
|
|
11
11
|
# Strings and symbols are not equal in Ruby, and string-typed values
|
|
12
12
|
# arrive from boundaries the developer doesn't control: HTTP params,
|
|
13
13
|
# request headers, ENV. Comparing one of these directly against a
|
|
14
|
-
# symbol literal is a guaranteed bug.
|
|
15
|
-
# — see
|
|
14
|
+
# symbol literal is a guaranteed bug. Allow-list into a symbol at the
|
|
15
|
+
# boundary instead — see
|
|
16
|
+
# `best_practices/backend/en/01a_defensive_programming.md` item 7.
|
|
16
17
|
#
|
|
17
18
|
# ❌
|
|
18
19
|
# if params[:status] == :draft # always false
|
|
19
20
|
#
|
|
20
|
-
# ✔
|
|
21
|
-
# @filter_status = params[:status]
|
|
21
|
+
# ✔ Allow-list into a symbol at the boundary, then compare
|
|
22
|
+
# @filter_status = glib_allowlist_symbol_param(params[:status], STATUSES, default: :draft)
|
|
22
23
|
# if @filter_status == :draft
|
|
23
24
|
#
|
|
25
|
+
# A bare `&.to_sym` conversion would also fix the always-false bug,
|
|
26
|
+
# but it lets ANY client value into the symbol domain — the sibling
|
|
27
|
+
# `DevDoc/Style/AvoidSymbolizingBoundaryInput` flags exactly that.
|
|
28
|
+
# The two cops are a complementary pair: this one polices comparing
|
|
29
|
+
# without converting; that one polices converting without validating.
|
|
30
|
+
#
|
|
24
31
|
# ## Sources detected
|
|
25
32
|
# - `params[…]`
|
|
26
33
|
# - `request.headers[…]`
|
|
@@ -39,12 +46,13 @@ module RuboCop
|
|
|
39
46
|
# request.headers['X-Mode'] != :debug
|
|
40
47
|
#
|
|
41
48
|
# # good
|
|
42
|
-
# params[:status]
|
|
49
|
+
# glib_allowlist_symbol_param(params[:status], STATUSES, default: :draft) == :draft
|
|
43
50
|
# ENV['MODE'] == 'production'
|
|
44
51
|
class StringSymbolComparison < Base
|
|
45
52
|
MSG = 'Comparing string-typed `%<source>s` to a symbol literal is always false. ' \
|
|
46
|
-
'
|
|
47
|
-
'(
|
|
53
|
+
'Allow-list it into a symbol at the boundary ' \
|
|
54
|
+
'(`glib_allowlist_symbol_param(value, ALLOWED, default:)`) or compare against ' \
|
|
55
|
+
'a string instead (see backend/01a_defensive_programming.md item 7).'.freeze
|
|
48
56
|
|
|
49
57
|
RESTRICT_ON_SEND = %i[== !=].freeze
|
|
50
58
|
|
|
@@ -17,8 +17,8 @@ module RuboCop
|
|
|
17
17
|
# ## Escape hatch
|
|
18
18
|
# Disable per-line with a comment explaining why `glib_travel` doesn't work:
|
|
19
19
|
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
20
|
+
# # rubocop:disable DevDoc/Test/AvoidGlibTravelFreeze -- <explanation>
|
|
21
|
+
# glib_travel_freeze(time) do
|
|
22
22
|
# end
|
|
23
23
|
#
|
|
24
24
|
# @example
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module DevDoc
|
|
4
|
+
module Test
|
|
5
|
+
# Integration tests must be BOUNDARY tests: exercise one entry point
|
|
6
|
+
# (an HTTP request, or a job for cron-driven flows with no HTTP
|
|
7
|
+
# surface) and assert only its observable outputs — response status
|
|
8
|
+
# and body, database state, delivered/enqueued mail. Unit-test idioms
|
|
9
|
+
# under the integration base class are dishonest by construction: the
|
|
10
|
+
# base class is a declaration of WHAT KIND of test this is, and the
|
|
11
|
+
# runtime/static lints trust that declaration.
|
|
12
|
+
#
|
|
13
|
+
# ## What flags, and why
|
|
14
|
+
# - `@controller`, `assigns`, `instance_variable_get`: reaching into
|
|
15
|
+
# request-internal state the boundary is supposed to hide.
|
|
16
|
+
# - `FooService.new` / `FooPolicy.new` (any suffix in
|
|
17
|
+
# `UnitClassSuffixes`): instantiating app-layer objects to test or
|
|
18
|
+
# query them directly. When used to COMPUTE an expected value, this
|
|
19
|
+
# is a circular assertion — the test asks production code what to
|
|
20
|
+
# expect, so a bug in that code passes its own test. Pin literals
|
|
21
|
+
# or fixture facts instead.
|
|
22
|
+
# - `FooMailer.with(...)` and any other direct mailer invocation:
|
|
23
|
+
# manufacture mail through the flow that actually sends it, then
|
|
24
|
+
# read `deliveries`.
|
|
25
|
+
# - `include FooHelper`: importing production helpers to compute
|
|
26
|
+
# expectations — circular, same as above (`*TestHelper` modules are
|
|
27
|
+
# exempt).
|
|
28
|
+
# - `Minitest::Mock`, `.stub(...)`, `require 'minitest/mock'`:
|
|
29
|
+
# mock/stub machinery is unit-test tooling by definition.
|
|
30
|
+
#
|
|
31
|
+
# ## What deliberately does NOT flag
|
|
32
|
+
# - Model reads (`Organization.last`, `Member.find_by!`, counts,
|
|
33
|
+
# attribute readers): the database is an OUTBOUND edge of the
|
|
34
|
+
# boundary — asserting persisted state is the point — and fixture
|
|
35
|
+
# lookup is setup.
|
|
36
|
+
# - Job classes (`FooJob.new`, `perform_now`, `perform_enqueued_jobs`):
|
|
37
|
+
# jobs are an INBOUND edge — the entry point for cron flows.
|
|
38
|
+
#
|
|
39
|
+
# ## The honesty rule
|
|
40
|
+
# If the property cannot be observed at any boundary, the test is a
|
|
41
|
+
# unit test. Declare it: `Glib::LastResortUnitTest` in an exempt
|
|
42
|
+
# directory with a justification header. An observed evasion (2026-07,
|
|
43
|
+
# a guard-aware agent bolting a GET onto direct policy assertions)
|
|
44
|
+
# is why this cop assumes disguise rather than accident. Do not
|
|
45
|
+
# disable this cop.
|
|
46
|
+
class NoUnitIdiomsInIntegrationTests < Base
|
|
47
|
+
MSG = '`%<ref>s` is a unit-test idiom inside an integration test. A boundary test ' \
|
|
48
|
+
'exercises one entry point (HTTP request, or a job for cron flows) and asserts ' \
|
|
49
|
+
'only its observable outputs (status, body, DB state, mails). Computing an ' \
|
|
50
|
+
'expectation with production code is a CIRCULAR assertion (the code grades its ' \
|
|
51
|
+
'own homework); instantiating app objects or reaching into the controller is ' \
|
|
52
|
+
'unit testing in disguise, and a bolted-on request does not change what it is. ' \
|
|
53
|
+
'Be honest about what this test is: assert through the boundary, or declare a ' \
|
|
54
|
+
'real unit test (Glib::LastResortUnitTest + exempt directory + justification ' \
|
|
55
|
+
'header). Do not disable this cop.'.freeze
|
|
56
|
+
|
|
57
|
+
CONTROLLER_INTERNALS = %i[assigns instance_variable_get].freeze
|
|
58
|
+
HELPER_EXEMPT = /TestHelper\z|MailerTester\z/
|
|
59
|
+
MOCK_REQUIRES = ['minitest/mock'].freeze
|
|
60
|
+
|
|
61
|
+
# Baseline lives in code (config lists are droppable; see
|
|
62
|
+
# CurrentUserBranching's DEFAULT_ALLOWED_METHODS for the incident
|
|
63
|
+
# that taught this). The yml key ADDS suffixes, it cannot remove
|
|
64
|
+
# these.
|
|
65
|
+
DEFAULT_UNIT_CLASS_SUFFIXES = %w[Service Policy Form Presenter Serializer Decorator].freeze
|
|
66
|
+
|
|
67
|
+
def on_ivar(node)
|
|
68
|
+
return unless node.children.first == :@controller
|
|
69
|
+
|
|
70
|
+
add_offense(node, message: format(MSG, ref: '@controller'))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def on_send(node)
|
|
74
|
+
ref = offense_ref(node)
|
|
75
|
+
return unless ref
|
|
76
|
+
|
|
77
|
+
add_offense(node, message: format(MSG, ref: ref))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def offense_ref(node)
|
|
83
|
+
return node.method_name.to_s if CONTROLLER_INTERNALS.include?(node.method_name)
|
|
84
|
+
return "#{node.receiver.const_name}.new" if unit_class_instantiation?(node)
|
|
85
|
+
return "#{node.receiver.const_name}.#{node.method_name}" if mailer_invocation?(node)
|
|
86
|
+
return "include #{node.first_argument.const_name}" if helper_include?(node)
|
|
87
|
+
|
|
88
|
+
mock_machinery_ref(node)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def unit_class_instantiation?(node)
|
|
92
|
+
node.method_name == :new && receiver_const_suffix?(node, unit_class_suffixes)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def mailer_invocation?(node)
|
|
96
|
+
receiver_const_suffix?(node, ['Mailer'])
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def helper_include?(node)
|
|
100
|
+
return false unless node.method_name == :include && node.first_argument&.const_type?
|
|
101
|
+
|
|
102
|
+
name = node.first_argument.const_name
|
|
103
|
+
name.end_with?('Helper') && !name.match?(HELPER_EXEMPT)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def mock_machinery_ref(node)
|
|
107
|
+
return 'Minitest::Mock' if receiver_const_suffix?(node, ['Minitest::Mock']) || minitest_mock_const?(node)
|
|
108
|
+
return 'stub' if node.method_name == :stub && node.receiver
|
|
109
|
+
return "require 'minitest/mock'" if mock_require?(node)
|
|
110
|
+
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def minitest_mock_const?(node)
|
|
115
|
+
node.method_name == :new && node.receiver&.const_type? && node.receiver.const_name == 'Minitest::Mock'
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def mock_require?(node)
|
|
119
|
+
node.method_name == :require && node.first_argument&.str_type? &&
|
|
120
|
+
MOCK_REQUIRES.include?(node.first_argument.value)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def receiver_const_suffix?(node, suffixes)
|
|
124
|
+
node.receiver&.const_type? && suffixes.any? { |suffix| node.receiver.const_name.end_with?(suffix) }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def unit_class_suffixes
|
|
128
|
+
DEFAULT_UNIT_CLASS_SUFFIXES + cop_config.fetch('UnitClassSuffixes', []).map(&:to_s)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
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.12.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-07-
|
|
11
|
+
date: 2026-07-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -90,6 +90,8 @@ files:
|
|
|
90
90
|
- config/obsoletion.yml
|
|
91
91
|
- lib/dev_doc/i18n/pseudo_locale.rb
|
|
92
92
|
- lib/dev_doc/test/best_practice_lints.rb
|
|
93
|
+
- lib/dev_doc/test/lints/cop_drift_check.rb
|
|
94
|
+
- lib/dev_doc/test/lints/cop_drift_test.rb
|
|
93
95
|
- lib/dev_doc/test/lints/cron_schedule.rb
|
|
94
96
|
- lib/dev_doc/test/lints/duplicate_snapshot.rb
|
|
95
97
|
- lib/dev_doc/test/lints/http_driven_controller_tests.rb
|
|
@@ -137,6 +139,7 @@ files:
|
|
|
137
139
|
- lib/rubocop/cop/dev_doc/style/avoid_head_response.rb
|
|
138
140
|
- lib/rubocop/cop/dev_doc/style/avoid_insecure_send.rb
|
|
139
141
|
- lib/rubocop/cop/dev_doc/style/avoid_options_hash.rb
|
|
142
|
+
- lib/rubocop/cop/dev_doc/style/avoid_symbolizing_boundary_input.rb
|
|
140
143
|
- lib/rubocop/cop/dev_doc/style/case_else_decision.rb
|
|
141
144
|
- lib/rubocop/cop/dev_doc/style/literal_operator_in_condition.rb
|
|
142
145
|
- lib/rubocop/cop/dev_doc/style/literal_or_in_when_clause.rb
|
|
@@ -152,6 +155,7 @@ files:
|
|
|
152
155
|
- lib/rubocop/cop/dev_doc/test/avoid_unit_test.rb
|
|
153
156
|
- lib/rubocop/cop/dev_doc/test/justification_header.rb
|
|
154
157
|
- lib/rubocop/cop/dev_doc/test/no_persistence_in_mailer_previews.rb
|
|
158
|
+
- lib/rubocop/cop/dev_doc/test/no_unit_idioms_in_integration_tests.rb
|
|
155
159
|
- lib/rubocop/cop/dev_doc/test/require_glib_integration_base.rb
|
|
156
160
|
- lib/rubocop/cop/dev_doc/test/require_glib_travel.rb
|
|
157
161
|
- lib/rubocop/cop/dev_doc/test/require_glib_travel_block.rb
|