rubocop-thread_safety 0.4.0 → 0.4.1
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +6 -0
- data/README.md +4 -0
- data/config/default.yml +30 -0
- data/lib/rubocop-thread_safety.rb +5 -0
- data/lib/rubocop/cop/thread_safety/class_and_module_attributes.rb +16 -2
- data/lib/rubocop/cop/thread_safety/instance_variable_in_class_method.rb +36 -0
- data/lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb +227 -0
- data/lib/rubocop/thread_safety.rb +12 -0
- data/lib/rubocop/thread_safety/inject.rb +20 -0
- data/lib/rubocop/thread_safety/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d2f9c0ded508d65e482cd339fc6bafe4a843f19982884ea8c03aa0f6ceb1f4cd
|
4
|
+
data.tar.gz: bcfb88db93ee4c0bacdb1df6e60e72bdd044209ecd1c9c27fc8b8e8597f5eff9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 81a037e33675b2aa5cf77bad8732b43e4b09bcbb0fc1127fe64eda0481516a20068e70f3afd32d4a69673729c8ce68422374b81ad7788a9de6bbe0ab9d4ae4bd
|
7
|
+
data.tar.gz: e6c0c3e2626e371eaf47b7fc144eb7659661d98184c47e2002d49a3f276e744f6ebc67c932fa361ff4379e536322d775579303b51e0dcf806907e9e712e61532
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -34,6 +34,10 @@ 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
|
+
|
37
41
|
## Development
|
38
42
|
|
39
43
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/config/default.yml
ADDED
@@ -0,0 +1,30 @@
|
|
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.
|
@@ -2,8 +2,13 @@
|
|
2
2
|
|
3
3
|
require 'rubocop'
|
4
4
|
|
5
|
+
require 'rubocop/thread_safety'
|
5
6
|
require 'rubocop/thread_safety/version'
|
7
|
+
require 'rubocop/thread_safety/inject'
|
8
|
+
|
9
|
+
RuboCop::ThreadSafety::Inject.defaults!
|
6
10
|
|
7
11
|
require 'rubocop/cop/thread_safety/instance_variable_in_class_method'
|
8
12
|
require 'rubocop/cop/thread_safety/class_and_module_attributes'
|
13
|
+
require 'rubocop/cop/thread_safety/mutable_class_instance_variable'
|
9
14
|
require 'rubocop/cop/thread_safety/new_thread'
|
@@ -27,8 +27,21 @@ module RuboCop
|
|
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) ||
|
43
|
+
return unless mattr?(node) || class_attr?(node) ||
|
44
|
+
singleton_attr?(node)
|
32
45
|
|
33
46
|
add_offense(node, message: MSG)
|
34
47
|
end
|
@@ -36,7 +49,8 @@ module RuboCop
|
|
36
49
|
private
|
37
50
|
|
38
51
|
def singleton_attr?(node)
|
39
|
-
attr?(node)
|
52
|
+
(attr?(node) || attr_internal?(node)) &&
|
53
|
+
node.ancestors.map(&:type).include?(:sclass)
|
40
54
|
end
|
41
55
|
end
|
42
56
|
end
|
@@ -13,9 +13,33 @@ 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
|
16
32
|
class InstanceVariableInClassMethod < Cop
|
17
33
|
MSG = 'Avoid instance variables in class methods.'
|
18
34
|
|
35
|
+
def_node_matcher :instance_variable_set_call?, <<-MATCHER
|
36
|
+
(send nil? :instance_variable_set (...) (...))
|
37
|
+
MATCHER
|
38
|
+
|
39
|
+
def_node_matcher :instance_variable_get_call?, <<-MATCHER
|
40
|
+
(send nil? :instance_variable_get (...))
|
41
|
+
MATCHER
|
42
|
+
|
19
43
|
def on_ivar(node)
|
20
44
|
return unless class_method_definition?(node)
|
21
45
|
return if synchronized?(node)
|
@@ -24,6 +48,14 @@ module RuboCop
|
|
24
48
|
end
|
25
49
|
alias on_ivasgn on_ivar
|
26
50
|
|
51
|
+
def on_send(node)
|
52
|
+
return unless instance_variable_call?(node)
|
53
|
+
return unless class_method_definition?(node)
|
54
|
+
return if synchronized?(node)
|
55
|
+
|
56
|
+
add_offense(node, message: MSG)
|
57
|
+
end
|
58
|
+
|
27
59
|
private
|
28
60
|
|
29
61
|
def class_method_definition?(node)
|
@@ -64,6 +96,10 @@ module RuboCop
|
|
64
96
|
s.send_type? && s.children.last == :synchronize
|
65
97
|
end
|
66
98
|
end
|
99
|
+
|
100
|
+
def instance_variable_call?(node)
|
101
|
+
instance_variable_set_call?(node) || instance_variable_get_call?(node)
|
102
|
+
end
|
67
103
|
end
|
68
104
|
end
|
69
105
|
end
|
@@ -0,0 +1,227 @@
|
|
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 frozen_string_literal?(value)
|
142
|
+
|
143
|
+
add_offense(value)
|
144
|
+
end
|
145
|
+
|
146
|
+
def check(value)
|
147
|
+
return unless mutable_literal?(value) ||
|
148
|
+
range_enclosed_in_parentheses?(value)
|
149
|
+
return if frozen_string_literal?(value)
|
150
|
+
|
151
|
+
add_offense(value)
|
152
|
+
end
|
153
|
+
|
154
|
+
def in_class?(node)
|
155
|
+
container = node.ancestors.find do |ancestor|
|
156
|
+
container?(ancestor)
|
157
|
+
end
|
158
|
+
return false if container.nil?
|
159
|
+
|
160
|
+
%i[class module].include?(container.type)
|
161
|
+
end
|
162
|
+
|
163
|
+
def container?(node)
|
164
|
+
return true if define_singleton_method?(node)
|
165
|
+
|
166
|
+
%i[def defs class module].include?(node.type)
|
167
|
+
end
|
168
|
+
|
169
|
+
def mutable_literal?(node)
|
170
|
+
node&.mutable_literal?
|
171
|
+
end
|
172
|
+
|
173
|
+
def immutable_literal?(node)
|
174
|
+
node.nil? || node.immutable_literal?
|
175
|
+
end
|
176
|
+
|
177
|
+
def frozen_string_literal?(node)
|
178
|
+
FROZEN_STRING_LITERAL_TYPES.include?(node.type) &&
|
179
|
+
frozen_string_literals_enabled?
|
180
|
+
end
|
181
|
+
|
182
|
+
def requires_parentheses?(node)
|
183
|
+
node.range_type? ||
|
184
|
+
(node.send_type? && node.loc.dot.nil?)
|
185
|
+
end
|
186
|
+
|
187
|
+
def correct_splat_expansion(corrector, expr, splat_value)
|
188
|
+
if range_enclosed_in_parentheses?(splat_value)
|
189
|
+
corrector.replace(expr, "#{splat_value.source}.to_a")
|
190
|
+
else
|
191
|
+
corrector.replace(expr, "(#{splat_value.source}).to_a")
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def_node_matcher :define_singleton_method?, <<-PATTERN
|
196
|
+
(block (send nil? :define_singleton_method ...) ...)
|
197
|
+
PATTERN
|
198
|
+
|
199
|
+
def_node_matcher :splat_value, <<-PATTERN
|
200
|
+
(array (splat $_))
|
201
|
+
PATTERN
|
202
|
+
|
203
|
+
# NOTE: Some of these patterns may not actually return an immutable
|
204
|
+
# object but we will consider them immutable for this cop.
|
205
|
+
def_node_matcher :operation_produces_immutable_object?, <<-PATTERN
|
206
|
+
{
|
207
|
+
(const _ _)
|
208
|
+
(send (const nil? :Struct) :new ...)
|
209
|
+
(block (send (const nil? :Struct) :new ...) ...)
|
210
|
+
(send _ :freeze)
|
211
|
+
(send {float int} {:+ :- :* :** :/ :% :<<} _)
|
212
|
+
(send _ {:+ :- :* :** :/ :%} {float int})
|
213
|
+
(send _ {:== :=== :!= :<= :>= :< :>} _)
|
214
|
+
(send (const nil? :ENV) :[] _)
|
215
|
+
(or (send (const nil? :ENV) :[] _) _)
|
216
|
+
(send _ {:count :length :size} ...)
|
217
|
+
(block (send _ {:count :length :size} ...) ...)
|
218
|
+
}
|
219
|
+
PATTERN
|
220
|
+
|
221
|
+
def_node_matcher :range_enclosed_in_parentheses?, <<-PATTERN
|
222
|
+
(begin ({irange erange} _ _))
|
223
|
+
PATTERN
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
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
|
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.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Gee
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-06-
|
11
|
+
date: 2020-06-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rubocop
|
@@ -119,10 +119,14 @@ files:
|
|
119
119
|
- Rakefile
|
120
120
|
- bin/console
|
121
121
|
- bin/setup
|
122
|
+
- config/default.yml
|
122
123
|
- lib/rubocop-thread_safety.rb
|
123
124
|
- lib/rubocop/cop/thread_safety/class_and_module_attributes.rb
|
124
125
|
- lib/rubocop/cop/thread_safety/instance_variable_in_class_method.rb
|
126
|
+
- lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb
|
125
127
|
- lib/rubocop/cop/thread_safety/new_thread.rb
|
128
|
+
- lib/rubocop/thread_safety.rb
|
129
|
+
- lib/rubocop/thread_safety/inject.rb
|
126
130
|
- lib/rubocop/thread_safety/version.rb
|
127
131
|
- rubocop-thread_safety.gemspec
|
128
132
|
homepage: https://github.com/covermymeds/rubocop-thread_safety
|