rubocop-thread_safety 0.3.4 → 0.4.3

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
- SHA1:
3
- metadata.gz: 8a02cd99185995c9ca8f553177ed0036e4dc8c4c
4
- data.tar.gz: 2008714ed96b7a96406062af5e11d89842eb2e1d
2
+ SHA256:
3
+ metadata.gz: 660c4521fd4055a7a13af1cebe1d9e53131dcdc668ff8f76e121337fda376aec
4
+ data.tar.gz: 8d1e6713ce9d79d70ec461b547018b29b651cbbf25d5176741d21841c9b6eee0
5
5
  SHA512:
6
- metadata.gz: '0982b0a820e6d58d183c01f42ff8da9d5071bb02ee0c2fc16bf81c9efa52d16b8a928ca265bcf00397ca1f49fe99132949fb8e7c43907f7441644ad25c3a7f31'
7
- data.tar.gz: 16d36ef00a8eb2010431f968521702f934d4163c97c33a68fd236f2025e41c05a6ef70c1ae80f7e2202a53356539d5cfdf7e37085d6c12d360922dd577a40193
6
+ metadata.gz: 715c83c5a8b65e8ea4ff968c106f4e067457e88a4e8d2fe3849b6d8361c2007f7dfcc1c1b0f5bbb0051c6311519ade6f3b7bddc1598348dd31fee58e7d3ea4d6
7
+ data.tar.gz: 33f0bd084a199b296baf3b03ba2cc8224eab161ebc821dd7052242eb62d649a084209fe73e2b26f08dddd7dfd907d6e69a73f153453b32618a4f8ad1f30df412
data/.gitignore CHANGED
@@ -1,6 +1,7 @@
1
1
  /.bundle/
2
2
  /.yardoc
3
3
  /Gemfile.lock
4
+ /gemfiles/*.lock
4
5
  /_yardoc/
5
6
  /coverage/
6
7
  /doc/
data/.rubocop.yml CHANGED
@@ -1,15 +1,23 @@
1
1
  AllCops:
2
2
  DisplayCopNames: true
3
- Include:
4
- - Gemfile
5
- - Rakefile
6
- Exclude:
7
- - vendor/**/*
3
+ TargetRubyVersion: 2.3
4
+
5
+ Lint/RaiseException:
6
+ Enabled: true
7
+
8
+ Lint/StructNewOverride:
9
+ Enabled: true
8
10
 
9
11
  Metrics/BlockLength:
10
12
  Exclude:
11
13
  - "spec/**/*"
12
14
 
15
+ Metrics/ClassLength:
16
+ Enabled: false
17
+
18
+ Metrics/MethodLength:
19
+ Max: 14
20
+
13
21
  Naming/FileName:
14
22
  Exclude:
15
23
  - lib/rubocop-thread_safety.rb
@@ -23,6 +31,19 @@ Style/AutoResourceCleanup:
23
31
  Style/CollectionMethods:
24
32
  Enabled: true
25
33
 
34
+ Style/FrozenStringLiteralComment:
35
+ Exclude:
36
+ - "gemfiles/*.gemfile"
37
+
38
+ Style/HashEachMethods:
39
+ Enabled: true
40
+
41
+ Style/HashTransformKeys:
42
+ Enabled: false
43
+
44
+ Style/HashTransformValues:
45
+ Enabled: false
46
+
26
47
  Style/MethodCalledOnDoEndBlock:
27
48
  Enabled: true
28
49
  Exclude:
@@ -38,6 +59,10 @@ Style/OptionHash:
38
59
  Style/Send:
39
60
  Enabled: true
40
61
 
62
+ Style/StringLiterals:
63
+ Exclude:
64
+ - "gemfiles/*.gemfile"
65
+
41
66
  Style/StringMethods:
42
67
  Enabled: true
43
68
 
data/.travis.yml CHANGED
@@ -1,19 +1,64 @@
1
- sudo: false
2
1
  cache: bundler
3
2
  language: ruby
4
3
  rvm:
5
- - jruby-9.1.14.0
6
- - 2.1
7
- - 2.2
4
+ - jruby-9.2.9.0
8
5
  - 2.3.0
9
6
  - 2.4
7
+ - 2.5
8
+ - 2.6
9
+ - 2.7
10
+ - 3.0
10
11
 
11
- matrix:
12
+ gemfile:
13
+ - gemfiles/rubocop_0.53.gemfile
14
+ - gemfiles/rubocop_0.81.gemfile
15
+ - gemfiles/rubocop_0.86.gemfile
16
+ - gemfiles/rubocop_1.20.gemfile
17
+
18
+ script_rubocop: &script_rubocop
19
+ - bundle exec rspec
20
+ - bundle exec rubocop
21
+
22
+ jobs:
12
23
  fast_finish: true
24
+ exclude:
25
+ - rvm: 2.3.0
26
+ gemfile: gemfiles/rubocop_0.86.gemfile
27
+ - rvm: 2.3.0
28
+ gemfile: gemfiles/rubocop_1.20.gemfile
29
+ - rvm: 2.5
30
+ gemfile: gemfiles/rubocop_0.53.gemfile
31
+ - rvm: 2.6
32
+ gemfile: gemfiles/rubocop_0.53.gemfile
33
+ - rvm: 2.7
34
+ gemfile: gemfiles/rubocop_0.53.gemfile
35
+ - rvm: 3.0
36
+ gemfile: gemfiles/rubocop_0.53.gemfile
37
+ include:
38
+ - rvm: jruby-9.2.9.0
39
+ gemfile: gemfiles/rubocop_0.81.gemfile
40
+ script: *script_rubocop
41
+ - rvm: 2.3.0
42
+ gemfile: gemfiles/rubocop_0.81.gemfile
43
+ script: *script_rubocop
44
+ - rvm: 2.4
45
+ gemfile: gemfiles/rubocop_0.81.gemfile
46
+ script: *script_rubocop
47
+ - rvm: 2.5
48
+ gemfile: gemfiles/rubocop_0.81.gemfile
49
+ script: *script_rubocop
50
+ - rvm: 2.6
51
+ gemfile: gemfiles/rubocop_0.81.gemfile
52
+ script: *script_rubocop
53
+ - rvm: 2.7
54
+ gemfile: gemfiles/rubocop_0.81.gemfile
55
+ script: *script_rubocop
56
+ - rvm: 3.0
57
+ gemfile: gemfiles/rubocop_0.81.gemfile
58
+ script: *script_rubocop
13
59
 
14
60
  before_install: gem install --remote bundler
15
61
  install:
16
62
  - bundle install --retry=3
17
63
  script:
18
64
  - bundle exec rspec
19
- - bundle exec rubocop
data/Appraisals ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ appraise 'rubocop-0.53' do
4
+ gem 'rubocop', '~> 0.53.0'
5
+ end
6
+
7
+ appraise 'rubocop-0.81' do
8
+ gem 'rubocop', '~> 0.81.0'
9
+ end
10
+
11
+ if Gem::Requirement.new('>= 2.4.0')
12
+ .satisfied_by?(Gem::Version.new(RUBY_VERSION))
13
+ appraise 'rubocop-0.86' do
14
+ gem 'rubocop', '~> 0.86.0'
15
+ end
16
+ end
17
+
18
+ if Gem::Requirement.new('>= 2.5.0')
19
+ .satisfied_by?(Gem::Version.new(RUBY_VERSION))
20
+ appraise 'rubocop-1.20' do
21
+ gem 'rubocop', '~> 1.20.0'
22
+ end
23
+ end
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in rubocop-thread_safety.gemspec
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2016-2017 CoverMyMeds
1
+ Copyright 2016-2021 CoverMyMeds
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
4
 
data/README.md CHANGED
@@ -34,6 +34,32 @@ Scan the application for just thread-safety issues:
34
34
 
35
35
  $ rubocop -r rubocop-thread_safety --only ThreadSafety,Style/GlobalVars,Style/ClassVars,Style/MutableConstant
36
36
 
37
+ ### Configuration
38
+
39
+ There are some added [configuration options](https://github.com/covermymeds/rubocop-thread_safety/blob/master/config/default.yml) that can be tweaked to modify the behaviour of these thread-safety cops.
40
+
41
+ ### Correcting code for thread-safety
42
+
43
+ There are a few ways to improve thread-safety that stem around avoiding
44
+ unsynchronized mutation of state that is shared between multiple threads.
45
+
46
+ State shared between threads may take various forms, including:
47
+
48
+ * Class variables (`@@name`). Note: these affect child classes too.
49
+ * Class instance variables (`@name` in class context or class methods)
50
+ * Constants (`NAME`). Ruby will warn if a constant is re-assigned to a new value but will allow it. Mutable objects can still be mutated (e.g. push to an array) even if they are assigned to a constant.
51
+ * Globals (`$name`), with the possible exception of some special globals provided by ruby that are documented as thread-local like regular expression results.
52
+ * Variables in the scope of created threads (where `Thread.new` is called).
53
+
54
+ Improvements that would make shared state thread-safe include:
55
+
56
+ * `freeze` objects to protect against mutation. Note: `freeze` is shallow, i.e. freezing an array will not also freeze its elements.
57
+ * Use data structures or concurrency abstractions from [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby), e.g. `Concurrent::Map`
58
+ * Use a `Mutex` or similar to `synchronize` access.
59
+ * Use [`ActiveSupport::CurrentAttributes`](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html)
60
+ * Use [`RequestStore`](https://github.com/steveklabnik/request_store)
61
+ * Use `Thread.current[:name]`
62
+
37
63
  ## Development
38
64
 
39
65
  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.
@@ -46,5 +72,5 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/coverm
46
72
 
47
73
  ## Copyright
48
74
 
49
- Copyright (c) 2016-2017 CoverMyMeds.
75
+ Copyright (c) 2016-2021 CoverMyMeds.
50
76
  See [LICENSE.txt](LICENSE.txt) for further details.
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
  require 'rspec/core/rake_task'
3
5
 
data/bin/console CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ # frozen_string_literal: true
4
+
3
5
  require 'bundler/setup'
4
6
  require 'rubocop-thread_safety'
5
7
 
@@ -0,0 +1,31 @@
1
+ # Additional configuration for thread_safety cops
2
+ #
3
+ # Without adding these to your rubocop config, these values will be the default.
4
+
5
+ ThreadSafety/ClassAndModuleAttributes:
6
+ Description: 'Avoid mutating class and module attributes.'
7
+ Enabled: true
8
+
9
+ ThreadSafety/InstanceVariableInClassMethod:
10
+ Description: 'Avoid using instance variables in class methods.'
11
+ Enabled: true
12
+
13
+ ThreadSafety/MutableClassInstanceVariable:
14
+ Description: 'Do not assign mutable objects to class instance variables.'
15
+ Enabled: true
16
+ EnforcedStyle: literals
17
+ SupportedStyles:
18
+ # literals: freeze literals assigned to constants
19
+ # strict: freeze all constants
20
+ # Strict mode is considered an experimental feature. It has not been updated
21
+ # with an exhaustive list of all methods that will produce frozen objects so
22
+ # there is a decent chance of getting some false positives. Luckily, there is
23
+ # no harm in freezing an already frozen object.
24
+ - literals
25
+ - strict
26
+
27
+ ThreadSafety/NewThread:
28
+ Description: >-
29
+ Avoid starting new threads.
30
+ Let a framework like Sidekiq handle the threads.
31
+ Enabled: true
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rubocop", "~> 0.53.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rubocop", "~> 0.81.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rubocop", "~> 0.86.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rubocop", "~> 1.20.0"
6
+
7
+ gemspec path: "../"
@@ -13,29 +13,56 @@ module RuboCop
13
13
  # cattr_accessor :current_user
14
14
  # end
15
15
  class ClassAndModuleAttributes < Cop
16
- MSG = 'Avoid mutating class and module attributes.'.freeze
16
+ MSG = 'Avoid mutating class and module attributes.'
17
17
 
18
- def_node_matcher :mattr?, <<-MATCHER
18
+ def_node_matcher :mattr?, <<~MATCHER
19
19
  (send nil?
20
20
  {:mattr_writer :mattr_accessor :cattr_writer :cattr_accessor}
21
21
  ...)
22
22
  MATCHER
23
23
 
24
- def_node_matcher :attr?, <<-MATCHER
24
+ def_node_matcher :attr?, <<~MATCHER
25
25
  (send nil?
26
26
  {:attr :attr_accessor :attr_writer}
27
27
  ...)
28
28
  MATCHER
29
29
 
30
+ def_node_matcher :attr_internal?, <<~MATCHER
31
+ (send nil?
32
+ {:attr_internal :attr_internal_accessor :attr_internal_writer}
33
+ ...)
34
+ MATCHER
35
+
36
+ def_node_matcher :class_attr?, <<~MATCHER
37
+ (send nil?
38
+ :class_attribute
39
+ ...)
40
+ MATCHER
41
+
30
42
  def on_send(node)
31
- return unless mattr?(node) || singleton_attr?(node)
43
+ return unless mattr?(node) || class_attr?(node) ||
44
+ singleton_attr?(node)
45
+
32
46
  add_offense(node, message: MSG)
33
47
  end
34
48
 
35
49
  private
36
50
 
37
51
  def singleton_attr?(node)
38
- attr?(node) && node.ancestors.map(&:type).include?(:sclass)
52
+ (attr?(node) || attr_internal?(node)) &&
53
+ defined_in_singleton_class?(node)
54
+ end
55
+
56
+ def defined_in_singleton_class?(node)
57
+ node.ancestors.each do |ancestor|
58
+ case ancestor.type
59
+ when :def then return false
60
+ when :sclass then return true
61
+ else next
62
+ end
63
+ end
64
+
65
+ false
39
66
  end
40
67
  end
41
68
  end
@@ -13,8 +13,40 @@ module RuboCop
13
13
  # Notifier.new(@info).deliver
14
14
  # end
15
15
  # end
16
+ #
17
+ # class Model
18
+ # class << self
19
+ # def table_name(name)
20
+ # @table_name = name
21
+ # end
22
+ # end
23
+ # end
24
+ #
25
+ # class Host
26
+ # %i[uri port].each do |key|
27
+ # define_singleton_method("#{key}=") do |value|
28
+ # instance_variable_set("@#{key}", value)
29
+ # end
30
+ # end
31
+ # end
32
+ #
33
+ # module Example
34
+ # module ClassMethods
35
+ # def test(params)
36
+ # @params = params
37
+ # end
38
+ # end
39
+ # end
16
40
  class InstanceVariableInClassMethod < Cop
17
- MSG = 'Avoid instance variables in class methods.'.freeze
41
+ MSG = 'Avoid instance variables in class methods.'
42
+
43
+ def_node_matcher :instance_variable_set_call?, <<~MATCHER
44
+ (send nil? :instance_variable_set (...) (...))
45
+ MATCHER
46
+
47
+ def_node_matcher :instance_variable_get_call?, <<~MATCHER
48
+ (send nil? :instance_variable_get (...))
49
+ MATCHER
18
50
 
19
51
  def on_ivar(node)
20
52
  return unless class_method_definition?(node)
@@ -24,11 +56,20 @@ module RuboCop
24
56
  end
25
57
  alias on_ivasgn on_ivar
26
58
 
59
+ def on_send(node)
60
+ return unless instance_variable_call?(node)
61
+ return unless class_method_definition?(node)
62
+ return if synchronized?(node)
63
+
64
+ add_offense(node, message: MSG)
65
+ end
66
+
27
67
  private
28
68
 
29
69
  def class_method_definition?(node)
30
70
  in_defs?(node) ||
31
71
  in_def_sclass?(node) ||
72
+ in_def_class_methods?(node) ||
32
73
  singleton_method_definition?(node)
33
74
  end
34
75
 
@@ -43,14 +84,27 @@ module RuboCop
43
84
  ancestor.type == :def
44
85
  end
45
86
 
46
- defn && defn.ancestors.any? do |ancestor|
87
+ defn&.ancestors&.any? do |ancestor|
47
88
  ancestor.type == :sclass
48
89
  end
49
90
  end
50
91
 
92
+ def in_def_class_methods?(node)
93
+ defn = node.ancestors.find(&:def_type?)
94
+ return unless defn
95
+
96
+ mod = defn.ancestors.find do |ancestor|
97
+ %i[class module].include?(ancestor.type)
98
+ end
99
+ return unless mod
100
+
101
+ class_methods_module?(mod)
102
+ end
103
+
51
104
  def singleton_method_definition?(node)
52
105
  node.ancestors.any? do |ancestor|
53
106
  next unless ancestor.children.first.is_a? AST::SendNode
107
+
54
108
  ancestor.children.first.command? :define_singleton_method
55
109
  end
56
110
  end
@@ -58,10 +112,19 @@ module RuboCop
58
112
  def synchronized?(node)
59
113
  node.ancestors.find do |ancestor|
60
114
  next unless ancestor.block_type?
115
+
61
116
  s = ancestor.children.first
62
117
  s.send_type? && s.children.last == :synchronize
63
118
  end
64
119
  end
120
+
121
+ def instance_variable_call?(node)
122
+ instance_variable_set_call?(node) || instance_variable_get_call?(node)
123
+ end
124
+
125
+ def_node_matcher :class_methods_module?, <<~PATTERN
126
+ (module (const _ :ClassMethods) ...)
127
+ PATTERN
65
128
  end
66
129
  end
67
130
  end
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module ThreadSafety
6
+ # This cop checks whether some class instance variable isn't a
7
+ # mutable literal (e.g. array or hash).
8
+ #
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
11
+ #
12
+ # Class instance variables are a risk to threaded code as they are shared
13
+ # between threads. A mutable object such as an array or hash may be
14
+ # updated via an attr_reader so would not be detected by the
15
+ # ThreadSafety/ClassAndModuleAttributes cop.
16
+ #
17
+ # Strict mode can be used to freeze all class instance variables, rather
18
+ # than just literals.
19
+ # Strict mode is considered an experimental feature. It has not been
20
+ # updated with an exhaustive list of all methods that will produce frozen
21
+ # objects so there is a decent chance of getting some false positives.
22
+ # Luckily, there is no harm in freezing an already frozen object.
23
+ #
24
+ # @example EnforcedStyle: literals (default)
25
+ # # bad
26
+ # class Model
27
+ # @list = [1, 2, 3]
28
+ # end
29
+ #
30
+ # # good
31
+ # class Model
32
+ # @list = [1, 2, 3].freeze
33
+ # end
34
+ #
35
+ # # good
36
+ # class Model
37
+ # @var = <<~TESTING.freeze
38
+ # This is a heredoc
39
+ # TESTING
40
+ # end
41
+ #
42
+ # # good
43
+ # class Model
44
+ # @var = Something.new
45
+ # end
46
+ #
47
+ # @example EnforcedStyle: strict
48
+ # # bad
49
+ # class Model
50
+ # @var = Something.new
51
+ # end
52
+ #
53
+ # # bad
54
+ # class Model
55
+ # @var = Struct.new do
56
+ # def foo
57
+ # puts 1
58
+ # end
59
+ # end
60
+ # end
61
+ #
62
+ # # good
63
+ # class Model
64
+ # @var = Something.new.freeze
65
+ # end
66
+ #
67
+ # # good
68
+ # class Model
69
+ # @var = Struct.new do
70
+ # def foo
71
+ # puts 1
72
+ # end
73
+ # end.freeze
74
+ # end
75
+ class MutableClassInstanceVariable < Cop
76
+ include FrozenStringLiteral
77
+ include ConfigurableEnforcedStyle
78
+
79
+ MSG = 'Freeze mutable objects assigned to class instance variables.'
80
+
81
+ def on_ivasgn(node)
82
+ return unless in_class?(node)
83
+
84
+ _, value = *node
85
+ on_assignment(value)
86
+ end
87
+
88
+ def on_or_asgn(node)
89
+ lhs, value = *node
90
+ return unless lhs&.ivasgn_type?
91
+ return unless in_class?(node)
92
+
93
+ on_assignment(value)
94
+ end
95
+
96
+ def on_masgn(node)
97
+ return unless in_class?(node)
98
+
99
+ mlhs, values = *node
100
+ return unless values.array_type?
101
+
102
+ mlhs.to_a.zip(values.to_a).each do |lhs, value|
103
+ next unless lhs.ivasgn_type?
104
+
105
+ on_assignment(value)
106
+ end
107
+ end
108
+
109
+ def autocorrect(node)
110
+ expr = node.source_range
111
+
112
+ lambda do |corrector|
113
+ splat_value = splat_value(node)
114
+ if splat_value
115
+ correct_splat_expansion(corrector, expr, splat_value)
116
+ elsif node.array_type? && !node.bracketed?
117
+ corrector.insert_before(expr, '[')
118
+ corrector.insert_after(expr, ']')
119
+ elsif requires_parentheses?(node)
120
+ corrector.insert_before(expr, '(')
121
+ corrector.insert_after(expr, ')')
122
+ end
123
+
124
+ corrector.insert_after(expr, '.freeze')
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def on_assignment(value)
131
+ if style == :strict
132
+ strict_check(value)
133
+ else
134
+ check(value)
135
+ end
136
+ end
137
+
138
+ def strict_check(value)
139
+ return if immutable_literal?(value)
140
+ return if operation_produces_immutable_object?(value)
141
+ return if operation_produces_threadsafe_object?(value)
142
+ return if frozen_string_literal?(value)
143
+
144
+ add_offense(value)
145
+ end
146
+
147
+ def check(value)
148
+ return unless mutable_literal?(value) ||
149
+ range_enclosed_in_parentheses?(value)
150
+ return if frozen_string_literal?(value)
151
+
152
+ add_offense(value)
153
+ end
154
+
155
+ def in_class?(node)
156
+ container = node.ancestors.find do |ancestor|
157
+ container?(ancestor)
158
+ end
159
+ return false if container.nil?
160
+
161
+ %i[class module].include?(container.type)
162
+ end
163
+
164
+ def container?(node)
165
+ return true if define_singleton_method?(node)
166
+
167
+ %i[def defs class module].include?(node.type)
168
+ end
169
+
170
+ def mutable_literal?(node)
171
+ return if node.nil?
172
+
173
+ node.mutable_literal? || range_type?(node)
174
+ end
175
+
176
+ def immutable_literal?(node)
177
+ node.nil? || node.immutable_literal?
178
+ end
179
+
180
+ def requires_parentheses?(node)
181
+ range_type?(node) ||
182
+ (node.send_type? && node.loc.dot.nil?)
183
+ end
184
+
185
+ def range_type?(node)
186
+ node.erange_type? || node.irange_type?
187
+ end
188
+
189
+ def correct_splat_expansion(corrector, expr, splat_value)
190
+ if range_enclosed_in_parentheses?(splat_value)
191
+ corrector.replace(expr, "#{splat_value.source}.to_a")
192
+ else
193
+ corrector.replace(expr, "(#{splat_value.source}).to_a")
194
+ end
195
+ end
196
+
197
+ def_node_matcher :define_singleton_method?, <<~PATTERN
198
+ (block (send nil? :define_singleton_method ...) ...)
199
+ PATTERN
200
+
201
+ def_node_matcher :splat_value, <<~PATTERN
202
+ (array (splat $_))
203
+ PATTERN
204
+
205
+ # NOTE: Some of these patterns may not actually return an immutable
206
+ # object but we will consider them immutable for this cop.
207
+ def_node_matcher :operation_produces_immutable_object?, <<~PATTERN
208
+ {
209
+ (const _ _)
210
+ (send (const {nil? cbase} :Struct) :new ...)
211
+ (block (send (const {nil? cbase} :Struct) :new ...) ...)
212
+ (send _ :freeze)
213
+ (send {float int} {:+ :- :* :** :/ :% :<<} _)
214
+ (send _ {:+ :- :* :** :/ :%} {float int})
215
+ (send _ {:== :=== :!= :<= :>= :< :>} _)
216
+ (send (const {nil? cbase} :ENV) :[] _)
217
+ (or (send (const {nil? cbase} :ENV) :[] _) _)
218
+ (send _ {:count :length :size} ...)
219
+ (block (send _ {:count :length :size} ...) ...)
220
+ }
221
+ PATTERN
222
+
223
+ def_node_matcher :operation_produces_threadsafe_object?, <<~PATTERN
224
+ {
225
+ (send (const {nil? cbase} :Queue) :new ...)
226
+ (send
227
+ (const (const {nil? cbase} :ThreadSafe) {:Hash :Array})
228
+ :new ...)
229
+ (block
230
+ (send
231
+ (const (const {nil? cbase} :ThreadSafe) {:Hash :Array})
232
+ :new ...)
233
+ ...)
234
+ (send (const (const {nil? cbase} :Concurrent) _) :new ...)
235
+ (block
236
+ (send (const (const {nil? cbase} :Concurrent) _) :new ...)
237
+ ...)
238
+ (send (const (const (const {nil? cbase} :Concurrent) _) _) :new ...)
239
+ (block
240
+ (send
241
+ (const (const (const {nil? cbase} :Concurrent) _) _)
242
+ :new ...)
243
+ ...)
244
+ (send
245
+ (const (const (const (const {nil? cbase} :Concurrent) _) _) _)
246
+ :new ...)
247
+ (block
248
+ (send
249
+ (const (const (const (const {nil? cbase} :Concurrent) _) _) _)
250
+ :new ...)
251
+ ...)
252
+ }
253
+ PATTERN
254
+
255
+ def_node_matcher :range_enclosed_in_parentheses?, <<~PATTERN
256
+ (begin ({irange erange} _ _))
257
+ PATTERN
258
+ end
259
+ end
260
+ end
261
+ end
@@ -11,14 +11,15 @@ module RuboCop
11
11
  # # bad
12
12
  # Thread.new { do_work }
13
13
  class NewThread < Cop
14
- MSG = 'Avoid starting new threads.'.freeze
14
+ MSG = 'Avoid starting new threads.'
15
15
 
16
- def_node_matcher :new_thread?, <<-MATCHER
17
- (send (const nil? :Thread) :new)
16
+ def_node_matcher :new_thread?, <<~MATCHER
17
+ (send (const {nil? cbase} :Thread) :new)
18
18
  MATCHER
19
19
 
20
20
  def on_send(node)
21
21
  return unless new_thread?(node)
22
+
22
23
  add_offense(node, message: MSG)
23
24
  end
24
25
  end
@@ -0,0 +1,20 @@
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,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RuboCop
2
4
  module ThreadSafety
3
- VERSION = '0.3.4'.freeze
5
+ VERSION = '0.4.3'
4
6
  end
5
7
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ # RuboCop::ThreadSafety detects some potential thread safety issues.
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
+ end
12
+ end
@@ -1,7 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rubocop'
2
4
 
5
+ require 'rubocop/thread_safety'
3
6
  require 'rubocop/thread_safety/version'
7
+ require 'rubocop/thread_safety/inject'
8
+
9
+ RuboCop::ThreadSafety::Inject.defaults!
4
10
 
5
11
  require 'rubocop/cop/thread_safety/instance_variable_in_class_method'
6
12
  require 'rubocop/cop/thread_safety/class_and_module_attributes'
13
+ require 'rubocop/cop/thread_safety/mutable_class_instance_variable'
7
14
  require 'rubocop/cop/thread_safety/new_thread'
@@ -1,4 +1,6 @@
1
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
2
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
5
  require 'rubocop/thread_safety/version'
4
6
 
@@ -24,10 +26,13 @@ Gem::Specification.new do |spec|
24
26
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
27
  spec.require_paths = ['lib']
26
28
 
27
- spec.add_runtime_dependency 'rubocop', '>= 0.51.0'
29
+ spec.required_ruby_version = '>= 2.3.0'
30
+
31
+ spec.add_runtime_dependency 'rubocop', '>= 0.53.0'
28
32
 
29
- spec.add_development_dependency 'bundler', '~> 1.10'
33
+ spec.add_development_dependency 'appraisal'
34
+ spec.add_development_dependency 'bundler', '>= 1.10', '< 3'
30
35
  spec.add_development_dependency 'pry' unless ENV['CI']
31
- spec.add_development_dependency 'rake', '~> 10.0'
36
+ spec.add_development_dependency 'rake', '>= 10.0'
32
37
  spec.add_development_dependency 'rspec', '~> 3.0'
33
38
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-thread_safety
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Gee
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-11-30 00:00:00.000000000 Z
11
+ date: 2021-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop
@@ -16,28 +16,48 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.51.0
19
+ version: 0.53.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 0.51.0
26
+ version: 0.53.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: appraisal
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
- - - "~>"
45
+ - - ">="
32
46
  - !ruby/object:Gem::Version
33
47
  version: '1.10'
48
+ - - "<"
49
+ - !ruby/object:Gem::Version
50
+ version: '3'
34
51
  type: :development
35
52
  prerelease: false
36
53
  version_requirements: !ruby/object:Gem::Requirement
37
54
  requirements:
38
- - - "~>"
55
+ - - ">="
39
56
  - !ruby/object:Gem::Version
40
57
  version: '1.10'
58
+ - - "<"
59
+ - !ruby/object:Gem::Version
60
+ version: '3'
41
61
  - !ruby/object:Gem::Dependency
42
62
  name: pry
43
63
  requirement: !ruby/object:Gem::Requirement
@@ -56,14 +76,14 @@ dependencies:
56
76
  name: rake
57
77
  requirement: !ruby/object:Gem::Requirement
58
78
  requirements:
59
- - - "~>"
79
+ - - ">="
60
80
  - !ruby/object:Gem::Version
61
81
  version: '10.0'
62
82
  type: :development
63
83
  prerelease: false
64
84
  version_requirements: !ruby/object:Gem::Requirement
65
85
  requirements:
66
- - - "~>"
86
+ - - ">="
67
87
  - !ruby/object:Gem::Version
68
88
  version: '10.0'
69
89
  - !ruby/object:Gem::Dependency
@@ -93,23 +113,32 @@ files:
93
113
  - ".rspec"
94
114
  - ".rubocop.yml"
95
115
  - ".travis.yml"
116
+ - Appraisals
96
117
  - Gemfile
97
118
  - LICENSE.txt
98
119
  - README.md
99
120
  - Rakefile
100
121
  - bin/console
101
122
  - bin/setup
123
+ - config/default.yml
124
+ - gemfiles/rubocop_0.53.gemfile
125
+ - gemfiles/rubocop_0.81.gemfile
126
+ - gemfiles/rubocop_0.86.gemfile
127
+ - gemfiles/rubocop_1.20.gemfile
102
128
  - lib/rubocop-thread_safety.rb
103
129
  - lib/rubocop/cop/thread_safety/class_and_module_attributes.rb
104
130
  - lib/rubocop/cop/thread_safety/instance_variable_in_class_method.rb
131
+ - lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb
105
132
  - lib/rubocop/cop/thread_safety/new_thread.rb
133
+ - lib/rubocop/thread_safety.rb
134
+ - lib/rubocop/thread_safety/inject.rb
106
135
  - lib/rubocop/thread_safety/version.rb
107
136
  - rubocop-thread_safety.gemspec
108
137
  homepage: https://github.com/covermymeds/rubocop-thread_safety
109
138
  licenses:
110
139
  - MIT
111
140
  metadata: {}
112
- post_install_message:
141
+ post_install_message:
113
142
  rdoc_options: []
114
143
  require_paths:
115
144
  - lib
@@ -117,16 +146,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
117
146
  requirements:
118
147
  - - ">="
119
148
  - !ruby/object:Gem::Version
120
- version: '0'
149
+ version: 2.3.0
121
150
  required_rubygems_version: !ruby/object:Gem::Requirement
122
151
  requirements:
123
152
  - - ">="
124
153
  - !ruby/object:Gem::Version
125
154
  version: '0'
126
155
  requirements: []
127
- rubyforge_project:
128
- rubygems_version: 2.6.8
129
- signing_key:
156
+ rubygems_version: 3.0.3.1
157
+ signing_key:
130
158
  specification_version: 4
131
159
  summary: Thread-safety checks via static analysis
132
160
  test_files: []