rubocop-thread_safety 0.5.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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