forked from rubocop/rubocop-thread_safety
/
mutable_class_instance_variable.rb
280 lines (244 loc) · 8.39 KB
/
mutable_class_instance_variable.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# frozen_string_literal: true
module RuboCop
module Cop
module ThreadSafety
# This cop checks whether some class instance variable isn't a
# mutable literal (e.g. array or hash).
#
# It is based on Style/MutableConstant from RuboCop.
# See https://github.com/rubocop-hq/rubocop/blob/master/lib/rubocop/cop/style/mutable_constant.rb
#
# Class instance variables are a risk to threaded code as they are shared
# between threads. A mutable object such as an array or hash may be
# updated via an attr_reader so would not be detected by the
# ThreadSafety/ClassAndModuleAttributes cop.
#
# Strict mode can be used to freeze all class instance variables, rather
# than just literals.
# Strict mode is considered an experimental feature. It has not been
# updated with an exhaustive list of all methods that will produce frozen
# objects so there is a decent chance of getting some false positives.
# Luckily, there is no harm in freezing an already frozen object.
#
# @example EnforcedStyle: literals (default)
# # bad
# class Model
# @list = [1, 2, 3]
# end
#
# # good
# class Model
# @list = [1, 2, 3].freeze
# end
#
# # good
# class Model
# @var = <<~TESTING.freeze
# This is a heredoc
# TESTING
# end
#
# # good
# class Model
# @var = Something.new
# end
#
# @example EnforcedStyle: strict
# # bad
# class Model
# @var = Something.new
# end
#
# # bad
# class Model
# @var = Struct.new do
# def foo
# puts 1
# end
# end
# end
#
# # good
# class Model
# @var = Something.new.freeze
# end
#
# # good
# class Model
# @var = Struct.new do
# def foo
# puts 1
# end
# end.freeze
# end
class MutableClassInstanceVariable < Base
extend AutoCorrector
include FrozenStringLiteral
include ConfigurableEnforcedStyle
MSG = 'Freeze mutable objects assigned to class instance variables.'
FROZEN_STRING_LITERAL_TYPES_RUBY27 = %i[str dstr].freeze
FROZEN_STRING_LITERAL_TYPES_RUBY30 = %i[str].freeze
def on_ivasgn(node)
return unless in_class?(node)
_, value = *node
on_assignment(value)
end
def on_or_asgn(node)
lhs, value = *node
return unless lhs&.ivasgn_type?
return unless in_class?(node)
on_assignment(value)
end
def on_masgn(node)
return unless in_class?(node)
mlhs, values = *node
return unless values.array_type?
mlhs.to_a.zip(values.to_a).each do |lhs, value|
next unless lhs.ivasgn_type?
on_assignment(value)
end
end
def autocorrect(corrector, node)
expr = node.source_range
splat_value = splat_value(node)
if splat_value
correct_splat_expansion(corrector, expr, splat_value)
elsif node.array_type? && !node.bracketed?
corrector.insert_before(expr, '[')
corrector.insert_after(expr, ']')
elsif requires_parentheses?(node)
corrector.insert_before(expr, '(')
corrector.insert_after(expr, ')')
end
corrector.insert_after(expr, '.freeze')
end
private
def frozen_string_literal?(node)
literal_types = if target_ruby_version >= 3.0
FROZEN_STRING_LITERAL_TYPES_RUBY30
else
FROZEN_STRING_LITERAL_TYPES_RUBY27
end
literal_types.include?(node.type) && frozen_string_literals_enabled?
end
def on_assignment(value)
if style == :strict
strict_check(value)
else
check(value)
end
end
def strict_check(value)
return if immutable_literal?(value)
return if operation_produces_immutable_object?(value)
return if operation_produces_threadsafe_object?(value)
return if frozen_string_literal?(value)
add_offense(value) do |corrector|
autocorrect(corrector, value)
end
end
def check(value)
return unless mutable_literal?(value) ||
range_enclosed_in_parentheses?(value)
return if frozen_string_literal?(value)
add_offense(value) do |corrector|
autocorrect(corrector, value)
end
end
def in_class?(node)
container = node.ancestors.find do |ancestor|
container?(ancestor)
end
return false if container.nil?
%i[class module].include?(container.type)
end
def container?(node)
return true if define_singleton_method?(node)
return true if define_method?(node)
%i[def defs class module].include?(node.type)
end
def mutable_literal?(node)
return if node.nil?
node.mutable_literal? || range_type?(node)
end
def immutable_literal?(node)
node.nil? || node.immutable_literal?
end
def requires_parentheses?(node)
range_type?(node) ||
(node.send_type? && node.loc.dot.nil?)
end
def range_type?(node)
node.erange_type? || node.irange_type?
end
def correct_splat_expansion(corrector, expr, splat_value)
if range_enclosed_in_parentheses?(splat_value)
corrector.replace(expr, "#{splat_value.source}.to_a")
else
corrector.replace(expr, "(#{splat_value.source}).to_a")
end
end
def_node_matcher :define_singleton_method?, <<~PATTERN
(block (send nil? :define_singleton_method ...) ...)
PATTERN
def_node_matcher :define_method?, <<~PATTERN
(block (send nil? :define_method ...) ...)
PATTERN
def_node_matcher :splat_value, <<~PATTERN
(array (splat $_))
PATTERN
# NOTE: Some of these patterns may not actually return an immutable
# object but we will consider them immutable for this cop.
def_node_matcher :operation_produces_immutable_object?, <<~PATTERN
{
(const _ _)
(send (const {nil? cbase} :Struct) :new ...)
(block (send (const {nil? cbase} :Struct) :new ...) ...)
(send _ :freeze)
(send {float int} {:+ :- :* :** :/ :% :<<} _)
(send _ {:+ :- :* :** :/ :%} {float int})
(send _ {:== :=== :!= :<= :>= :< :>} _)
(send (const {nil? cbase} :ENV) :[] _)
(or (send (const {nil? cbase} :ENV) :[] _) _)
(send _ {:count :length :size} ...)
(block (send _ {:count :length :size} ...) ...)
}
PATTERN
def_node_matcher :operation_produces_threadsafe_object?, <<~PATTERN
{
(send (const {nil? cbase} :Queue) :new ...)
(send
(const (const {nil? cbase} :ThreadSafe) {:Hash :Array})
:new ...)
(block
(send
(const (const {nil? cbase} :ThreadSafe) {:Hash :Array})
:new ...)
...)
(send (const (const {nil? cbase} :Concurrent) _) :new ...)
(block
(send (const (const {nil? cbase} :Concurrent) _) :new ...)
...)
(send (const (const (const {nil? cbase} :Concurrent) _) _) :new ...)
(block
(send
(const (const (const {nil? cbase} :Concurrent) _) _)
:new ...)
...)
(send
(const (const (const (const {nil? cbase} :Concurrent) _) _) _)
:new ...)
(block
(send
(const (const (const (const {nil? cbase} :Concurrent) _) _) _)
:new ...)
...)
}
PATTERN
def_node_matcher :range_enclosed_in_parentheses?, <<~PATTERN
(begin ({irange erange} _ _))
PATTERN
end
end
end
end