rubocop-thread_safety 0.5.1 → 0.6.0

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