rubocop-thread_safety 0.5.1 → 0.7.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: c4f37c2216cfdace60b7ef3f2e2f0b5bb743b9b8a3ee2543bb3a2ed10c90de1a
4
- data.tar.gz: 2cfa26d1da425d7d39ee0897d8137d24a5af25cd8911637dfd8093c7097b5a30
3
+ metadata.gz: 440e483486f3d67ad21fa72aa04eb47535020324672c3e08646894cec3517450
4
+ data.tar.gz: 50fe186da5956ee780dd5c2839d7792fc57f10ed4860d0781d2f0d68180d635d
5
5
  SHA512:
6
- metadata.gz: 405f5f9007c0d1aef55173a0ee4aefb44d16ec4ae5113acdb05750a8792fcbc7759ceab4267132f47906ac999194f2a550b2ce63eef2d117a35c5c9cd1c7d438
7
- data.tar.gz: 78e4b72c55d6745a979bff3acf5bf4b393534ca448dfec5aaa765f77dd32a66db5b0291a5fb55d8fa02044407ec95c72185b7e28add1801b456b1eb1f0be904a
6
+ metadata.gz: bb89cc31949d9803344f78556dc6f3351369bd2936ee8b26f93e6caa477b3bcfa6b7db9e8d78dade90eca9003455af4411c3ef064c9399f63455d6c7cd45c36c
7
+ data.tar.gz: b03de48386d763afe6d7a66aa2f604bcfc1fda637dfd864c2f09b52122fb8637c26f9a675299fe85fae464804e4e8cc2b569ba9fa8104cca2a592ef6fcb6c8c1
data/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Change log
2
+
3
+ ## master
4
+
5
+ ## 0.7.0
6
+
7
+ - [#80](https://github.com/rubocop/rubocop-thread_safety/pull/80) Make RuboCop ThreadSafety work as a RuboCop plugin. ([@bquorning](https://github.com/bquorning))
8
+ - [#76](https://github.com/rubocop/rubocop-thread_safety/pull/76): Detect offenses when using safe navigation for `ThreadSafety/DirChdir`, `ThreadSafety/NewThread` and `ThreadSafety/RackMiddlewareInstanceVariable` cops. ([@viralpraxis](https://github.com/viralpraxis))
9
+ - [#73](https://github.com/rubocop/rubocop-thread_safety/pull/73): Add `AllowCallWithBlock` option to `ThreadSafety/DirChdir` cop. ([@viralpraxis](https://github.com/viralpraxis))
10
+
11
+ ## 0.6.0
12
+
13
+ * [#59](https://github.com/rubocop/rubocop-thread_safety/pull/59): Rename `ThreadSafety::InstanceVariableInClassMethod` cop to `ThreadSafety::ClassInstanceVariable` to better reflect its purpose. ([@viralpraxis](https://github.com/viralpraxis))
14
+ * [#55](https://github.com/rubocop/rubocop-thread_safety/pull/55): Enhance `ThreadSafety::InstanceVariableInClassMethod` cop to detect offenses within `class_eval/exec` blocks. ([@viralpraxis](https://github.com/viralpraxis))
15
+ * [#54](https://github.com/rubocop/rubocop-thread_safety/pull/54): Drop support for RuboCop older than 1.48. ([@viralpraxis](https://github.com/viralpraxis))
16
+ * [#52](https://github.com/rubocop/rubocop-thread_safety/pull/52): Add new `RackMiddlewareInstanceVariable` cop to detect instance variables in Rack middleware. ([@viralpraxis](https://github.com/viralpraxis))
17
+ * [#48](https://github.com/rubocop/rubocop-thread_safety/pull/48): Do not report instance variables in `ActionDispatch` callbacks in singleton methods. ([@viralpraxis](https://github.com/viralpraxis))
18
+ * [#43](https://github.com/rubocop/rubocop-thread_safety/pull/43): Make detection of ActiveSupport's `class_attribute` configurable. ([@viralpraxis](https://github.com/viralpraxis))
19
+ * [#42](https://github.com/rubocop/rubocop-thread_safety/pull/42): Fix some `InstanceVariableInClassMethod` cop false positive offenses. ([@viralpraxis](https://github.com/viralpraxis))
20
+ * [#41](https://github.com/rubocop/rubocop-thread_safety/pull/41): Drop support for MRI older than 2.7. ([@viralpraxis](https://github.com/viralpraxis))
21
+ * [#38](https://github.com/rubocop/rubocop-thread_safety/pull/38): Fix `NewThread` cop detection is case of `Thread.start`, `Thread.fork`, or `Thread.new` with arguments. ([@viralpraxis](https://github.com/viralpraxis))
22
+ * [#36](https://github.com/rubocop/rubocop-thread_safety/pull/36): Add new `DirChdir` cop to detect `Dir.chdir` calls. ([@viralpraxis](https://github.com/viralpraxis))
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # RuboCop::ThreadSafety
2
2
 
3
3
  Thread-safety analysis for your projects, as an extension to
4
- [RuboCop](https://github.com/bbatsov/rubocop).
4
+ [RuboCop](https://github.com/rubocop/rubocop).
5
5
 
6
6
  ## Installation and Usage
7
7
 
@@ -19,11 +19,14 @@ Install it with Bundler by invoking:
19
19
 
20
20
  Add this line to your application's `.rubocop.yml`:
21
21
 
22
- require: rubocop-thread_safety
22
+ plugins: rubocop-thread_safety
23
23
 
24
24
  Now you can run `rubocop` and it will automatically load the RuboCop
25
25
  Thread-Safety cops together with the standard cops.
26
26
 
27
+ > [!NOTE]
28
+ > The plugin system is supported in RuboCop 1.72+. In earlier versions, use `require` instead of `plugins`.
29
+
27
30
  ### Scanning an application without adding it to the Gemfile
28
31
 
29
32
  Install the gem:
@@ -32,7 +35,7 @@ Install the gem:
32
35
 
33
36
  Scan the application for just thread-safety issues:
34
37
 
35
- $ rubocop -r rubocop-thread_safety --only ThreadSafety,Style/GlobalVars,Style/ClassVars,Style/MutableConstant
38
+ $ rubocop --plugin rubocop-thread_safety --only ThreadSafety,Style/GlobalVars,Style/ClassVars,Style/MutableConstant
36
39
 
37
40
  ### Configuration
38
41
 
@@ -60,6 +63,8 @@ Improvements that would make shared state thread-safe include:
60
63
  * Use [`RequestStore`](https://github.com/steveklabnik/request_store)
61
64
  * Use `Thread.current[:name]`
62
65
 
66
+ Certain system calls, such as `chdir`, affect the entire process. To avoid potential thread-safety issues, it's preferable to use (if possible) the `chdir` option in methods like `Kernel.system` and `IO.popen` rather than relying on `Dir.chdir`.
67
+
63
68
  ## Development
64
69
 
65
70
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/config/default.yml CHANGED
@@ -5,6 +5,7 @@
5
5
  ThreadSafety/ClassAndModuleAttributes:
6
6
  Description: 'Avoid mutating class and module attributes.'
7
7
  Enabled: true
8
+ ActiveSupportClassAttributeAllowed: false
8
9
 
9
10
  ThreadSafety/InstanceVariableInClassMethod:
10
11
  Description: 'Avoid using instance variables in class methods.'
@@ -30,3 +31,18 @@ ThreadSafety/NewThread:
30
31
  Avoid starting new threads.
31
32
  Let a framework like Sidekiq handle the threads.
32
33
  Enabled: true
34
+
35
+ ThreadSafety/DirChdir:
36
+ Description: Avoid using `Dir.chdir` due to its process-wide effect.
37
+ Enabled: true
38
+ AllowCallWithBlock: false
39
+
40
+ ThreadSafety/RackMiddlewareInstanceVariable:
41
+ Description: Avoid instance variables in Rack middleware.
42
+ Enabled: true
43
+ Include:
44
+ - 'app/middleware/**/*.rb'
45
+ - 'lib/middleware/**/*.rb'
46
+ - 'app/middlewares/**/*.rb'
47
+ - 'lib/middlewares/**/*.rb'
48
+ AllowedIdentifiers: []
@@ -0,0 +1,2 @@
1
+ renamed:
2
+ ThreadSafety/InstanceVariableInClassMethod: ThreadSafety/ClassInstanceVariable
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ # Common functionality for checking if a well-known operation
6
+ # produces an object with thread-safe semantics.
7
+ module OperationWithThreadsafeResult
8
+ extend NodePattern::Macros
9
+
10
+ # @!method operation_produces_threadsafe_object?(node)
11
+ def_node_matcher :operation_produces_threadsafe_object?, <<~PATTERN
12
+ {
13
+ (send (const {nil? cbase} :Queue) :new ...)
14
+ (send
15
+ (const (const {nil? cbase} :ThreadSafe) {:Hash :Array})
16
+ :new ...)
17
+ (block
18
+ (send
19
+ (const (const {nil? cbase} :ThreadSafe) {:Hash :Array})
20
+ :new ...)
21
+ ...)
22
+ (send (const (const {nil? cbase} :Concurrent) _) :new ...)
23
+ (block
24
+ (send (const (const {nil? cbase} :Concurrent) _) :new ...)
25
+ ...)
26
+ (send (const (const (const {nil? cbase} :Concurrent) _) _) :new ...)
27
+ (block
28
+ (send
29
+ (const (const (const {nil? cbase} :Concurrent) _) _)
30
+ :new ...)
31
+ ...)
32
+ (send
33
+ (const (const (const (const {nil? cbase} :Concurrent) _) _) _)
34
+ :new ...)
35
+ (block
36
+ (send
37
+ (const (const (const (const {nil? cbase} :Concurrent) _) _) _)
38
+ :new ...)
39
+ ...)
40
+ }
41
+ PATTERN
42
+ end
43
+ end
44
+ end
@@ -49,9 +49,8 @@ module RuboCop
49
49
  ...)
50
50
  MATCHER
51
51
 
52
- def on_send(node)
53
- return unless mattr?(node) || class_attr?(node) ||
54
- singleton_attr?(node)
52
+ def on_send(node) # rubocop:disable InternalAffairs/OnSendWithoutOnCSend
53
+ return unless mattr?(node) || (!class_attribute_allowed? && class_attr?(node)) || singleton_attr?(node)
55
54
 
56
55
  add_offense(node)
57
56
  end
@@ -74,6 +73,10 @@ module RuboCop
74
73
 
75
74
  false
76
75
  end
76
+
77
+ def class_attribute_allowed?
78
+ cop_config['ActiveSupportClassAttributeAllowed']
79
+ end
77
80
  end
78
81
  end
79
82
  end
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module ThreadSafety
6
- # Avoid instance variables in class methods.
6
+ # Avoid class instance variables.
7
7
  #
8
8
  # @example
9
9
  # # bad
@@ -61,8 +61,8 @@ module RuboCop
61
61
  #
62
62
  # module_function :test
63
63
  # end
64
- class InstanceVariableInClassMethod < Base
65
- MSG = 'Avoid instance variables in class methods.'
64
+ class ClassInstanceVariable < Base
65
+ MSG = 'Avoid class instance variables.'
66
66
  RESTRICT_ON_SEND = %i[
67
67
  instance_variable_set
68
68
  instance_variable_get
@@ -87,7 +87,7 @@ module RuboCop
87
87
  end
88
88
  alias on_ivasgn on_ivar
89
89
 
90
- def on_send(node)
90
+ def on_send(node) # rubocop:disable InternalAffairs/OnSendWithoutOnCSend
91
91
  return unless instance_variable_call?(node)
92
92
  return unless class_method_definition?(node)
93
93
  return if method_definition?(node)
@@ -99,21 +99,28 @@ module RuboCop
99
99
  private
100
100
 
101
101
  def class_method_definition?(node)
102
- return false if method_definition?(node)
103
-
104
102
  in_defs?(node) ||
105
103
  in_def_sclass?(node) ||
106
104
  in_def_class_methods?(node) ||
107
105
  in_def_module_function?(node) ||
106
+ in_class_eval?(node) ||
108
107
  singleton_method_definition?(node)
109
108
  end
110
109
 
111
110
  def in_defs?(node)
112
- node.ancestors.any?(&:defs_type?)
111
+ node.ancestors.any? do |ancestor|
112
+ break false if new_lexical_scope?(ancestor)
113
+
114
+ ancestor.defs_type?
115
+ end
113
116
  end
114
117
 
115
118
  def in_def_sclass?(node)
116
- defn = node.ancestors.find(&:def_type?)
119
+ defn = node.ancestors.find do |ancestor|
120
+ break if new_lexical_scope?(ancestor)
121
+
122
+ ancestor.def_type?
123
+ end
117
124
 
118
125
  defn&.ancestors&.any?(&:sclass_type?)
119
126
  end
@@ -124,35 +131,51 @@ module RuboCop
124
131
 
125
132
  def in_def_class_methods_dsl?(node)
126
133
  node.ancestors.any? do |ancestor|
134
+ break if new_lexical_scope?(ancestor)
127
135
  next unless ancestor.block_type?
128
- next unless ancestor.children.first.is_a? AST::SendNode
129
136
 
130
137
  ancestor.children.first.command? :class_methods
131
138
  end
132
139
  end
133
140
 
134
141
  def in_def_class_methods_module?(node)
135
- defn = node.ancestors.find(&:def_type?)
136
- return unless defn
142
+ defn = node.ancestors.find do |ancestor|
143
+ break if new_lexical_scope?(ancestor)
144
+
145
+ ancestor.def_type?
146
+ end
147
+ return false unless defn
137
148
 
138
149
  mod = defn.ancestors.find do |ancestor|
139
150
  %i[class module].include?(ancestor.type)
140
151
  end
141
- return unless mod
152
+ return false unless mod
142
153
 
143
154
  class_methods_module?(mod)
144
155
  end
145
156
 
146
157
  def in_def_module_function?(node)
147
158
  defn = node.ancestors.find(&:def_type?)
148
- return unless defn
159
+ return false unless defn
149
160
 
150
161
  defn.left_siblings.any? { |sibling| module_function_bare_access_modifier?(sibling) } ||
151
162
  defn.right_siblings.any? { |sibling| module_function_for?(sibling, defn.method_name) }
152
163
  end
153
164
 
165
+ def in_class_eval?(node)
166
+ defn = node.ancestors.find do |ancestor|
167
+ break if ancestor.def_type? || new_lexical_scope?(ancestor)
168
+
169
+ ancestor.block_type?
170
+ end
171
+ return false unless defn
172
+
173
+ class_eval_scope?(defn)
174
+ end
175
+
154
176
  def singleton_method_definition?(node)
155
177
  node.ancestors.any? do |ancestor|
178
+ break if new_lexical_scope?(ancestor)
156
179
  next unless ancestor.children.first.is_a? AST::SendNode
157
180
 
158
181
  ancestor.children.first.command? :define_singleton_method
@@ -161,6 +184,7 @@ module RuboCop
161
184
 
162
185
  def method_definition?(node)
163
186
  node.ancestors.any? do |ancestor|
187
+ break if new_lexical_scope?(ancestor)
164
188
  next unless ancestor.children.first.is_a? AST::SendNode
165
189
 
166
190
  ancestor.children.first.command? :define_method
@@ -199,6 +223,36 @@ module RuboCop
199
223
  def_node_matcher :module_function_for?, <<~PATTERN
200
224
  (send nil? {:module_function} ({sym str} #match_name?(%1)))
201
225
  PATTERN
226
+
227
+ # @!method new_lexical_scope?(node)
228
+ def_node_matcher :new_lexical_scope?, <<~PATTERN
229
+ {
230
+ (block (send (const nil? :Struct) :new ...) _ ({def defs} ...))
231
+ (block (send (const nil? :Class) :new ...) _ ({def defs} ...))
232
+ (block (send (const nil? :Data) :define ...) _ ({def defs} ...))
233
+ (block
234
+ (send nil?
235
+ {
236
+ :prepend_around_action
237
+ :prepend_before_action
238
+ :before_action
239
+ :append_before_action
240
+ :around_action
241
+ :append_around_action
242
+ :append_after_action
243
+ :after_action
244
+ :prepend_after_action
245
+ }
246
+ )
247
+ ...
248
+ )
249
+ }
250
+ PATTERN
251
+
252
+ # @!method class_eval_scope?(node)
253
+ def_node_matcher :class_eval_scope?, <<~PATTERN
254
+ (block (send (const {nil? cbase} _) {:class_eval :class_exec}) ...)
255
+ PATTERN
202
256
  end
203
257
  end
204
258
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module ThreadSafety
6
+ # Avoid using `Dir.chdir` due to its process-wide effect.
7
+ # If `AllowCallWithBlock` (disabled by default) option is enabled,
8
+ # calling `Dir.chdir` with block will be allowed.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # Dir.chdir("/var/run")
13
+ #
14
+ # # bad
15
+ # FileUtils.chdir("/var/run")
16
+ #
17
+ # @example AllowCallWithBlock: false (default)
18
+ # # good
19
+ # Dir.chdir("/var/run") do
20
+ # puts Dir.pwd
21
+ # end
22
+ #
23
+ # @example AllowCallWithBlock: true
24
+ # # bad
25
+ # Dir.chdir("/var/run") do
26
+ # puts Dir.pwd
27
+ # end
28
+ #
29
+ class DirChdir < Base
30
+ MESSAGE = 'Avoid using `%<module>s%<dot>s%<method>s` due to its process-wide effect.'
31
+ RESTRICT_ON_SEND = %i[chdir cd].freeze
32
+
33
+ # @!method chdir?(node)
34
+ def_node_matcher :chdir?, <<~MATCHER
35
+ {
36
+ (call (const {nil? cbase} {:Dir :FileUtils}) :chdir ...)
37
+ (call (const {nil? cbase} :FileUtils) :cd ...)
38
+ }
39
+ MATCHER
40
+
41
+ def on_send(node)
42
+ return unless chdir?(node)
43
+ return if allow_call_with_block? && (node.block_argument? || node.parent&.block_type?)
44
+
45
+ add_offense(
46
+ node,
47
+ message: format(
48
+ MESSAGE,
49
+ module: node.receiver.short_name,
50
+ method: node.method_name,
51
+ dot: node.loc.dot.source
52
+ )
53
+ )
54
+ end
55
+ alias on_csend on_send
56
+
57
+ private
58
+
59
+ def allow_call_with_block?
60
+ !!cop_config['AllowCallWithBlock']
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -7,7 +7,7 @@ module RuboCop
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
@@ -74,8 +74,10 @@ module RuboCop
74
74
  # end
75
75
  class MutableClassInstanceVariable < Base
76
76
  extend AutoCorrector
77
+
77
78
  include FrozenStringLiteral
78
79
  include ConfigurableEnforcedStyle
80
+ include OperationWithThreadsafeResult
79
81
 
80
82
  MSG = 'Freeze mutable objects assigned to class instance variables.'
81
83
  FROZEN_STRING_LITERAL_TYPES_RUBY27 = %i[str dstr].freeze
@@ -84,22 +86,20 @@ module RuboCop
84
86
  def on_ivasgn(node)
85
87
  return unless in_class?(node)
86
88
 
87
- _, value = *node
88
- on_assignment(value)
89
+ on_assignment(node.expression)
89
90
  end
90
91
 
91
92
  def on_or_asgn(node)
92
- lhs, value = *node
93
- return unless lhs&.ivasgn_type?
93
+ return unless node.assignment_node.ivasgn_type?
94
94
  return unless in_class?(node)
95
95
 
96
- on_assignment(value)
96
+ on_assignment(node.expression)
97
97
  end
98
98
 
99
99
  def on_masgn(node)
100
100
  return unless in_class?(node)
101
101
 
102
- mlhs, values = *node
102
+ mlhs, values = *node # rubocop:disable InternalAffairs/NodeDestructuring
103
103
  return unless values.array_type?
104
104
 
105
105
  mlhs.to_a.zip(values.to_a).each do |lhs, value|
@@ -183,7 +183,7 @@ module RuboCop
183
183
  end
184
184
 
185
185
  def mutable_literal?(node)
186
- return if node.nil?
186
+ return false if node.nil?
187
187
 
188
188
  node.mutable_literal? || range_type?(node)
189
189
  end
@@ -198,7 +198,7 @@ module RuboCop
198
198
  end
199
199
 
200
200
  def range_type?(node)
201
- node.erange_type? || node.irange_type?
201
+ node.type?(:erange, :irange)
202
202
  end
203
203
 
204
204
  def correct_splat_expansion(corrector, expr, splat_value)
@@ -243,42 +243,9 @@ module RuboCop
243
243
  }
244
244
  PATTERN
245
245
 
246
- # @!method operation_produces_threadsafe_object?(node)
247
- def_node_matcher :operation_produces_threadsafe_object?, <<~PATTERN
248
- {
249
- (send (const {nil? cbase} :Queue) :new ...)
250
- (send
251
- (const (const {nil? cbase} :ThreadSafe) {:Hash :Array})
252
- :new ...)
253
- (block
254
- (send
255
- (const (const {nil? cbase} :ThreadSafe) {:Hash :Array})
256
- :new ...)
257
- ...)
258
- (send (const (const {nil? cbase} :Concurrent) _) :new ...)
259
- (block
260
- (send (const (const {nil? cbase} :Concurrent) _) :new ...)
261
- ...)
262
- (send (const (const (const {nil? cbase} :Concurrent) _) _) :new ...)
263
- (block
264
- (send
265
- (const (const (const {nil? cbase} :Concurrent) _) _)
266
- :new ...)
267
- ...)
268
- (send
269
- (const (const (const (const {nil? cbase} :Concurrent) _) _) _)
270
- :new ...)
271
- (block
272
- (send
273
- (const (const (const (const {nil? cbase} :Concurrent) _) _) _)
274
- :new ...)
275
- ...)
276
- }
277
- PATTERN
278
-
279
246
  # @!method range_enclosed_in_parentheses?(node)
280
247
  def_node_matcher :range_enclosed_in_parentheses?, <<~PATTERN
281
- (begin ({irange erange} _ _))
248
+ (begin (range _ _))
282
249
  PATTERN
283
250
  end
284
251
  end
@@ -12,18 +12,17 @@ module RuboCop
12
12
  # Thread.new { do_work }
13
13
  class NewThread < Base
14
14
  MSG = 'Avoid starting new threads.'
15
- RESTRICT_ON_SEND = %i[new].freeze
15
+ RESTRICT_ON_SEND = %i[new fork start].freeze
16
16
 
17
17
  # @!method new_thread?(node)
18
18
  def_node_matcher :new_thread?, <<~MATCHER
19
- (send (const {nil? cbase} :Thread) :new)
19
+ (call (const {nil? cbase} :Thread) {:new :fork :start} ...)
20
20
  MATCHER
21
21
 
22
22
  def on_send(node)
23
- return unless new_thread?(node)
24
-
25
- add_offense(node)
23
+ new_thread?(node) { add_offense(node) }
26
24
  end
25
+ alias on_csend on_send
27
26
  end
28
27
  end
29
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,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module ThreadSafety
5
- VERSION = '0.5.1'
5
+ VERSION = '0.7.0'
6
6
  end
7
7
  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.5.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Gee
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2023-03-29 00:00:00.000000000 Z
10
+ date: 2025-02-22 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.90.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.90.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,37 +52,31 @@ executables: []
109
52
  extensions: []
110
53
  extra_rdoc_files: []
111
54
  files:
112
- - ".github/dependabot.yml"
113
- - ".github/workflows/ci.yml"
114
- - ".github/workflows/lint.yml"
115
- - ".gitignore"
116
- - ".rspec"
117
- - ".rubocop.yml"
118
- - ".rubocop_todo.yml"
119
- - Appraisals
120
- - Gemfile
55
+ - CHANGELOG.md
121
56
  - LICENSE.txt
122
57
  - README.md
123
- - Rakefile
124
- - bin/console
125
- - bin/setup
126
58
  - config/default.yml
127
- - gemfiles/rubocop_0.90.gemfile
128
- - gemfiles/rubocop_1.20.gemfile
59
+ - config/obsoletion.yml
129
60
  - lib/rubocop-thread_safety.rb
61
+ - lib/rubocop/cop/mixin/operation_with_threadsafe_result.rb
130
62
  - lib/rubocop/cop/thread_safety/class_and_module_attributes.rb
131
- - 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
132
65
  - lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb
133
66
  - lib/rubocop/cop/thread_safety/new_thread.rb
67
+ - lib/rubocop/cop/thread_safety/rack_middleware_instance_variable.rb
134
68
  - lib/rubocop/thread_safety.rb
135
- - lib/rubocop/thread_safety/inject.rb
69
+ - lib/rubocop/thread_safety/plugin.rb
136
70
  - lib/rubocop/thread_safety/version.rb
137
- - rubocop-thread_safety.gemspec
138
71
  homepage: https://github.com/rubocop/rubocop-thread_safety
139
72
  licenses:
140
73
  - MIT
141
- metadata: {}
142
- 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
143
80
  rdoc_options: []
144
81
  require_paths:
145
82
  - lib
@@ -147,15 +84,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
147
84
  requirements:
148
85
  - - ">="
149
86
  - !ruby/object:Gem::Version
150
- version: 2.5.0
87
+ version: 2.7.0
151
88
  required_rubygems_version: !ruby/object:Gem::Requirement
152
89
  requirements:
153
90
  - - ">="
154
91
  - !ruby/object:Gem::Version
155
92
  version: '0'
156
93
  requirements: []
157
- rubygems_version: 3.3.26
158
- signing_key:
94
+ rubygems_version: 3.6.3
159
95
  specification_version: 4
160
96
  summary: Thread-safety checks via static analysis
161
97
  test_files: []
@@ -1,6 +0,0 @@
1
- version: 2
2
- updates:
3
- - package-ecosystem: 'github-actions'
4
- directory: '/'
5
- schedule:
6
- interval: 'weekly'
@@ -1,26 +0,0 @@
1
- name: CI
2
-
3
- on: [push, pull_request]
4
-
5
- jobs:
6
- test:
7
-
8
- runs-on: ubuntu-latest
9
-
10
- strategy:
11
- fail-fast: false
12
- matrix:
13
- ruby: ["2.5", "2.6", "2.7", "3.0", "3.1", "3.2", ruby-head, jruby-9.2, jruby-9.3]
14
- rubocop_version: ["0.90", "1.20"]
15
- env:
16
- BUNDLE_GEMFILE: "gemfiles/rubocop_${{ matrix.rubocop_version }}.gemfile"
17
- steps:
18
- - uses: actions/checkout@v3
19
- - name: Set up Ruby
20
- uses: ruby/setup-ruby@v1
21
- with:
22
- bundler-cache: true # 'bundle install' and cache gems
23
- ruby-version: ${{ matrix.ruby }}
24
- bundler: 2.3.26
25
- - name: Run tests
26
- run: bundle exec rspec
@@ -1,19 +0,0 @@
1
- name: Lint
2
-
3
- on: [push, pull_request]
4
-
5
- jobs:
6
- lint:
7
-
8
- runs-on: ubuntu-latest
9
- name: Rubocop
10
-
11
- steps:
12
- - uses: actions/checkout@v3
13
- - name: Set up Ruby
14
- uses: ruby/setup-ruby@v1
15
- with:
16
- bundler-cache: true # 'bundle install' and cache gems
17
- ruby-version: "2.7"
18
- - name: Run Rubocop
19
- run: bundle exec rubocop
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
data/.rubocop.yml DELETED
@@ -1,79 +0,0 @@
1
- inherit_from: .rubocop_todo.yml
2
-
3
- require:
4
- - rubocop/cop/internal_affairs
5
-
6
- AllCops:
7
- DisplayCopNames: true
8
- TargetRubyVersion: 2.5
9
-
10
- Lint/RaiseException:
11
- Enabled: true
12
-
13
- Lint/StructNewOverride:
14
- Enabled: true
15
-
16
- Metrics/BlockLength:
17
- Exclude:
18
- - "spec/**/*"
19
-
20
- Metrics/ClassLength:
21
- Enabled: false
22
-
23
- Metrics/MethodLength:
24
- Max: 14
25
-
26
- Naming/FileName:
27
- Exclude:
28
- - lib/rubocop-thread_safety.rb
29
- - rubocop-thread_safety.gemspec
30
-
31
- # Enable more cops that are disabled by default:
32
-
33
- Style/AutoResourceCleanup:
34
- Enabled: true
35
-
36
- Style/CollectionMethods:
37
- Enabled: true
38
-
39
- Style/FormatStringToken:
40
- Exclude:
41
- - spec/**/*
42
-
43
- Style/FrozenStringLiteralComment:
44
- Exclude:
45
- - "gemfiles/*.gemfile"
46
-
47
- Style/HashEachMethods:
48
- Enabled: true
49
-
50
- Style/HashTransformKeys:
51
- Enabled: false
52
-
53
- Style/HashTransformValues:
54
- Enabled: false
55
-
56
- Style/MethodCalledOnDoEndBlock:
57
- Enabled: true
58
- Exclude:
59
- - spec/**/*
60
-
61
- Style/MissingElse:
62
- Enabled: true
63
- EnforcedStyle: case
64
-
65
- Style/OptionHash:
66
- Enabled: true
67
-
68
- Style/Send:
69
- Enabled: true
70
-
71
- Style/StringLiterals:
72
- Exclude:
73
- - "gemfiles/*.gemfile"
74
-
75
- Style/StringMethods:
76
- Enabled: true
77
-
78
- Style/SymbolArray:
79
- Enabled: true
data/.rubocop_todo.yml DELETED
@@ -1,12 +0,0 @@
1
- # This configuration was generated by
2
- # `rubocop --auto-gen-config --no-auto-gen-timestamp`
3
- # using RuboCop version 1.12.1.
4
- # The point is for the user to remove these configuration records
5
- # one by one as the offenses are removed from the code base.
6
- # Note that changes in the inspected code, or installation of new
7
- # versions of RuboCop, may require this file to be generated again.
8
-
9
- # Offense count: 3
10
- InternalAffairs/NodeDestructuring:
11
- Exclude:
12
- - 'lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb'
data/Appraisals DELETED
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- appraise 'rubocop-0.90' do
4
- gem 'rubocop', '~> 0.90.0'
5
- end
6
-
7
- appraise 'rubocop-1.20' do
8
- gem 'rubocop', '~> 1.20.0'
9
- end
data/Gemfile DELETED
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source 'https://rubygems.org'
4
-
5
- # Specify your gem's dependencies in rubocop-thread_safety.gemspec
6
- gemspec
data/Rakefile DELETED
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'bundler/gem_tasks'
4
- require 'rspec/core/rake_task'
5
-
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- task default: :spec
data/bin/console DELETED
@@ -1,15 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- # frozen_string_literal: true
4
-
5
- require 'bundler/setup'
6
- require 'rubocop-thread_safety'
7
-
8
- # You can add fixtures and/or initialization code here to make experimenting
9
- # with your gem easier. You can also use a different console, if you like.
10
-
11
- require 'pry'
12
- Pry.start
13
-
14
- # require "irb"
15
- # IRB.start
data/bin/setup DELETED
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
@@ -1,7 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "rubocop", "~> 0.90.0"
6
-
7
- gemspec path: "../"
@@ -1,7 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "rubocop", "~> 1.20.0"
6
-
7
- gemspec path: "../"
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # The original code is from https://github.com/rubocop-hq/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb
4
- # See https://github.com/rubocop-hq/rubocop-rspec/blob/master/MIT_LICENSE.md
5
- module RuboCop
6
- module ThreadSafety
7
- # Because RuboCop doesn't yet support plugins, we have to monkey patch in a
8
- # bit of our configuration.
9
- module Inject
10
- def self.defaults!
11
- path = CONFIG_DEFAULT.to_s
12
- hash = ConfigLoader.__send__(:load_yaml_configuration, path)
13
- config = Config.new(hash, path).tap(&:make_excludes_absolute)
14
- puts "configuration from \#{path}" if ConfigLoader.debug?
15
- config = ConfigLoader.merge_with_default(config, path)
16
- ConfigLoader.instance_variable_set(:@default_configuration, config)
17
- end
18
- end
19
- end
20
- end
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- lib = File.expand_path('lib', __dir__)
4
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
- require 'rubocop/thread_safety/version'
6
-
7
- Gem::Specification.new do |spec|
8
- spec.name = 'rubocop-thread_safety'
9
- spec.version = RuboCop::ThreadSafety::VERSION
10
- spec.authors = ['Michael Gee']
11
- spec.email = ['michaelpgee@gmail.com']
12
-
13
- spec.summary = 'Thread-safety checks via static analysis'
14
- spec.description = <<-DESCRIPTION
15
- Thread-safety checks via static analysis.
16
- A plugin for the RuboCop code style enforcing & linting tool.
17
- DESCRIPTION
18
- spec.homepage = 'https://github.com/rubocop/rubocop-thread_safety'
19
- spec.licenses = ['MIT']
20
-
21
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
22
- f.match(%r{^(test|spec|features)/})
23
- end
24
-
25
- spec.bindir = 'exe'
26
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
- spec.require_paths = ['lib']
28
-
29
- spec.required_ruby_version = '>= 2.5.0'
30
-
31
- spec.add_runtime_dependency 'rubocop', '>= 0.90.0'
32
-
33
- spec.add_development_dependency 'appraisal'
34
- spec.add_development_dependency 'bundler', '>= 1.10', '< 3'
35
- spec.add_development_dependency 'pry' unless ENV['CI']
36
- spec.add_development_dependency 'rake', '>= 10.0'
37
- spec.add_development_dependency 'rspec', '~> 3.0'
38
- end