carwow_rubocop 6.6.0 → 6.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e153a088c0b69fa290062f7af76614067043033ccc9dc5cad5e96253b5c85bf
4
- data.tar.gz: 02bb3cb5fef08391e34a518a6722b1fc23672784be9f62a32b54bea54943c3ea
3
+ metadata.gz: 9ba532ee22497e6dad682fcd4d4c8f452faf929df1ce5284e3fb5b2a1d3e5ab8
4
+ data.tar.gz: b54f7cc75cf53f47012ffde9f5253c06c33cc959fe6f38c7aed526aff30f990c
5
5
  SHA512:
6
- metadata.gz: 9a991a2a692417cd5833058df9de060471684ecd0a59e2700438a1c2655bbc624884ae88d200174ec4bdf8780164ec2755e28b508fe73c981abe5986824749f4
7
- data.tar.gz: 9c0c9b783c7288a0ed8f6fc0e7dd70b49116ff96bca7007e38874a77e3e1667f7e587033daf2c595d550646b6c75ca0ea95592b7bf31cb213fc4e3823362c506
6
+ metadata.gz: 7f6d34e7a2768754b1b6171aa09dba55b2a3eff404bc93d6039794630f9440c04304615de5ade41d82e58dcefa8f6ff98396053b68a3dee5da6e104746f49084
7
+ data.tar.gz: 47a4d2cfd627a6fda855df105eb4e6ccc8b246e602badaf823f253c9ddf3952aa6deca294c90c85e9dd1dc3eb1284846291880663dbce6bead17ffb187fa518d
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- carwow_rubocop (6.6.0)
4
+ carwow_rubocop (6.8.0)
5
5
  rubocop (>= 1.75)
6
6
  rubocop-factory_bot
7
7
  rubocop-performance
@@ -39,3 +39,17 @@ Carwow/AddColumnWithComment:
39
39
 
40
40
  Carwow/NoVehicleBrandModelSideload:
41
41
  Enabled: true
42
+
43
+ Carwow/NoChangeMatcherWithBrowserAction:
44
+ Enabled: true
45
+ Include:
46
+ - "**/*_spec.rb"
47
+ - "**/spec/**/*"
48
+
49
+ Carwow/ProductAreaRequired:
50
+ Enabled: true
51
+ Include:
52
+ - "app/controllers/**/*.rb"
53
+ - "app/jobs/**/*.rb"
54
+ Exclude:
55
+ - "**/concerns/**/*.rb"
@@ -1,5 +1,5 @@
1
1
  module RuboCop
2
2
  module Carwow
3
- VERSION = '6.6.0'.freeze
3
+ VERSION = '6.8.0'.freeze
4
4
  end
5
5
  end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Carwow
6
+ # Detects the use of `expect { browser_action }.to change { db_access }`
7
+ # in feature/system specs. This pattern is racy because `change{}` takes
8
+ # DB snapshots before and after the block with no guarantee the browser
9
+ # has finished processing the action.
10
+ #
11
+ # @example Bad
12
+ # expect { submit }.not_to(change { listing.reload.state })
13
+ # expect { click_button('Save') }.to change { User.count }.by(1)
14
+ #
15
+ # @example Good
16
+ # submit
17
+ # expect(page).to have_content('Success')
18
+ # expect(listing.reload.state).to eq('complete')
19
+ #
20
+ # click_button('Save')
21
+ # expect(page).to have_current_path(success_path)
22
+ # expect(User.count).to eq(2)
23
+ #
24
+ class NoChangeMatcherWithBrowserAction < Base
25
+ MSG = 'Avoid wrapping Capybara browser actions in `expect { }.to change { }`. ' \
26
+ 'The DB snapshot races the async browser. Instead: call the action, ' \
27
+ 'wait with `expect(page).to have_*`, then assert the DB state.'
28
+
29
+ CAPYBARA_ACTIONS = %i[
30
+ visit
31
+ click_button click_link click_on click
32
+ fill_in choose check uncheck select attach_file
33
+ find find_field find_button find_by_id find_link
34
+ within within_frame within_window
35
+ execute_script evaluate_script
36
+ scroll_to scroll_by
37
+ hover drag_to drop
38
+ dismiss_confirm accept_confirm dismiss_prompt accept_prompt
39
+ submit
40
+ ].to_set.freeze
41
+
42
+ # AST shape for `expect { ... }.to change { ... }`
43
+ # (possibly chained with .by / .by_at_least etc., which wraps the whole thing)
44
+ #
45
+ # The `.to`/`.not_to` send node:
46
+ # (send
47
+ # (block (send nil :expect) (args) <body>) <- expect { body }
48
+ # {:to :not_to :to_not}
49
+ # {
50
+ # (block (send nil :change) (args) _) <- change { ... }
51
+ # (send (block (send nil :change) ...) _) <- change { }.by(n)
52
+ # }
53
+ # )
54
+ #
55
+ # We also handle the parenthesised form: .not_to(change { ... })
56
+ # which produces identical AST.
57
+ def_node_matcher :expect_to_change?, <<~PATTERN
58
+ (send
59
+ (block
60
+ (send nil? :expect)
61
+ (args)
62
+ ...)
63
+ {:to :not_to :to_not}
64
+ {
65
+ (block (send nil? :change) ...)
66
+ (send (block (send nil? :change) ...) ...)
67
+ }
68
+ )
69
+ PATTERN
70
+
71
+ def on_send(node)
72
+ return unless expect_to_change?(node)
73
+
74
+ expect_block = node.receiver
75
+ return unless block_contains_capybara_action?(expect_block)
76
+
77
+ add_offense(node)
78
+ end
79
+
80
+ private
81
+
82
+ def block_contains_capybara_action?(block_node)
83
+ block_node.each_descendant(:send).any? do |send_node|
84
+ CAPYBARA_ACTIONS.include?(send_node.method_name)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Carwow
8
+ # Controllers and jobs are attributed to a team via the `product_area` DSL
9
+ # (CarwowCore::ProductArea::Taggable) so their Honeycomb spans and Bugsnag
10
+ # errors route to the owning team automatically. A class with no declared
11
+ # area falls back to manual triage.
12
+ #
13
+ # This only checks that `product_area` is called somewhere in the class's
14
+ # own body — it cannot verify, statically, that a superclass declared in
15
+ # another file already sets one. Shared base classes that are
16
+ # intentionally left untagged (e.g. ApplicationController, ApplicationJob,
17
+ # or a namespaced API base controller) should be added to this cop's
18
+ # `Exclude` in the consuming app's .rubocop.yml rather than tagged.
19
+ #
20
+ # A class nested inside another class (rather than merely inside a
21
+ # namespacing module) is assumed to be a helper — e.g. a custom error
22
+ # raised by the enclosing job — not an entry point of its own, so it is
23
+ # not checked. Only the enclosing class needs the tag.
24
+ #
25
+ # A class that subclasses a known Ruby exception (StandardError,
26
+ # RuntimeError, ...) or whose superclass name ends in `Error`/`Exception`
27
+ # is never checked, wherever it's defined: exceptions are raised inside
28
+ # an entry point, not routed to one of their own.
29
+ #
30
+ # A class that doesn't look like a controller/job entry point itself (no
31
+ # Controller/Job-ish superclass, no `include Sidekiq::Worker`/`Sidekiq::Job`,
32
+ # no `perform` method) is skipped if it sits alongside a sibling class in
33
+ # the same file/module that already declares `product_area` — a common
34
+ # pattern for small POROs colocated with the controller/job that uses
35
+ # them. Two genuinely independent entry points defined in the same file
36
+ # still both need their own tag.
37
+ #
38
+ # @example
39
+ # # bad
40
+ # class PaymentsController < ApplicationController
41
+ # end
42
+ #
43
+ # # good
44
+ # class PaymentsController < ApplicationController
45
+ # product_area 'smc-payments'
46
+ # end
47
+ #
48
+ # # good - nested error class does not need its own tag
49
+ # class SyncListing < ApplicationJob
50
+ # product_area 'salesforce'
51
+ #
52
+ # class UnknownReference < StandardError; end
53
+ # end
54
+ #
55
+ # # good - sibling error class, and sibling PORO helper, do not need their own tag
56
+ # module Payments
57
+ # class RefundFailed < StandardError; end
58
+ #
59
+ # class RefundsController < ApplicationController
60
+ # product_area 'smc-payments'
61
+ # end
62
+ #
63
+ # class RefundPresenter
64
+ # def initialize(refund); end
65
+ # end
66
+ # end
67
+ #
68
+ class ProductAreaRequired < ::RuboCop::Cop::Base
69
+ MSG = 'Declare a `product_area` for this class so its Honeycomb spans ' \
70
+ 'and Bugsnag errors route to the owning team.'
71
+
72
+ KNOWN_ERROR_SUPERCLASSES = %w[StandardError RuntimeError Exception ScriptError].freeze
73
+ ERROR_NAME_PATTERN = /Error\z|Exception\z/
74
+ ENTRY_POINT_SUPERCLASS_PATTERN = /Controller|Job/
75
+
76
+ def_node_matcher :product_area_declaration?, <<~PATTERN
77
+ (send nil? :product_area _)
78
+ PATTERN
79
+
80
+ def_node_matcher :sidekiq_worker_include?, <<~PATTERN
81
+ (send nil? :include (const (const nil? :Sidekiq) {:Worker :Job}))
82
+ PATTERN
83
+
84
+ def_node_search :defines_perform_method?, <<~PATTERN
85
+ (def :perform ...)
86
+ PATTERN
87
+
88
+ def on_class(node)
89
+ return if nested_in_class?(node)
90
+ return if subclasses_known_error?(node)
91
+ return if declares_product_area?(node)
92
+ return if !looks_like_entry_point?(node) && sibling_already_tagged?(node)
93
+
94
+ class_node, = *node
95
+ add_offense(class_node)
96
+ end
97
+
98
+ private
99
+
100
+ def nested_in_class?(node)
101
+ node.each_ancestor(:class).any?
102
+ end
103
+
104
+ def subclasses_known_error?(class_node)
105
+ _name, superclass, = *class_node
106
+ return false unless superclass
107
+
108
+ superclass_name = superclass.source
109
+
110
+ KNOWN_ERROR_SUPERCLASSES.include?(superclass_name) || superclass_name.match?(ERROR_NAME_PATTERN)
111
+ end
112
+
113
+ def declares_product_area?(class_node)
114
+ top_level_statements(class_node).any? { |stmt| product_area_declaration?(stmt) }
115
+ end
116
+
117
+ def looks_like_entry_point?(class_node)
118
+ _name, superclass, body = *class_node
119
+
120
+ return true if superclass&.source&.match?(ENTRY_POINT_SUPERCLASS_PATTERN)
121
+
122
+ statements = top_level_statements(class_node)
123
+ statements.any? { |stmt| sidekiq_worker_include?(stmt) } || (body && defines_perform_method?(body))
124
+ end
125
+
126
+ def sibling_already_tagged?(class_node)
127
+ siblings_scope = class_node.each_ancestor(:module, :class, :begin).first || class_node.parent
128
+ return false unless siblings_scope
129
+
130
+ sibling_classes(siblings_scope, class_node).any? { |sibling| declares_product_area?(sibling) }
131
+ end
132
+
133
+ def sibling_classes(scope, exclude)
134
+ statements = scope.begin_type? ? scope.children : [scope]
135
+ statements.select { |stmt| stmt&.class_type? && stmt != exclude }
136
+ end
137
+
138
+ def top_level_statements(class_node)
139
+ body = class_node.body
140
+ return [] unless body
141
+
142
+ body.begin_type? ? body.children : [body]
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: carwow_rubocop
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.6.0
4
+ version: 6.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - carwow Developers
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-08-12 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: bundler
@@ -168,13 +169,16 @@ files:
168
169
  - lib/rubocop/cop/carwow/jobs.rb
169
170
  - lib/rubocop/cop/carwow/jobs_must_define_queue.rb
170
171
  - lib/rubocop/cop/carwow/jobs_queue_name_style.rb
172
+ - lib/rubocop/cop/carwow/no_change_matcher_with_browser_action.rb
171
173
  - lib/rubocop/cop/carwow/no_stubbing_business_event.rb
172
174
  - lib/rubocop/cop/carwow/no_vehicle_brand_model_sideload.rb
175
+ - lib/rubocop/cop/carwow/product_area_required.rb
173
176
  homepage: https://github.com/carwow/carwow_rubocop
174
177
  licenses:
175
178
  - MIT
176
179
  metadata:
177
180
  rubygems_mfa_required: 'true'
181
+ post_install_message:
178
182
  rdoc_options: []
179
183
  require_paths:
180
184
  - lib
@@ -189,7 +193,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
189
193
  - !ruby/object:Gem::Version
190
194
  version: '0'
191
195
  requirements: []
192
- rubygems_version: 4.0.9
196
+ rubygems_version: 3.5.20
197
+ signing_key:
193
198
  specification_version: 4
194
199
  summary: carwow's rubocop configuration
195
200
  test_files: []