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 +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile.lock +59 -38
- data/default.yml +14 -0
- data/lib/rubocop/cop/sequra/async_job_pattern.rb +166 -0
- data/lib/sequra/style/version.rb +1 -1
- data/lib/sequra_style.rb +1 -0
- data/sequra-style.gemspec +1 -0
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ac36e918663e99576328aa7740655149cb2a44bdf26909a53d43eb1ff2a8d3aa
|
|
4
|
+
data.tar.gz: 2e490af77771f9bdcba719b988182c527d121dcc1a91037432f1dded77741836
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 00117fdcaf8ba15aa5b69f6cc078705982dec612445f1bbee9ad314fc52bc5802c67623b5b4aae8b398f842c6787758f0c25d4556602823afc1cc1d5759e69af
|
|
7
|
+
data.tar.gz: 33983c2d39255eb9fc88d1f0de7558be7fcb7f7f204db7ffd5ac19b626bccf99000386808e2ec5695cf52a9de727a962dc7e2d3dcfaec8dc7d873265ac2064f0
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
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.
|
|
5
|
-
rubocop (~> 1)
|
|
6
|
-
rubocop-performance (~> 1)
|
|
7
|
-
rubocop-rails (~> 2)
|
|
8
|
-
rubocop-rspec (~>
|
|
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.
|
|
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.
|
|
27
|
-
base64 (0.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
drb (2.2.
|
|
33
|
-
i18n (1.14.
|
|
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.
|
|
36
|
-
language_server-protocol (3.17.0.
|
|
35
|
+
json (2.18.1)
|
|
36
|
+
language_server-protocol (3.17.0.5)
|
|
37
37
|
lint_roller (1.1.0)
|
|
38
|
-
logger (1.
|
|
39
|
-
minitest (
|
|
40
|
-
|
|
41
|
-
|
|
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.
|
|
47
|
+
rack (3.2.4)
|
|
46
48
|
rainbow (3.1.1)
|
|
47
49
|
rake (13.0.6)
|
|
48
|
-
regexp_parser (2.
|
|
49
|
-
|
|
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.
|
|
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.
|
|
61
|
-
parser (>= 3.3.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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.
|
|
69
|
-
|
|
70
|
-
|
|
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.
|
|
76
|
-
unicode-emoji (~> 4.
|
|
77
|
-
unicode-emoji (4.0
|
|
78
|
-
uri (1.
|
|
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
|
data/lib/sequra/style/version.rb
CHANGED
data/lib/sequra_style.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require_relative "rubocop/cop/sequra/async_job_pattern"
|
data/sequra-style.gemspec
CHANGED
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.
|
|
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-
|
|
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:
|