rubocop-thread_safety 0.4.4 → 0.7.2

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: 9f779f952ad93ee231e590b53de736e1aa6f8bfc421032e1093e495a6a1b58d5
4
- data.tar.gz: 31afd6b2f36af51336751d2de2b31b794c61e64bd8363c71bde8750df1e0b728
3
+ metadata.gz: cdf9af170c6dcfe7ac085b6e68467551d4c14d1eac843fefb97f39645cf556a6
4
+ data.tar.gz: e6efe781ead225cbdcf14185a67851a57fa60fe0e31627342a7204eb5f656c3f
5
5
  SHA512:
6
- metadata.gz: 71ac5c818dce793fdf24c6cb7e3f486bf8e897e9a9f8066bdf8b2cd25ac6637060cf65314b25fa2d7f4450dccf55e0eb4310679d32a2488aeef545effa6b0c06
7
- data.tar.gz: 3c90b593c9f948b1c75eec96edafeb14cb83934eae67e49fc032098cba86855fe5b39178c1f44376a6da05db994b8b8560bc0d4b48bf76239f3b6411474d5a24
6
+ metadata.gz: 18e2788dfc505506dd73e7231008200edca8c5055612442b14e6639ff362af00e4de983fc4be0980e6f3820a365175138cd7d04708ced0bf401db3c00eb429df
7
+ data.tar.gz: 94da6513c5ed7f1561f7e88385b3eddb6da8984127ac1da7e16f0a4c6c4c871390af0599b7b8c43c53bfc792af82363fdbe16b848fc7a93c6a10ea6d630e60d4
data/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Change log
2
+
3
+ ## master
4
+
5
+ ## 0.7.2
6
+
7
+ - [#88](https://github.com/rubocop/rubocop-thread_safety/pull/88): Fix incorrect plugin metadata version. ([@viralpraxis](https://github.com/viralpraxis))
8
+
9
+ ## 0.7.1
10
+
11
+ - [#84](https://github.com/rubocop/rubocop-thread_safety/pull/84): Rename `InstanceVariableInClassMethod` in default config ([@sambostock](https://github.com/sambostock))
12
+
13
+ ## 0.7.0
14
+
15
+ - [#80](https://github.com/rubocop/rubocop-thread_safety/pull/80) Make RuboCop ThreadSafety work as a RuboCop plugin. ([@bquorning](https://github.com/bquorning))
16
+ - [#76](https://github.com/rubocop/rubocop-thread_safety/pull/76): Detect offenses when using safe navigation for `ThreadSafety/DirChdir`, `ThreadSafety/NewThread` and `ThreadSafety/RackMiddlewareInstanceVariable` cops. ([@viralpraxis](https://github.com/viralpraxis))
17
+ - [#73](https://github.com/rubocop/rubocop-thread_safety/pull/73): Add `AllowCallWithBlock` option to `ThreadSafety/DirChdir` cop. ([@viralpraxis](https://github.com/viralpraxis))
18
+
19
+ ## 0.6.0
20
+
21
+ * [#59](https://github.com/rubocop/rubocop-thread_safety/pull/59): Rename `ThreadSafety::InstanceVariableInClassMethod` cop to `ThreadSafety::ClassInstanceVariable` to better reflect its purpose. ([@viralpraxis](https://github.com/viralpraxis))
22
+ * [#55](https://github.com/rubocop/rubocop-thread_safety/pull/55): Enhance `ThreadSafety::InstanceVariableInClassMethod` cop to detect offenses within `class_eval/exec` blocks. ([@viralpraxis](https://github.com/viralpraxis))
23
+ * [#54](https://github.com/rubocop/rubocop-thread_safety/pull/54): Drop support for RuboCop older than 1.48. ([@viralpraxis](https://github.com/viralpraxis))
24
+ * [#52](https://github.com/rubocop/rubocop-thread_safety/pull/52): Add new `RackMiddlewareInstanceVariable` cop to detect instance variables in Rack middleware. ([@viralpraxis](https://github.com/viralpraxis))
25
+ * [#48](https://github.com/rubocop/rubocop-thread_safety/pull/48): Do not report instance variables in `ActionDispatch` callbacks in singleton methods. ([@viralpraxis](https://github.com/viralpraxis))
26
+ * [#43](https://github.com/rubocop/rubocop-thread_safety/pull/43): Make detection of ActiveSupport's `class_attribute` configurable. ([@viralpraxis](https://github.com/viralpraxis))
27
+ * [#42](https://github.com/rubocop/rubocop-thread_safety/pull/42): Fix some `InstanceVariableInClassMethod` cop false positive offenses. ([@viralpraxis](https://github.com/viralpraxis))
28
+ * [#41](https://github.com/rubocop/rubocop-thread_safety/pull/41): Drop support for MRI older than 2.7. ([@viralpraxis](https://github.com/viralpraxis))
29
+ * [#38](https://github.com/rubocop/rubocop-thread_safety/pull/38): Fix `NewThread` cop detection is case of `Thread.start`, `Thread.fork`, or `Thread.new` with arguments. ([@viralpraxis](https://github.com/viralpraxis))
30
+ * [#36](https://github.com/rubocop/rubocop-thread_safety/pull/36): Add new `DirChdir` cop to detect `Dir.chdir` calls. ([@viralpraxis](https://github.com/viralpraxis))
data/LICENSE.txt CHANGED
@@ -1,4 +1,5 @@
1
- Copyright 2016-2021 CoverMyMeds
1
+ Portions Copyright 2016-2023 Michael Gee and contributors
2
+ Portions Copyright 2016-2022 CoverMyMeds
2
3
 
3
4
  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
 
data/README.md CHANGED
@@ -1,16 +1,16 @@
1
1
  # RuboCop::ThreadSafety
2
2
 
3
3
  Thread-safety analysis for your projects, as an extension to
4
- [RuboCop](https://github.com/bbatsov/rubocop).
4
+ [RuboCop](https://github.com/rubocop/rubocop).
5
5
 
6
6
  ## Installation and Usage
7
7
 
8
8
  ### Installation into an application
9
9
 
10
- Add this line to your application's Gemfile:
10
+ Add this line to your application's Gemfile (using `require: false` as it's a standalone tool):
11
11
 
12
12
  ```ruby
13
- gem 'rubocop-thread_safety'
13
+ gem 'rubocop-thread_safety', require: false
14
14
  ```
15
15
 
16
16
  Install it with Bundler by invoking:
@@ -19,11 +19,14 @@ Install it with Bundler by invoking:
19
19
 
20
20
  Add this line to your application's `.rubocop.yml`:
21
21
 
22
- require: rubocop-thread_safety
22
+ plugins: rubocop-thread_safety
23
23
 
24
24
  Now you can run `rubocop` and it will automatically load the RuboCop
25
25
  Thread-Safety cops together with the standard cops.
26
26
 
27
+ > [!NOTE]
28
+ > The plugin system is supported in RuboCop 1.72+. In earlier versions, use `require` instead of `plugins`.
29
+
27
30
  ### Scanning an application without adding it to the Gemfile
28
31
 
29
32
  Install the gem:
@@ -32,11 +35,11 @@ Install the gem:
32
35
 
33
36
  Scan the application for just thread-safety issues:
34
37
 
35
- $ rubocop -r rubocop-thread_safety --only ThreadSafety,Style/GlobalVars,Style/ClassVars,Style/MutableConstant
38
+ $ rubocop --plugin rubocop-thread_safety --only ThreadSafety,Style/GlobalVars,Style/ClassVars,Style/MutableConstant
36
39
 
37
40
  ### Configuration
38
41
 
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.
42
+ There are some added [configuration options](https://github.com/rubocop/rubocop-thread_safety/blob/master/config/default.yml) that can be tweaked to modify the behaviour of these thread-safety cops.
40
43
 
41
44
  ### Correcting code for thread-safety
42
45
 
@@ -60,6 +63,8 @@ Improvements that would make shared state thread-safe include:
60
63
  * Use [`RequestStore`](https://github.com/steveklabnik/request_store)
61
64
  * Use `Thread.current[:name]`
62
65
 
66
+ Certain system calls, such as `chdir`, affect the entire process. To avoid potential thread-safety issues, it's preferable to use (if possible) the `chdir` option in methods like `Kernel.system` and `IO.popen` rather than relying on `Dir.chdir`.
67
+
63
68
  ## Development
64
69
 
65
70
  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.
@@ -68,9 +73,11 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
68
73
 
69
74
  ## Contributing
70
75
 
71
- Bug reports and pull requests are welcome on GitHub at https://github.com/covermymeds/rubocop-thread_safety.
76
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rubocop/rubocop-thread_safety.
72
77
 
73
78
  ## Copyright
74
79
 
75
- Copyright (c) 2016-2021 CoverMyMeds.
80
+ Portions Copyright (c) 2016-2023 Michael Gee and [contributors](https://github.com/rubocop/rubocop-thread_safety/graphs/contributors).
81
+ Portions Copyright (c) 2016-2023 CoverMyMeds.
82
+
76
83
  See [LICENSE.txt](LICENSE.txt) for further details.
data/config/default.yml CHANGED
@@ -5,15 +5,17 @@
5
5
  ThreadSafety/ClassAndModuleAttributes:
6
6
  Description: 'Avoid mutating class and module attributes.'
7
7
  Enabled: true
8
+ ActiveSupportClassAttributeAllowed: false
8
9
 
9
- ThreadSafety/InstanceVariableInClassMethod:
10
- Description: 'Avoid using instance variables in class methods.'
10
+ ThreadSafety/ClassInstanceVariable:
11
+ Description: 'Avoid class instance variables.'
11
12
  Enabled: true
12
13
 
13
14
  ThreadSafety/MutableClassInstanceVariable:
14
15
  Description: 'Do not assign mutable objects to class instance variables.'
15
16
  Enabled: true
16
17
  EnforcedStyle: literals
18
+ SafeAutoCorrect: false
17
19
  SupportedStyles:
18
20
  # literals: freeze literals assigned to constants
19
21
  # strict: freeze all constants
@@ -29,3 +31,18 @@ ThreadSafety/NewThread:
29
31
  Avoid starting new threads.
30
32
  Let a framework like Sidekiq handle the threads.
31
33
  Enabled: true
34
+
35
+ ThreadSafety/DirChdir:
36
+ Description: Avoid using `Dir.chdir` due to its process-wide effect.
37
+ Enabled: true
38
+ AllowCallWithBlock: false
39
+
40
+ ThreadSafety/RackMiddlewareInstanceVariable:
41
+ Description: Avoid instance variables in Rack middleware.
42
+ Enabled: true
43
+ Include:
44
+ - 'app/middleware/**/*.rb'
45
+ - 'lib/middleware/**/*.rb'
46
+ - 'app/middlewares/**/*.rb'
47
+ - 'lib/middlewares/**/*.rb'
48
+ AllowedIdentifiers: []
@@ -0,0 +1,2 @@
1
+ renamed:
2
+ ThreadSafety/InstanceVariableInClassMethod: ThreadSafety/ClassInstanceVariable
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ # Common functionality for checking if a well-known operation
6
+ # produces an object with thread-safe semantics.
7
+ module OperationWithThreadsafeResult
8
+ extend NodePattern::Macros
9
+
10
+ # @!method operation_produces_threadsafe_object?(node)
11
+ def_node_matcher :operation_produces_threadsafe_object?, <<~PATTERN
12
+ {
13
+ (send (const {nil? cbase} :Queue) :new ...)
14
+ (send
15
+ (const (const {nil? cbase} :ThreadSafe) {:Hash :Array})
16
+ :new ...)
17
+ (block
18
+ (send
19
+ (const (const {nil? cbase} :ThreadSafe) {:Hash :Array})
20
+ :new ...)
21
+ ...)
22
+ (send (const (const {nil? cbase} :Concurrent) _) :new ...)
23
+ (block
24
+ (send (const (const {nil? cbase} :Concurrent) _) :new ...)
25
+ ...)
26
+ (send (const (const (const {nil? cbase} :Concurrent) _) _) :new ...)
27
+ (block
28
+ (send
29
+ (const (const (const {nil? cbase} :Concurrent) _) _)
30
+ :new ...)
31
+ ...)
32
+ (send
33
+ (const (const (const (const {nil? cbase} :Concurrent) _) _) _)
34
+ :new ...)
35
+ (block
36
+ (send
37
+ (const (const (const (const {nil? cbase} :Concurrent) _) _) _)
38
+ :new ...)
39
+ ...)
40
+ }
41
+ PATTERN
42
+ end
43
+ end
44
+ end
@@ -12,38 +12,47 @@ module RuboCop
12
12
  # class User
13
13
  # cattr_accessor :current_user
14
14
  # end
15
- class ClassAndModuleAttributes < Cop
15
+ class ClassAndModuleAttributes < Base
16
16
  MSG = 'Avoid mutating class and module attributes.'
17
+ RESTRICT_ON_SEND = %i[
18
+ mattr_writer mattr_accessor cattr_writer cattr_accessor
19
+ class_attribute
20
+ attr attr_accessor attr_writer
21
+ attr_internal attr_internal_accessor attr_internal_writer
22
+ ].freeze
17
23
 
24
+ # @!method mattr?(node)
18
25
  def_node_matcher :mattr?, <<~MATCHER
19
26
  (send nil?
20
27
  {:mattr_writer :mattr_accessor :cattr_writer :cattr_accessor}
21
28
  ...)
22
29
  MATCHER
23
30
 
31
+ # @!method attr?(node)
24
32
  def_node_matcher :attr?, <<~MATCHER
25
33
  (send nil?
26
34
  {:attr :attr_accessor :attr_writer}
27
35
  ...)
28
36
  MATCHER
29
37
 
38
+ # @!method attr_internal?(node)
30
39
  def_node_matcher :attr_internal?, <<~MATCHER
31
40
  (send nil?
32
41
  {:attr_internal :attr_internal_accessor :attr_internal_writer}
33
42
  ...)
34
43
  MATCHER
35
44
 
45
+ # @!method class_attr?(node)
36
46
  def_node_matcher :class_attr?, <<~MATCHER
37
47
  (send nil?
38
48
  :class_attribute
39
49
  ...)
40
50
  MATCHER
41
51
 
42
- def on_send(node)
43
- return unless mattr?(node) || class_attr?(node) ||
44
- singleton_attr?(node)
52
+ def on_send(node) # rubocop:disable InternalAffairs/OnSendWithoutOnCSend
53
+ return unless mattr?(node) || (!class_attribute_allowed? && class_attr?(node)) || singleton_attr?(node)
45
54
 
46
- add_offense(node, message: MSG)
55
+ add_offense(node)
47
56
  end
48
57
 
49
58
  private
@@ -64,6 +73,10 @@ module RuboCop
64
73
 
65
74
  false
66
75
  end
76
+
77
+ def class_attribute_allowed?
78
+ cop_config['ActiveSupportClassAttributeAllowed']
79
+ end
67
80
  end
68
81
  end
69
82
  end
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module ThreadSafety
6
+ # Avoid class instance variables.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # class User
11
+ # def self.notify(info)
12
+ # @info = validate(info)
13
+ # Notifier.new(@info).deliver
14
+ # end
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
40
+ #
41
+ # module Example
42
+ # class_methods do
43
+ # def test(params)
44
+ # @params = params
45
+ # end
46
+ # end
47
+ # end
48
+ #
49
+ # module Example
50
+ # module_function
51
+ #
52
+ # def test(params)
53
+ # @params = params
54
+ # end
55
+ # end
56
+ #
57
+ # module Example
58
+ # def test(params)
59
+ # @params = params
60
+ # end
61
+ #
62
+ # module_function :test
63
+ # end
64
+ class ClassInstanceVariable < Base
65
+ MSG = 'Avoid class instance variables.'
66
+ RESTRICT_ON_SEND = %i[
67
+ instance_variable_set
68
+ instance_variable_get
69
+ ].freeze
70
+
71
+ # @!method instance_variable_set_call?(node)
72
+ def_node_matcher :instance_variable_set_call?, <<~MATCHER
73
+ (send nil? :instance_variable_set (...) (...))
74
+ MATCHER
75
+
76
+ # @!method instance_variable_get_call?(node)
77
+ def_node_matcher :instance_variable_get_call?, <<~MATCHER
78
+ (send nil? :instance_variable_get (...))
79
+ MATCHER
80
+
81
+ def on_ivar(node)
82
+ return unless class_method_definition?(node)
83
+ return if method_definition?(node)
84
+ return if synchronized?(node)
85
+
86
+ add_offense(node.loc.name)
87
+ end
88
+ alias on_ivasgn on_ivar
89
+
90
+ def on_send(node) # rubocop:disable InternalAffairs/OnSendWithoutOnCSend
91
+ return unless instance_variable_call?(node)
92
+ return unless class_method_definition?(node)
93
+ return if method_definition?(node)
94
+ return if synchronized?(node)
95
+
96
+ add_offense(node)
97
+ end
98
+
99
+ private
100
+
101
+ def class_method_definition?(node)
102
+ in_defs?(node) ||
103
+ in_def_sclass?(node) ||
104
+ in_def_class_methods?(node) ||
105
+ in_def_module_function?(node) ||
106
+ in_class_eval?(node) ||
107
+ singleton_method_definition?(node)
108
+ end
109
+
110
+ def in_defs?(node)
111
+ node.ancestors.any? do |ancestor|
112
+ break false if new_lexical_scope?(ancestor)
113
+
114
+ ancestor.defs_type?
115
+ end
116
+ end
117
+
118
+ def in_def_sclass?(node)
119
+ defn = node.ancestors.find do |ancestor|
120
+ break if new_lexical_scope?(ancestor)
121
+
122
+ ancestor.def_type?
123
+ end
124
+
125
+ defn&.ancestors&.any?(&:sclass_type?)
126
+ end
127
+
128
+ def in_def_class_methods?(node)
129
+ in_def_class_methods_dsl?(node) || in_def_class_methods_module?(node)
130
+ end
131
+
132
+ def in_def_class_methods_dsl?(node)
133
+ node.ancestors.any? do |ancestor|
134
+ break if new_lexical_scope?(ancestor)
135
+ next unless ancestor.block_type?
136
+
137
+ ancestor.children.first.command? :class_methods
138
+ end
139
+ end
140
+
141
+ def in_def_class_methods_module?(node)
142
+ defn = node.ancestors.find do |ancestor|
143
+ break if new_lexical_scope?(ancestor)
144
+
145
+ ancestor.def_type?
146
+ end
147
+ return false unless defn
148
+
149
+ mod = defn.ancestors.find do |ancestor|
150
+ %i[class module].include?(ancestor.type)
151
+ end
152
+ return false unless mod
153
+
154
+ class_methods_module?(mod)
155
+ end
156
+
157
+ def in_def_module_function?(node)
158
+ defn = node.ancestors.find(&:def_type?)
159
+ return false unless defn
160
+
161
+ defn.left_siblings.any? { |sibling| module_function_bare_access_modifier?(sibling) } ||
162
+ defn.right_siblings.any? { |sibling| module_function_for?(sibling, defn.method_name) }
163
+ end
164
+
165
+ def in_class_eval?(node)
166
+ defn = node.ancestors.find do |ancestor|
167
+ break if ancestor.def_type? || new_lexical_scope?(ancestor)
168
+
169
+ ancestor.block_type?
170
+ end
171
+ return false unless defn
172
+
173
+ class_eval_scope?(defn)
174
+ end
175
+
176
+ def singleton_method_definition?(node)
177
+ node.ancestors.any? do |ancestor|
178
+ break if new_lexical_scope?(ancestor)
179
+ next unless ancestor.children.first.is_a? AST::SendNode
180
+
181
+ ancestor.children.first.command? :define_singleton_method
182
+ end
183
+ end
184
+
185
+ def method_definition?(node)
186
+ node.ancestors.any? do |ancestor|
187
+ break if new_lexical_scope?(ancestor)
188
+ next unless ancestor.children.first.is_a? AST::SendNode
189
+
190
+ ancestor.children.first.command? :define_method
191
+ end
192
+ end
193
+
194
+ def synchronized?(node)
195
+ node.ancestors.find do |ancestor|
196
+ next unless ancestor.block_type?
197
+
198
+ s = ancestor.children.first
199
+ s.send_type? && s.children.last == :synchronize
200
+ end
201
+ end
202
+
203
+ def instance_variable_call?(node)
204
+ instance_variable_set_call?(node) || instance_variable_get_call?(node)
205
+ end
206
+
207
+ def module_function_bare_access_modifier?(node)
208
+ return false unless node.respond_to?(:send_type?)
209
+
210
+ node.send_type? && node.bare_access_modifier? && node.method?(:module_function)
211
+ end
212
+
213
+ def match_name?(arg_name, method_name)
214
+ arg_name.to_sym == method_name.to_sym
215
+ end
216
+
217
+ # @!method class_methods_module?(node)
218
+ def_node_matcher :class_methods_module?, <<~PATTERN
219
+ (module (const _ :ClassMethods) ...)
220
+ PATTERN
221
+
222
+ # @!method module_function_for?(node)
223
+ def_node_matcher :module_function_for?, <<~PATTERN
224
+ (send nil? {:module_function} ({sym str} #match_name?(%1)))
225
+ PATTERN
226
+
227
+ # @!method new_lexical_scope?(node)
228
+ def_node_matcher :new_lexical_scope?, <<~PATTERN
229
+ {
230
+ (block (send (const nil? :Struct) :new ...) _ ({def defs} ...))
231
+ (block (send (const nil? :Class) :new ...) _ ({def defs} ...))
232
+ (block (send (const nil? :Data) :define ...) _ ({def defs} ...))
233
+ (block
234
+ (send nil?
235
+ {
236
+ :prepend_around_action
237
+ :prepend_before_action
238
+ :before_action
239
+ :append_before_action
240
+ :around_action
241
+ :append_around_action
242
+ :append_after_action
243
+ :after_action
244
+ :prepend_after_action
245
+ }
246
+ )
247
+ ...
248
+ )
249
+ }
250
+ PATTERN
251
+
252
+ # @!method class_eval_scope?(node)
253
+ def_node_matcher :class_eval_scope?, <<~PATTERN
254
+ (block (send (const {nil? cbase} _) {:class_eval :class_exec}) ...)
255
+ PATTERN
256
+ end
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module ThreadSafety
6
+ # Avoid using `Dir.chdir` due to its process-wide effect.
7
+ # If `AllowCallWithBlock` (disabled by default) option is enabled,
8
+ # calling `Dir.chdir` with block will be allowed.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # Dir.chdir("/var/run")
13
+ #
14
+ # # bad
15
+ # FileUtils.chdir("/var/run")
16
+ #
17
+ # @example AllowCallWithBlock: false (default)
18
+ # # good
19
+ # Dir.chdir("/var/run") do
20
+ # puts Dir.pwd
21
+ # end
22
+ #
23
+ # @example AllowCallWithBlock: true
24
+ # # bad
25
+ # Dir.chdir("/var/run") do
26
+ # puts Dir.pwd
27
+ # end
28
+ #
29
+ class DirChdir < Base
30
+ MESSAGE = 'Avoid using `%<module>s%<dot>s%<method>s` due to its process-wide effect.'
31
+ RESTRICT_ON_SEND = %i[chdir cd].freeze
32
+
33
+ # @!method chdir?(node)
34
+ def_node_matcher :chdir?, <<~MATCHER
35
+ {
36
+ (call (const {nil? cbase} {:Dir :FileUtils}) :chdir ...)
37
+ (call (const {nil? cbase} :FileUtils) :cd ...)
38
+ }
39
+ MATCHER
40
+
41
+ def on_send(node)
42
+ return unless chdir?(node)
43
+ return if allow_call_with_block? && (node.block_argument? || node.parent&.block_type?)
44
+
45
+ add_offense(
46
+ node,
47
+ message: format(
48
+ MESSAGE,
49
+ module: node.receiver.short_name,
50
+ method: node.method_name,
51
+ dot: node.loc.dot.source
52
+ )
53
+ )
54
+ end
55
+ alias on_csend on_send
56
+
57
+ private
58
+
59
+ def allow_call_with_block?
60
+ !!cop_config['AllowCallWithBlock']
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end