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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 47fd0a2a3ef6c5f2ce5a46aafdc13129ec0893fc
4
- data.tar.gz: 7447310f5b032a34163982774b479063e0adb585
2
+ SHA256:
3
+ metadata.gz: ca09c8c0f8772fca865f55df6d350d28c0d3b6ab11a81f2837b76b74ce29b8bb
4
+ data.tar.gz: 16ac028cb86f4853370d6863974e2ac6cacd64253fbe9b93d2f4084ec0e89125
5
5
  SHA512:
6
- metadata.gz: c9ad6327c7f3a21054bdd18ffd11351222c5888d04527274dd4b21f8aa4101acae2f91945a16e66a4bfec4826142b06141295d071e7c618dfafee2a369a97272
7
- data.tar.gz: ffdcda7e4a5142cb8fa628570f08622cfab801cc1727491af81015f002fc2016f14e8bb866d4fb6cea01f4f7a6b943d7af95d38df1e93f34e6606d63a10a3dda
6
+ metadata.gz: 5c303f9423fe7dccfc38f47ce3a22bcc2dbb573b6754c46c3216bf49787e0bf4c5ab162af9cd1471021b5b6eea1a57ec5358d4cb699bd5a369d2116fb0d4b25b
7
+ data.tar.gz: 57ca57201e7dbc8e36458d1d97d95aae84a6b7ec7cd7d4282fd632b8f29caf54f98402315d678dbbf98f9a83d2cfecd411cb345907dead297d19f56ad1d94ee3
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/
@@ -1,14 +1,27 @@
1
1
  AllCops:
2
2
  DisplayCopNames: true
3
- Include:
4
- - Gemfile
5
- - Rakefile
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
- - vendor/**/*
13
+ - "spec/**/*"
14
+
15
+ Metrics/ClassLength:
16
+ Enabled: false
8
17
 
9
- Style/FileName:
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
 
@@ -1,24 +1,55 @@
1
- sudo: false
2
1
  cache: bundler
3
2
  language: ruby
4
3
  rvm:
5
- - 2.0.0
6
- - 2.1
7
- - 2.2
4
+ - jruby-9.2.9.0
8
5
  - 2.3.0
9
- - ruby-head
10
- - jruby-9.0.1.0
11
- - rbx-3
6
+ - 2.4
7
+ - 2.5
8
+ - 2.6
9
+ - 2.7
12
10
 
13
- matrix:
14
- allow_failures:
15
- - rvm: ruby-head
16
- - rvm: rbx-3
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
@@ -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
@@ -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
@@ -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 Threadsafety,Style/GlobalVars,Style/ClassVars,Style/MutableConstant
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
@@ -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
 
@@ -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: "../"
@@ -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.'.freeze
16
+ MSG = 'Avoid mutating class and module attributes.'
18
17
 
19
- def_node_matcher :mattr?, <<-END
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
- END
22
+ MATCHER
24
23
 
25
- def_node_matcher :attr?, <<-END
26
- (send nil
24
+ def_node_matcher :attr?, <<~MATCHER
25
+ (send nil?
27
26
  {:attr :attr_accessor :attr_writer}
28
27
  ...)
29
- END
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) || singleton_attr?(node)
33
- add_offense(node, :expression, format(MSG, node.source))
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) && 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
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.'.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
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 && defn.ancestors.any? do |ancestor|
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::Node
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.'.freeze
14
+ MSG = 'Avoid starting new threads.'
16
15
 
17
- def_node_matcher :new_thread?, <<-END
18
- (send (const nil :Thread) :new)
19
- END
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
- add_offense(node, :expression, format(MSG, node.source))
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,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RuboCop
2
4
  module ThreadSafety
3
- VERSION = '0.3.2'.freeze
5
+ VERSION = '0.4.2'
4
6
  end
5
7
  end
@@ -1,5 +1,6 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
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 = <<-end_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
- end_description
17
- spec.homepage = 'https://github.com/covermymeds/rubocop-thread_safety'
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.add_runtime_dependency 'rubocop', '>= 0.47.0'
29
+ spec.required_ruby_version = '>= 2.3.0'
25
30
 
26
- spec.add_development_dependency 'bundler', '~> 1.10'
27
- spec.add_development_dependency 'rake', '~> 10.0'
28
- spec.add_development_dependency 'rspec', '~> 3.0'
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.3.2
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: 2017-01-18 00:00:00.000000000 Z
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.47.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.47.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: rake
62
+ name: pry
43
63
  requirement: !ruby/object:Gem::Requirement
44
64
  requirements:
45
- - - "~>"
65
+ - - ">="
46
66
  - !ruby/object:Gem::Version
47
- version: '10.0'
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: '10.0'
74
+ version: '0'
55
75
  - !ruby/object:Gem::Dependency
56
- name: rspec
76
+ name: rake
57
77
  requirement: !ruby/object:Gem::Requirement
58
78
  requirements:
59
- - - "~>"
79
+ - - ">="
60
80
  - !ruby/object:Gem::Version
61
- version: '3.0'
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: '3.0'
88
+ version: '10.0'
69
89
  - !ruby/object:Gem::Dependency
70
- name: pry
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: '0'
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
- rubyforge_project:
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