sequra-style 1.9.0 → 1.10.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: d4fbdb8adc90c530fbc5f62fb0619e920bda0e85fd44176eca0f9e9162dd0bc9
4
- data.tar.gz: ff36093e496d8d23d922f0c612cff9fd5b6c488aba54f36fdd1e7be7c2b670cc
3
+ metadata.gz: ac36e918663e99576328aa7740655149cb2a44bdf26909a53d43eb1ff2a8d3aa
4
+ data.tar.gz: 2e490af77771f9bdcba719b988182c527d121dcc1a91037432f1dded77741836
5
5
  SHA512:
6
- metadata.gz: 68ff7381c13b8d95b8ddbe2d22d1f8cb17e86fdb48041f67b6f985be42ad571ce9a3cfcb5f346a56734420086916dedb60b89cf790ae515fed5314dd6480906e
7
- data.tar.gz: bf4881096eecf81ec3d4f3929b3d2f11168bbdc9767db1ab214deb5722b4b197b11485b9f9497d0e02c026a9f05b57dbedf6766e0d13cceeb5d952be1b4b9915
6
+ metadata.gz: 00117fdcaf8ba15aa5b69f6cc078705982dec612445f1bbee9ad314fc52bc5802c67623b5b4aae8b398f842c6787758f0c25d4556602823afc1cc1d5759e69af
7
+ data.tar.gz: 33983c2d39255eb9fc88d1f0de7558be7fcb7f7f204db7ffd5ac19b626bccf99000386808e2ec5695cf52a9de727a962dc7e2d3dcfaec8dc7d873265ac2064f0
data/.gitignore CHANGED
@@ -6,3 +6,4 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ .claude
data/.rubocop.yml CHANGED
@@ -1,2 +1,5 @@
1
+ require:
2
+ - ./lib/sequra_style
3
+
1
4
  inherit_from:
2
5
  - default.yml
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.10.0](https://github.com/sequra/sequra-style/compare/v1.9.0...v1.10.0) (2026-02-05)
4
+
5
+
6
+ ### Features
7
+
8
+ * [COR-689] Add Sequra/AsyncJobPattern cop ([#57](https://github.com/sequra/sequra-style/issues/57)) ([fe8007f](https://github.com/sequra/sequra-style/commit/fe8007f2932670de3203ff1d486d4c2fc90b123c))
9
+
3
10
  ## [1.9.0](https://github.com/sequra/sequra-style/compare/v1.8.1...v1.9.0) (2026-01-29)
4
11
 
5
12
 
data/Gemfile.lock CHANGED
@@ -1,52 +1,67 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sequra-style (1.9.0)
5
- rubocop (~> 1)
6
- rubocop-performance (~> 1)
7
- rubocop-rails (~> 2)
8
- rubocop-rspec (~> 2)
4
+ sequra-style (1.10.0)
5
+ rubocop (~> 1.75)
6
+ rubocop-performance (~> 1.25)
7
+ rubocop-rails (~> 2.31)
8
+ rubocop-rspec (~> 3.5)
9
9
 
10
10
  GEM
11
11
  remote: https://rubygems.org/
12
12
  specs:
13
- activesupport (8.0.1)
13
+ activesupport (8.1.2)
14
14
  base64
15
- benchmark (>= 0.3)
16
15
  bigdecimal
17
16
  concurrent-ruby (~> 1.0, >= 1.3.1)
18
17
  connection_pool (>= 2.2.5)
19
18
  drb
20
19
  i18n (>= 1.6, < 2)
20
+ json
21
21
  logger (>= 1.4.2)
22
22
  minitest (>= 5.1)
23
23
  securerandom (>= 0.3)
24
24
  tzinfo (~> 2.0, >= 2.0.5)
25
25
  uri (>= 0.13.1)
26
- ast (2.4.2)
27
- base64 (0.2.0)
28
- benchmark (0.4.0)
29
- bigdecimal (3.1.9)
30
- concurrent-ruby (1.3.5)
31
- connection_pool (2.5.0)
32
- drb (2.2.1)
33
- i18n (1.14.7)
26
+ ast (2.4.3)
27
+ base64 (0.3.0)
28
+ bigdecimal (4.0.1)
29
+ concurrent-ruby (1.3.6)
30
+ connection_pool (3.0.2)
31
+ diff-lcs (1.6.2)
32
+ drb (2.2.3)
33
+ i18n (1.14.8)
34
34
  concurrent-ruby (~> 1.0)
35
- json (2.10.1)
36
- language_server-protocol (3.17.0.4)
35
+ json (2.18.1)
36
+ language_server-protocol (3.17.0.5)
37
37
  lint_roller (1.1.0)
38
- logger (1.6.6)
39
- minitest (5.25.4)
40
- parallel (1.26.3)
41
- parser (3.3.7.1)
38
+ logger (1.7.0)
39
+ minitest (6.0.1)
40
+ prism (~> 1.5)
41
+ parallel (1.27.0)
42
+ parser (3.3.10.1)
42
43
  ast (~> 2.4.1)
43
44
  racc
45
+ prism (1.9.0)
44
46
  racc (1.8.1)
45
- rack (3.0.0)
47
+ rack (3.2.4)
46
48
  rainbow (3.1.1)
47
49
  rake (13.0.6)
48
- regexp_parser (2.10.0)
49
- rubocop (1.73.0)
50
+ regexp_parser (2.11.3)
51
+ rspec (3.13.2)
52
+ rspec-core (~> 3.13.0)
53
+ rspec-expectations (~> 3.13.0)
54
+ rspec-mocks (~> 3.13.0)
55
+ rspec-core (3.13.6)
56
+ rspec-support (~> 3.13.0)
57
+ rspec-expectations (3.13.5)
58
+ diff-lcs (>= 1.2.0, < 2.0)
59
+ rspec-support (~> 3.13.0)
60
+ rspec-mocks (3.13.7)
61
+ diff-lcs (>= 1.2.0, < 2.0)
62
+ rspec-support (~> 3.13.0)
63
+ rspec-support (3.13.7)
64
+ rubocop (1.84.1)
50
65
  json (~> 2.3)
51
66
  language_server-protocol (~> 3.17.0.2)
52
67
  lint_roller (~> 1.1.0)
@@ -54,28 +69,33 @@ GEM
54
69
  parser (>= 3.3.0.2)
55
70
  rainbow (>= 2.2.2, < 4.0)
56
71
  regexp_parser (>= 2.9.3, < 3.0)
57
- rubocop-ast (>= 1.38.0, < 2.0)
72
+ rubocop-ast (>= 1.49.0, < 2.0)
58
73
  ruby-progressbar (~> 1.7)
59
74
  unicode-display_width (>= 2.4.0, < 4.0)
60
- rubocop-ast (1.38.1)
61
- parser (>= 3.3.1.0)
62
- rubocop-performance (1.15.1)
63
- rubocop (>= 1.7.0, < 2.0)
64
- rubocop-ast (>= 0.4.0)
65
- rubocop-rails (2.17.2)
75
+ rubocop-ast (1.49.0)
76
+ parser (>= 3.3.7.2)
77
+ prism (~> 1.7)
78
+ rubocop-performance (1.26.1)
79
+ lint_roller (~> 1.1)
80
+ rubocop (>= 1.75.0, < 2.0)
81
+ rubocop-ast (>= 1.47.1, < 2.0)
82
+ rubocop-rails (2.34.3)
66
83
  activesupport (>= 4.2.0)
84
+ lint_roller (~> 1.1)
67
85
  rack (>= 1.1)
68
- rubocop (>= 1.33.0, < 2.0)
69
- rubocop-rspec (2.15.0)
70
- rubocop (~> 1.33)
86
+ rubocop (>= 1.75.0, < 2.0)
87
+ rubocop-ast (>= 1.44.0, < 2.0)
88
+ rubocop-rspec (3.9.0)
89
+ lint_roller (~> 1.1)
90
+ rubocop (~> 1.81)
71
91
  ruby-progressbar (1.13.0)
72
92
  securerandom (0.4.1)
73
93
  tzinfo (2.0.6)
74
94
  concurrent-ruby (~> 1.0)
75
- unicode-display_width (3.1.4)
76
- unicode-emoji (~> 4.0, >= 4.0.4)
77
- unicode-emoji (4.0.4)
78
- uri (1.0.3)
95
+ unicode-display_width (3.2.0)
96
+ unicode-emoji (~> 4.1)
97
+ unicode-emoji (4.2.0)
98
+ uri (1.1.1)
79
99
 
80
100
  PLATFORMS
81
101
  ruby
@@ -83,6 +103,7 @@ PLATFORMS
83
103
  DEPENDENCIES
84
104
  bundler (~> 2.1.4)
85
105
  rake (~> 13.0.1)
106
+ rspec (~> 3.13)
86
107
  sequra-style!
87
108
 
88
109
  BUNDLED WITH
data/default.yml CHANGED
@@ -810,3 +810,17 @@ Style/TrivialAccessors:
810
810
  Style/WordArray:
811
811
  EnforcedStyle: brackets
812
812
  Enabled: true
813
+
814
+ # Custom Sequra cops
815
+
816
+ # Enforce async job pattern: each job should delegate to exactly one Operation
817
+ # and stay within the maximum class length (10 lines).
818
+ Sequra/AsyncJobPattern:
819
+ Enabled: true
820
+ Include:
821
+ - "app/jobs/**/*.rb"
822
+ - "app/workers/**/*.rb"
823
+ - "packs/*/app/jobs/**/*.rb"
824
+ - "packs/*/app/workers/**/*.rb"
825
+ Exclude:
826
+ - "**/application_job.rb"
@@ -0,0 +1,166 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Sequra
4
+ # Detects Sidekiq jobs that haven't adopted the ApplicationOperation pattern.
5
+ #
6
+ # A job is considered migrated when it meets ALL conditions:
7
+ # 1. Has exactly ONE `*Operation.call` invocation (delegates to single operation)
8
+ # 2. Class length ≤ 10 "smart lines" (using CountAsOne semantics)
9
+ #
10
+ # A job is considered unmigrated (offense) when:
11
+ # - Zero `Operation.call` → business logic lives in the job itself
12
+ # - Multiple `Operation.call` → job orchestrates multiple operations
13
+ # - Exceeds class length → job has too much logic beyond delegation
14
+ #
15
+ # @example
16
+ # # bad - no operation delegation
17
+ # class MyJob < ApplicationJob
18
+ # def perform(id)
19
+ # user = User.find(id)
20
+ # user.update!(status: :active)
21
+ # UserMailer.welcome(user).deliver_later
22
+ # end
23
+ # end
24
+ #
25
+ # # bad - multiple operations
26
+ # class MyJob < ApplicationJob
27
+ # def perform(id, type)
28
+ # if type == :a
29
+ # OperationA.call(id: id)
30
+ # else
31
+ # OperationB.call(id: id)
32
+ # end
33
+ # end
34
+ # end
35
+ #
36
+ # # good - single operation delegation
37
+ # class MyJob < ApplicationJob
38
+ # def perform(id)
39
+ # MyOperation.call(id:)
40
+ # end
41
+ # end
42
+ #
43
+ class AsyncJobPattern < Base
44
+ MSG = "Sidekiq job should delegate to exactly one Operation".freeze
45
+ MSG_CLASS_LENGTH = "Job has too much logic to be a simple Operation delegate. " \
46
+ "Consider moving logic into the Operation".freeze
47
+
48
+ MAX_CLASS_LENGTH = 10
49
+ COUNT_AS_ONE = ["array", "hash", "heredoc", "method_call"].freeze
50
+ OPERATION_CLASS_PATTERN = /Operation$/
51
+
52
+ def_node_matcher :application_job_subclass?, <<~PATTERN
53
+ (class _ (const nil? :ApplicationJob) ...)
54
+ PATTERN
55
+
56
+ def_node_matcher :includes_sidekiq_worker?, <<~PATTERN
57
+ (send nil? :include (const (const nil? :Sidekiq) {:Worker :Job}))
58
+ PATTERN
59
+
60
+ def_node_matcher :operation_call?, <<~PATTERN
61
+ (send (const _ OPERATION_CLASS_PATTERN) :call ...)
62
+ PATTERN
63
+
64
+ def on_class(node)
65
+ return unless sidekiq_job?(node)
66
+
67
+ check_operation_delegation(node)
68
+ check_class_length(node)
69
+ end
70
+
71
+ private
72
+
73
+ def sidekiq_job?(node)
74
+ return true if application_job_subclass?(node)
75
+
76
+ node.body&.each_descendant(:send)&.any? { |send_node| includes_sidekiq_worker?(send_node) }
77
+ end
78
+
79
+ def check_operation_delegation(node)
80
+ operation_calls = find_operation_calls(node)
81
+
82
+ return if operation_calls.size == 1
83
+
84
+ add_offense(node.loc.name, message: MSG)
85
+ end
86
+
87
+ def find_operation_calls(node)
88
+ return [] unless node.body
89
+
90
+ node.body.each_descendant(:send).select { |send_node| operation_call?(send_node) }
91
+ end
92
+
93
+ def check_class_length(node)
94
+ return unless node.body
95
+
96
+ length = class_length(node)
97
+ return if length <= MAX_CLASS_LENGTH
98
+
99
+ add_offense(node.loc.keyword, message: MSG_CLASS_LENGTH)
100
+ end
101
+
102
+ def class_length(node)
103
+ return 0 unless node.body
104
+
105
+ body_lines = line_range(node.body)
106
+ count_lines(body_lines, node)
107
+ end
108
+
109
+ def line_range(node)
110
+ node.loc.first_line..node.loc.last_line
111
+ end
112
+
113
+ def count_lines(range, node)
114
+ source_lines = processed_source.lines[(range.begin - 1)..(range.end - 1)]
115
+ return 0 if source_lines.nil?
116
+
117
+ effective_lines = source_lines.reject { |line| irrelevant_line?(line) }
118
+
119
+ # Subtract lines that should count as one
120
+ count_as_one_adjustment = count_as_one_lines(node)
121
+
122
+ [effective_lines.size - count_as_one_adjustment, 0].max
123
+ end
124
+
125
+ def irrelevant_line?(line)
126
+ line.strip.empty? || line.strip.start_with?("#")
127
+ end
128
+
129
+ def count_as_one_lines(node)
130
+ adjustment = 0
131
+ return adjustment unless node.body
132
+
133
+ node.body.each_descendant do |descendant|
134
+ next unless count_as_one_node?(descendant)
135
+
136
+ lines = descendant.loc.last_line - descendant.loc.first_line
137
+ adjustment += lines if lines.positive?
138
+ end
139
+
140
+ adjustment
141
+ end
142
+
143
+ def count_as_one_node?(node)
144
+ COUNT_AS_ONE.any? { |type| node_matches_type?(node, type) }
145
+ end
146
+
147
+ def node_matches_type?(node, type)
148
+ case type
149
+ when "array" then node.array_type?
150
+ when "hash" then node.hash_type?
151
+ when "heredoc" then node.str_type? && node.heredoc?
152
+ when "method_call" then multiline_method_call?(node)
153
+ else false
154
+ end
155
+ end
156
+
157
+ def multiline_method_call?(node)
158
+ node.send_type? &&
159
+ node.loc.respond_to?(:selector) &&
160
+ node.loc.selector &&
161
+ node.loc.first_line != node.loc.last_line
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -1,5 +1,5 @@
1
1
  module Sequra
2
2
  module Style
3
- VERSION = "1.9.0"
3
+ VERSION = "1.10.0"
4
4
  end
5
5
  end
@@ -0,0 +1 @@
1
+ require_relative "rubocop/cop/sequra/async_job_pattern"
data/sequra-style.gemspec CHANGED
@@ -28,4 +28,5 @@ Gem::Specification.new do |spec|
28
28
 
29
29
  spec.add_development_dependency "bundler", "~> 2.1.4"
30
30
  spec.add_development_dependency "rake", "~> 13.0.1"
31
+ spec.add_development_dependency "rspec", "~> 3.13"
31
32
  end
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.9.0
4
+ version: 1.10.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-01-29 00:00:00.000000000 Z
11
+ date: 2026-02-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: 13.0.1
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.13'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.13'
97
111
  description:
98
112
  email:
99
113
  - rubygems@sequra.es
@@ -120,8 +134,10 @@ files:
120
134
  - docs/decisions/README.md
121
135
  - docs/decisions/index.md
122
136
  - docs/decisions/template.md
137
+ - lib/rubocop/cop/sequra/async_job_pattern.rb
123
138
  - lib/sequra/style.rb
124
139
  - lib/sequra/style/version.rb
140
+ - lib/sequra_style.rb
125
141
  - sequra-style.gemspec
126
142
  homepage: https://github.com/sequra/sequra-style
127
143
  licenses: