sequra-style 1.13.0 → 1.15.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: 638d231a616190beeb2b468652fe080b7b1e7e5b40f71aec06009ec44d14b17c
4
- data.tar.gz: 27d7a897202882a5535cefb4af8de185132f7e05f2aa00be2735aff1df4848ed
3
+ metadata.gz: 7c280cada803065823f62008f14c7b9b1e6dab10dcf40536d7bf5785fd9fb5fc
4
+ data.tar.gz: 337cb5e29a589dee29075f2138a61d3c5e268b3c1e75e59d39fb53bdf94a100a
5
5
  SHA512:
6
- metadata.gz: ea38ecc3ed4116d5c854683a74c3ff670a540c70a0b5ecbd609a4207a02874c469cfabefca9ec45bdc5d15da56beb42ec1aef5d8279dfdacf8f70f8413ae2241
7
- data.tar.gz: cd260081df82a6e2fcb7b23d5338872f1571a4c9f380fd826b6647a57756def2e3996e7be3ea9ef84c32529d3411054687da1f56fac7e959fd322d51d1f968f7
6
+ metadata.gz: 4aa25b39900724c7b9d15211a330c55555e10998ca3b5f0229759179b3329fc5afcb8bc1e39953226de8e9281a9573c9d20ea37ab25a857439f74a5fad222af9
7
+ data.tar.gz: 9f547e557e2d3db25be396746468b13ef16fe2385bd64e02faf9b852c75a97bcec49d516e141e44289a0db3f01092fdd333f83d25bb618216d1d63394a9e7965
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.15.0](https://github.com/sequra/sequra-style/compare/v1.14.0...v1.15.0) (2026-05-18)
4
+
5
+
6
+ ### Features
7
+
8
+ * Expand Metrics/ParameterLists to not count keyword arguments ([#72](https://github.com/sequra/sequra-style/issues/72)) ([b948319](https://github.com/sequra/sequra-style/commit/b948319c93397e02fae97f7eeb2c5a58cdbcdee2))
9
+
10
+ ## [1.14.0](https://github.com/sequra/sequra-style/compare/v1.13.0...v1.14.0) (2026-05-14)
11
+
12
+
13
+ ### Features
14
+
15
+ * [COR-1992] Add Sequra/NoSidekiqPerformStubs cop ([#70](https://github.com/sequra/sequra-style/issues/70)) ([1a89ca4](https://github.com/sequra/sequra-style/commit/1a89ca4a9bcf2f12837f4d54d4aa5f4e7d54c899))
16
+
3
17
  ## [1.13.0](https://github.com/sequra/sequra-style/compare/v1.12.1...v1.13.0) (2026-04-24)
4
18
 
5
19
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sequra-style (1.13.0)
4
+ sequra-style (1.15.0)
5
5
  rubocop (~> 1.75)
6
6
  rubocop-performance (~> 1.25)
7
7
  rubocop-rails (~> 2.31)
data/default.yml CHANGED
@@ -385,6 +385,7 @@ Metrics/MethodLength:
385
385
  Metrics/ParameterLists:
386
386
  Max: 4
387
387
  Enabled: true
388
+ CountKeywordArgs: false
388
389
 
389
390
  # Avoid get_/set_ prefixes for accessor methods
390
391
  # https://rubystyle.guide/#accessor_mutator_method_names
@@ -832,3 +833,16 @@ Sequra/AsyncJobPattern:
832
833
  - "packs/*/app/workers/**/*.rb"
833
834
  Exclude:
834
835
  - "**/application_job.rb"
836
+
837
+ # Forbid stubbing/spying on Sidekiq enqueue methods in specs. Stubs bypass
838
+ # `Sidekiq.strict_args!` validation, which has caused silent production
839
+ # failures (COR-1923). Use `have_enqueued_sidekiq_job` or
840
+ # `change(Job.jobs, :size)` instead. `.and_call_original` is exempt.
841
+ # Ships disabled so consumer repos opt in on their own timeline (paired with
842
+ # `.rubocop_todo.yml` to grandfather existing offenses).
843
+ Sequra/NoSidekiqPerformStubs:
844
+ Enabled: false
845
+ Include:
846
+ - "**/*_spec.rb"
847
+ - "**/spec/support/**/*.rb"
848
+ - "**/spec/shared_examples/**/*.rb"
@@ -0,0 +1,77 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Sequra
4
+ # Detects RSpec stubs and spy assertions on Sidekiq enqueue methods
5
+ # (`perform_async`, `perform_at`, `perform_in`).
6
+ #
7
+ # Stubbing these methods bypasses `Sidekiq.strict_args!` validation,
8
+ # which lets symbol-keyed hashes (and other non-JSON-native types)
9
+ # slip through unchecked. The same code would raise `ArgumentError`
10
+ # in production at enqueue time, and the bug only surfaces when the
11
+ # job actually runs — by which point retries, timing windows, and
12
+ # rescue-swallowers can mask the failure. This pattern caused a
13
+ # silent push-notification drop in COR-1923.
14
+ #
15
+ # Use `have_enqueued_sidekiq_job` (or `change(SomeJob.jobs, :size)`)
16
+ # instead. Both go through Sidekiq's client middleware chain, so
17
+ # `strict_args!` validates the arguments as it would in production.
18
+ #
19
+ # `.and_call_original` is exempt: the stub spies on the call but
20
+ # then runs the real `perform_*`, which still exercises the
21
+ # serialization contract.
22
+ #
23
+ # @example
24
+ # # bad - stub bypasses strict_args! validation
25
+ # allow(SomeJob).to receive(:perform_async)
26
+ #
27
+ # # bad - even with arg matchers, the real enqueue is suppressed
28
+ # expect(SomeJob).to receive(:perform_at).with(time, data)
29
+ #
30
+ # # bad - negative assertion still bypasses the contract
31
+ # expect(SomeJob).not_to receive(:perform_async)
32
+ #
33
+ # # bad - spy verification on a stubbed enqueue
34
+ # expect(SomeJob).to have_received(:perform_async).with(id)
35
+ #
36
+ # # good - exercises strict_args! through Sidekiq's middleware
37
+ # expect(SomeJob).to have_enqueued_sidekiq_job(id, name: "value")
38
+ #
39
+ # # good - state-based assertion, also exercises the middleware
40
+ # expect { subject }.to change(SomeJob.jobs, :size).by(1)
41
+ #
42
+ # # good - escape hatch: spy AND run the real method
43
+ # expect(SomeJob).to receive(:perform_async).and_call_original
44
+ #
45
+ class NoSidekiqPerformStubs < Base
46
+ MSG = "Avoid stubbing Sidekiq enqueue methods (`perform_async`/`perform_at`/`perform_in`). " \
47
+ "Stubs bypass `Sidekiq.strict_args!` validation and can hide symbol-keyed hash bugs " \
48
+ "(see COR-1923). Use `have_enqueued_sidekiq_job` or `change(Job.jobs, :size)` instead. " \
49
+ "Use `.and_call_original` only as an escape hatch.".freeze
50
+
51
+ def_node_matcher :perform_method_stub?, <<~PATTERN
52
+ (send nil? {:receive :have_received} (sym {:perform_async :perform_at :perform_in}))
53
+ PATTERN
54
+
55
+ def on_send(node)
56
+ return unless perform_method_stub?(node)
57
+ return if chained_with_call_original?(node)
58
+
59
+ add_offense(node)
60
+ end
61
+
62
+ private
63
+
64
+ def chained_with_call_original?(node)
65
+ current = node
66
+ loop do
67
+ parent = current.parent
68
+ return false unless parent&.send_type? && parent.receiver == current
69
+ return true if parent.method_name == :and_call_original
70
+
71
+ current = parent
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -1,5 +1,5 @@
1
1
  module Sequra
2
2
  module Style
3
- VERSION = "1.13.0"
3
+ VERSION = "1.15.0"
4
4
  end
5
5
  end
data/lib/sequra_style.rb CHANGED
@@ -1,2 +1,3 @@
1
1
  require "rubocop"
2
2
  require_relative "rubocop/cop/sequra/async_job_pattern"
3
+ require_relative "rubocop/cop/sequra/no_sidekiq_perform_stubs"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequra-style
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.13.0
4
+ version: 1.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sequra engineering
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-24 00:00:00.000000000 Z
11
+ date: 2026-05-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop
@@ -135,6 +135,7 @@ files:
135
135
  - docs/decisions/index.md
136
136
  - docs/decisions/template.md
137
137
  - lib/rubocop/cop/sequra/async_job_pattern.rb
138
+ - lib/rubocop/cop/sequra/no_sidekiq_perform_stubs.rb
138
139
  - lib/sequra/style.rb
139
140
  - lib/sequra/style/version.rb
140
141
  - lib/sequra_style.rb