audition 0.2.2 → 0.2.4
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 +23 -5
- data/lib/audition/static/checks/mutable_constants.rb +64 -7
- data/lib/audition/static/checks/unsafe_calls.rb +43 -23
- data/lib/audition/static/graph_audit.rb +16 -9
- data/lib/audition/static/literal_classifier.rb +54 -2
- data/lib/audition/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 76a1b0c57c7a373182f7b9f88f765fa8e0167a492e28d028998335bc246c10d9
|
|
4
|
+
data.tar.gz: 2c5ecb49ed0a63ebd7a58cb3db9e8b5e8f9893953198d1c3e9f0cc5ca74cfb3a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 633d141cdc23a3e2e24df8b983b9112ffa9a759194669c4f9a6f62f0bf07219b507bd72cea8235de3c7193ab934c60ca8c98ee2095eabe21fad2e80f682ea8e2
|
|
7
|
+
data.tar.gz: bfd863839b4d5fbea291d22282487de79d261fd55bba5662d82d285e013827290257ca61d5b637000581453ec8f3a0510b1571bf6492d1b9f6f9e7de9f3af225
|
data/README.md
CHANGED
|
@@ -116,6 +116,7 @@ proxying) and its verified semantics.
|
|
|
116
116
|
- [Agent skill](#agent-skill)
|
|
117
117
|
- [Extending](#extending)
|
|
118
118
|
- [Development](#development)
|
|
119
|
+
- [Acknowledgements](#acknowledgements)
|
|
119
120
|
- [License](#license)
|
|
120
121
|
|
|
121
122
|
## Installation
|
|
@@ -212,10 +213,12 @@ Static, with file:line precision:
|
|
|
212
213
|
body, `def self.`, and `class << self`, across files; the classic
|
|
213
214
|
`@cache ||= {}` and `return @x if defined?(@x)` memoizations.
|
|
214
215
|
- **Constants that are not deeply shareable**: bare mutable
|
|
215
|
-
literals, interpolated strings,
|
|
216
|
-
(`[[1], [2]].freeze` still raises; audition explains why)
|
|
217
|
-
|
|
218
|
-
|
|
216
|
+
literals, interpolated strings, the subtle shallow freeze
|
|
217
|
+
(`[[1], [2]].freeze` still raises; audition explains why), and
|
|
218
|
+
call results the magic comment never covers (`X.tr(":", "")`,
|
|
219
|
+
`Regexp.new`, `Regexp.union`, `format`), the shapes Rails fixed
|
|
220
|
+
last in its own ractorization. Honors `# frozen_string_literal:`
|
|
221
|
+
and `# shareable_constant_value:` magic comments.
|
|
219
222
|
- **Sync primitives and Procs in constants** (Mutex, Queue,
|
|
220
223
|
lambdas), including `Hash.new { }` default procs, which stay
|
|
221
224
|
unshareable even after `.freeze`.
|
|
@@ -228,7 +231,9 @@ Static, with file:line precision:
|
|
|
228
231
|
- **`Ractor.new` blocks capturing outer locals** (the ArgumentError
|
|
229
232
|
at creation time), resolved through Prism's exact scope depths.
|
|
230
233
|
- **Hostile or removed APIs**: `Ractor.yield`/`take` (gone in 4.0),
|
|
231
|
-
ActiveSupport `
|
|
234
|
+
ActiveSupport `cattr_*`/`mattr_*` class variables (with the
|
|
235
|
+
`class_attribute` migration Rails itself made) and
|
|
236
|
+
`class_attribute` without copy-on-write writes,
|
|
232
237
|
`include Singleton`, `fork`, `ObjectSpace._id2ref`, ENV mutation.
|
|
233
238
|
|
|
234
239
|
Dynamic, on the live object graph:
|
|
@@ -312,6 +317,19 @@ The design notes in `docs/design.md` include the empirically
|
|
|
312
317
|
verified Ruby 4.0 Ractor semantics table that the checks are
|
|
313
318
|
calibrated against.
|
|
314
319
|
|
|
320
|
+
## Acknowledgements
|
|
321
|
+
|
|
322
|
+
The whole-program checks stand on
|
|
323
|
+
[rubydex](https://github.com/Shopify/rubydex), Shopify's
|
|
324
|
+
high-performance static analysis suite for Ruby: audition feeds
|
|
325
|
+
every file into its graph and reads state ownership back out.
|
|
326
|
+
Thanks to its authors, in particular the top five contributors:
|
|
327
|
+
[Alexandre Terrasa](https://github.com/Morriar),
|
|
328
|
+
[Vinicius Stock](https://github.com/vinistock),
|
|
329
|
+
[Alex Rocha](https://github.com/alexcrocha),
|
|
330
|
+
[Stan Lo](https://github.com/st0012), and
|
|
331
|
+
[Soutaro Matsumoto](https://github.com/soutaro).
|
|
332
|
+
|
|
315
333
|
## Assisted by
|
|
316
334
|
|
|
317
335
|
Claude Fable 5.
|
|
@@ -22,6 +22,19 @@ module Audition
|
|
|
22
22
|
fix: "Add `# frozen_string_literal: true` to the " \
|
|
23
23
|
"file, or append `.freeze`."
|
|
24
24
|
|
|
25
|
+
explain :mutable_call,
|
|
26
|
+
severity: :error,
|
|
27
|
+
message: "constant %{name} holds an unfrozen %{type} " \
|
|
28
|
+
"returned by %{method}",
|
|
29
|
+
why: "`# frozen_string_literal: true` freezes " \
|
|
30
|
+
"literals only; a method call returns a fresh " \
|
|
31
|
+
"unfrozen object, so a non-main Ractor reading " \
|
|
32
|
+
"this constant raises Ractor::IsolationError. " \
|
|
33
|
+
"Rails hit this with `.tr` and `Regexp.new` " \
|
|
34
|
+
"during its ractorization.",
|
|
35
|
+
fix: "Append `.freeze` to the call; a frozen String " \
|
|
36
|
+
"or Regexp is deeply shareable."
|
|
37
|
+
|
|
25
38
|
explain :mutable_container,
|
|
26
39
|
severity: :error,
|
|
27
40
|
message: "constant %{name} holds a mutable %{type} " \
|
|
@@ -56,8 +69,10 @@ module Audition
|
|
|
56
69
|
"touching this constant raises " \
|
|
57
70
|
"Ractor::IsolationError.",
|
|
58
71
|
fix: "Use Ractor::Port for cross-Ractor " \
|
|
59
|
-
"coordination
|
|
60
|
-
"via Ractor.store_if_absent
|
|
72
|
+
"coordination; keep a per-Ractor primitive " \
|
|
73
|
+
"via Ractor.store_if_absent when the state it " \
|
|
74
|
+
"guards is per-Ractor too (Rails moved its " \
|
|
75
|
+
"template digest mutex this way); or use " \
|
|
61
76
|
"Ractor-safe structures (ractor_safe, ratomic " \
|
|
62
77
|
"gems)."
|
|
63
78
|
|
|
@@ -97,7 +112,12 @@ module Audition
|
|
|
97
112
|
"freeze it (each_with_object then .freeze), " \
|
|
98
113
|
"or move the registry behind a writer that " \
|
|
99
114
|
"rebuilds and refreezes on each change, the " \
|
|
100
|
-
"copy-on-write style Rails registries use."
|
|
115
|
+
"copy-on-write style Rails registries use. A " \
|
|
116
|
+
"registry that plugins extend during boot is " \
|
|
117
|
+
"frozen in the last boot hook (after_initialize) " \
|
|
118
|
+
"rather than at definition, and writes after the " \
|
|
119
|
+
"freeze merge into a fresh frozen copy with a " \
|
|
120
|
+
"deprecation instead of raising."
|
|
101
121
|
|
|
102
122
|
on :constant_write_node, :constant_or_write_node do |node|
|
|
103
123
|
examine(node.name.to_s, node, node.value)
|
|
@@ -131,6 +151,10 @@ module Audition
|
|
|
131
151
|
when :mutable_string
|
|
132
152
|
flag(node, :mutable_string, name: name,
|
|
133
153
|
autofix: fix_ok ? append_freeze(value) : nil)
|
|
154
|
+
when :mutable_call
|
|
155
|
+
flag(node, :mutable_call, name: name,
|
|
156
|
+
type: call_type(value), method: call_display(value),
|
|
157
|
+
autofix: fix_ok ? append_freeze(value) : nil)
|
|
134
158
|
when :mutable_container
|
|
135
159
|
flag(node, :mutable_container, name: name,
|
|
136
160
|
type: container_type(value),
|
|
@@ -151,6 +175,7 @@ module Audition
|
|
|
151
175
|
# a class or module body get the wrap.
|
|
152
176
|
body = proc_body(value)
|
|
153
177
|
wrappable = fix_ok && namespaced?(node) &&
|
|
178
|
+
!opaque_proc?(value) &&
|
|
154
179
|
(body.nil? ||
|
|
155
180
|
RactorIsolation::CaptureScanner
|
|
156
181
|
.scan(body).empty?)
|
|
@@ -175,10 +200,16 @@ module Audition
|
|
|
175
200
|
def proc_body(value)
|
|
176
201
|
case value
|
|
177
202
|
when Prism::LambdaNode then value.body
|
|
178
|
-
when Prism::CallNode
|
|
203
|
+
when Prism::CallNode
|
|
204
|
+
block = value.block
|
|
205
|
+
block.body if block.is_a?(Prism::BlockNode)
|
|
179
206
|
end
|
|
180
207
|
end
|
|
181
208
|
|
|
209
|
+
def opaque_proc?(value)
|
|
210
|
+
value.is_a?(Prism::CallNode) && value.block.is_a?(Prism::BlockArgumentNode)
|
|
211
|
+
end
|
|
212
|
+
|
|
182
213
|
def namespaced?(node)
|
|
183
214
|
offset = node.location.start_offset
|
|
184
215
|
module_ranges.any? { |range| range.cover?(offset) }
|
|
@@ -245,10 +276,36 @@ module Audition
|
|
|
245
276
|
|
|
246
277
|
# Ternaries classify as strings when both branches are;
|
|
247
278
|
# `.freeze` binds tighter than `?:`, so they get parens.
|
|
279
|
+
def call_type(call)
|
|
280
|
+
owner = classifier.const_name(call.receiver)
|
|
281
|
+
(owner == "Regexp") ? "Regexp" : "String"
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def call_display(call)
|
|
285
|
+
return call.name.to_s if call.receiver.nil?
|
|
286
|
+
|
|
287
|
+
owner = classifier.const_name(call.receiver)
|
|
288
|
+
owner ? "#{owner}.#{call.name}" : "String##{call.name}"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# `.freeze` binds tighter than an operator: `"a" + "b".freeze`
|
|
292
|
+
# freezes only "b", so operator calls get parentheses while
|
|
293
|
+
# literals and parenthesized or argument-free calls take
|
|
294
|
+
# the bare suffix.
|
|
295
|
+
def bare_freezable?(value)
|
|
296
|
+
case value
|
|
297
|
+
when Prism::StringNode, Prism::InterpolatedStringNode
|
|
298
|
+
true
|
|
299
|
+
when Prism::CallNode
|
|
300
|
+
!value.opening_loc.nil? ||
|
|
301
|
+
(!value.receiver.nil? && value.arguments.nil?)
|
|
302
|
+
else
|
|
303
|
+
false
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
248
307
|
def append_freeze(value)
|
|
249
|
-
|
|
250
|
-
value.is_a?(Prism::InterpolatedStringNode)
|
|
251
|
-
if string
|
|
308
|
+
if bare_freezable?(value)
|
|
252
309
|
offset = value.location.end_offset
|
|
253
310
|
Autofix.new(
|
|
254
311
|
start_offset: offset,
|
|
@@ -20,19 +20,36 @@ module Audition
|
|
|
20
20
|
"Ractor, and use port.send/port.receive; " \
|
|
21
21
|
"collect results with Ractor#value."
|
|
22
22
|
|
|
23
|
-
explain :
|
|
23
|
+
explain :rails_class_variable_macro,
|
|
24
24
|
severity: :error,
|
|
25
|
-
message: "%{method} stores state
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"Ractor.
|
|
25
|
+
message: "%{method} stores state in a class variable",
|
|
26
|
+
why: "The cattr_* and mattr_* macros define @@ class " \
|
|
27
|
+
"variables; reads and writes alike raise " \
|
|
28
|
+
"Ractor::IsolationError from a non-main Ractor, " \
|
|
29
|
+
"whatever the value holds.",
|
|
30
|
+
fix: "Rails itself migrated these to class_attribute " \
|
|
31
|
+
"(a class-level ivar whose frozen value any " \
|
|
32
|
+
"Ractor may read) or to a module ivar behind a " \
|
|
33
|
+
"reader; give it a frozen default and rebuild " \
|
|
34
|
+
"and refreeze on write, at boot on the main " \
|
|
35
|
+
"Ractor."
|
|
36
|
+
|
|
37
|
+
explain :rails_class_attribute,
|
|
38
|
+
severity: :warning,
|
|
39
|
+
message: "%{method} stores state on the class object",
|
|
40
|
+
why: "The value lives in a class-level instance " \
|
|
41
|
+
"variable. Rails 8.2 made the reader Ractor-safe, " \
|
|
42
|
+
"so a frozen value is readable from any Ractor; " \
|
|
43
|
+
"earlier readers are define_method closures that " \
|
|
44
|
+
"raise on the first call from a non-main Ractor " \
|
|
45
|
+
"(thread_mattr_accessor memoized its key the " \
|
|
46
|
+
"same way). Writes always need the main Ractor.",
|
|
47
|
+
fix: "Give it a frozen default (default: [].freeze) " \
|
|
48
|
+
"and write copy-on-write: self.x = (x | [v])" \
|
|
49
|
+
".freeze, the idiom Rails applied across Action " \
|
|
50
|
+
"Pack and Active Record; do every write at boot " \
|
|
51
|
+
"on the main Ractor. The dynamic probe reports " \
|
|
52
|
+
"ground truth for the installed Rails."
|
|
36
53
|
|
|
37
54
|
explain :objectspace_id2ref,
|
|
38
55
|
severity: :warning,
|
|
@@ -91,13 +108,15 @@ module Audition
|
|
|
91
108
|
"Ractor raises Ractor::IsolationError " \
|
|
92
109
|
"(\"defined with an un-shareable Proc\") " \
|
|
93
110
|
"unless the block was made shareable first.",
|
|
94
|
-
fix: "
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
111
|
+
fix: "Pass a shareable lambda instead of a block: " \
|
|
112
|
+
"define_method(:x, Ractor.shareable_lambda " \
|
|
113
|
+
"{ ... }), as Rails did for its date selectors " \
|
|
114
|
+
"and url helpers. Captured locals must be " \
|
|
115
|
+
"shareable (strings become symbols) and assigned " \
|
|
116
|
+
"before the lambda is created, and super is " \
|
|
117
|
+
"unavailable. When the captures are literals, " \
|
|
118
|
+
"generate the method with class_eval and a " \
|
|
119
|
+
"source string instead."
|
|
101
120
|
|
|
102
121
|
explain :singleton_include,
|
|
103
122
|
severity: :warning,
|
|
@@ -115,10 +134,11 @@ module Audition
|
|
|
115
134
|
RULES = Ractor.make_shareable([
|
|
116
135
|
{key: :ractor_yield_removed, receiver: "Ractor",
|
|
117
136
|
methods: %i[yield take]},
|
|
118
|
-
{key: :
|
|
119
|
-
methods: %i[
|
|
120
|
-
|
|
121
|
-
|
|
137
|
+
{key: :rails_class_variable_macro, receiver: nil,
|
|
138
|
+
methods: %i[cattr_accessor cattr_reader cattr_writer
|
|
139
|
+
mattr_accessor mattr_reader mattr_writer]},
|
|
140
|
+
{key: :rails_class_attribute, receiver: nil,
|
|
141
|
+
methods: %i[class_attribute thread_mattr_accessor]},
|
|
122
142
|
{key: :objectspace_id2ref, receiver: "ObjectSpace",
|
|
123
143
|
methods: %i[_id2ref]},
|
|
124
144
|
{key: :at_exit, receiver: nil, methods: %i[at_exit]},
|
|
@@ -27,14 +27,18 @@ module Audition
|
|
|
27
27
|
"it while it holds a non-shareable value (verified on " \
|
|
28
28
|
"Ruby 4.0)."
|
|
29
29
|
STATE_FIX =
|
|
30
|
-
"Precompute and freeze the value at load time
|
|
31
|
-
"
|
|
30
|
+
"Precompute and freeze the value at load time: a memo " \
|
|
31
|
+
"that needs no configuration becomes a frozen private " \
|
|
32
|
+
"constant (EMPTY = new(nil, nil).freeze), a cheap " \
|
|
33
|
+
"derivation drops its memo altogether, and " \
|
|
34
|
+
"per-subclass values compute in the inherited hook " \
|
|
35
|
+
"(guard on subclass.name for anonymous classes). For " \
|
|
32
36
|
"collections, rebuild and refreeze on write, " \
|
|
33
37
|
"Rails-style copy-on-write: self.list = " \
|
|
34
38
|
"(list + [item]).freeze; never mutate in place. As a " \
|
|
35
|
-
"last resort use Ractor.store_if_absent for
|
|
36
|
-
"
|
|
37
|
-
"Ractor.
|
|
39
|
+
"last resort use Ractor.store_if_absent for " \
|
|
40
|
+
"per-Ractor state, or read the ivar first and proxy " \
|
|
41
|
+
"the write to the main Ractor."
|
|
38
42
|
FROZEN_MEMO_WHY =
|
|
39
43
|
"Every write memoizes a shareable (frozen) value, so " \
|
|
40
44
|
"non-main Ractors can read it once it has been " \
|
|
@@ -44,10 +48,13 @@ module Audition
|
|
|
44
48
|
"memoized class state."
|
|
45
49
|
FROZEN_MEMO_FIX =
|
|
46
50
|
"Warm the cache at boot, before spawning Ractors: call " \
|
|
47
|
-
"the memoizing method from an initializer
|
|
48
|
-
"hook
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
+
"the memoizing method from an initializer, an on_load " \
|
|
52
|
+
"hook, an eager_load! override, or the inherited hook. " \
|
|
53
|
+
"A value that can be nil or false never sticks under " \
|
|
54
|
+
"||=, so guard it with defined? instead. If the value " \
|
|
55
|
+
"genuinely must be computed at runtime, read the ivar " \
|
|
56
|
+
"first and proxy only the write to the main Ractor, or " \
|
|
57
|
+
"use Ractor.store_if_absent."
|
|
51
58
|
BEST_EFFORT_WHY =
|
|
52
59
|
"Writes wrap their value in Ractor.make_shareable with " \
|
|
53
60
|
"a rescue fallback: shareable values are deeply frozen " \
|
|
@@ -8,6 +8,8 @@ module Audition
|
|
|
8
8
|
# :shareable proven deeply shareable
|
|
9
9
|
# :mutable_string unfrozen String literal
|
|
10
10
|
# :mutable_container Array/Hash literal or constructor
|
|
11
|
+
# :mutable_call unfrozen String or Regexp returned by
|
|
12
|
+
# a call (`.tr`, `format`, `Regexp.new`)
|
|
11
13
|
# :shallow_freeze frozen container with mutable elements
|
|
12
14
|
# :sync_primitive Mutex/Queue/... constructor
|
|
13
15
|
# :proc lambda or proc
|
|
@@ -22,6 +24,24 @@ module Audition
|
|
|
22
24
|
].freeze
|
|
23
25
|
SHAREABLE_FACTORIES = %w[Struct Class Module].freeze
|
|
24
26
|
|
|
27
|
+
# Calls returning a fresh, unfrozen String or Regexp;
|
|
28
|
+
# `# frozen_string_literal: true` covers literals only.
|
|
29
|
+
# Rails hit both shapes (`.tr` and `Regexp.new`) in
|
|
30
|
+
# constants during its ractorization. These names belong
|
|
31
|
+
# to String alone in core, so any receiver qualifies.
|
|
32
|
+
STRING_ONLY_METHODS = %i[
|
|
33
|
+
tr tr_s gsub sub squeeze strip lstrip rstrip chomp chop
|
|
34
|
+
center ljust rjust encode scrub unicode_normalize
|
|
35
|
+
].freeze
|
|
36
|
+
# Unambiguous only on a String literal receiver: Symbols
|
|
37
|
+
# and numbers define these too and return shareable values.
|
|
38
|
+
STRING_LITERAL_METHODS = %i[
|
|
39
|
+
+ * % upcase downcase capitalize swapcase reverse dup
|
|
40
|
+
succ next
|
|
41
|
+
].freeze
|
|
42
|
+
FORMATTERS = %i[format sprintf].freeze
|
|
43
|
+
REGEXP_FACTORIES = %i[new union compile].freeze
|
|
44
|
+
|
|
25
45
|
# @param frozen_string_literal [Boolean] whether the file has
|
|
26
46
|
# the frozen_string_literal magic comment
|
|
27
47
|
def initialize(frozen_string_literal:)
|
|
@@ -92,6 +112,9 @@ module Audition
|
|
|
92
112
|
end
|
|
93
113
|
|
|
94
114
|
def classify_call(node)
|
|
115
|
+
return :mutable_call if fresh_string?(node) ||
|
|
116
|
+
fresh_regexp?(node)
|
|
117
|
+
|
|
95
118
|
receiver = node.receiver
|
|
96
119
|
case node.name
|
|
97
120
|
when :freeze
|
|
@@ -130,13 +153,42 @@ module Audition
|
|
|
130
153
|
when Prism::ArrayNode, Prism::HashNode
|
|
131
154
|
deep_classify(receiver.elements)
|
|
132
155
|
when Prism::CallNode
|
|
133
|
-
# A default proc survives freezing the Hash
|
|
134
|
-
|
|
156
|
+
# A default proc survives freezing the Hash; a frozen
|
|
157
|
+
# String or Regexp from a call is deeply shareable.
|
|
158
|
+
case classify(receiver)
|
|
159
|
+
when :default_proc then :default_proc
|
|
160
|
+
when :mutable_call then :shareable
|
|
161
|
+
else :unknown
|
|
162
|
+
end
|
|
135
163
|
else
|
|
136
164
|
:unknown
|
|
137
165
|
end
|
|
138
166
|
end
|
|
139
167
|
|
|
168
|
+
def fresh_string?(node)
|
|
169
|
+
receiver = node.receiver
|
|
170
|
+
name = node.name
|
|
171
|
+
case receiver
|
|
172
|
+
when nil
|
|
173
|
+
FORMATTERS.include?(name)
|
|
174
|
+
when Prism::StringNode, Prism::InterpolatedStringNode
|
|
175
|
+
STRING_ONLY_METHODS.include?(name) ||
|
|
176
|
+
STRING_LITERAL_METHODS.include?(name)
|
|
177
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
178
|
+
owner = const_name(receiver)
|
|
179
|
+
(name == :new && owner == "String") ||
|
|
180
|
+
(owner == "Kernel" && FORMATTERS.include?(name)) ||
|
|
181
|
+
STRING_ONLY_METHODS.include?(name)
|
|
182
|
+
else
|
|
183
|
+
false
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def fresh_regexp?(node)
|
|
188
|
+
REGEXP_FACTORIES.include?(node.name) &&
|
|
189
|
+
const_name(node.receiver) == "Regexp"
|
|
190
|
+
end
|
|
191
|
+
|
|
140
192
|
# A container holding a sync primitive can never become
|
|
141
193
|
# shareable; Ractor.make_shareable raises on it (multi_json
|
|
142
194
|
# keeps a frozen Hash of Mutexes). The classification
|
data/lib/audition/version.rb
CHANGED