audition 0.3.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 +4 -4
- data/README.md +125 -49
- data/lib/audition/bundle_sweep.rb +25 -15
- data/lib/audition/cli.rb +128 -140
- data/lib/audition/config.rb +30 -4
- data/lib/audition/dynamic/harness.rb +415 -41
- data/lib/audition/dynamic/prober.rb +271 -54
- data/lib/audition/finding.rb +14 -2
- data/lib/audition/progress.rb +418 -0
- data/lib/audition/reconciliation.rb +86 -0
- data/lib/audition/report/github.rb +4 -3
- data/lib/audition/report/json.rb +5 -1
- data/lib/audition/report/sweep.rb +182 -0
- data/lib/audition/report/text.rb +13 -2
- data/lib/audition/report.rb +8 -1
- data/lib/audition/rewriters.rb +29 -2
- data/lib/audition/static/analyzer.rb +86 -17
- data/lib/audition/static/checks/dependency_class_state.rb +160 -0
- data/lib/audition/static/checks/instance_memoization.rb +226 -0
- data/lib/audition/static/checks/mutable_constants.rb +172 -9
- data/lib/audition/static/checks/ractor_isolation.rb +214 -15
- data/lib/audition/static/checks/unsafe_calls.rb +7 -4
- data/lib/audition/static/checks/unshareable_reads.rb +259 -0
- data/lib/audition/static/checks.rb +6 -2
- data/lib/audition/static/gem_calls.rb +1770 -0
- data/lib/audition/static/graph_audit.rb +1113 -15
- data/lib/audition/static/literal_classifier.rb +385 -20
- data/lib/audition/static/native_extensions.rb +28 -16
- data/lib/audition/static/work_split.rb +54 -0
- data/lib/audition/target.rb +82 -13
- data/lib/audition/version.rb +1 -1
- data/lib/audition.rb +3 -0
- metadata +12 -4
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Audition
|
|
4
|
+
module Static
|
|
5
|
+
module Checks
|
|
6
|
+
# Lazy memoization on an instance is harmless until the
|
|
7
|
+
# instance is frozen: Ractor.make_shareable freezes every
|
|
8
|
+
# object it reaches, and the next `@x ||=` raises FrozenError.
|
|
9
|
+
# Two shapes make the freeze provable from the class alone: an
|
|
10
|
+
# initialize that ends by freezing self (a value object), and a
|
|
11
|
+
# `freeze` override (the class expects to be frozen). In the
|
|
12
|
+
# first the memo can never run; in the second it must be
|
|
13
|
+
# warmed inside the override, before super, which is the
|
|
14
|
+
# compute-on-freeze pattern. Class-level memos belong to the
|
|
15
|
+
# graph audit.
|
|
16
|
+
class InstanceMemoization < Base
|
|
17
|
+
explain :memo_after_self_freeze,
|
|
18
|
+
severity: :error,
|
|
19
|
+
message: "instance memoization %{ivar} in #%{method} on " \
|
|
20
|
+
"a class that freezes itself in initialize",
|
|
21
|
+
why: "The instance is frozen before any other method " \
|
|
22
|
+
"runs, so the first call writes an instance " \
|
|
23
|
+
"variable on a frozen object and raises " \
|
|
24
|
+
"FrozenError.",
|
|
25
|
+
fix: "Compute the value in initialize, before the " \
|
|
26
|
+
"freeze, and expose it with attr_reader; or drop " \
|
|
27
|
+
"the memo and recompute on each call."
|
|
28
|
+
|
|
29
|
+
explain :memo_not_warmed,
|
|
30
|
+
severity: :warning,
|
|
31
|
+
message: "freeze override leaves %{ivar} cold; " \
|
|
32
|
+
"#%{method} memoizes it lazily",
|
|
33
|
+
why: "Ractor.make_shareable calls freeze, so an " \
|
|
34
|
+
"instance frozen through this override raises " \
|
|
35
|
+
"FrozenError the first time #%{method} runs " \
|
|
36
|
+
"afterwards.",
|
|
37
|
+
fix: "Warm it in the override: call #%{method} (or " \
|
|
38
|
+
"assign %{ivar}) before super, the " \
|
|
39
|
+
"compute-on-freeze pattern; or compute it in " \
|
|
40
|
+
"initialize."
|
|
41
|
+
|
|
42
|
+
def initialize(file)
|
|
43
|
+
super
|
|
44
|
+
@contexts = []
|
|
45
|
+
@sclass_depth = 0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def visit_class_node(node) = scoped { super }
|
|
49
|
+
|
|
50
|
+
def visit_module_node(node) = scoped { super }
|
|
51
|
+
|
|
52
|
+
def visit_singleton_class_node(node)
|
|
53
|
+
@sclass_depth += 1
|
|
54
|
+
super
|
|
55
|
+
ensure
|
|
56
|
+
@sclass_depth -= 1
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Only plain instance methods count; a def is not entered,
|
|
60
|
+
# so nothing inside a method body opens a context.
|
|
61
|
+
def visit_def_node(node)
|
|
62
|
+
context = @contexts.last
|
|
63
|
+
return unless context && node.receiver.nil? &&
|
|
64
|
+
@sclass_depth.zero?
|
|
65
|
+
|
|
66
|
+
record_method(context, node)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def scoped
|
|
72
|
+
@contexts.push(
|
|
73
|
+
{memos: {}, methods: {}, self_freeze: false, warm: nil}
|
|
74
|
+
)
|
|
75
|
+
saved = @sclass_depth
|
|
76
|
+
@sclass_depth = 0
|
|
77
|
+
yield
|
|
78
|
+
ensure
|
|
79
|
+
@sclass_depth = saved
|
|
80
|
+
report(@contexts.pop)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def record_method(context, node)
|
|
84
|
+
case node.name
|
|
85
|
+
when :initialize
|
|
86
|
+
context[:self_freeze] = self_freezing?(node)
|
|
87
|
+
when :freeze
|
|
88
|
+
context[:warm] = touched_by(node)
|
|
89
|
+
else
|
|
90
|
+
context[:methods][node.name] ||= touched_by(node)
|
|
91
|
+
memo_sites(node).each do |ivar, write|
|
|
92
|
+
context[:memos][ivar] ||= {method: node.name, node: write}
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def report(context)
|
|
98
|
+
memos = context[:memos]
|
|
99
|
+
return if memos.empty?
|
|
100
|
+
|
|
101
|
+
if context[:self_freeze]
|
|
102
|
+
memos.each do |ivar, memo|
|
|
103
|
+
flag(memo[:node], :memo_after_self_freeze,
|
|
104
|
+
ivar: ivar, method: memo[:method])
|
|
105
|
+
end
|
|
106
|
+
elsif context[:warm]
|
|
107
|
+
reached, warmed = warmed_closure(context)
|
|
108
|
+
memos.each do |ivar, memo|
|
|
109
|
+
next if reached.include?(memo[:method]) ||
|
|
110
|
+
warmed.include?(ivar)
|
|
111
|
+
|
|
112
|
+
flag(memo[:node], :memo_not_warmed,
|
|
113
|
+
ivar: ivar, method: memo[:method])
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Everything the override reaches through the class's own
|
|
119
|
+
# instance methods: a memo is warm when its method runs on
|
|
120
|
+
# the way, or when any method on the way assigns its ivar.
|
|
121
|
+
def warmed_closure(context)
|
|
122
|
+
methods = context[:methods]
|
|
123
|
+
reached = []
|
|
124
|
+
warmed = context[:warm][:ivars].dup
|
|
125
|
+
queue = context[:warm][:calls].dup
|
|
126
|
+
until queue.empty?
|
|
127
|
+
name = queue.shift
|
|
128
|
+
next if reached.include?(name)
|
|
129
|
+
|
|
130
|
+
reached << name
|
|
131
|
+
touched = methods[name] or next
|
|
132
|
+
|
|
133
|
+
warmed.concat(touched[:ivars])
|
|
134
|
+
queue.concat(touched[:calls])
|
|
135
|
+
end
|
|
136
|
+
[reached, warmed]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# initialize ends with `freeze`, `self.freeze`, or
|
|
140
|
+
# `Ractor.make_shareable(self)`.
|
|
141
|
+
def self_freezing?(node)
|
|
142
|
+
last = statements_of(node.body)&.last
|
|
143
|
+
return false unless last.is_a?(Prism::CallNode)
|
|
144
|
+
|
|
145
|
+
receiver = last.receiver
|
|
146
|
+
if last.name == :freeze
|
|
147
|
+
(receiver.nil? || receiver.is_a?(Prism::SelfNode)) &&
|
|
148
|
+
last.arguments.nil?
|
|
149
|
+
elsif last.name == :make_shareable
|
|
150
|
+
last.arguments&.arguments&.first.is_a?(Prism::SelfNode)
|
|
151
|
+
else
|
|
152
|
+
false
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def statements_of(body)
|
|
157
|
+
case body
|
|
158
|
+
when Prism::StatementsNode then body.body
|
|
159
|
+
when Prism::BeginNode then body.statements&.body
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# The methods a body calls on self and the ivars it assigns.
|
|
164
|
+
def touched_by(node)
|
|
165
|
+
calls = []
|
|
166
|
+
ivars = []
|
|
167
|
+
each_descendant(node.body) do |child|
|
|
168
|
+
case child
|
|
169
|
+
when Prism::CallNode
|
|
170
|
+
receiver = child.receiver
|
|
171
|
+
if receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
172
|
+
calls << child.name
|
|
173
|
+
end
|
|
174
|
+
when Prism::InstanceVariableWriteNode,
|
|
175
|
+
Prism::InstanceVariableOrWriteNode
|
|
176
|
+
ivars << child.name.to_s
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
{calls: calls, ivars: ivars}
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# `@x ||= v`, and `@x = v` guarded by `defined?(@x)` in the
|
|
183
|
+
# same method.
|
|
184
|
+
def memo_sites(node)
|
|
185
|
+
or_writes = []
|
|
186
|
+
writes = {}
|
|
187
|
+
guarded = []
|
|
188
|
+
each_descendant(node.body) do |child|
|
|
189
|
+
case child
|
|
190
|
+
when Prism::InstanceVariableOrWriteNode
|
|
191
|
+
or_writes << [child.name.to_s, child]
|
|
192
|
+
when Prism::InstanceVariableWriteNode
|
|
193
|
+
writes[child.name.to_s] ||= child
|
|
194
|
+
when Prism::DefinedNode
|
|
195
|
+
value = child.value
|
|
196
|
+
if value.is_a?(Prism::InstanceVariableReadNode)
|
|
197
|
+
guarded << value.name.to_s
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
guarded.each do |ivar|
|
|
202
|
+
or_writes << [ivar, writes[ivar]] if writes[ivar]
|
|
203
|
+
end
|
|
204
|
+
or_writes
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Blocks and lambdas inside a method still write self's
|
|
208
|
+
# ivars; a nested def or class does not.
|
|
209
|
+
def each_descendant(node)
|
|
210
|
+
queue = [node].compact
|
|
211
|
+
until queue.empty?
|
|
212
|
+
current = queue.shift
|
|
213
|
+
yield current
|
|
214
|
+
current.compact_child_nodes.each do |child|
|
|
215
|
+
next if child.is_a?(Prism::DefNode) ||
|
|
216
|
+
child.is_a?(Prism::ClassNode) ||
|
|
217
|
+
child.is_a?(Prism::ModuleNode)
|
|
218
|
+
|
|
219
|
+
queue << child
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
@@ -37,6 +37,33 @@ module Audition
|
|
|
37
37
|
"BasicObject has no #freeze: use " \
|
|
38
38
|
"Object.new.freeze for such a sentinel."
|
|
39
39
|
|
|
40
|
+
explain :fresh_container,
|
|
41
|
+
severity: :error,
|
|
42
|
+
message: "constant %{name} holds an unfrozen %{type} " \
|
|
43
|
+
"returned by %{method}",
|
|
44
|
+
why: "The call allocates a new %{type} on every core " \
|
|
45
|
+
"receiver that defines it, and " \
|
|
46
|
+
"`# frozen_string_literal: true` covers literals " \
|
|
47
|
+
"only, so the constant holds an unfrozen object " \
|
|
48
|
+
"and a non-main Ractor reading it raises " \
|
|
49
|
+
"Ractor::IsolationError.",
|
|
50
|
+
fix: "Make the value deeply shareable at definition " \
|
|
51
|
+
"time with Ractor.make_shareable(...), or append " \
|
|
52
|
+
"`.freeze` when every element is itself shareable, " \
|
|
53
|
+
"since a bare `.freeze` is shallow."
|
|
54
|
+
|
|
55
|
+
explain :unshareable_object,
|
|
56
|
+
severity: :error,
|
|
57
|
+
message: "constant %{name} holds a Method object from " \
|
|
58
|
+
"%{method}, which is never shareable",
|
|
59
|
+
why: "Method and UnboundMethod objects are not " \
|
|
60
|
+
"Ractor-shareable and freezing one does not help " \
|
|
61
|
+
"(verified on Ruby 4.0); a non-main Ractor reading " \
|
|
62
|
+
"this constant raises Ractor::IsolationError.",
|
|
63
|
+
fix: "Store the method name as a Symbol and look the " \
|
|
64
|
+
"method up where it is called, or keep the Method " \
|
|
65
|
+
"object per-Ractor with Ractor.store_if_absent."
|
|
66
|
+
|
|
40
67
|
explain :mutable_container,
|
|
41
68
|
severity: :error,
|
|
42
69
|
message: "constant %{name} holds a mutable %{type} " \
|
|
@@ -46,7 +73,11 @@ module Audition
|
|
|
46
73
|
"shareable; otherwise make it deeply shareable with " \
|
|
47
74
|
"`# shareable_constant_value: literal` or " \
|
|
48
75
|
"Ractor.make_shareable(...), since a bare " \
|
|
49
|
-
"`.freeze` is shallow."
|
|
76
|
+
"`.freeze` is shallow. A public constant that " \
|
|
77
|
+
"applications mutate keeps its name and is read " \
|
|
78
|
+
"through an initialize keyword default into an " \
|
|
79
|
+
"ivar, so a shareable instance carries a frozen " \
|
|
80
|
+
"copy; mutating the constant is then deprecated."
|
|
50
81
|
|
|
51
82
|
explain :shallow_freeze,
|
|
52
83
|
severity: :error,
|
|
@@ -103,6 +134,44 @@ module Audition
|
|
|
103
134
|
"or drop the default proc and fetch with a " \
|
|
104
135
|
"literal default: hash.fetch(key, [])."
|
|
105
136
|
|
|
137
|
+
explain :unshareable_instance,
|
|
138
|
+
severity: :warning,
|
|
139
|
+
message: "constant %{name} holds an unfrozen %{klass} " \
|
|
140
|
+
"instance",
|
|
141
|
+
why: "A fresh instance is unfrozen, so non-main " \
|
|
142
|
+
"Ractors raise Ractor::IsolationError reading " \
|
|
143
|
+
"it. A warning, not an error: .new can be " \
|
|
144
|
+
"overridden to return a shareable value.",
|
|
145
|
+
fix: "Freeze it when deeply immutable, wrap in " \
|
|
146
|
+
"Ractor.make_shareable, or keep a per-Ractor " \
|
|
147
|
+
"copy via Ractor.store_if_absent."
|
|
148
|
+
|
|
149
|
+
explain :shallow_opaque,
|
|
150
|
+
severity: :warning,
|
|
151
|
+
message: "constant %{name} is frozen at the top level " \
|
|
152
|
+
"only; what it holds is unproven",
|
|
153
|
+
why: "Freezing is shallow: elements and instance " \
|
|
154
|
+
"variables stay as their calls returned them, and " \
|
|
155
|
+
"unless those are deeply frozen a non-main Ractor " \
|
|
156
|
+
"reading the constant raises " \
|
|
157
|
+
"Ractor::IsolationError. The dynamic probe settles " \
|
|
158
|
+
"it when the target boots.",
|
|
159
|
+
fix: "Build the value with Ractor.make_shareable for a " \
|
|
160
|
+
"deep freeze, or silence a known-shareable value " \
|
|
161
|
+
"with a disable comment."
|
|
162
|
+
|
|
163
|
+
explain :opaque_constant,
|
|
164
|
+
severity: :warning,
|
|
165
|
+
message: "constant %{name} holds the result of " \
|
|
166
|
+
"%{method}; shareability unproven",
|
|
167
|
+
why: "Unless the call returns a deeply frozen value, " \
|
|
168
|
+
"non-main Ractors raise Ractor::IsolationError " \
|
|
169
|
+
"reading the constant. The dynamic probe settles " \
|
|
170
|
+
"it when the target boots.",
|
|
171
|
+
fix: "Freeze the result at definition time, or " \
|
|
172
|
+
"silence a known-shareable value with a disable " \
|
|
173
|
+
"comment."
|
|
174
|
+
|
|
106
175
|
explain :constant_mutation,
|
|
107
176
|
severity: :warning,
|
|
108
177
|
message: "in-place %{method} on constant %{name}",
|
|
@@ -115,12 +184,16 @@ module Audition
|
|
|
115
184
|
"freeze it (each_with_object then .freeze), " \
|
|
116
185
|
"or move the registry behind a writer that " \
|
|
117
186
|
"rebuilds and refreezes on each change " \
|
|
118
|
-
"(copy-on-write
|
|
187
|
+
"(copy-on-write; Ractor.make_shareable when the " \
|
|
188
|
+
"additions may be unfrozen). A " \
|
|
119
189
|
"registry that plugins extend during boot is " \
|
|
120
190
|
"frozen in the last boot hook (after_initialize) " \
|
|
121
191
|
"rather than at definition, and writes after the " \
|
|
122
192
|
"freeze merge into a fresh frozen copy with a " \
|
|
123
|
-
"deprecation instead of raising."
|
|
193
|
+
"deprecation instead of raising. A public " \
|
|
194
|
+
"constant that applications mutate keeps its " \
|
|
195
|
+
"name and is read through an initialize keyword " \
|
|
196
|
+
"default into an ivar; mutating it is deprecated."
|
|
124
197
|
|
|
125
198
|
on :constant_write_node, :constant_or_write_node do |node|
|
|
126
199
|
examine(node.name.to_s, node, node.value)
|
|
@@ -152,6 +225,10 @@ module Audition
|
|
|
152
225
|
# finding stays, the autofix goes.
|
|
153
226
|
fix_ok = !mutated?(name) && !customized?(name)
|
|
154
227
|
kind = classifier.classify(value)
|
|
228
|
+
# A Sorbet cast returns its argument and a begin
|
|
229
|
+
# block its last statement: fixes, type names and
|
|
230
|
+
# depth checks target the value inside.
|
|
231
|
+
value = classifier.unwrap(value)
|
|
155
232
|
# Build-then-freeze: a bare `NAME.freeze` later in the
|
|
156
233
|
# same body makes the literal as good as frozen, so
|
|
157
234
|
# only provably mutable elements remain to report.
|
|
@@ -171,6 +248,19 @@ module Audition
|
|
|
171
248
|
flag(node, :mutable_container, name: name,
|
|
172
249
|
type: container_type(value),
|
|
173
250
|
autofix: fix_ok ? freeze_container(value) : nil)
|
|
251
|
+
when :fresh_container
|
|
252
|
+
# A later bare freeze settles the top level; only
|
|
253
|
+
# provably mutable elements are still worth a report.
|
|
254
|
+
unless frozen_later?(name) &&
|
|
255
|
+
classifier.fresh_elements(value) != :mutable
|
|
256
|
+
flag(node, :fresh_container, name: name,
|
|
257
|
+
type: classifier.fresh_type(value),
|
|
258
|
+
method: call_display(value),
|
|
259
|
+
autofix: fix_ok ? freeze_fresh(value) : nil)
|
|
260
|
+
end
|
|
261
|
+
when :unshareable_object
|
|
262
|
+
flag(node, :unshareable_object, name: name,
|
|
263
|
+
method: call_display(value))
|
|
174
264
|
when :shallow_freeze
|
|
175
265
|
flag(node, :shallow_freeze, name: name,
|
|
176
266
|
autofix:
|
|
@@ -195,6 +285,20 @@ module Audition
|
|
|
195
285
|
autofix: wrappable ? wrap_make_shareable(value) : nil)
|
|
196
286
|
when :default_proc
|
|
197
287
|
flag(node, :hash_default_proc, name: name)
|
|
288
|
+
when :shallow_opaque
|
|
289
|
+
flag(node, :shallow_opaque, name: name)
|
|
290
|
+
# Build-then-freeze leaves depth unknown: stay silent.
|
|
291
|
+
when :instance_new
|
|
292
|
+
unless frozen_later?(name)
|
|
293
|
+
flag(node, :unshareable_instance, name: name,
|
|
294
|
+
klass:
|
|
295
|
+
classifier.const_name(value.receiver) || "new")
|
|
296
|
+
end
|
|
297
|
+
when :opaque_call
|
|
298
|
+
unless frozen_later?(name)
|
|
299
|
+
flag(node, :opaque_constant, name: name,
|
|
300
|
+
method: opaque_display(value))
|
|
301
|
+
end
|
|
198
302
|
end
|
|
199
303
|
end
|
|
200
304
|
|
|
@@ -298,25 +402,72 @@ module Audition
|
|
|
298
402
|
|
|
299
403
|
# Ternaries classify as strings when both branches are;
|
|
300
404
|
# `.freeze` binds tighter than `?:`, so they get parens.
|
|
405
|
+
# `X + "s"` is a String or a Pathname, whatever X is; only
|
|
406
|
+
# a string literal receiver pins the type.
|
|
301
407
|
def call_type(call)
|
|
302
408
|
case classifier.const_name(call.receiver)
|
|
303
409
|
when "Regexp" then "Regexp"
|
|
304
410
|
when "Object", "BasicObject" then "Object"
|
|
305
|
-
else
|
|
411
|
+
else
|
|
412
|
+
if call.name == :+ && !string_receiver?(call)
|
|
413
|
+
"object"
|
|
414
|
+
else
|
|
415
|
+
"String"
|
|
416
|
+
end
|
|
306
417
|
end
|
|
307
418
|
end
|
|
308
419
|
|
|
420
|
+
def string_receiver?(call)
|
|
421
|
+
receiver = call.receiver
|
|
422
|
+
receiver.is_a?(Prism::StringNode) ||
|
|
423
|
+
receiver.is_a?(Prism::InterpolatedStringNode)
|
|
424
|
+
end
|
|
425
|
+
|
|
309
426
|
def call_display(call)
|
|
310
|
-
|
|
427
|
+
name = call.name
|
|
428
|
+
return "the #{name} operator" if name.match?(/\A[^a-z_]/i)
|
|
429
|
+
return name.to_s if call.receiver.nil?
|
|
430
|
+
|
|
431
|
+
owner = classifier.const_name(call.receiver)
|
|
432
|
+
return "#{owner}.#{name}" if owner
|
|
433
|
+
return ".#{name}" unless literal_receiver?(call)
|
|
434
|
+
|
|
435
|
+
"#{classifier.fresh_string_owner(call)}##{name}"
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def literal_receiver?(call)
|
|
439
|
+
receiver = call.receiver
|
|
440
|
+
receiver.is_a?(Prism::StringNode) ||
|
|
441
|
+
receiver.is_a?(Prism::InterpolatedStringNode) ||
|
|
442
|
+
receiver.is_a?(Prism::SymbolNode) ||
|
|
443
|
+
receiver.is_a?(Prism::RegularExpressionNode) ||
|
|
444
|
+
receiver.is_a?(Prism::ArrayNode) ||
|
|
445
|
+
receiver.is_a?(Prism::HashNode) ||
|
|
446
|
+
LiteralClassifier::NUMERIC_LITERALS
|
|
447
|
+
.any? { |type| receiver.is_a?(type) } ||
|
|
448
|
+
!classifier.array_root(receiver).nil?
|
|
449
|
+
end
|
|
311
450
|
|
|
451
|
+
# The receiver is arbitrary, often a chain, so the
|
|
452
|
+
# fallback names only the method.
|
|
453
|
+
def opaque_display(call)
|
|
312
454
|
owner = classifier.const_name(call.receiver)
|
|
313
|
-
|
|
455
|
+
method =
|
|
456
|
+
if owner
|
|
457
|
+
"#{owner}.#{call.name}"
|
|
458
|
+
elsif call.receiver
|
|
459
|
+
".#{call.name}"
|
|
460
|
+
else
|
|
461
|
+
call.name.to_s
|
|
462
|
+
end
|
|
463
|
+
"a #{method} call"
|
|
314
464
|
end
|
|
315
465
|
|
|
316
466
|
# `.freeze` binds tighter than an operator: `"a" + "b".freeze`
|
|
317
|
-
# freezes only "b"
|
|
318
|
-
#
|
|
319
|
-
#
|
|
467
|
+
# freezes only "b" and `+"a".freeze` dups the frozen
|
|
468
|
+
# literal back into a mutable one, so operator and unary
|
|
469
|
+
# calls get parentheses while literals and parenthesized
|
|
470
|
+
# or argument-free calls take the bare suffix.
|
|
320
471
|
def bare_freezable?(value)
|
|
321
472
|
case value
|
|
322
473
|
when Prism::StringNode, Prism::InterpolatedStringNode,
|
|
@@ -325,6 +476,8 @@ module Audition
|
|
|
325
476
|
when Prism::ArrayNode
|
|
326
477
|
!value.opening_loc.nil?
|
|
327
478
|
when Prism::CallNode
|
|
479
|
+
return false if value.name.end_with?("@")
|
|
480
|
+
|
|
328
481
|
!value.opening_loc.nil? ||
|
|
329
482
|
(!value.receiver.nil? && value.arguments.nil?)
|
|
330
483
|
else
|
|
@@ -374,6 +527,16 @@ module Audition
|
|
|
374
527
|
)
|
|
375
528
|
end
|
|
376
529
|
|
|
530
|
+
# A fresh container's elements are unknown, so the deep
|
|
531
|
+
# wrap is the fix, except for arrays of Integers.
|
|
532
|
+
def freeze_fresh(value)
|
|
533
|
+
if classifier.fresh_elements(value) == :shareable
|
|
534
|
+
append_freeze(value)
|
|
535
|
+
else
|
|
536
|
+
wrap_make_shareable(value)
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
|
|
377
540
|
# Plain `.freeze` where every element is provably
|
|
378
541
|
# shareable, the plain-Ruby shape; the deep wrap only
|
|
379
542
|
# where a shallow freeze would not be enough.
|