rubocop-thread_safety 0.4.0 → 0.4.1

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
2
  SHA256:
3
- metadata.gz: 33c8f2c01aa5f51c5ae09093a3ebb895a764330600ef32c05995836891cc1bd2
4
- data.tar.gz: 5d4bc3124aff9203407cb61b000564084962e0ad18d72fff954d0994eb944276
3
+ metadata.gz: d2f9c0ded508d65e482cd339fc6bafe4a843f19982884ea8c03aa0f6ceb1f4cd
4
+ data.tar.gz: bcfb88db93ee4c0bacdb1df6e60e72bdd044209ecd1c9c27fc8b8e8597f5eff9
5
5
  SHA512:
6
- metadata.gz: 130074abe1ba664fad05877bf2f215ed6965a8da36a92ba8865d05305c1f3661d9f2acfe67836620446c43bee81b3819111c764f37cbe2506474de3b0597de87
7
- data.tar.gz: ed769cdad040ba59dba88a68af2cadbf04e3eaedd1c389261050cd0917ec4a629b775798c20d3e893650796deea250dc01c80c05a32d5c2c19151fd9d88667d0
6
+ metadata.gz: 81a037e33675b2aa5cf77bad8732b43e4b09bcbb0fc1127fe64eda0481516a20068e70f3afd32d4a69673729c8ce68422374b81ad7788a9de6bbe0ab9d4ae4bd
7
+ data.tar.gz: e6c0c3e2626e371eaf47b7fc144eb7659661d98184c47e2002d49a3f276e744f6ebc67c932fa361ff4379e536322d775579303b51e0dcf806907e9e712e61532
@@ -5,6 +5,12 @@ Metrics/BlockLength:
5
5
  Exclude:
6
6
  - "spec/**/*"
7
7
 
8
+ Metrics/ClassLength:
9
+ Enabled: false
10
+
11
+ Metrics/MethodLength:
12
+ Max: 14
13
+
8
14
  Naming/FileName:
9
15
  Exclude:
10
16
  - lib/rubocop-thread_safety.rb
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.
@@ -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) || singleton_attr?(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) && node.ancestors.map(&:type).include?(:sclass)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module ThreadSafety
5
- VERSION = '0.4.0'
5
+ VERSION = '0.4.1'
6
6
  end
7
7
  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.0
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-22 00:00:00.000000000 Z
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