rubocop-thread_safety 0.3.2 → 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.rubocop.yml +35 -5
- data/.travis.yml +43 -12
- data/Appraisals +16 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +7 -0
- data/README.md +30 -1
- data/Rakefile +2 -0
- data/bin/console +2 -0
- data/config/default.yml +31 -0
- data/gemfiles/rubocop_0.53.gemfile +7 -0
- data/gemfiles/rubocop_0.81.gemfile +7 -0
- data/gemfiles/rubocop_0.86.gemfile +7 -0
- data/lib/rubocop-thread_safety.rb +7 -0
- data/lib/rubocop/cop/thread_safety/class_and_module_attributes.rb +37 -11
- data/lib/rubocop/cop/thread_safety/instance_variable_in_class_method.rb +67 -5
- data/lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb +266 -0
- data/lib/rubocop/cop/thread_safety/new_thread.rb +6 -6
- data/lib/rubocop/thread_safety.rb +12 -0
- data/lib/rubocop/thread_safety/inject.rb +20 -0
- data/lib/rubocop/thread_safety/version.rb +3 -1
- data/rubocop-thread_safety.gemspec +18 -10
- metadata +54 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ca09c8c0f8772fca865f55df6d350d28c0d3b6ab11a81f2837b76b74ce29b8bb
|
4
|
+
data.tar.gz: 16ac028cb86f4853370d6863974e2ac6cacd64253fbe9b93d2f4084ec0e89125
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5c303f9423fe7dccfc38f47ce3a22bcc2dbb573b6754c46c3216bf49787e0bf4c5ab162af9cd1471021b5b6eea1a57ec5358d4cb699bd5a369d2116fb0d4b25b
|
7
|
+
data.tar.gz: 57ca57201e7dbc8e36458d1d97d95aae84a6b7ec7cd7d4282fd632b8f29caf54f98402315d678dbbf98f9a83d2cfecd411cb345907dead297d19f56ad1d94ee3
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -1,14 +1,27 @@
|
|
1
1
|
AllCops:
|
2
2
|
DisplayCopNames: true
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
TargetRubyVersion: 2.3
|
4
|
+
|
5
|
+
Lint/RaiseException:
|
6
|
+
Enabled: true
|
7
|
+
|
8
|
+
Lint/StructNewOverride:
|
9
|
+
Enabled: true
|
10
|
+
|
11
|
+
Metrics/BlockLength:
|
6
12
|
Exclude:
|
7
|
-
-
|
13
|
+
- "spec/**/*"
|
14
|
+
|
15
|
+
Metrics/ClassLength:
|
16
|
+
Enabled: false
|
8
17
|
|
9
|
-
|
18
|
+
Metrics/MethodLength:
|
19
|
+
Max: 14
|
20
|
+
|
21
|
+
Naming/FileName:
|
10
22
|
Exclude:
|
11
23
|
- lib/rubocop-thread_safety.rb
|
24
|
+
- rubocop-thread_safety.gemspec
|
12
25
|
|
13
26
|
# Enable more cops that are disabled by default:
|
14
27
|
|
@@ -18,6 +31,19 @@ Style/AutoResourceCleanup:
|
|
18
31
|
Style/CollectionMethods:
|
19
32
|
Enabled: true
|
20
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
|
+
|
21
47
|
Style/MethodCalledOnDoEndBlock:
|
22
48
|
Enabled: true
|
23
49
|
Exclude:
|
@@ -33,6 +59,10 @@ Style/OptionHash:
|
|
33
59
|
Style/Send:
|
34
60
|
Enabled: true
|
35
61
|
|
62
|
+
Style/StringLiterals:
|
63
|
+
Exclude:
|
64
|
+
- "gemfiles/*.gemfile"
|
65
|
+
|
36
66
|
Style/StringMethods:
|
37
67
|
Enabled: true
|
38
68
|
|
data/.travis.yml
CHANGED
@@ -1,24 +1,55 @@
|
|
1
|
-
sudo: false
|
2
1
|
cache: bundler
|
3
2
|
language: ruby
|
4
3
|
rvm:
|
5
|
-
- 2.
|
6
|
-
- 2.1
|
7
|
-
- 2.2
|
4
|
+
- jruby-9.2.9.0
|
8
5
|
- 2.3.0
|
9
|
-
-
|
10
|
-
-
|
11
|
-
-
|
6
|
+
- 2.4
|
7
|
+
- 2.5
|
8
|
+
- 2.6
|
9
|
+
- 2.7
|
12
10
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
gemfile:
|
12
|
+
- gemfiles/rubocop_0.53.gemfile
|
13
|
+
- gemfiles/rubocop_0.81.gemfile
|
14
|
+
- gemfiles/rubocop_0.86.gemfile
|
15
|
+
|
16
|
+
script_rubocop: &script_rubocop
|
17
|
+
- bundle exec rspec
|
18
|
+
- bundle exec rubocop
|
19
|
+
|
20
|
+
jobs:
|
17
21
|
fast_finish: true
|
22
|
+
exclude:
|
23
|
+
- rvm: 2.3.0
|
24
|
+
gemfile: gemfiles/rubocop_0.86.gemfile
|
25
|
+
- rvm: 2.5
|
26
|
+
gemfile: gemfiles/rubocop_0.53.gemfile
|
27
|
+
- rvm: 2.6
|
28
|
+
gemfile: gemfiles/rubocop_0.53.gemfile
|
29
|
+
- rvm: 2.7
|
30
|
+
gemfile: gemfiles/rubocop_0.53.gemfile
|
31
|
+
include:
|
32
|
+
- rvm: jruby-9.2.9.0
|
33
|
+
gemfile: gemfiles/rubocop_0.81.gemfile
|
34
|
+
script: *script_rubocop
|
35
|
+
- rvm: 2.3.0
|
36
|
+
gemfile: gemfiles/rubocop_0.81.gemfile
|
37
|
+
script: *script_rubocop
|
38
|
+
- rvm: 2.4
|
39
|
+
gemfile: gemfiles/rubocop_0.81.gemfile
|
40
|
+
script: *script_rubocop
|
41
|
+
- rvm: 2.5
|
42
|
+
gemfile: gemfiles/rubocop_0.81.gemfile
|
43
|
+
script: *script_rubocop
|
44
|
+
- rvm: 2.6
|
45
|
+
gemfile: gemfiles/rubocop_0.81.gemfile
|
46
|
+
script: *script_rubocop
|
47
|
+
- rvm: 2.7
|
48
|
+
gemfile: gemfiles/rubocop_0.81.gemfile
|
49
|
+
script: *script_rubocop
|
18
50
|
|
19
51
|
before_install: gem install --remote bundler
|
20
52
|
install:
|
21
53
|
- bundle install --retry=3
|
22
54
|
script:
|
23
55
|
- bundle exec rspec
|
24
|
-
- bundle exec rubocop
|
data/Appraisals
ADDED
@@ -0,0 +1,16 @@
|
|
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
|
data/Gemfile
CHANGED
data/LICENSE.txt
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright 2016-2020 CoverMyMeds
|
2
|
+
|
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
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -32,7 +32,32 @@ Install the gem:
|
|
32
32
|
|
33
33
|
Scan the application for just thread-safety issues:
|
34
34
|
|
35
|
-
$ rubocop -r rubocop-thread_safety --only
|
35
|
+
$ rubocop -r rubocop-thread_safety --only ThreadSafety,Style/GlobalVars,Style/ClassVars,Style/MutableConstant
|
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 [`RequestStore`](https://github.com/steveklabnik/request_store)
|
60
|
+
* Use `Thread.current[:name]`
|
36
61
|
|
37
62
|
## Development
|
38
63
|
|
@@ -44,3 +69,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
44
69
|
|
45
70
|
Bug reports and pull requests are welcome on GitHub at https://github.com/covermymeds/rubocop-thread_safety.
|
46
71
|
|
72
|
+
## Copyright
|
73
|
+
|
74
|
+
Copyright (c) 2016-2020 CoverMyMeds.
|
75
|
+
See [LICENSE.txt](LICENSE.txt) for further details.
|
data/Rakefile
CHANGED
data/bin/console
CHANGED
data/config/default.yml
ADDED
@@ -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
|
@@ -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,3 @@
|
|
1
|
-
# encoding: utf-8
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
module RuboCop
|
@@ -14,29 +13,56 @@ module RuboCop
|
|
14
13
|
# cattr_accessor :current_user
|
15
14
|
# end
|
16
15
|
class ClassAndModuleAttributes < Cop
|
17
|
-
MSG = 'Avoid mutating class and module attributes.'
|
16
|
+
MSG = 'Avoid mutating class and module attributes.'
|
18
17
|
|
19
|
-
def_node_matcher :mattr?,
|
20
|
-
(send nil
|
18
|
+
def_node_matcher :mattr?, <<~MATCHER
|
19
|
+
(send nil?
|
21
20
|
{:mattr_writer :mattr_accessor :cattr_writer :cattr_accessor}
|
22
21
|
...)
|
23
|
-
|
22
|
+
MATCHER
|
24
23
|
|
25
|
-
def_node_matcher :attr?,
|
26
|
-
(send nil
|
24
|
+
def_node_matcher :attr?, <<~MATCHER
|
25
|
+
(send nil?
|
27
26
|
{:attr :attr_accessor :attr_writer}
|
28
27
|
...)
|
29
|
-
|
28
|
+
MATCHER
|
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
|
30
41
|
|
31
42
|
def on_send(node)
|
32
|
-
return unless mattr?(node) ||
|
33
|
-
|
43
|
+
return unless mattr?(node) || class_attr?(node) ||
|
44
|
+
singleton_attr?(node)
|
45
|
+
|
46
|
+
add_offense(node, message: MSG)
|
34
47
|
end
|
35
48
|
|
36
49
|
private
|
37
50
|
|
38
51
|
def singleton_attr?(node)
|
39
|
-
attr?(node)
|
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
|
40
66
|
end
|
41
67
|
end
|
42
68
|
end
|
@@ -1,4 +1,3 @@
|
|
1
|
-
# encoding: utf-8
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
module RuboCop
|
@@ -14,22 +13,63 @@ module RuboCop
|
|
14
13
|
# Notifier.new(@info).deliver
|
15
14
|
# end
|
16
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
|
17
40
|
class InstanceVariableInClassMethod < Cop
|
18
|
-
MSG = 'Avoid instance variables in class methods.'
|
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
|
19
50
|
|
20
51
|
def on_ivar(node)
|
21
52
|
return unless class_method_definition?(node)
|
22
53
|
return if synchronized?(node)
|
23
54
|
|
24
|
-
add_offense(node, :name, MSG)
|
55
|
+
add_offense(node, location: :name, message: MSG)
|
25
56
|
end
|
26
57
|
alias on_ivasgn on_ivar
|
27
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
|
+
|
28
67
|
private
|
29
68
|
|
30
69
|
def class_method_definition?(node)
|
31
70
|
in_defs?(node) ||
|
32
71
|
in_def_sclass?(node) ||
|
72
|
+
in_def_class_methods?(node) ||
|
33
73
|
singleton_method_definition?(node)
|
34
74
|
end
|
35
75
|
|
@@ -44,14 +84,27 @@ module RuboCop
|
|
44
84
|
ancestor.type == :def
|
45
85
|
end
|
46
86
|
|
47
|
-
defn
|
87
|
+
defn&.ancestors&.any? do |ancestor|
|
48
88
|
ancestor.type == :sclass
|
49
89
|
end
|
50
90
|
end
|
51
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
|
+
|
52
104
|
def singleton_method_definition?(node)
|
53
105
|
node.ancestors.any? do |ancestor|
|
54
|
-
next unless ancestor.children.first.is_a? AST::
|
106
|
+
next unless ancestor.children.first.is_a? AST::SendNode
|
107
|
+
|
55
108
|
ancestor.children.first.command? :define_singleton_method
|
56
109
|
end
|
57
110
|
end
|
@@ -59,10 +112,19 @@ module RuboCop
|
|
59
112
|
def synchronized?(node)
|
60
113
|
node.ancestors.find do |ancestor|
|
61
114
|
next unless ancestor.block_type?
|
115
|
+
|
62
116
|
s = ancestor.children.first
|
63
117
|
s.send_type? && s.children.last == :synchronize
|
64
118
|
end
|
65
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
|
66
128
|
end
|
67
129
|
end
|
68
130
|
end
|
@@ -0,0 +1,266 @@
|
|
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 frozen_string_literal?(node)
|
181
|
+
FROZEN_STRING_LITERAL_TYPES.include?(node.type) &&
|
182
|
+
frozen_string_literals_enabled?
|
183
|
+
end
|
184
|
+
|
185
|
+
def requires_parentheses?(node)
|
186
|
+
range_type?(node) ||
|
187
|
+
(node.send_type? && node.loc.dot.nil?)
|
188
|
+
end
|
189
|
+
|
190
|
+
def range_type?(node)
|
191
|
+
node.erange_type? || node.irange_type?
|
192
|
+
end
|
193
|
+
|
194
|
+
def correct_splat_expansion(corrector, expr, splat_value)
|
195
|
+
if range_enclosed_in_parentheses?(splat_value)
|
196
|
+
corrector.replace(expr, "#{splat_value.source}.to_a")
|
197
|
+
else
|
198
|
+
corrector.replace(expr, "(#{splat_value.source}).to_a")
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def_node_matcher :define_singleton_method?, <<~PATTERN
|
203
|
+
(block (send nil? :define_singleton_method ...) ...)
|
204
|
+
PATTERN
|
205
|
+
|
206
|
+
def_node_matcher :splat_value, <<~PATTERN
|
207
|
+
(array (splat $_))
|
208
|
+
PATTERN
|
209
|
+
|
210
|
+
# NOTE: Some of these patterns may not actually return an immutable
|
211
|
+
# object but we will consider them immutable for this cop.
|
212
|
+
def_node_matcher :operation_produces_immutable_object?, <<~PATTERN
|
213
|
+
{
|
214
|
+
(const _ _)
|
215
|
+
(send (const {nil? cbase} :Struct) :new ...)
|
216
|
+
(block (send (const {nil? cbase} :Struct) :new ...) ...)
|
217
|
+
(send _ :freeze)
|
218
|
+
(send {float int} {:+ :- :* :** :/ :% :<<} _)
|
219
|
+
(send _ {:+ :- :* :** :/ :%} {float int})
|
220
|
+
(send _ {:== :=== :!= :<= :>= :< :>} _)
|
221
|
+
(send (const {nil? cbase} :ENV) :[] _)
|
222
|
+
(or (send (const {nil? cbase} :ENV) :[] _) _)
|
223
|
+
(send _ {:count :length :size} ...)
|
224
|
+
(block (send _ {:count :length :size} ...) ...)
|
225
|
+
}
|
226
|
+
PATTERN
|
227
|
+
|
228
|
+
def_node_matcher :operation_produces_threadsafe_object?, <<~PATTERN
|
229
|
+
{
|
230
|
+
(send (const {nil? cbase} :Queue) :new ...)
|
231
|
+
(send
|
232
|
+
(const (const {nil? cbase} :ThreadSafe) {:Hash :Array})
|
233
|
+
:new ...)
|
234
|
+
(block
|
235
|
+
(send
|
236
|
+
(const (const {nil? cbase} :ThreadSafe) {:Hash :Array})
|
237
|
+
:new ...)
|
238
|
+
...)
|
239
|
+
(send (const (const {nil? cbase} :Concurrent) _) :new ...)
|
240
|
+
(block
|
241
|
+
(send (const (const {nil? cbase} :Concurrent) _) :new ...)
|
242
|
+
...)
|
243
|
+
(send (const (const (const {nil? cbase} :Concurrent) _) _) :new ...)
|
244
|
+
(block
|
245
|
+
(send
|
246
|
+
(const (const (const {nil? cbase} :Concurrent) _) _)
|
247
|
+
:new ...)
|
248
|
+
...)
|
249
|
+
(send
|
250
|
+
(const (const (const (const {nil? cbase} :Concurrent) _) _) _)
|
251
|
+
:new ...)
|
252
|
+
(block
|
253
|
+
(send
|
254
|
+
(const (const (const (const {nil? cbase} :Concurrent) _) _) _)
|
255
|
+
:new ...)
|
256
|
+
...)
|
257
|
+
}
|
258
|
+
PATTERN
|
259
|
+
|
260
|
+
def_node_matcher :range_enclosed_in_parentheses?, <<~PATTERN
|
261
|
+
(begin ({irange erange} _ _))
|
262
|
+
PATTERN
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
@@ -1,4 +1,3 @@
|
|
1
|
-
# encoding: utf-8
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
module RuboCop
|
@@ -12,15 +11,16 @@ module RuboCop
|
|
12
11
|
# # bad
|
13
12
|
# Thread.new { do_work }
|
14
13
|
class NewThread < Cop
|
15
|
-
MSG = 'Avoid starting new threads.'
|
14
|
+
MSG = 'Avoid starting new threads.'
|
16
15
|
|
17
|
-
def_node_matcher :new_thread?,
|
18
|
-
(send (const nil :Thread) :new)
|
19
|
-
|
16
|
+
def_node_matcher :new_thread?, <<~MATCHER
|
17
|
+
(send (const {nil? cbase} :Thread) :new)
|
18
|
+
MATCHER
|
20
19
|
|
21
20
|
def on_send(node)
|
22
21
|
return unless new_thread?(node)
|
23
|
-
|
22
|
+
|
23
|
+
add_offense(node, message: MSG)
|
24
24
|
end
|
25
25
|
end
|
26
26
|
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
|
@@ -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,6 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
require 'rubocop/thread_safety/version'
|
5
6
|
|
@@ -10,21 +11,28 @@ Gem::Specification.new do |spec|
|
|
10
11
|
spec.email = ['michaelpgee@gmail.com']
|
11
12
|
|
12
13
|
spec.summary = 'Thread-safety checks via static analysis'
|
13
|
-
spec.description = <<-
|
14
|
+
spec.description = <<-DESCRIPTION
|
14
15
|
Thread-safety checks via static analysis.
|
15
16
|
A plugin for the RuboCop code style enforcing & linting tool.
|
16
|
-
|
17
|
-
spec.homepage
|
17
|
+
DESCRIPTION
|
18
|
+
spec.homepage = 'https://github.com/covermymeds/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
|
18
24
|
|
19
|
-
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
20
25
|
spec.bindir = 'exe'
|
21
26
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
27
|
spec.require_paths = ['lib']
|
23
28
|
|
24
|
-
spec.
|
29
|
+
spec.required_ruby_version = '>= 2.3.0'
|
25
30
|
|
26
|
-
spec.
|
27
|
-
|
28
|
-
spec.add_development_dependency '
|
31
|
+
spec.add_runtime_dependency 'rubocop', '>= 0.53.0'
|
32
|
+
|
33
|
+
spec.add_development_dependency 'appraisal'
|
34
|
+
spec.add_development_dependency 'bundler', '>= 1.10', '< 3'
|
29
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'
|
30
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.
|
4
|
+
version: 0.4.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Gee
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-10-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rubocop
|
@@ -16,70 +16,90 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 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.
|
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
|
-
name:
|
62
|
+
name: pry
|
43
63
|
requirement: !ruby/object:Gem::Requirement
|
44
64
|
requirements:
|
45
|
-
- - "
|
65
|
+
- - ">="
|
46
66
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
67
|
+
version: '0'
|
48
68
|
type: :development
|
49
69
|
prerelease: false
|
50
70
|
version_requirements: !ruby/object:Gem::Requirement
|
51
71
|
requirements:
|
52
|
-
- - "
|
72
|
+
- - ">="
|
53
73
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
74
|
+
version: '0'
|
55
75
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
76
|
+
name: rake
|
57
77
|
requirement: !ruby/object:Gem::Requirement
|
58
78
|
requirements:
|
59
|
-
- - "
|
79
|
+
- - ">="
|
60
80
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
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
|
-
version: '
|
88
|
+
version: '10.0'
|
69
89
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
90
|
+
name: rspec
|
71
91
|
requirement: !ruby/object:Gem::Requirement
|
72
92
|
requirements:
|
73
|
-
- - "
|
93
|
+
- - "~>"
|
74
94
|
- !ruby/object:Gem::Version
|
75
|
-
version: '0'
|
95
|
+
version: '3.0'
|
76
96
|
type: :development
|
77
97
|
prerelease: false
|
78
98
|
version_requirements: !ruby/object:Gem::Requirement
|
79
99
|
requirements:
|
80
|
-
- - "
|
100
|
+
- - "~>"
|
81
101
|
- !ruby/object:Gem::Version
|
82
|
-
version: '0'
|
102
|
+
version: '3.0'
|
83
103
|
description: |2
|
84
104
|
Thread-safety checks via static analysis.
|
85
105
|
A plugin for the RuboCop code style enforcing & linting tool.
|
@@ -93,19 +113,29 @@ files:
|
|
93
113
|
- ".rspec"
|
94
114
|
- ".rubocop.yml"
|
95
115
|
- ".travis.yml"
|
116
|
+
- Appraisals
|
96
117
|
- Gemfile
|
118
|
+
- LICENSE.txt
|
97
119
|
- README.md
|
98
120
|
- Rakefile
|
99
121
|
- bin/console
|
100
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
|
101
127
|
- lib/rubocop-thread_safety.rb
|
102
128
|
- lib/rubocop/cop/thread_safety/class_and_module_attributes.rb
|
103
129
|
- lib/rubocop/cop/thread_safety/instance_variable_in_class_method.rb
|
130
|
+
- lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb
|
104
131
|
- lib/rubocop/cop/thread_safety/new_thread.rb
|
132
|
+
- lib/rubocop/thread_safety.rb
|
133
|
+
- lib/rubocop/thread_safety/inject.rb
|
105
134
|
- lib/rubocop/thread_safety/version.rb
|
106
135
|
- rubocop-thread_safety.gemspec
|
107
136
|
homepage: https://github.com/covermymeds/rubocop-thread_safety
|
108
|
-
licenses:
|
137
|
+
licenses:
|
138
|
+
- MIT
|
109
139
|
metadata: {}
|
110
140
|
post_install_message:
|
111
141
|
rdoc_options: []
|
@@ -115,15 +145,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
115
145
|
requirements:
|
116
146
|
- - ">="
|
117
147
|
- !ruby/object:Gem::Version
|
118
|
-
version:
|
148
|
+
version: 2.3.0
|
119
149
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
150
|
requirements:
|
121
151
|
- - ">="
|
122
152
|
- !ruby/object:Gem::Version
|
123
153
|
version: '0'
|
124
154
|
requirements: []
|
125
|
-
|
126
|
-
rubygems_version: 2.6.8
|
155
|
+
rubygems_version: 3.0.3
|
127
156
|
signing_key:
|
128
157
|
specification_version: 4
|
129
158
|
summary: Thread-safety checks via static analysis
|