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 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