rubocop-dev_doc 0.1.0 → 0.3.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +318 -33
  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 +230 -0
  9. data/lib/rubocop/cop/dev_doc/migration/amount_column_in_cents.rb +92 -0
  10. data/lib/rubocop/cop/dev_doc/migration/avoid_bypassing_validation.rb +86 -0
  11. data/lib/rubocop/cop/dev_doc/migration/avoid_column_default.rb +68 -13
  12. data/lib/rubocop/cop/dev_doc/migration/avoid_conditional_schema_changes.rb +89 -0
  13. data/lib/rubocop/cop/dev_doc/migration/avoid_json_column.rb +18 -3
  14. data/lib/rubocop/cop/dev_doc/migration/avoid_non_null.rb +121 -0
  15. data/lib/rubocop/cop/dev_doc/migration/no_create_join_table.rb +53 -0
  16. data/lib/rubocop/cop/dev_doc/migration/require_primary_key.rb +55 -0
  17. data/lib/rubocop/cop/dev_doc/migration/require_timestamps.rb +4 -13
  18. data/lib/rubocop/cop/dev_doc/rails/application_record_transaction.rb +56 -0
  19. data/lib/rubocop/cop/dev_doc/rails/avoid_rails_callbacks.rb +135 -0
  20. data/lib/rubocop/cop/dev_doc/rails/bang_save_in_transaction.rb +127 -0
  21. data/lib/rubocop/cop/dev_doc/rails/enum_column_not_null.rb +99 -0
  22. data/lib/rubocop/cop/dev_doc/rails/enum_must_be_symbolized.rb +83 -0
  23. data/lib/rubocop/cop/dev_doc/rails/no_block_predicate_on_relation.rb +236 -0
  24. data/lib/rubocop/cop/dev_doc/rails/no_deliver_later_in_transaction.rb +22 -5
  25. data/lib/rubocop/cop/dev_doc/rails/strong_parameters_expect.rb +137 -0
  26. data/lib/rubocop/cop/dev_doc/route/no_custom_actions.rb +171 -0
  27. data/lib/rubocop/cop/dev_doc/route/resource_name_number.rb +77 -0
  28. data/lib/rubocop/cop/dev_doc/route/resources_require_only.rb +29 -15
  29. data/lib/rubocop/cop/dev_doc/style/avoid_head_response.rb +56 -22
  30. data/lib/rubocop/cop/dev_doc/style/avoid_options_hash.rb +102 -0
  31. data/lib/rubocop/cop/dev_doc/style/avoid_send.rb +42 -10
  32. data/lib/rubocop/cop/dev_doc/style/minimize_variable_scope.rb +158 -0
  33. data/lib/rubocop/cop/dev_doc/style/no_unscoped_method_definitions.rb +129 -0
  34. data/lib/rubocop/cop/dev_doc/style/repeated_bracket_read.rb +150 -0
  35. data/lib/rubocop/cop/dev_doc/style/repeated_safe_navigation_receiver.rb +118 -0
  36. data/lib/rubocop/cop/dev_doc/style/string_symbol_comparison.rb +91 -0
  37. data/lib/rubocop/cop/dev_doc/test/avoid_glib_travel_freeze.rb +53 -0
  38. data/lib/rubocop/cop/dev_doc/test/avoid_unit_test.rb +66 -0
  39. data/lib/rubocop/cop/dev_doc/test/response_assert_equal.rb +179 -0
  40. data/lib/rubocop/dev_doc/version.rb +1 -1
  41. data/lib/rubocop-dev_doc.rb +1 -0
  42. metadata +73 -10
  43. data/lib/rubocop/cop/dev_doc/migration/avoid_update_column.rb +0 -53
@@ -0,0 +1,118 @@
1
+ module RuboCop
2
+ module Cop
3
+ module DevDoc
4
+ module Style
5
+ # Avoid using `&.` on the same receiver more than once in a method body.
6
+ #
7
+ # ## Rationale
8
+ # When a receiver appears with `&.` repeatedly across a method, the
9
+ # reader has to mentally re-evaluate the nil case at every call, and
10
+ # the *intent* of each `&.` becomes ambiguous — is this call nullable
11
+ # for a fresh reason, or is the dev just mirroring the earlier `&.`
12
+ # out of habit? Repetition also propagates nil silently into
13
+ # downstream comparisons (`current_user&.id == something` evaluates
14
+ # to `nil == something`, which is false but not for the reason the
15
+ # reader expects).
16
+ #
17
+ # Resolve the nil case once at the top of the method, then use the
18
+ # local with plain `.`:
19
+ #
20
+ # ❌
21
+ # def show?
22
+ # current_user&.super_admin? ||
23
+ # current_user&.developer? ||
24
+ # current_user&.admin_of_organization?(organization)
25
+ # end
26
+ #
27
+ # ✔️ Guard once, then plain calls
28
+ # def show?
29
+ # return false unless current_user
30
+ #
31
+ # current_user.super_admin? ||
32
+ # current_user.developer? ||
33
+ # current_user.admin_of_organization?(organization)
34
+ # end
35
+ #
36
+ # ✔️ Alternative — assign-and-test in the condition
37
+ # def label
38
+ # if (user = current_user)
39
+ # "#{user.full_name} <#{user.email}>"
40
+ # end
41
+ # end
42
+ #
43
+ # The cop is policy-safe by design: it does not assume anything
44
+ # about `current_user` non-nullability or branching semantics — it
45
+ # only flags repeated `&.` on the same receiver, which is a smell
46
+ # regardless of whether the receiver is `current_user`, a model
47
+ # attribute, or a local variable.
48
+ #
49
+ # ## Exception
50
+ # Legitimate cases (e.g. when the repeated calls are conceptually
51
+ # distinct sources that happen to share source spelling) go through
52
+ # inline `# rubocop:disable` with a reason.
53
+ #
54
+ # NOTE: Receivers are compared by source text — two `&.` calls
55
+ # share a receiver if their source spelling matches verbatim. The
56
+ # cop does not perform flow analysis, so a receiver reassigned
57
+ # between uses is not detected (false negative).
58
+ #
59
+ # NOTE: Scope is the enclosing `def`/`defs` body. The cop does not
60
+ # partition by inner blocks, so the same parameter name reused
61
+ # across separate block bodies in one method may produce a false
62
+ # positive — inline-disable with a reason if it surfaces.
63
+ #
64
+ # @example
65
+ # # bad — same receiver, multiple safe-nav reads
66
+ # def card
67
+ # name = current_user&.full_name
68
+ # email = current_user&.email
69
+ # end
70
+ #
71
+ # # bad — same receiver, multiple safe-nav predicates
72
+ # def show?
73
+ # user&.super_admin? || user&.developer?
74
+ # end
75
+ #
76
+ # # good — assign once, then plain calls
77
+ # def show?
78
+ # return false unless user
79
+ #
80
+ # user.super_admin? || user.developer?
81
+ # end
82
+ #
83
+ # # good — single use of safe-nav
84
+ # def label
85
+ # current_user&.full_name
86
+ # end
87
+ class RepeatedSafeNavigationReceiver < Base
88
+ MSG = "Receiver `%<receiver>s` already used with `&.` earlier in this method — assign once and use `.` after.".freeze
89
+
90
+ def on_def(node)
91
+ check_method(node)
92
+ end
93
+ alias on_defs on_def
94
+
95
+ private
96
+
97
+ def check_method(def_node)
98
+ body = def_node.body
99
+ return unless body
100
+
101
+ seen = {}
102
+ body.each_descendant(:csend) do |csend|
103
+ receiver = csend.receiver
104
+ next unless receiver
105
+
106
+ key = receiver.source
107
+ if seen[key]
108
+ add_offense(csend.loc.dot, message: format(MSG, receiver: key))
109
+ else
110
+ seen[key] = true
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,91 @@
1
+ module RuboCop
2
+ module Cop
3
+ module DevDoc
4
+ module Style
5
+ # Flag direct `==` / `!=` comparisons between a known-string source
6
+ # and a symbol literal. Such comparisons are silently always false
7
+ # (`'draft' == :draft` is `false` in Ruby), so they signal that the
8
+ # caller forgot to convert at the boundary.
9
+ #
10
+ # ## Rationale
11
+ # Strings and symbols are not equal in Ruby, and string-typed values
12
+ # arrive from boundaries the developer doesn't control: HTTP params,
13
+ # request headers, ENV. Comparing one of these directly against a
14
+ # symbol literal is a guaranteed bug. Convert at the boundary instead
15
+ # — see `best_practices/backend/en/01a_defensive_programming.md` item 7.
16
+ #
17
+ # ❌
18
+ # if params[:status] == :draft # always false
19
+ #
20
+ # ✔ Convert at the boundary, then compare
21
+ # @filter_status = params[:status]&.to_sym
22
+ # if @filter_status == :draft
23
+ #
24
+ # ## Sources detected
25
+ # - `params[…]`
26
+ # - `request.headers[…]`
27
+ # - `ENV[…]`
28
+ #
29
+ # ## Limitations
30
+ # The cop only catches the *direct* comparison form. Once the value
31
+ # flows into a local or instance variable, type information is lost
32
+ # and Rubocop cannot trace it back to the original string source.
33
+ # Those cases are caught by review.
34
+ #
35
+ # @example
36
+ # # bad
37
+ # params[:status] == :draft
38
+ # :production == ENV['MODE']
39
+ # request.headers['X-Mode'] != :debug
40
+ #
41
+ # # good
42
+ # params[:status]&.to_sym == :draft
43
+ # ENV['MODE'] == 'production'
44
+ class StringSymbolComparison < Base
45
+ MSG = 'Comparing string-typed `%<source>s` to a symbol literal is always false. ' \
46
+ 'Convert at the boundary with `&.to_sym` or compare against a string instead ' \
47
+ '(see backend/01a_defensive_programming.md item 7).'.freeze
48
+
49
+ RESTRICT_ON_SEND = %i[== !=].freeze
50
+
51
+ def_node_matcher :params_access?, <<~PATTERN
52
+ (send (send nil? :params) :[] _)
53
+ PATTERN
54
+
55
+ def_node_matcher :request_headers_access?, <<~PATTERN
56
+ (send (send _ :headers) :[] _)
57
+ PATTERN
58
+
59
+ def_node_matcher :env_access?, <<~PATTERN
60
+ (send (const nil? :ENV) :[] _)
61
+ PATTERN
62
+
63
+ def on_send(node)
64
+ lhs = node.receiver
65
+ rhs = node.first_argument
66
+ return unless lhs && rhs
67
+
68
+ source_node = pick_string_source(lhs, rhs)
69
+ return unless source_node
70
+
71
+ add_offense(node, message: format(MSG, source: source_node.source))
72
+ end
73
+
74
+ private
75
+
76
+ def pick_string_source(left, right)
77
+ if string_source?(left) && right.sym_type?
78
+ left
79
+ elsif string_source?(right) && left.sym_type?
80
+ right
81
+ end
82
+ end
83
+
84
+ def string_source?(node)
85
+ params_access?(node) || request_headers_access?(node) || env_access?(node)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,53 @@
1
+ module RuboCop
2
+ module Cop
3
+ module DevDoc
4
+ module Test
5
+ # Flag `glib_travel_freeze` calls in test files — use `glib_travel` instead.
6
+ #
7
+ # ## Rationale
8
+ # `glib_travel_freeze` stops the clock completely, which is not representative
9
+ # of real-world behavior: in production, time ticks as code executes.
10
+ # We have had bugs that went undetected in tests because `glib_travel_freeze`
11
+ # masked timing issues, only to surface in production.
12
+ #
13
+ # `glib_travel` advances time normally inside the block — use it instead.
14
+ # Reserve `glib_travel_freeze` only as a last resort when `glib_travel`
15
+ # truly cannot work for a specific test, and document why.
16
+ #
17
+ # ## Escape hatch
18
+ # Disable per-line with a comment explaining why `glib_travel` doesn't work:
19
+ #
20
+ # glib_travel_freeze(time) do # rubocop:disable DevDoc/Test/AvoidGlibTravelFreeze
21
+ # # Reason: <explanation>
22
+ # end
23
+ #
24
+ # @example
25
+ # # bad
26
+ # glib_travel_freeze(Time.current) do
27
+ # expect(order.expired?).to be true
28
+ # end
29
+ #
30
+ # # bad (non-block form)
31
+ # glib_travel_freeze(Time.current)
32
+ # expect(order.expired?).to be true
33
+ #
34
+ # # good
35
+ # glib_travel(Time.current) do
36
+ # expect(order.expired?).to be true
37
+ # end
38
+ class AvoidGlibTravelFreeze < Base
39
+ MSG = 'Avoid `glib_travel_freeze` — use `glib_travel` instead. ' \
40
+ 'Frozen time masks timing bugs that surface in production. ' \
41
+ 'Use `# rubocop:disable DevDoc/Test/AvoidGlibTravelFreeze` ' \
42
+ 'with a comment explaining why `glib_travel` cannot work for this test.'
43
+
44
+ RESTRICT_ON_SEND = %i[glib_travel_freeze].freeze
45
+
46
+ def on_send(node)
47
+ add_offense(node.loc.selector)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,66 @@
1
+ module RuboCop
2
+ module Cop
3
+ module DevDoc
4
+ module Test
5
+ # Prefer controller tests; flag unit/service tests (`< ActiveSupport::TestCase`).
6
+ #
7
+ # ## Rationale
8
+ # What the user sees and experiences is what matters; internal
9
+ # implementation does not. A controller test exercises behaviour
10
+ # end-to-end through the same path a user takes, so it catches the
11
+ # regressions that actually reach production. A unit/service test is only
12
+ # needed when a code path genuinely cannot be reached through a
13
+ # controller test (very rare) — e.g. a search-ranking detail the
14
+ # controller never exposes.
15
+ #
16
+ # This cop flags only the literal `< ActiveSupport::TestCase`
17
+ # superclass. The blessed blackbox bases —
18
+ # `ActionDispatch::IntegrationTest`, `Glib::IntegrationTest`,
19
+ # `ActionMailer::TestCase`, `ActiveJob::TestCase` — are NOT flagged, even
20
+ # though they inherit from `ActiveSupport::TestCase` transitively.
21
+ #
22
+ # ## Escape hatch
23
+ # When a unit test is genuinely necessary, suppress with a reason that
24
+ # explains why a controller test can't cover the path. That reason IS the
25
+ # required justification — keep it specific and reviewable:
26
+ #
27
+ # # rubocop:disable DevDoc/Test/AvoidUnitTest -- search ranking isn't visible through the controller
28
+ # class Ai::Retrieval::PgSearchStrategyTest < ActiveSupport::TestCase
29
+ # # ...
30
+ # end
31
+ # # rubocop:enable DevDoc/Test/AvoidUnitTest
32
+ #
33
+ # NOTE: The cop matches the *direct* superclass only. A project base
34
+ # (`class ApplicationServiceTest < ActiveSupport::TestCase`) is flagged
35
+ # once (justify it there); subclasses of that base are not re-flagged.
36
+ #
37
+ # @example
38
+ # # bad
39
+ # class NoteRenderingTest < ActiveSupport::TestCase
40
+ # end
41
+ #
42
+ # # good — exercised through the controller
43
+ # class NotesControllerTest < ActionDispatch::IntegrationTest
44
+ # end
45
+ #
46
+ # # good — genuinely unit-only, justified with a reason
47
+ # # rubocop:disable DevDoc/Test/AvoidUnitTest -- <why a controller test can't cover this>
48
+ # class SomeServiceTest < ActiveSupport::TestCase
49
+ # end
50
+ # # rubocop:enable DevDoc/Test/AvoidUnitTest
51
+ class AvoidUnitTest < Base
52
+ MSG = 'Prefer a controller test — unit tests are a rare exception. If a controller test ' \
53
+ 'genuinely cannot cover this path, disable this cop on the class with a reason.'.freeze
54
+
55
+ def on_class(node)
56
+ superclass = node.parent_class
57
+ return unless superclass&.const_type?
58
+ return unless superclass.const_name == 'ActiveSupport::TestCase'
59
+
60
+ add_offense(superclass)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -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.0".freeze
4
4
  end
5
5
  end
@@ -1,3 +1,4 @@
1
1
  require "rubocop"
2
+ require "rubocop-rails"
2
3
  require "lint_roller"
3
4
  require_relative "rubocop/dev_doc"
metadata CHANGED
@@ -1,42 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-dev_doc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
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:
12
13
  - !ruby/object:Gem::Dependency
13
- name: rubocop
14
+ name: activesupport
14
15
  requirement: !ruby/object:Gem::Requirement
15
16
  requirements:
16
17
  - - ">="
17
18
  - !ruby/object:Gem::Version
18
- version: '1.72'
19
+ version: '4.2'
19
20
  type: :runtime
20
21
  prerelease: false
21
22
  version_requirements: !ruby/object:Gem::Requirement
22
23
  requirements:
23
24
  - - ">="
24
25
  - !ruby/object:Gem::Version
25
- version: '1.72'
26
+ version: '4.2'
26
27
  - !ruby/object:Gem::Dependency
27
- name: rubocop-rails
28
+ name: fugit
28
29
  requirement: !ruby/object:Gem::Requirement
29
30
  requirements:
30
31
  - - ">="
31
32
  - !ruby/object:Gem::Version
32
- version: '2.0'
33
+ version: '1.9'
33
34
  type: :runtime
34
35
  prerelease: false
35
36
  version_requirements: !ruby/object:Gem::Requirement
36
37
  requirements:
37
38
  - - ">="
38
39
  - !ruby/object:Gem::Version
39
- version: '2.0'
40
+ version: '1.9'
40
41
  - !ruby/object:Gem::Dependency
41
42
  name: lint_roller
42
43
  requirement: !ruby/object:Gem::Requirement
@@ -51,31 +52,92 @@ dependencies:
51
52
  - - ">="
52
53
  - !ruby/object:Gem::Version
53
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '1.72'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '1.72'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop-rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '2.0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
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
97
+ - lib/rubocop/cop/dev_doc/migration/amount_column_in_cents.rb
98
+ - lib/rubocop/cop/dev_doc/migration/avoid_bypassing_validation.rb
60
99
  - lib/rubocop/cop/dev_doc/migration/avoid_column_default.rb
100
+ - lib/rubocop/cop/dev_doc/migration/avoid_conditional_schema_changes.rb
61
101
  - lib/rubocop/cop/dev_doc/migration/avoid_json_column.rb
62
- - lib/rubocop/cop/dev_doc/migration/avoid_update_column.rb
102
+ - lib/rubocop/cop/dev_doc/migration/avoid_non_null.rb
63
103
  - lib/rubocop/cop/dev_doc/migration/avoid_vague_column_names.rb
64
104
  - lib/rubocop/cop/dev_doc/migration/date_column_naming.rb
105
+ - lib/rubocop/cop/dev_doc/migration/no_create_join_table.rb
65
106
  - lib/rubocop/cop/dev_doc/migration/prefer_belongs_to.rb
107
+ - lib/rubocop/cop/dev_doc/migration/require_primary_key.rb
66
108
  - lib/rubocop/cop/dev_doc/migration/require_timestamps.rb
109
+ - lib/rubocop/cop/dev_doc/rails/application_record_transaction.rb
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
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
67
115
  - lib/rubocop/cop/dev_doc/rails/no_deliver_later_in_transaction.rb
68
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
69
120
  - lib/rubocop/cop/dev_doc/route/resources_require_only.rb
70
121
  - lib/rubocop/cop/dev_doc/style/avoid_head_response.rb
122
+ - lib/rubocop/cop/dev_doc/style/avoid_options_hash.rb
71
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
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
72
132
  - lib/rubocop/dev_doc.rb
73
133
  - lib/rubocop/dev_doc/plugin.rb
74
134
  - lib/rubocop/dev_doc/version.rb
135
+ homepage:
75
136
  licenses: []
76
137
  metadata:
77
138
  default_lint_roller_plugin: RuboCop::DevDoc::Plugin
78
139
  rubygems_mfa_required: 'true'
140
+ post_install_message:
79
141
  rdoc_options: []
80
142
  require_paths:
81
143
  - lib
@@ -90,7 +152,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
152
  - !ruby/object:Gem::Version
91
153
  version: '0'
92
154
  requirements: []
93
- rubygems_version: 4.0.6
155
+ rubygems_version: 3.4.6
156
+ signing_key:
94
157
  specification_version: 4
95
158
  summary: RuboCop cops enforcing dev-doc best practices
96
159
  test_files: []