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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +235 -61
  3. data/lib/dev_doc/test/best_practice_lints.rb +31 -0
  4. data/lib/dev_doc/test/lints/cron_schedule.rb +345 -0
  5. data/lib/dev_doc/test/lints/duplicate_snapshot.rb +197 -0
  6. data/lib/dev_doc/test/lints/no_file_excludes.rb +128 -0
  7. data/lib/rubocop/cop/dev_doc/auth/current_user_branching.rb +203 -0
  8. data/lib/rubocop/cop/dev_doc/auth/load_resource_current_user_guard.rb +287 -0
  9. data/lib/rubocop/cop/dev_doc/migration/avoid_conditional_schema_changes.rb +89 -0
  10. data/lib/rubocop/cop/dev_doc/migration/avoid_non_null.rb +121 -0
  11. data/lib/rubocop/cop/dev_doc/rails/application_record_transaction.rb +1 -1
  12. data/lib/rubocop/cop/dev_doc/rails/bang_save_in_transaction.rb +127 -0
  13. data/lib/rubocop/cop/dev_doc/rails/enum_column_not_null.rb +99 -0
  14. data/lib/rubocop/cop/dev_doc/rails/enum_must_be_symbolized.rb +2 -2
  15. data/lib/rubocop/cop/dev_doc/rails/no_block_predicate_on_relation.rb +236 -0
  16. data/lib/rubocop/cop/dev_doc/rails/strong_parameters_expect.rb +137 -0
  17. data/lib/rubocop/cop/dev_doc/route/no_custom_actions.rb +171 -0
  18. data/lib/rubocop/cop/dev_doc/route/resource_name_number.rb +77 -0
  19. data/lib/rubocop/cop/dev_doc/style/avoid_send.rb +31 -4
  20. data/lib/rubocop/cop/dev_doc/style/minimize_variable_scope.rb +158 -0
  21. data/lib/rubocop/cop/dev_doc/style/no_unscoped_method_definitions.rb +129 -0
  22. data/lib/rubocop/cop/dev_doc/style/repeated_bracket_read.rb +150 -0
  23. data/lib/rubocop/cop/dev_doc/style/repeated_safe_navigation_receiver.rb +118 -0
  24. data/lib/rubocop/cop/dev_doc/test/avoid_glib_travel_freeze.rb +53 -0
  25. data/lib/rubocop/cop/dev_doc/test/avoid_unit_test.rb +66 -0
  26. data/lib/rubocop/cop/dev_doc/test/response_assert_equal.rb +179 -0
  27. data/lib/rubocop/dev_doc/version.rb +1 -1
  28. 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
@@ -1,5 +1,5 @@
1
1
  module RuboCop
2
2
  module DevDoc
3
- VERSION = "0.1.0".freeze
3
+ VERSION = "0.3.1".freeze
4
4
  end
5
5
  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.2.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: 1980-01-02 00:00:00.000000000 Z
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.0.6
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: []