rubocop-thread_safety 0.5.1 → 0.6.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.
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file was generated by Appraisal
4
+
5
+ source 'https://rubygems.org'
6
+
7
+ gem 'appraisal'
8
+ gem 'bundler', '>= 1.10', '< 3'
9
+ gem 'prism', '~> 1.2.0'
10
+ gem 'pry'
11
+ gem 'rake', '>= 10.0'
12
+ gem 'rspec', '~> 3.0'
13
+ gem 'rubocop', '~> 1.66.0'
14
+ gem 'rubocop-rake', '~> 0.6.0'
15
+ gem 'rubocop-rspec'
16
+ gem 'simplecov'
17
+ gem 'yard'
18
+
19
+ gemspec path: '../'
@@ -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
@@ -50,8 +50,7 @@ module RuboCop
50
50
  MATCHER
51
51
 
52
52
  def on_send(node)
53
- return unless mattr?(node) || class_attr?(node) ||
54
- singleton_attr?(node)
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
@@ -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,37 @@
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
+ #
8
+ # @example
9
+ # # bad
10
+ # Dir.chdir("/var/run")
11
+ #
12
+ # # bad
13
+ # FileUtils.chdir("/var/run")
14
+ class DirChdir < Base
15
+ MESSAGE = 'Avoid using `%<module>s.%<method>s` due to its process-wide effect.'
16
+ RESTRICT_ON_SEND = %i[chdir cd].freeze
17
+
18
+ # @!method chdir?(node)
19
+ def_node_matcher :chdir?, <<~MATCHER
20
+ {
21
+ (send (const {nil? cbase} {:Dir :FileUtils}) :chdir ...)
22
+ (send (const {nil? cbase} :FileUtils) :cd ...)
23
+ }
24
+ MATCHER
25
+
26
+ def on_send(node)
27
+ chdir?(node) do
28
+ add_offense(
29
+ node,
30
+ message: format(MESSAGE, module: node.receiver.short_name, method: node.method_name)
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ 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
@@ -243,39 +243,6 @@ 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
248
  (begin ({irange erange} _ _))
@@ -12,17 +12,15 @@ 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
+ (send (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
27
25
  end
28
26
  end
@@ -0,0 +1,120 @@
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&.sym_type? || argument&.str_type?
92
+ return if allowed_identifier?(argument.value)
93
+
94
+ add_offense node
95
+ end
96
+
97
+ private
98
+
99
+ def find_constructor_method(class_node)
100
+ class_node
101
+ .each_node(:def)
102
+ .find { |node| node.method?(:initialize) && node.arguments.size >= 1 }
103
+ end
104
+
105
+ def extract_application_variable_from_contructor_method(constructor_method)
106
+ constructor_method
107
+ .then { |node| app_variable(node) }
108
+ .then { |variables| variables.first[1] if variables.first }
109
+ end
110
+
111
+ def extract_safe_variables_from_constructor_method(constructor_method)
112
+ constructor_method
113
+ .each_node(:ivasgn)
114
+ .select { |ivasgn_node| operation_produces_threadsafe_object?(ivasgn_node.to_a[1]) }
115
+ .map { _1.to_a[0] }
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
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
3
+ # The original code is from https://github.com/rubocop/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb
4
+ # See https://github.com/rubocop/rubocop-rspec/blob/master/MIT-LICENSE.md
5
5
  module RuboCop
6
6
  module ThreadSafety
7
7
  # Because RuboCop doesn't yet support plugins, we have to monkey patch in a
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module ThreadSafety
5
- VERSION = '0.5.1'
5
+ VERSION = '0.6.0'
6
6
  end
7
7
  end
@@ -8,5 +8,7 @@ module RuboCop
8
8
  CONFIG = YAML.safe_load(CONFIG_DEFAULT.read).freeze
9
9
 
10
10
  private_constant(:CONFIG_DEFAULT, :PROJECT_ROOT)
11
+
12
+ ::RuboCop::ConfigObsoletion.files << PROJECT_ROOT.join('config', 'obsoletion.yml')
11
13
  end
12
14
  end
@@ -8,7 +8,11 @@ require 'rubocop/thread_safety/inject'
8
8
 
9
9
  RuboCop::ThreadSafety::Inject.defaults!
10
10
 
11
- require 'rubocop/cop/thread_safety/instance_variable_in_class_method'
11
+ require 'rubocop/cop/mixin/operation_with_threadsafe_result'
12
+
13
+ require 'rubocop/cop/thread_safety/class_instance_variable'
12
14
  require 'rubocop/cop/thread_safety/class_and_module_attributes'
13
15
  require 'rubocop/cop/thread_safety/mutable_class_instance_variable'
14
16
  require 'rubocop/cop/thread_safety/new_thread'
17
+ require 'rubocop/cop/thread_safety/dir_chdir'
18
+ require 'rubocop/cop/thread_safety/rack_middleware_instance_variable'
@@ -22,17 +22,18 @@ Gem::Specification.new do |spec|
22
22
  f.match(%r{^(test|spec|features)/})
23
23
  end
24
24
 
25
+ spec.metadata = {
26
+ 'changelog_uri' => 'https://github.com/rubocop/rubocop-thread_safety/blob/master/CHANGELOG.md',
27
+ 'source_code_uri' => 'https://github.com/rubocop/rubocop-thread_safety',
28
+ 'bug_tracker_uri' => 'https://github.com/rubocop/rubocop-thread_safety/issues',
29
+ 'rubygems_mfa_required' => 'true'
30
+ }
31
+
25
32
  spec.bindir = 'exe'
26
33
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
34
  spec.require_paths = ['lib']
28
35
 
29
- spec.required_ruby_version = '>= 2.5.0'
30
-
31
- spec.add_runtime_dependency 'rubocop', '>= 0.90.0'
36
+ spec.required_ruby_version = '>= 2.7.0'
32
37
 
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
+ spec.add_dependency 'rubocop', '>= 1.48.1'
38
39
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+ require 'rubocop-thread_safety'
5
+ require 'rubocop/cops_documentation_generator'
6
+ require 'yard'
7
+
8
+ YARD::Rake::YardocTask.new(:yard_for_generate_documentation) do |task|
9
+ task.files = ['lib/rubocop/cop/**/*.rb']
10
+ task.options = ['--no-output']
11
+ end
12
+
13
+ desc 'Generate docs of all cops departments'
14
+ task generate_cops_documentation: :yard_for_generate_documentation do
15
+ deps = ['ThreadSafety']
16
+ CopsDocumentationGenerator.new(departments: deps).call
17
+ end
18
+
19
+ desc 'Syntax check for the documentation comments'
20
+ task documentation_syntax_check: :yard_for_generate_documentation do
21
+ require 'parser/ruby31'
22
+
23
+ ok = true
24
+ YARD::Registry.load!
25
+ cops = RuboCop::Cop::Registry.global
26
+ cops.each do |cop|
27
+ examples = YARD::Registry.all(:class).find do |code_object|
28
+ next unless RuboCop::Cop::Badge.for(code_object.to_s) == cop.badge
29
+
30
+ break code_object.tags('example')
31
+ end
32
+
33
+ examples.to_a.each do |example|
34
+ buffer = Parser::Source::Buffer.new('<code>', 1)
35
+ buffer.source = example.text
36
+ parser = Parser::Ruby31.new(RuboCop::AST::Builder.new)
37
+ parser.diagnostics.all_errors_are_fatal = true
38
+ parser.parse(buffer)
39
+ rescue Parser::SyntaxError => e
40
+ path = example.object.file
41
+ puts "#{path}: Syntax Error in an example. #{e}"
42
+ ok = false
43
+ end
44
+ end
45
+ abort unless ok
46
+ end