audition 0.3.0 → 0.4.0

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.
@@ -16,6 +16,10 @@ module Audition
16
16
  # :proc lambda or proc
17
17
  # :default_proc Hash.new with a block; the block
18
18
  # survives .freeze and stays unshareable
19
+ # :instance_new unfrozen instance of an arbitrary class
20
+ # :opaque_call method call with an unprovable return
21
+ # :shallow_opaque frozen at the top, but what it holds
22
+ # is an unprovable call result
19
23
  # :unknown cannot tell statically
20
24
  class LiteralClassifier
21
25
  SYNC_PRIMITIVES = %w[
@@ -51,6 +55,18 @@ module Audition
51
55
  ].freeze
52
56
  FORMATTERS = %i[format sprintf].freeze
53
57
  REGEXP_FACTORIES = %i[new union compile].freeze
58
+ # File methods returning a fresh path String.
59
+ FILE_PATH_METHODS = %i[
60
+ expand_path join dirname basename absolute_path realpath
61
+ ].freeze
62
+
63
+ # Calls returning a shareable primitive on any receiver.
64
+ SHAREABLE_RETURNS = %i[
65
+ to_i to_int to_f to_r to_c to_sym size length count
66
+ bytesize ord hash
67
+ ].freeze
68
+ # Sorbet's inline casts, which return their value argument.
69
+ SORBET_CASTS = %i[let cast must].freeze
54
70
 
55
71
  # @param frozen_string_literal [Boolean] whether the file has
56
72
  # the frozen_string_literal magic comment
@@ -61,6 +77,7 @@ module Audition
61
77
  # @param node [Prism::Node] an expression node
62
78
  # @return [Symbol] classification, see class docs
63
79
  def classify(node)
80
+ node = begin_value(node)
64
81
  case node
65
82
  when Prism::IntegerNode, Prism::FloatNode,
66
83
  Prism::RationalNode, Prism::ImaginaryNode,
@@ -94,6 +111,29 @@ module Audition
94
111
  end
95
112
  end
96
113
 
114
+ # The value an expression hands back once begin blocks
115
+ # and inline casts are peeled off.
116
+ def unwrap(node)
117
+ loop do
118
+ inner = begin_value(node)
119
+ inner = cast_value(inner) || inner
120
+ break node if inner.equal?(node)
121
+
122
+ node = inner
123
+ end
124
+ end
125
+
126
+ # A begin block's value is its last statement. A rescue,
127
+ # else, or ensure clause can supply a different one, so
128
+ # only the plain form resolves.
129
+ def begin_value(node)
130
+ return node unless node.is_a?(Prism::BeginNode) &&
131
+ node.rescue_clause.nil? && node.else_clause.nil? &&
132
+ node.ensure_clause.nil?
133
+
134
+ node.statements&.body&.last || node
135
+ end
136
+
97
137
  # `::Mutex` and `Mutex` are the same constant for matching
98
138
  # purposes; the leading colons are stripped.
99
139
  def const_name(node)
@@ -105,6 +145,17 @@ module Audition
105
145
  end
106
146
  end
107
147
 
148
+ # The value inside a Sorbet inline cast, or nil. The cast
149
+ # returns its argument, so fixes and type names belong on
150
+ # the value, not the cast.
151
+ def cast_value(node)
152
+ return nil unless node.is_a?(Prism::CallNode)
153
+ return nil unless const_name(node.receiver) == "T" &&
154
+ SORBET_CASTS.include?(node.name)
155
+
156
+ node.arguments&.arguments&.first
157
+ end
158
+
108
159
  # What `value.freeze` would classify as, for the check to
109
160
  # choose a plain `.freeze` over a deep wrap: :shareable
110
161
  # when every element is provably shareable, :shallow_freeze
@@ -165,9 +216,11 @@ module Audition
165
216
  return :mutable_container
166
217
  end
167
218
 
168
- :unknown
169
- when :[], :to_set
219
+ name ? :instance_new : :opaque_call
220
+ when :to_set
170
221
  set_kind(node)
222
+ when :[]
223
+ index_kind(node)
171
224
  when :define
172
225
  (const_name(receiver) == "Data") ? :shareable : :unknown
173
226
  when :make_shareable
@@ -175,14 +228,46 @@ module Audition
175
228
  when :lambda, :proc
176
229
  (receiver.nil? && node.block) ? :proc : :unknown
177
230
  else
178
- :unknown
231
+ opaque_kind(node)
179
232
  end
180
233
  end
181
234
 
235
+ # Catch-all for unrecognized calls: shareable returns pass,
236
+ # predicates stay silent, everything else is opaque.
237
+ def opaque_kind(node)
238
+ return :shareable if SHAREABLE_RETURNS.include?(node.name)
239
+ return :unknown if node.name.end_with?("?")
240
+
241
+ if (value = cast_value(node))
242
+ return classify(value)
243
+ end
244
+
245
+ :opaque_call
246
+ end
247
+
248
+ # Indexing into a constant, another call's result, or a
249
+ # fresh instance returns a value of unprovable shareability.
250
+ # Sorbet type constructors and ENV, whose values are frozen
251
+ # strings, stay silent.
252
+ def index_kind(node)
253
+ owner = const_name(node.receiver)
254
+ return set_kind(node) if owner == "Set"
255
+ if owner.nil?
256
+ receiver_kind = classify(node.receiver)
257
+ return :opaque_call if %i[opaque_call instance_new]
258
+ .include?(receiver_kind)
259
+ return :unknown
260
+ end
261
+ return :unknown if owner == "ENV" ||
262
+ owner == "T" || owner.start_with?("T::")
263
+
264
+ :opaque_call
265
+ end
266
+
182
267
  def classify_freeze(node, receiver)
183
268
  return :unknown unless node.arguments.nil? && receiver
184
269
 
185
- case receiver
270
+ case (receiver = begin_value(receiver))
186
271
  when Prism::StringNode
187
272
  :shareable
188
273
  when Prism::ArrayNode, Prism::HashNode
@@ -194,6 +279,7 @@ module Audition
194
279
  when :default_proc then :default_proc
195
280
  when :mutable_call then :shareable
196
281
  when :mutable_container then frozen_kind(receiver)
282
+ when :instance_new, :opaque_call then :shallow_opaque
197
283
  else :unknown
198
284
  end
199
285
  else
@@ -214,6 +300,7 @@ module Audition
214
300
  owner = const_name(receiver)
215
301
  (name == :new && owner == "String") ||
216
302
  (owner == "Kernel" && FORMATTERS.include?(name)) ||
303
+ (owner == "File" && FILE_PATH_METHODS.include?(name)) ||
217
304
  STRING_ONLY_METHODS.include?(name)
218
305
  else
219
306
  false
@@ -243,12 +330,12 @@ module Audition
243
330
  end
244
331
 
245
332
  # A Set built from literals is a container of them:
246
- # `Set.new([...])`, `Set[...]`, `%w[...].to_set`. Anything
247
- # else, such as `Set.new(compute)` or a mapping block,
248
- # stays unknown.
333
+ # `Set.new([...])`, `Set[...]`, `%w[...].to_set`. A Set
334
+ # built from an opaque source or a mapping block is still
335
+ # a fresh unfrozen Set, mutable no matter its contents.
249
336
  def set_kind(node)
250
337
  elements = set_elements(node)
251
- elements ? elements_kind(elements) : :unknown
338
+ elements ? elements_kind(elements) : :mutable_container
252
339
  end
253
340
 
254
341
  def set_elements(node)
@@ -308,20 +395,33 @@ module Audition
308
395
  body && body.size == 1 && body[0]
309
396
  end
310
397
 
311
- # Fold element classifications: everything provably shareable
312
- # gives :shareable; anything provably mutable gives
313
- # :shallow_freeze; a sync primitive poisons the whole
314
- # container; anything unknowable gives :unknown (stay silent
315
- # rather than guess).
398
+ # Fold element classifications by the strongest evidence:
399
+ # a sync primitive poisons the whole container; a provably
400
+ # mutable element makes it :shallow_freeze; an opaque call
401
+ # result makes it :shallow_opaque. A bare constant read is
402
+ # commonly a class or another frozen constant, so on its
403
+ # own it keeps the container silent (:unknown), but it
404
+ # cannot excuse a bad element elsewhere.
405
+ VERDICT_RANK = {
406
+ shareable: 0, unknown: 1, shallow_opaque: 2,
407
+ shallow_freeze: 3
408
+ }.freeze
409
+
316
410
  def deep_classify(elements)
317
411
  verdict = :shareable
318
412
  elements.each do |element|
319
413
  element_children(element).each do |child|
320
- case classify(child)
321
- when :shareable then nil
322
- when :sync_primitive then return :sync_primitive
323
- when :unknown then return :unknown
324
- else verdict = :shallow_freeze
414
+ kind =
415
+ case classify(child)
416
+ when :shareable then :shareable
417
+ when :sync_primitive then return :sync_primitive
418
+ when :unknown then :unknown
419
+ when :instance_new, :opaque_call, :shallow_opaque
420
+ :shallow_opaque
421
+ else :shallow_freeze
422
+ end
423
+ if VERDICT_RANK[kind] > VERDICT_RANK[verdict]
424
+ verdict = kind
325
425
  end
326
426
  end
327
427
  end
@@ -22,9 +22,9 @@ module Audition
22
22
  INIT = /\bInit_\w+\s*\(/
23
23
  SOURCES = "*.{c,cc,cpp,cxx,m,mm,h,hpp,rs,zig}"
24
24
  BUILD_FILES = %w[Cargo.toml build.zig extconf.rb].freeze
25
- # Harness crates and test trees under ext/ are not compiled
26
- # into the extension; a fuzz target that links the VM itself
27
- # may call rb_ext_ractor_safe without the extension doing so.
25
+ # Harness crates and test trees are not compiled into the
26
+ # extension; a fuzz target that links the VM itself may call
27
+ # rb_ext_ractor_safe without the extension doing so.
28
28
  HARNESS_DIRS = %w[fuzz benches tests examples test spec].freeze
29
29
 
30
30
  SILENT_WHY =
@@ -51,7 +51,7 @@ module Audition
51
51
  DECLARED_WHY =
52
52
  "rb_ext_ractor_safe(true) is the maintainer's assertion " \
53
53
  "that the extension keeps no process-global mutable state; " \
54
- "Ruby does not verify it and neither can audition. Its " \
54
+ "Ruby does not verify it and neither can Audition. Its " \
55
55
  "methods run from any Ractor, in parallel, on the strength " \
56
56
  "of that assertion alone."
57
57
  DECLARED_FIX =
@@ -91,12 +91,16 @@ module Audition
91
91
  nil
92
92
  end
93
93
 
94
- # One finding per extension directory (ext/<name>), anchored
94
+ # One finding per extension directory, anchored
95
95
  # at the declaration when there is one, else at Init_* or the
96
96
  # build file, where the declaration belongs.
97
97
  def source_finding(dir, root)
98
98
  sources = sources_under(dir)
99
- label = dir.delete_prefix("#{root}/")
99
+ label = if dir == root
100
+ File.basename(root)
101
+ else
102
+ dir.delete_prefix("#{root}/")
103
+ end
100
104
  path, line = locate(sources, DECLARATION)
101
105
  if path
102
106
  return finding(:info, path, line,
@@ -111,18 +115,26 @@ module Audition
111
115
  "safety", SILENT_WHY + SOURCE_TAIL, SILENT_FIX)
112
116
  end
113
117
 
114
- # ext/<name> directories holding native sources; ext/ itself
115
- # when the sources sit directly in it.
118
+ # Directories holding native sources, found by the build file
119
+ # that compiles them rather than by convention: a gem is free
120
+ # to put its extension anywhere. A manifest above another one
121
+ # is dropped, so a workspace does not report the sources of
122
+ # the crates under it a second time.
116
123
  def extension_dirs(root)
117
- ext = File.join(root, "ext")
118
- return [] unless File.directory?(ext)
119
-
120
- dirs = Dir[File.join(ext, "*")].sort.select do |dir|
121
- File.directory?(dir) && !skipped?(File.basename(dir)) &&
122
- sources_under(dir).any?
124
+ dirs = build_dirs(root).select { |dir| sources_under(dir).any? }
125
+ dirs.reject do |dir|
126
+ dirs.any? { |other| other.start_with?("#{dir}/") }
123
127
  end
124
- dirs << ext if Dir[File.join(ext, SOURCES)].any?
125
- dirs
128
+ end
129
+
130
+ def build_dirs(root)
131
+ pattern = "{#{BUILD_FILES.join(",")}}"
132
+ Dir[File.join(root, "**", pattern)].filter_map do |path|
133
+ relative = path.delete_prefix("#{root}/").split("/")
134
+ next if relative[0..-2].any? { |part| skipped?(part) }
135
+
136
+ File.dirname(path)
137
+ end.uniq.sort
126
138
  end
127
139
 
128
140
  def sources_under(dir)
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "etc"
4
+
5
+ module Audition
6
+ module Static
7
+ # How a scan is divided between Ractor workers.
8
+ module WorkSplit
9
+ # Ractors run on a fixed pool of native threads sized by
10
+ # RUBY_MAX_CPU, which only the environment can set. Spawning
11
+ # past it buys no parallelism and costs a Ractor each.
12
+ RACTOR_CPU_DEFAULT = 8
13
+
14
+ module_function
15
+
16
+ # One worker per core the Ractor pool can actually run. The
17
+ # main Ractor only waits while workers scan, so no core is
18
+ # held back for it.
19
+ #
20
+ # @return [Integer]
21
+ def workers
22
+ Etc.nprocessors.clamp(1, ractor_cpu_limit)
23
+ end
24
+
25
+ def ractor_cpu_limit
26
+ limit = ENV["RUBY_MAX_CPU"].to_i
27
+ limit.positive? ? limit : RACTOR_CPU_DEFAULT
28
+ end
29
+
30
+ # Longest-processing-time-first: deal the heaviest item onto
31
+ # the lightest worker. Consecutive files are neighbors in the
32
+ # tree and so alike in size, so contiguous slices come out
33
+ # lopsided: one worker can draw a slice weighing several times
34
+ # the mean and still be running once the rest have finished.
35
+ # Greedy is enough here: LPT finishes within 4/3 of an
36
+ # optimal split.
37
+ #
38
+ # @param weighted [Array<Array>] `[item, weight]` pairs
39
+ # @param count [Integer] worker count
40
+ # @return [Array<Array>] one chunk of items per busy worker
41
+ def chunks(weighted, count)
42
+ chunks = Array.new(count) { [] }
43
+ loads = Array.new(count, 0)
44
+ weighted.sort_by { |item, weight| [-weight, item] }
45
+ .each do |item, weight|
46
+ lightest = loads.index(loads.min)
47
+ chunks[lightest] << item
48
+ loads[lightest] += weight
49
+ end
50
+ chunks.reject(&:empty?)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -12,6 +12,12 @@ module Audition
12
12
  vendor node_modules tmp log coverage pkg .git .bundle
13
13
  ].freeze
14
14
 
15
+ # Directories holding the target's tests rather than the code
16
+ # a production boot loads. The default; `test_dirs` in
17
+ # .audition.yml replaces it for a project that files them
18
+ # elsewhere.
19
+ TEST_DIRS = %w[test spec features].freeze
20
+
15
21
  # Cargo and Zig build output: full of .so/.dylib artifacts that
16
22
  # are not the extension `require` loads.
17
23
  BUILD_DIRS = %w[target zig-out].freeze
@@ -20,6 +26,11 @@ module Audition
20
26
  # "bundle" on macOS, "so" everywhere else, Windows included).
21
27
  COMPILED = "*.{bundle,so}"
22
28
 
29
+ # Generated type stubs. The extension is the convention; where
30
+ # the generator writes them is not, so they are found by
31
+ # extension anywhere in the tree.
32
+ STUBS = "*.rbi"
33
+
23
34
  # @return [Symbol] one of `:script`, `:gem`, `:rack`, `:rails`,
24
35
  # `:directory`, `:bundle`
25
36
  attr_reader :type
@@ -34,6 +45,10 @@ module Audition
34
45
  # shipped with the target; empty for scripts and file lists
35
46
  attr_reader :compiled_files
36
47
 
48
+ # @return [Array<String>] generated type stubs (.rbi) found
49
+ # anywhere in the target's tree
50
+ attr_reader :stub_files
51
+
37
52
  # @return [Hash, nil] dynamic probe entry (`:mode` plus
38
53
  # mode-specific keys), nil for static-only targets
39
54
  attr_reader :entry
@@ -75,7 +90,8 @@ module Audition
75
90
  type: :files,
76
91
  root: Dir.pwd,
77
92
  ruby_files: paths,
78
- entry: nil
93
+ entry: nil,
94
+ stub_files: stubs(Dir.pwd)
79
95
  )
80
96
  end
81
97
 
@@ -96,10 +112,26 @@ module Audition
96
112
  type: :script,
97
113
  root: File.dirname(path),
98
114
  ruby_files: [path],
99
- entry: {mode: :script, path: path}
115
+ # A file inside a Rails root cannot run standalone; the
116
+ # script probe would report a misleading boot failure, so
117
+ # such files stay static-only.
118
+ entry: rails_member?(path) ? nil : {mode: :script, path: path},
119
+ stub_files: stubs(File.dirname(path))
100
120
  )
101
121
  end
102
122
 
123
+ def self.rails_member?(path)
124
+ dir = File.dirname(File.expand_path(path))
125
+ until dir == (parent = File.dirname(dir))
126
+ return true if File.file?(
127
+ File.join(dir, "config", "application.rb")
128
+ )
129
+
130
+ dir = parent
131
+ end
132
+ false
133
+ end
134
+
103
135
  def self.from_directory(dir)
104
136
  application_rb = File.join(dir, "config", "application.rb")
105
137
  config_ru = File.join(dir, "config.ru")
@@ -117,7 +149,8 @@ module Audition
117
149
  root: dir,
118
150
  ruby_files: glob(dir),
119
151
  entry: nil,
120
- compiled_files: compiled(dir)
152
+ compiled_files: compiled(dir),
153
+ stub_files: stubs(dir)
121
154
  )
122
155
  end
123
156
  end
@@ -131,16 +164,21 @@ module Audition
131
164
  glob(File.join(spec.full_gem_path, rp))
132
165
  end,
133
166
  entry: {mode: :require, feature: name,
167
+ load_paths: spec.full_require_paths,
134
168
  root: spec.full_gem_path},
135
- compiled_files: compiled_for(spec)
169
+ compiled_files: compiled_for(spec),
170
+ stub_files: stubs(spec.full_gem_path)
136
171
  )
137
172
  rescue Gem::MissingSpecError
138
173
  raise Error,
139
174
  "#{name} is not a file, directory, or installed gem"
140
175
  end
141
176
 
177
+ # An app's own Ruby is not confined to app/lib/config: local
178
+ # gems, engines and tests boot into the same process, so the
179
+ # whole root is scanned.
142
180
  def self.rails_target(dir)
143
- files = %w[app lib config].flat_map { |d| glob(File.join(dir, d)) }
181
+ files = glob(dir)
144
182
  files << File.join(dir, "config.ru")
145
183
  new(
146
184
  type: :rails,
@@ -151,7 +189,8 @@ module Audition
151
189
  environment: File.join(dir, "config", "environment.rb"),
152
190
  root: dir
153
191
  },
154
- compiled_files: compiled(dir)
192
+ compiled_files: compiled(dir),
193
+ stub_files: stubs(dir)
155
194
  )
156
195
  end
157
196
 
@@ -160,24 +199,31 @@ module Audition
160
199
  type: :rack,
161
200
  root: dir,
162
201
  ruby_files: [config_ru] + glob(dir),
163
- entry: {mode: :rack, config_ru: config_ru},
164
- compiled_files: compiled(dir)
202
+ entry: {mode: :rack, config_ru: config_ru, root: dir},
203
+ compiled_files: compiled(dir),
204
+ stub_files: stubs(dir)
165
205
  )
166
206
  end
167
207
 
208
+ # Reading require_paths off the gemspec would mean evaluating
209
+ # it, which the static pass never does; `lib` is the packaging
210
+ # default, and a gem that keeps its code elsewhere falls back to
211
+ # the whole checkout rather than scanning nothing.
168
212
  def self.gem_dir_target(dir, gemspec)
169
213
  lib = File.join(dir, "lib")
214
+ paths = File.directory?(lib) ? [lib] : [dir]
170
215
  new(
171
216
  type: :gem,
172
217
  root: dir,
173
- ruby_files: glob(lib),
218
+ ruby_files: paths.flat_map { |path| glob(path) },
174
219
  entry: {
175
220
  mode: :require,
176
221
  feature: File.basename(gemspec, ".gemspec"),
177
- load_paths: [lib],
222
+ load_paths: paths,
178
223
  root: dir
179
224
  },
180
- compiled_files: compiled(dir)
225
+ compiled_files: compiled(dir),
226
+ stub_files: stubs(dir)
181
227
  )
182
228
  end
183
229
 
@@ -199,6 +245,14 @@ module Audition
199
245
  end.sort
200
246
  end
201
247
 
248
+ # Generated type stubs anywhere under a directory.
249
+ #
250
+ # @param dir [String]
251
+ # @return [Array<String>]
252
+ def self.stubs(dir)
253
+ glob(dir, STUBS)
254
+ end
255
+
202
256
  # macOS debug-symbol bundles (x.bundle.dSYM/...) carry a file
203
257
  # with the extension's name that nothing ever loads.
204
258
  def self.compiled(dir)
@@ -222,15 +276,30 @@ module Audition
222
276
 
223
277
  private_class_method :from_file, :from_directory, :from_gem_name,
224
278
  :rails_target, :rack_target, :gem_dir_target,
225
- :glob, :compiled, :normalize
279
+ :rails_member?, :glob, :compiled, :normalize
226
280
 
227
281
  def initialize(type:, root:, ruby_files:, entry:,
228
- compiled_files: [])
282
+ compiled_files: [], stub_files: [])
229
283
  @type = type
230
284
  @root = root
231
285
  @ruby_files = ruby_files
232
286
  @entry = entry
233
287
  @compiled_files = compiled_files
288
+ @stub_files = stub_files
289
+ end
290
+
291
+ # Whether a path is test/spec code rather than code the
292
+ # production boot loads.
293
+ #
294
+ # @param path [String] absolute or root-relative
295
+ # @param dirs [Array<String>] directory names that hold tests
296
+ # @return [Boolean]
297
+ def test_file?(path, dirs: TEST_DIRS)
298
+ relative = File.expand_path(path, root)
299
+ .delete_prefix("#{root}/")
300
+ parts = relative.split("/")
301
+ parts[0..-2].any? { |p| dirs.include?(p) } ||
302
+ parts.last.to_s.end_with?("_test.rb", "_spec.rb")
234
303
  end
235
304
  end
236
305
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Audition
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/audition.rb CHANGED
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "audition/version"
4
+ require_relative "audition/progress"
4
5
  require_relative "audition/finding"
5
6
  require_relative "audition/target"
6
7
  require_relative "audition/static/source_file"
7
8
  require_relative "audition/static/literal_classifier"
8
9
  require_relative "audition/static/checks"
9
10
  require_relative "audition/static/graph_audit"
11
+ require_relative "audition/static/gem_calls"
10
12
  require_relative "audition/static/native_extensions"
11
13
  require_relative "audition/static/analyzer"
12
14
  require_relative "audition/dynamic/prober"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: audition
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Markin
@@ -43,14 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: '0.2'
46
+ version: '0.4'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: '0.2'
53
+ version: '0.4'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: table_tennis
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -102,24 +102,30 @@ files:
102
102
  - lib/audition/dynamic/prober.rb
103
103
  - lib/audition/finding.rb
104
104
  - lib/audition/fixer.rb
105
+ - lib/audition/progress.rb
105
106
  - lib/audition/report.rb
106
107
  - lib/audition/report/github.rb
107
108
  - lib/audition/report/json.rb
108
109
  - lib/audition/report/style.rb
110
+ - lib/audition/report/sweep.rb
109
111
  - lib/audition/report/text.rb
110
112
  - lib/audition/rewriters.rb
111
113
  - lib/audition/static/analyzer.rb
112
114
  - lib/audition/static/checks.rb
113
115
  - lib/audition/static/checks/base.rb
116
+ - lib/audition/static/checks/dependency_class_state.rb
114
117
  - lib/audition/static/checks/global_variables.rb
115
118
  - lib/audition/static/checks/mutable_constants.rb
116
119
  - lib/audition/static/checks/ractor_isolation.rb
117
120
  - lib/audition/static/checks/runtime_require.rb
118
121
  - lib/audition/static/checks/unsafe_calls.rb
122
+ - lib/audition/static/checks/unshareable_reads.rb
123
+ - lib/audition/static/gem_calls.rb
119
124
  - lib/audition/static/graph_audit.rb
120
125
  - lib/audition/static/literal_classifier.rb
121
126
  - lib/audition/static/native_extensions.rb
122
127
  - lib/audition/static/source_file.rb
128
+ - lib/audition/static/work_split.rb
123
129
  - lib/audition/target.rb
124
130
  - lib/audition/version.rb
125
131
  homepage: https://github.com/yaroslav/audition
@@ -142,7 +148,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
142
148
  - !ruby/object:Gem::Version
143
149
  version: '0'
144
150
  requirements: []
145
- rubygems_version: 4.0.16
151
+ rubygems_version: 4.0.20
146
152
  specification_version: 4
147
153
  summary: Probe Ruby code for Ractor-readiness
148
154
  test_files: []