rubocop-thread_safety 0.4.4 → 0.7.2

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.
@@ -3,11 +3,11 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module ThreadSafety
6
- # This cop checks whether some class instance variable isn't a
6
+ # Checks whether some class instance variable isn't a
7
7
  # mutable literal (e.g. array or hash).
8
8
  #
9
9
  # It is based on Style/MutableConstant from RuboCop.
10
- # See https://github.com/rubocop-hq/rubocop/blob/master/lib/rubocop/cop/style/mutable_constant.rb
10
+ # See https://github.com/rubocop/rubocop/blob/master/lib/rubocop/cop/style/mutable_constant.rb
11
11
  #
12
12
  # Class instance variables are a risk to threaded code as they are shared
13
13
  # between threads. A mutable object such as an array or hash may be
@@ -72,9 +72,12 @@ module RuboCop
72
72
  # end
73
73
  # end.freeze
74
74
  # end
75
- class MutableClassInstanceVariable < Cop
75
+ class MutableClassInstanceVariable < Base
76
+ extend AutoCorrector
77
+
76
78
  include FrozenStringLiteral
77
79
  include ConfigurableEnforcedStyle
80
+ include OperationWithThreadsafeResult
78
81
 
79
82
  MSG = 'Freeze mutable objects assigned to class instance variables.'
80
83
  FROZEN_STRING_LITERAL_TYPES_RUBY27 = %i[str dstr].freeze
@@ -83,22 +86,20 @@ module RuboCop
83
86
  def on_ivasgn(node)
84
87
  return unless in_class?(node)
85
88
 
86
- _, value = *node
87
- on_assignment(value)
89
+ on_assignment(node.expression)
88
90
  end
89
91
 
90
92
  def on_or_asgn(node)
91
- lhs, value = *node
92
- return unless lhs&.ivasgn_type?
93
+ return unless node.assignment_node.ivasgn_type?
93
94
  return unless in_class?(node)
94
95
 
95
- on_assignment(value)
96
+ on_assignment(node.expression)
96
97
  end
97
98
 
98
99
  def on_masgn(node)
99
100
  return unless in_class?(node)
100
101
 
101
- mlhs, values = *node
102
+ mlhs, values = *node # rubocop:disable InternalAffairs/NodeDestructuring
102
103
  return unless values.array_type?
103
104
 
104
105
  mlhs.to_a.zip(values.to_a).each do |lhs, value|
@@ -108,23 +109,21 @@ module RuboCop
108
109
  end
109
110
  end
110
111
 
111
- def autocorrect(node)
112
+ def autocorrect(corrector, node)
112
113
  expr = node.source_range
113
114
 
114
- lambda do |corrector|
115
- splat_value = splat_value(node)
116
- if splat_value
117
- correct_splat_expansion(corrector, expr, splat_value)
118
- elsif node.array_type? && !node.bracketed?
119
- corrector.insert_before(expr, '[')
120
- corrector.insert_after(expr, ']')
121
- elsif requires_parentheses?(node)
122
- corrector.insert_before(expr, '(')
123
- corrector.insert_after(expr, ')')
124
- end
125
-
126
- corrector.insert_after(expr, '.freeze')
115
+ splat_value = splat_value(node)
116
+ if splat_value
117
+ correct_splat_expansion(corrector, expr, splat_value)
118
+ elsif node.array_type? && !node.bracketed?
119
+ corrector.insert_before(expr, '[')
120
+ corrector.insert_after(expr, ']')
121
+ elsif requires_parentheses?(node)
122
+ corrector.insert_before(expr, '(')
123
+ corrector.insert_after(expr, ')')
127
124
  end
125
+
126
+ corrector.insert_after(expr, '.freeze')
128
127
  end
129
128
 
130
129
  private
@@ -152,7 +151,9 @@ module RuboCop
152
151
  return if operation_produces_threadsafe_object?(value)
153
152
  return if frozen_string_literal?(value)
154
153
 
155
- add_offense(value)
154
+ add_offense(value) do |corrector|
155
+ autocorrect(corrector, value)
156
+ end
156
157
  end
157
158
 
158
159
  def check(value)
@@ -160,7 +161,9 @@ module RuboCop
160
161
  range_enclosed_in_parentheses?(value)
161
162
  return if frozen_string_literal?(value)
162
163
 
163
- add_offense(value)
164
+ add_offense(value) do |corrector|
165
+ autocorrect(corrector, value)
166
+ end
164
167
  end
165
168
 
166
169
  def in_class?(node)
@@ -174,12 +177,13 @@ module RuboCop
174
177
 
175
178
  def container?(node)
176
179
  return true if define_singleton_method?(node)
180
+ return true if define_method?(node)
177
181
 
178
182
  %i[def defs class module].include?(node.type)
179
183
  end
180
184
 
181
185
  def mutable_literal?(node)
182
- return if node.nil?
186
+ return false if node.nil?
183
187
 
184
188
  node.mutable_literal? || range_type?(node)
185
189
  end
@@ -194,7 +198,7 @@ module RuboCop
194
198
  end
195
199
 
196
200
  def range_type?(node)
197
- node.erange_type? || node.irange_type?
201
+ node.type?(:range)
198
202
  end
199
203
 
200
204
  def correct_splat_expansion(corrector, expr, splat_value)
@@ -205,16 +209,24 @@ module RuboCop
205
209
  end
206
210
  end
207
211
 
212
+ # @!method define_singleton_method?(node)
208
213
  def_node_matcher :define_singleton_method?, <<~PATTERN
209
214
  (block (send nil? :define_singleton_method ...) ...)
210
215
  PATTERN
211
216
 
217
+ # @!method define_method?(node)
218
+ def_node_matcher :define_method?, <<~PATTERN
219
+ (block (send nil? :define_method ...) ...)
220
+ PATTERN
221
+
222
+ # @!method splat_value(node)
212
223
  def_node_matcher :splat_value, <<~PATTERN
213
224
  (array (splat $_))
214
225
  PATTERN
215
226
 
216
227
  # NOTE: Some of these patterns may not actually return an immutable
217
228
  # object but we will consider them immutable for this cop.
229
+ # @!method operation_produces_immutable_object?(node)
218
230
  def_node_matcher :operation_produces_immutable_object?, <<~PATTERN
219
231
  {
220
232
  (const _ _)
@@ -231,40 +243,9 @@ module RuboCop
231
243
  }
232
244
  PATTERN
233
245
 
234
- def_node_matcher :operation_produces_threadsafe_object?, <<~PATTERN
235
- {
236
- (send (const {nil? cbase} :Queue) :new ...)
237
- (send
238
- (const (const {nil? cbase} :ThreadSafe) {:Hash :Array})
239
- :new ...)
240
- (block
241
- (send
242
- (const (const {nil? cbase} :ThreadSafe) {:Hash :Array})
243
- :new ...)
244
- ...)
245
- (send (const (const {nil? cbase} :Concurrent) _) :new ...)
246
- (block
247
- (send (const (const {nil? cbase} :Concurrent) _) :new ...)
248
- ...)
249
- (send (const (const (const {nil? cbase} :Concurrent) _) _) :new ...)
250
- (block
251
- (send
252
- (const (const (const {nil? cbase} :Concurrent) _) _)
253
- :new ...)
254
- ...)
255
- (send
256
- (const (const (const (const {nil? cbase} :Concurrent) _) _) _)
257
- :new ...)
258
- (block
259
- (send
260
- (const (const (const (const {nil? cbase} :Concurrent) _) _) _)
261
- :new ...)
262
- ...)
263
- }
264
- PATTERN
265
-
246
+ # @!method range_enclosed_in_parentheses?(node)
266
247
  def_node_matcher :range_enclosed_in_parentheses?, <<~PATTERN
267
- (begin ({irange erange} _ _))
248
+ (begin (range _ _))
268
249
  PATTERN
269
250
  end
270
251
  end
@@ -10,18 +10,19 @@ module RuboCop
10
10
  # @example
11
11
  # # bad
12
12
  # Thread.new { do_work }
13
- class NewThread < Cop
13
+ class NewThread < Base
14
14
  MSG = 'Avoid starting new threads.'
15
+ RESTRICT_ON_SEND = %i[new fork start].freeze
15
16
 
17
+ # @!method new_thread?(node)
16
18
  def_node_matcher :new_thread?, <<~MATCHER
17
- (send (const {nil? cbase} :Thread) :new)
19
+ (call (const {nil? cbase} :Thread) {:new :fork :start} ...)
18
20
  MATCHER
19
21
 
20
22
  def on_send(node)
21
- return unless new_thread?(node)
22
-
23
- add_offense(node, message: MSG)
23
+ new_thread?(node) { add_offense(node) }
24
24
  end
25
+ alias on_csend on_send
25
26
  end
26
27
  end
27
28
  end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module ThreadSafety
6
+ # Avoid instance variables in rack middleware.
7
+ #
8
+ # Middlewares are initialized once, meaning any instance variables are shared between executor threads.
9
+ # To avoid potential race conditions, it's recommended to design middlewares to be stateless
10
+ # or to implement proper synchronization mechanisms.
11
+ #
12
+ # @example
13
+ # # bad
14
+ # class CounterMiddleware
15
+ # def initialize(app)
16
+ # @app = app
17
+ # @counter = 0
18
+ # end
19
+ #
20
+ # def call(env)
21
+ # app.call(env)
22
+ # ensure
23
+ # @counter += 1
24
+ # end
25
+ # end
26
+ #
27
+ # # good
28
+ # class CounterMiddleware
29
+ # def initialize(app)
30
+ # @app = app
31
+ # @counter = Concurrent::AtomicReference.new(0)
32
+ # end
33
+ #
34
+ # def call(env)
35
+ # app.call(env)
36
+ # ensure
37
+ # @counter.update { |ref| ref + 1 }
38
+ # end
39
+ # end
40
+ #
41
+ # class IdentityMiddleware
42
+ # def initialize(app)
43
+ # @app = app
44
+ # end
45
+ #
46
+ # def call(env)
47
+ # app.call(env)
48
+ # end
49
+ # end
50
+ class RackMiddlewareInstanceVariable < Base
51
+ include AllowedIdentifiers
52
+ include OperationWithThreadsafeResult
53
+
54
+ MSG = 'Avoid instance variables in Rack middleware.'
55
+
56
+ RESTRICT_ON_SEND = %i[instance_variable_get instance_variable_set].freeze
57
+
58
+ # @!method rack_middleware_like_class?(node)
59
+ def_node_matcher :rack_middleware_like_class?, <<~MATCHER
60
+ (class (const nil? _) nil? (begin <(def :initialize (args (arg _)+) ...) (def :call (args (arg _)) ...) ...>))
61
+ MATCHER
62
+
63
+ # @!method app_variable(node)
64
+ def_node_search :app_variable, <<~MATCHER
65
+ (def :initialize (args (arg $_) ...) `(ivasgn $_ (lvar $_)))
66
+ MATCHER
67
+
68
+ def on_class(node)
69
+ return unless rack_middleware_like_class?(node)
70
+
71
+ constructor_method = find_constructor_method(node)
72
+ return unless (application_variable = extract_application_variable_from_contructor_method(constructor_method))
73
+
74
+ safe_variables = extract_safe_variables_from_constructor_method(constructor_method)
75
+
76
+ node.each_node(:def) do |def_node|
77
+ def_node.each_node(:ivasgn, :ivar) do |ivar_node|
78
+ variable, = ivar_node.to_a
79
+ if variable == application_variable || safe_variables.include?(variable) || allowed_identifier?(variable)
80
+ next
81
+ end
82
+
83
+ add_offense ivar_node
84
+ end
85
+ end
86
+ end
87
+
88
+ def on_send(node)
89
+ argument = node.first_argument
90
+
91
+ return unless argument&.type?(:sym, :str)
92
+ return if allowed_identifier?(argument.value)
93
+
94
+ add_offense node
95
+ end
96
+ alias on_csend on_send
97
+
98
+ private
99
+
100
+ def find_constructor_method(class_node)
101
+ class_node
102
+ .each_node(:def)
103
+ .find { |node| node.method?(:initialize) && node.arguments.size >= 1 }
104
+ end
105
+
106
+ def extract_application_variable_from_contructor_method(constructor_method)
107
+ constructor_method
108
+ .then { |node| app_variable(node) }
109
+ .then { |variables| variables.first[1] if variables.first }
110
+ end
111
+
112
+ def extract_safe_variables_from_constructor_method(constructor_method)
113
+ constructor_method
114
+ .each_node(:ivasgn)
115
+ .select { |ivasgn_node| operation_produces_threadsafe_object?(ivasgn_node.to_a[1]) }
116
+ .map { _1.to_a[0] }
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lint_roller'
4
+
5
+ module RuboCop
6
+ module ThreadSafety
7
+ # A plugin that integrates RuboCop ThreadSafety with RuboCop's plugin system.
8
+ class Plugin < LintRoller::Plugin
9
+ # :nocov:
10
+ def about
11
+ LintRoller::About.new(
12
+ name: 'rubocop-thread_safety',
13
+ version: Version::STRING,
14
+ homepage: 'https://github.com/rubocop/rubocop-thread_safety',
15
+ description: 'Thread-safety checks via static analysis.'
16
+ )
17
+ end
18
+ # :nocov:
19
+
20
+ def supported?(context)
21
+ context.engine == :rubocop
22
+ end
23
+
24
+ def rules(_context)
25
+ project_root = Pathname.new(__dir__).join('../../..')
26
+
27
+ obsoletion = project_root.join('config', 'obsoletion.yml')
28
+ ConfigObsoletion.files << obsoletion
29
+
30
+ LintRoller::Rules.new(
31
+ type: :path,
32
+ config_format: :rubocop,
33
+ value: project_root.join('config/default.yml')
34
+ )
35
+ end
36
+ end
37
+ end
38
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module RuboCop
4
4
  module ThreadSafety
5
- VERSION = '0.4.4'
5
+ module Version
6
+ STRING = '0.7.2'
7
+ end
6
8
  end
7
9
  end
@@ -3,10 +3,5 @@
3
3
  module RuboCop
4
4
  # RuboCop::ThreadSafety detects some potential thread safety issues.
5
5
  module ThreadSafety
6
- PROJECT_ROOT = Pathname.new(File.expand_path('../../', __dir__))
7
- CONFIG_DEFAULT = PROJECT_ROOT.join('config', 'default.yml').freeze
8
- CONFIG = YAML.safe_load(CONFIG_DEFAULT.read).freeze
9
-
10
- private_constant(:CONFIG_DEFAULT, :PROJECT_ROOT)
11
6
  end
12
7
  end
@@ -4,11 +4,13 @@ require 'rubocop'
4
4
 
5
5
  require 'rubocop/thread_safety'
6
6
  require 'rubocop/thread_safety/version'
7
- require 'rubocop/thread_safety/inject'
7
+ require 'rubocop/thread_safety/plugin'
8
8
 
9
- RuboCop::ThreadSafety::Inject.defaults!
9
+ require 'rubocop/cop/mixin/operation_with_threadsafe_result'
10
10
 
11
- require 'rubocop/cop/thread_safety/instance_variable_in_class_method'
11
+ require 'rubocop/cop/thread_safety/class_instance_variable'
12
12
  require 'rubocop/cop/thread_safety/class_and_module_attributes'
13
13
  require 'rubocop/cop/thread_safety/mutable_class_instance_variable'
14
14
  require 'rubocop/cop/thread_safety/new_thread'
15
+ require 'rubocop/cop/thread_safety/dir_chdir'
16
+ require 'rubocop/cop/thread_safety/rack_middleware_instance_variable'
metadata CHANGED
@@ -1,105 +1,48 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-thread_safety
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.4
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Gee
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2021-09-10 00:00:00.000000000 Z
10
+ date: 2025-03-13 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: rubocop
13
+ name: lint_roller
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
- - - ">="
16
+ - - "~>"
18
17
  - !ruby/object:Gem::Version
19
- version: 0.53.0
18
+ version: '1.1'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: 0.53.0
27
- - !ruby/object:Gem::Dependency
28
- name: appraisal
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: bundler
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '1.10'
48
- - - "<"
49
- - !ruby/object:Gem::Version
50
- version: '3'
51
- type: :development
52
- prerelease: false
53
- version_requirements: !ruby/object:Gem::Requirement
54
- requirements:
55
- - - ">="
56
- - !ruby/object:Gem::Version
57
- version: '1.10'
58
- - - "<"
23
+ - - "~>"
59
24
  - !ruby/object:Gem::Version
60
- version: '3'
25
+ version: '1.1'
61
26
  - !ruby/object:Gem::Dependency
62
- name: pry
27
+ name: rubocop
63
28
  requirement: !ruby/object:Gem::Requirement
64
29
  requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: '0'
68
- type: :development
69
- prerelease: false
70
- version_requirements: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - ">="
30
+ - - "~>"
73
31
  - !ruby/object:Gem::Version
74
- version: '0'
75
- - !ruby/object:Gem::Dependency
76
- name: rake
77
- requirement: !ruby/object:Gem::Requirement
78
- requirements:
32
+ version: '1.72'
79
33
  - - ">="
80
34
  - !ruby/object:Gem::Version
81
- version: '10.0'
82
- type: :development
35
+ version: 1.72.1
36
+ type: :runtime
83
37
  prerelease: false
84
38
  version_requirements: !ruby/object:Gem::Requirement
85
- requirements:
86
- - - ">="
87
- - !ruby/object:Gem::Version
88
- version: '10.0'
89
- - !ruby/object:Gem::Dependency
90
- name: rspec
91
- requirement: !ruby/object:Gem::Requirement
92
39
  requirements:
93
40
  - - "~>"
94
41
  - !ruby/object:Gem::Version
95
- version: '3.0'
96
- type: :development
97
- prerelease: false
98
- version_requirements: !ruby/object:Gem::Requirement
99
- requirements:
100
- - - "~>"
42
+ version: '1.72'
43
+ - - ">="
101
44
  - !ruby/object:Gem::Version
102
- version: '3.0'
45
+ version: 1.72.1
103
46
  description: |2
104
47
  Thread-safety checks via static analysis.
105
48
  A plugin for the RuboCop code style enforcing & linting tool.
@@ -109,36 +52,31 @@ executables: []
109
52
  extensions: []
110
53
  extra_rdoc_files: []
111
54
  files:
112
- - ".gitignore"
113
- - ".rspec"
114
- - ".rubocop.yml"
115
- - ".travis.yml"
116
- - Appraisals
117
- - Gemfile
55
+ - CHANGELOG.md
118
56
  - LICENSE.txt
119
57
  - README.md
120
- - Rakefile
121
- - bin/console
122
- - bin/setup
123
58
  - config/default.yml
124
- - gemfiles/rubocop_0.53.gemfile
125
- - gemfiles/rubocop_0.81.gemfile
126
- - gemfiles/rubocop_0.86.gemfile
127
- - gemfiles/rubocop_1.20.gemfile
59
+ - config/obsoletion.yml
128
60
  - lib/rubocop-thread_safety.rb
61
+ - lib/rubocop/cop/mixin/operation_with_threadsafe_result.rb
129
62
  - lib/rubocop/cop/thread_safety/class_and_module_attributes.rb
130
- - lib/rubocop/cop/thread_safety/instance_variable_in_class_method.rb
63
+ - lib/rubocop/cop/thread_safety/class_instance_variable.rb
64
+ - lib/rubocop/cop/thread_safety/dir_chdir.rb
131
65
  - lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb
132
66
  - lib/rubocop/cop/thread_safety/new_thread.rb
67
+ - lib/rubocop/cop/thread_safety/rack_middleware_instance_variable.rb
133
68
  - lib/rubocop/thread_safety.rb
134
- - lib/rubocop/thread_safety/inject.rb
69
+ - lib/rubocop/thread_safety/plugin.rb
135
70
  - lib/rubocop/thread_safety/version.rb
136
- - rubocop-thread_safety.gemspec
137
- homepage: https://github.com/covermymeds/rubocop-thread_safety
71
+ homepage: https://github.com/rubocop/rubocop-thread_safety
138
72
  licenses:
139
73
  - MIT
140
- metadata: {}
141
- post_install_message:
74
+ metadata:
75
+ changelog_uri: https://github.com/rubocop/rubocop-thread_safety/blob/master/CHANGELOG.md
76
+ source_code_uri: https://github.com/rubocop/rubocop-thread_safety
77
+ bug_tracker_uri: https://github.com/rubocop/rubocop-thread_safety/issues
78
+ rubygems_mfa_required: 'true'
79
+ default_lint_roller_plugin: RuboCop::ThreadSafety::Plugin
142
80
  rdoc_options: []
143
81
  require_paths:
144
82
  - lib
@@ -146,15 +84,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
146
84
  requirements:
147
85
  - - ">="
148
86
  - !ruby/object:Gem::Version
149
- version: 2.3.0
87
+ version: 2.7.0
150
88
  required_rubygems_version: !ruby/object:Gem::Requirement
151
89
  requirements:
152
90
  - - ">="
153
91
  - !ruby/object:Gem::Version
154
92
  version: '0'
155
93
  requirements: []
156
- rubygems_version: 3.0.3.1
157
- signing_key:
94
+ rubygems_version: 3.6.3
158
95
  specification_version: 4
159
96
  summary: Thread-safety checks via static analysis
160
97
  test_files: []
data/.gitignore DELETED
@@ -1,10 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /Gemfile.lock
4
- /gemfiles/*.lock
5
- /_yardoc/
6
- /coverage/
7
- /doc/
8
- /pkg/
9
- /spec/reports/
10
- /tmp/
data/.rspec DELETED
@@ -1,2 +0,0 @@
1
- --color
2
- --require spec_helper