rubocop-dev_doc 0.2.0 → 0.3.1
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 +235 -61
- data/lib/dev_doc/test/best_practice_lints.rb +31 -0
- data/lib/dev_doc/test/lints/cron_schedule.rb +345 -0
- data/lib/dev_doc/test/lints/duplicate_snapshot.rb +197 -0
- data/lib/dev_doc/test/lints/no_file_excludes.rb +128 -0
- data/lib/rubocop/cop/dev_doc/auth/current_user_branching.rb +203 -0
- data/lib/rubocop/cop/dev_doc/auth/load_resource_current_user_guard.rb +287 -0
- data/lib/rubocop/cop/dev_doc/migration/avoid_conditional_schema_changes.rb +89 -0
- data/lib/rubocop/cop/dev_doc/migration/avoid_non_null.rb +121 -0
- data/lib/rubocop/cop/dev_doc/rails/application_record_transaction.rb +1 -1
- data/lib/rubocop/cop/dev_doc/rails/bang_save_in_transaction.rb +127 -0
- data/lib/rubocop/cop/dev_doc/rails/enum_column_not_null.rb +99 -0
- data/lib/rubocop/cop/dev_doc/rails/enum_must_be_symbolized.rb +2 -2
- data/lib/rubocop/cop/dev_doc/rails/no_block_predicate_on_relation.rb +236 -0
- data/lib/rubocop/cop/dev_doc/rails/strong_parameters_expect.rb +137 -0
- data/lib/rubocop/cop/dev_doc/route/no_custom_actions.rb +171 -0
- data/lib/rubocop/cop/dev_doc/route/resource_name_number.rb +77 -0
- data/lib/rubocop/cop/dev_doc/style/avoid_send.rb +31 -4
- data/lib/rubocop/cop/dev_doc/style/minimize_variable_scope.rb +158 -0
- data/lib/rubocop/cop/dev_doc/style/no_unscoped_method_definitions.rb +129 -0
- data/lib/rubocop/cop/dev_doc/style/repeated_bracket_read.rb +150 -0
- data/lib/rubocop/cop/dev_doc/style/repeated_safe_navigation_receiver.rb +118 -0
- data/lib/rubocop/cop/dev_doc/test/avoid_glib_travel_freeze.rb +53 -0
- data/lib/rubocop/cop/dev_doc/test/avoid_unit_test.rb +66 -0
- data/lib/rubocop/cop/dev_doc/test/response_assert_equal.rb +179 -0
- data/lib/rubocop/dev_doc/version.rb +1 -1
- metadata +58 -3
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module DevDoc
|
|
4
|
+
module Test
|
|
5
|
+
# Controller tests that assert on the rendered `response.body` should
|
|
6
|
+
# also snapshot the full response with `response_assert_equal`.
|
|
7
|
+
#
|
|
8
|
+
# ## Rationale
|
|
9
|
+
# `response_assert_equal` (glib-web's `Glib::TestHelpers`) is the most
|
|
10
|
+
# blackbox assertion available — it snapshots the entire rendered
|
|
11
|
+
# response, so it catches regressions anywhere in the output, not just
|
|
12
|
+
# the one substring a targeted assertion happens to check. Per the
|
|
13
|
+
# testing best practice, happy- and unhappy-path controller tests
|
|
14
|
+
# should always use it.
|
|
15
|
+
#
|
|
16
|
+
# The common slip this cop guards against: a test inspects the rendered
|
|
17
|
+
# body with `assert_match` / `assert_no_match` / `assert_includes` /
|
|
18
|
+
# `response.body.scan(...)` but forgets the snapshot. The targeted
|
|
19
|
+
# assertion documents intent, but on its own it only proves the one
|
|
20
|
+
# line — collateral changes elsewhere in the response slip through.
|
|
21
|
+
#
|
|
22
|
+
# ❌ Inspects the body, but no snapshot
|
|
23
|
+
# test 'index lists the item' do
|
|
24
|
+
# get items_url(format: :json)
|
|
25
|
+
# assert_response :success
|
|
26
|
+
# assert_match 'Widget', response.body
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# ✔️ Keep the focused assertion AND snapshot the whole response
|
|
30
|
+
# test 'index lists the item' do
|
|
31
|
+
# get items_url(format: :json)
|
|
32
|
+
# assert_response :success
|
|
33
|
+
# assert_match 'Widget', response.body
|
|
34
|
+
# response_assert_equal
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# ## What is NOT flagged
|
|
38
|
+
# - Tests that already call `response_assert_equal`.
|
|
39
|
+
# - Exception/redirect paths — a test asserting a non-success status
|
|
40
|
+
# (`assert_response :not_found`, `:forbidden`, `:redirect`, etc.)
|
|
41
|
+
# often has no stable rendered body to snapshot.
|
|
42
|
+
# - State-change tests that never read `response.body` (e.g.
|
|
43
|
+
# `assert_difference 'Model.count'`, mailer assertions). Passing
|
|
44
|
+
# `response.body` to a helper like `submit_form(response.body, ...)`
|
|
45
|
+
# is not a body assertion and is not flagged.
|
|
46
|
+
#
|
|
47
|
+
# ## Inline disable
|
|
48
|
+
# For the rare happy-path test whose response is genuinely
|
|
49
|
+
# non-deterministic and cannot be made stable, add a
|
|
50
|
+
# `rubocop:disable DevDoc/Test/ResponseAssertEqual` comment to the
|
|
51
|
+
# `test '...' do` line, with a written reason on the next line
|
|
52
|
+
# (e.g. "chunked response includes a per-run boundary token").
|
|
53
|
+
#
|
|
54
|
+
# @example
|
|
55
|
+
# # bad
|
|
56
|
+
# test 'shows the row' do
|
|
57
|
+
# get foo_url(format: :json)
|
|
58
|
+
# assert_match 'bar', response.body
|
|
59
|
+
# end
|
|
60
|
+
#
|
|
61
|
+
# # good
|
|
62
|
+
# test 'shows the row' do
|
|
63
|
+
# get foo_url(format: :json)
|
|
64
|
+
# assert_match 'bar', response.body
|
|
65
|
+
# response_assert_equal
|
|
66
|
+
# end
|
|
67
|
+
class ResponseAssertEqual < Base
|
|
68
|
+
MSG = 'Asserts on `response.body` but never calls `response_assert_equal`. ' \
|
|
69
|
+
'Add the snapshot assertion (usually last) — it captures the whole rendered ' \
|
|
70
|
+
'response, a stronger guard than a targeted body assertion. Exception-path ' \
|
|
71
|
+
'tests that cannot snapshot may disable this cop with a reason.'.freeze
|
|
72
|
+
|
|
73
|
+
# @!method test_block?(node)
|
|
74
|
+
def_node_matcher :test_block?, <<~PATTERN
|
|
75
|
+
(block (send nil? :test ...) _args _body)
|
|
76
|
+
PATTERN
|
|
77
|
+
|
|
78
|
+
# @!method response_body_read?(node)
|
|
79
|
+
def_node_matcher :response_body_read?, <<~PATTERN
|
|
80
|
+
(send (send nil? :response) {:body :parsed_body})
|
|
81
|
+
PATTERN
|
|
82
|
+
|
|
83
|
+
# @!method calls_snapshot?(node)
|
|
84
|
+
def_node_search :calls_snapshot?, <<~PATTERN
|
|
85
|
+
(send nil? :response_assert_equal)
|
|
86
|
+
PATTERN
|
|
87
|
+
|
|
88
|
+
# @!method response_status?(node)
|
|
89
|
+
def_node_matcher :response_status?, '(send (send nil? :response) :status)'
|
|
90
|
+
|
|
91
|
+
# Non-success HTTP statuses (symbol form). A test asserting any of
|
|
92
|
+
# these is an exception / redirect / empty-body path — no JSON body
|
|
93
|
+
# to snapshot.
|
|
94
|
+
NON_SUCCESS_STATUS_SYMBOLS = %i[
|
|
95
|
+
not_found forbidden unauthorized unprocessable_entity unprocessable_content
|
|
96
|
+
bad_request conflict gone no_content not_modified redirect moved_permanently
|
|
97
|
+
found see_other payment_required too_many_requests precondition_failed
|
|
98
|
+
].freeze
|
|
99
|
+
|
|
100
|
+
def on_block(node)
|
|
101
|
+
return unless test_block?(node)
|
|
102
|
+
return unless node.body
|
|
103
|
+
return if calls_snapshot?(node)
|
|
104
|
+
return if exempt_from_snapshot?(node)
|
|
105
|
+
return unless inspects_response_body?(node)
|
|
106
|
+
|
|
107
|
+
add_offense(node.send_node.loc.selector)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
# Exception / redirect / empty-body responses can't (and shouldn't) be
|
|
113
|
+
# snapshotted. Handles both the `assert_response :symbol` convention
|
|
114
|
+
# and the numeric `assert_response 404` / `assert_equal 404,
|
|
115
|
+
# response.status` convention, plus `assert_empty response.body`.
|
|
116
|
+
def exempt_from_snapshot?(test_node)
|
|
117
|
+
test_node.each_descendant(:send).any? { |s| non_snapshotable_assertion?(s) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def non_snapshotable_assertion?(send_node)
|
|
121
|
+
case send_node.method_name
|
|
122
|
+
when :assert_response then non_success_status?(send_node.first_argument)
|
|
123
|
+
when :assert_equal then non_success_status_equal?(send_node)
|
|
124
|
+
when :assert_empty then response_body_read?(send_node.first_argument)
|
|
125
|
+
else false
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# `assert_equal <non-2xx code>, response.status`
|
|
130
|
+
def non_success_status_equal?(send_node)
|
|
131
|
+
args = send_node.arguments
|
|
132
|
+
args.size >= 2 && non_success_status?(args[0]) && response_status?(args[1])
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# A non-success status: a symbol (`:not_found`) or a numeric code
|
|
136
|
+
# >= 300 (`404`). 2xx codes are NOT exempt — they should snapshot.
|
|
137
|
+
def non_success_status?(node)
|
|
138
|
+
return false unless node
|
|
139
|
+
|
|
140
|
+
(node.sym_type? && NON_SUCCESS_STATUS_SYMBOLS.include?(node.value)) ||
|
|
141
|
+
(node.int_type? && node.value >= 300)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# True when the test inspects the response *as JSON*. Three forms count:
|
|
145
|
+
# - `response.parsed_body` (any use — it's the JSON-parsed body)
|
|
146
|
+
# - `JSON.parse(response.body)` (explicit JSON parse, even if the
|
|
147
|
+
# result is stored in a local first)
|
|
148
|
+
# - `response.body` directly inside an assertion
|
|
149
|
+
# (`assert_match 'x', response.body`)
|
|
150
|
+
#
|
|
151
|
+
# Deliberately NOT counted: `response.body` fed to a format-specific
|
|
152
|
+
# parser (`parse_xlsx`, `CSV.parse`, `Nokogiri…parse`) or a render/submit
|
|
153
|
+
# helper (`submit_form`) — those are non-JSON or pass-through, and
|
|
154
|
+
# `response_assert_equal` (JSON-only) can't snapshot them.
|
|
155
|
+
def inspects_response_body?(test_node)
|
|
156
|
+
test_node.each_descendant(:send).any? do |read|
|
|
157
|
+
next false unless response_body_read?(read)
|
|
158
|
+
|
|
159
|
+
read.method_name == :parsed_body || asserted?(read) || json_parsed?(read)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# `response.body` / `response.parsed_body` read within an assertion.
|
|
164
|
+
def asserted?(read)
|
|
165
|
+
read.each_ancestor(:send).any? { |a| a.method_name.to_s.start_with?('assert') }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# `JSON.parse(response.body)` — the JSON constant specifically, so
|
|
169
|
+
# format-specific parsers (`parse_xlsx`, `CSV.parse`) don't match.
|
|
170
|
+
def json_parsed?(read)
|
|
171
|
+
parent = read.parent
|
|
172
|
+
parent&.send_type? && parent.method_name == :parse &&
|
|
173
|
+
parent.receiver&.const_type? && parent.receiver.const_name == 'JSON'
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,43 @@
|
|
|
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.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dev-doc contributors
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-06-11 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activesupport
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '4.2'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '4.2'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: fugit
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.9'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.9'
|
|
12
41
|
- !ruby/object:Gem::Dependency
|
|
13
42
|
name: lint_roller
|
|
14
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -51,16 +80,26 @@ dependencies:
|
|
|
51
80
|
- - ">="
|
|
52
81
|
- !ruby/object:Gem::Version
|
|
53
82
|
version: '2.0'
|
|
83
|
+
description:
|
|
84
|
+
email:
|
|
54
85
|
executables: []
|
|
55
86
|
extensions: []
|
|
56
87
|
extra_rdoc_files: []
|
|
57
88
|
files:
|
|
58
89
|
- config/default.yml
|
|
90
|
+
- lib/dev_doc/test/best_practice_lints.rb
|
|
91
|
+
- lib/dev_doc/test/lints/cron_schedule.rb
|
|
92
|
+
- lib/dev_doc/test/lints/duplicate_snapshot.rb
|
|
93
|
+
- lib/dev_doc/test/lints/no_file_excludes.rb
|
|
59
94
|
- lib/rubocop-dev_doc.rb
|
|
95
|
+
- lib/rubocop/cop/dev_doc/auth/current_user_branching.rb
|
|
96
|
+
- lib/rubocop/cop/dev_doc/auth/load_resource_current_user_guard.rb
|
|
60
97
|
- lib/rubocop/cop/dev_doc/migration/amount_column_in_cents.rb
|
|
61
98
|
- lib/rubocop/cop/dev_doc/migration/avoid_bypassing_validation.rb
|
|
62
99
|
- lib/rubocop/cop/dev_doc/migration/avoid_column_default.rb
|
|
100
|
+
- lib/rubocop/cop/dev_doc/migration/avoid_conditional_schema_changes.rb
|
|
63
101
|
- lib/rubocop/cop/dev_doc/migration/avoid_json_column.rb
|
|
102
|
+
- lib/rubocop/cop/dev_doc/migration/avoid_non_null.rb
|
|
64
103
|
- lib/rubocop/cop/dev_doc/migration/avoid_vague_column_names.rb
|
|
65
104
|
- lib/rubocop/cop/dev_doc/migration/date_column_naming.rb
|
|
66
105
|
- lib/rubocop/cop/dev_doc/migration/no_create_join_table.rb
|
|
@@ -69,21 +108,36 @@ files:
|
|
|
69
108
|
- lib/rubocop/cop/dev_doc/migration/require_timestamps.rb
|
|
70
109
|
- lib/rubocop/cop/dev_doc/rails/application_record_transaction.rb
|
|
71
110
|
- lib/rubocop/cop/dev_doc/rails/avoid_rails_callbacks.rb
|
|
111
|
+
- lib/rubocop/cop/dev_doc/rails/bang_save_in_transaction.rb
|
|
112
|
+
- lib/rubocop/cop/dev_doc/rails/enum_column_not_null.rb
|
|
72
113
|
- lib/rubocop/cop/dev_doc/rails/enum_must_be_symbolized.rb
|
|
114
|
+
- lib/rubocop/cop/dev_doc/rails/no_block_predicate_on_relation.rb
|
|
73
115
|
- lib/rubocop/cop/dev_doc/rails/no_deliver_later_in_transaction.rb
|
|
74
116
|
- lib/rubocop/cop/dev_doc/rails/no_perform_later_in_model.rb
|
|
117
|
+
- lib/rubocop/cop/dev_doc/rails/strong_parameters_expect.rb
|
|
118
|
+
- lib/rubocop/cop/dev_doc/route/no_custom_actions.rb
|
|
119
|
+
- lib/rubocop/cop/dev_doc/route/resource_name_number.rb
|
|
75
120
|
- lib/rubocop/cop/dev_doc/route/resources_require_only.rb
|
|
76
121
|
- lib/rubocop/cop/dev_doc/style/avoid_head_response.rb
|
|
77
122
|
- lib/rubocop/cop/dev_doc/style/avoid_options_hash.rb
|
|
78
123
|
- lib/rubocop/cop/dev_doc/style/avoid_send.rb
|
|
124
|
+
- lib/rubocop/cop/dev_doc/style/minimize_variable_scope.rb
|
|
125
|
+
- lib/rubocop/cop/dev_doc/style/no_unscoped_method_definitions.rb
|
|
126
|
+
- lib/rubocop/cop/dev_doc/style/repeated_bracket_read.rb
|
|
127
|
+
- lib/rubocop/cop/dev_doc/style/repeated_safe_navigation_receiver.rb
|
|
79
128
|
- lib/rubocop/cop/dev_doc/style/string_symbol_comparison.rb
|
|
129
|
+
- lib/rubocop/cop/dev_doc/test/avoid_glib_travel_freeze.rb
|
|
130
|
+
- lib/rubocop/cop/dev_doc/test/avoid_unit_test.rb
|
|
131
|
+
- lib/rubocop/cop/dev_doc/test/response_assert_equal.rb
|
|
80
132
|
- lib/rubocop/dev_doc.rb
|
|
81
133
|
- lib/rubocop/dev_doc/plugin.rb
|
|
82
134
|
- lib/rubocop/dev_doc/version.rb
|
|
135
|
+
homepage:
|
|
83
136
|
licenses: []
|
|
84
137
|
metadata:
|
|
85
138
|
default_lint_roller_plugin: RuboCop::DevDoc::Plugin
|
|
86
139
|
rubygems_mfa_required: 'true'
|
|
140
|
+
post_install_message:
|
|
87
141
|
rdoc_options: []
|
|
88
142
|
require_paths:
|
|
89
143
|
- lib
|
|
@@ -98,7 +152,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
98
152
|
- !ruby/object:Gem::Version
|
|
99
153
|
version: '0'
|
|
100
154
|
requirements: []
|
|
101
|
-
rubygems_version: 4.
|
|
155
|
+
rubygems_version: 3.4.6
|
|
156
|
+
signing_key:
|
|
102
157
|
specification_version: 4
|
|
103
158
|
summary: RuboCop cops enforcing dev-doc best practices
|
|
104
159
|
test_files: []
|