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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 28a9914f63873fd6bc4ec2e86154aa69e4e3dbfb08c3d9ee6218fc521e31836b
4
- data.tar.gz: e1033a810ea825b21980fb3cb406bfd7bda1bf69e970e82c988fb0d149c0d172
3
+ metadata.gz: 52c12c94d193fa6946553c81ab22b8c21272f269ad4f6f758e99fa4a1c05217e
4
+ data.tar.gz: 858d3ef4e5742ca430337656b9ecea865a89c3f3525ef990073499c8eb438bca
5
5
  SHA512:
6
- metadata.gz: 07b6250748bf1d3e854157b6ac81f8a2574259f77d2e28746d241e880763cb446410b27736740c29f803e4936fed3d5ff0f6ca1b95d73cc6cb664896630c17fe
7
- data.tar.gz: ab564046d55d28d6e94af9e045991574322e14074e4121e788c6b59f8ce3628f4499351d5f9cac7e670564bc9ea7472b3615056d9b91beaeb64dc3e131babc04
6
+ metadata.gz: 60d78e8622cfe35764e7dd30be43ba7e1ceda4f0faf10796230dd6ddb85d952736b345bd380b58823cf97c6687209f6c0fe709193f58326464b61950af1c8a70
7
+ data.tar.gz: 8edc6f7ba79f531317e26f5f1866540ec784d3791db0b6a43ae56d990b22bd173b5234a05618a8b75ba4c5bf1d95bb06a9898c726b493456c36604cdb7c90b66
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Point it at a Ruby script, a gem, a Rack app, or a Rails root and it
4
4
  tells you whether that code can run inside Ractors, why it cannot,
5
- and how to fix it. Unlike a linter, audition does not stop at
5
+ and how to fix it. Unlike a linter, Audition does not stop at
6
6
  pattern-matching your source: whole-program analysis is powered by
7
7
  [rubydex](https://github.com/Shopify/rubydex), Shopify's Ruby
8
8
  indexer, and the target is also loaded in a sandboxed subprocess
@@ -47,7 +47,7 @@ core itself is being ractorized; see the
47
47
  state, so `globalid` is not blamed for ActiveSupport's state.
48
48
  - **Dogfooding.** The scanner for Ractor compatibility is built
49
49
  using Ractors: static analysis fans out across CPU cores on
50
- Ractor workers, and audition passes its own audit. It runs on
50
+ Ractor workers, and Audition passes its own audit. It runs on
51
51
  itself on every commit (a lefthook pre-commit over the staged
52
52
  files) and on every push (a full self-audit in CI).
53
53
  - **Trained on Rails core.** Several checks and fix suggestions
@@ -58,26 +58,24 @@ core itself is being ractorized; see the
58
58
  findings are documented in
59
59
  [docs/rails_core_best_practices.md](docs/rails_core_best_practices.md).
60
60
  - **Terminal-native output.** Colors, glyphs, and OSC 8 hyperlinks;
61
- `path:line` is clickable in supporting terminals. JSON output for
62
- CI.
61
+ `path:line` is clickable in supporting terminals. Long scans
62
+ narrate their phases on stderr. JSON output for CI.
63
63
 
64
64
  ```console
65
65
  $ audition worker.rb
66
- * audition 0.1.0 ruby 4.0.6 · script at .
66
+ * Audition 0.3.0 ruby 4.0.6 · script at .
67
67
 
68
68
  worker.rb
69
- x raises inside a Ractor: Ractor::IsolationError: can not
70
- access global variable $jobs from non-main Ractor
71
- why: The script ran fine on the main Ractor but failed under
72
- Ractor.new; the static findings usually pinpoint the line.
73
- x worker.rb:1 write to global variable $jobs
74
- why: Non-main Ractors cannot access global variables; this
75
- raises Ractor::IsolationError the moment the line executes
76
- in a Ractor (verified on Ruby 4.0).
77
- fix: Pass the value into the Ractor explicitly
78
- (Ractor.new(value) { |v| ... }) or over a Ractor::Port; for
79
- per-Ractor state use Ractor.current[:key].
80
- x worker.rb:4 read of global variable $jobs
69
+ x worker.rb:1 write to global variable $jobs global-variables
70
+ why: Non-main Ractors cannot access global variables; this raises
71
+ Ractor::IsolationError the moment the line executes in a Ractor
72
+ (verified on Ruby 4.0).
73
+ fix: Pass the value into the Ractor explicitly (Ractor.new(value) {
74
+ |v| ... }) or over a Ractor::Port; for per-Ractor state use
75
+ Ractor.current[:key]; do one-time process setup on the main Ractor
76
+ before spawning.
77
+ x worker.rb:1 raises inside a Ractor: Ractor::IsolationError ...
78
+ x worker.rb:4 read of global variable $jobs global-variables
81
79
  ...
82
80
 
83
81
  dynamic probes
@@ -93,16 +91,29 @@ And the whole-bundle view:
93
91
 
94
92
  ```console
95
93
  $ audition Gemfile.lock --static-only
96
- ╭───────────────┬─────────┬───────────┬────────┬──────────┬─────────╮
97
- gem │ version verdict │ errors │ warnings │ fixable │
98
- ├───────────────┼─────────┼───────────┼────────┼──────────┼─────────┤
99
- activesupport8.1.0 not ready 157 97 77
100
- │ i18n │ 1.14.7 │ not ready │ 48 │ 40 │ 45 │
101
- mail 2.9.1 │ not ready │ 27 413
102
- rack │ 3.2.6 │ not ready │ 234560
103
- ... │ │
104
- ╰───────────────┴─────────┴───────────┴────────┴──────────┴─────────╯
105
- 0 of 11 gems ractor-ready
94
+ ╭───────────────────────────────────────────────────────────────────────────────────────╮
95
+ Audition bundle sweep
96
+ ├──────────┬──────────┬─────────────┬────────┬────────────┬──────────┬─────────┬────────┤
97
+ gem │ version verdict errors dep errors warnings fixable status
98
+ ├──────────┼──────────┼─────────────┼────────┼────────────┼──────────┼─────────┼────────┤
99
+ rubocop 1.88.2x not ready │ 194- 203 28 │ ok │
100
+ parser │ 3.3.12.0 x not ready │ 154 - │ 2626 ok │
101
+ rack 3.2.7 x not ready 31 - 61 13 │ ok │
102
+ │ rake │ 13.4.2 │ x not ready │ 30 │ - │ 24 │ 5 │ ok │
103
+ │ prism │ 1.9.0 x not ready │ 16 │ - │ 88 │ 15 │ ok │
104
+ │ json │ 2.21.2 │ x not ready │ 7 │ - │ 5 │ - │ ok │
105
+ │ pastel │ 0.8.0 │ x not ready │ 2 │ - │ 1 │ - │ ok │
106
+ │ tty-link │ 0.2.0 │ x not ready │ 1 │ - │ 1 │ - │ ok │
107
+ ╰──────────┴──────────┴─────────────┴────────┴────────────┴──────────┴─────────┴────────╯
108
+
109
+ x 0 of 8 gems ractor-ready · 8 not ready
110
+ ```
111
+
112
+ Verdicts are colored by severity, clean counts are left blank, and
113
+ the sweep names each gem as it finishes:
114
+
115
+ ```console
116
+ ◆ Audition sweep rubocop 8/8 (100%) 5.8s
106
117
  ```
107
118
 
108
119
  **Requires Ruby 4.0 or newer**, strictly: the tool targets the modern
@@ -172,6 +183,20 @@ Useful flags:
172
183
  | `--capabilities` | table of what this Ruby allows in Ractors |
173
184
  | `--timeout 60` | dynamic probe budget in seconds |
174
185
  | `--plain` | no colors or hyperlinks (also via NO_COLOR, pipes) |
186
+ | `--progress` / `--no-progress` | force / suppress the stderr narration |
187
+ | `-j 4` / `--workers 4` | scan Ractors (default: cores, capped by `RUBY_MAX_CPU`) |
188
+
189
+ Long runs narrate themselves on stderr, so stdout stays pipeable:
190
+ a rewriting status line on a terminal, one line per phase off it.
191
+
192
+ ```
193
+ ◆ Audition checking 50/919 5% (0.2s, on 8 ractors)
194
+ ```
195
+
196
+ A phase running in Ractors says how many. The narration turns on
197
+ by itself for a large tree or a bundle sweep and stays off for
198
+ `--format json` and `--format github`; the flags force either
199
+ way.
175
200
 
176
201
  Exit codes: `0` clean, `1` findings at or above the `--fail-on`
177
202
  threshold (or a failed dynamic probe), `2` usage error.
@@ -210,11 +235,20 @@ timeout: 60
210
235
  exclude:
211
236
  - legacy/**
212
237
  - db/schema.rb
238
+ test_dirs:
239
+ - qa
213
240
  checks:
214
241
  disable:
215
242
  - at-exit
216
243
  ```
217
244
 
245
+ `test_dirs` names the directories that hold tests rather than
246
+ code a production boot loads; findings in them are tagged
247
+ `test` and rated as test code. It defaults to `test`, `spec`,
248
+ `features`, and replaces that list rather than adding to it—the
249
+ `_test.rb` and `_spec.rb` suffixes always count, whatever it
250
+ says.
251
+
218
252
  ## CI and git hooks
219
253
 
220
254
  **GitHub Actions.** `--format github` turns findings into
@@ -260,7 +294,7 @@ With [pre-commit](https://pre-commit.com):
260
294
  - repo: local
261
295
  hooks:
262
296
  - id: audition
263
- name: audition
297
+ name: Audition
264
298
  language: system
265
299
  entry: audition --static-only --plain
266
300
  types: [ruby]
@@ -292,7 +326,7 @@ Static, with file:line precision:
292
326
  `@cache ||= {}` and `return @x if defined?(@x)` memoizations.
293
327
  - **Constants that are not deeply shareable**: bare mutable
294
328
  literals, interpolated strings, the subtle shallow freeze
295
- (`[[1], [2]].freeze` still raises; audition explains why), and
329
+ (`[[1], [2]].freeze` still raises; Audition explains why), and
296
330
  call results the magic comment never covers (`X.tr(":", "")`,
297
331
  `Regexp.new`, `Regexp.union`, `format`), the shapes Rails fixed
298
332
  last in its own ractorization. Honors `# frozen_string_literal:`
@@ -317,11 +351,21 @@ Static, with file:line precision:
317
351
  scan of every compiled `.bundle`/`.so` for the
318
352
  `rb_ext_ractor_safe` import, which also covers precompiled
319
353
  platform gems that ship no sources; an unbuilt checkout is
320
- scanned at the source level (`ext/**`, C, Rust, or Zig) instead.
354
+ scanned at the source level instead (C, Rust, or Zig sources
355
+ beside their `extconf.rb`, `Cargo.toml` or `build.zig`).
321
356
  A silent extension raises `Ractor::UnsafeError` on every call
322
357
  from a non-main Ractor, so it rates a warning; a declared one
323
358
  gets an info note, because the declaration is the maintainer's
324
359
  assertion, not a proof.
360
+ - **The static pass's own blind spots**: where rubydex reports an
361
+ expression it could not resolve and the shape could hide what
362
+ the checks above look for, the hole is reported rather than
363
+ read as a clean line — a singleton opened on a runtime receiver
364
+ that writes class-level state, a superclass or `include`
365
+ argument computed at runtime, a constant assigned through an
366
+ unresolved path. A clean report for such a class covers only
367
+ what the class itself declares; the dynamic probe reaches the
368
+ rest.
325
369
 
326
370
  Dynamic, on the live object graph:
327
371
 
@@ -346,7 +390,7 @@ Dynamic, on the live object graph:
346
390
  ## Agent skill
347
391
 
348
392
  This repository ships a `ractor-readiness` skill that teaches
349
- coding agents (Claude Code and friends) the full audition
393
+ coding agents (Claude Code and friends) the full Audition
350
394
  workflow: audit, fix tiers, suite-parity verification, and
351
395
  incremental adoption. It lives in
352
396
  [skills/ractor-readiness/SKILL.md](skills/ractor-readiness/SKILL.md).
@@ -402,7 +446,7 @@ bundle exec exe/audition --capabilities
402
446
  ```
403
447
 
404
448
  Static scanning is Ractor-parallel on large targets (one worker
405
- per core, minus one for the main Ractor); audition's own `lib/`
449
+ per core, minus one for the main Ractor); Audition's own `lib/`
406
450
  passes `audition lib` clean.
407
451
 
408
452
  The design notes in `docs/design.md` include the empirically
@@ -413,7 +457,7 @@ calibrated against.
413
457
 
414
458
  The whole-program checks stand on
415
459
  [rubydex](https://github.com/Shopify/rubydex), Shopify's
416
- high-performance static analysis suite for Ruby: audition feeds
460
+ high-performance static analysis suite for Ruby: Audition feeds
417
461
  every file into its graph and reads state ownership back out.
418
462
  Thanks to its authors, in particular the top five contributors:
419
463
  [Alexandre Terrasa](https://github.com/Morriar),
@@ -31,10 +31,10 @@ module Audition
31
31
 
32
32
  # Audits every locked gem and ranks the results, worst first.
33
33
  #
34
- # @param progress [Proc, nil] called with (row, done, total)
35
- # as each gem finishes
34
+ # @param progress [Progress] named per gem as each finishes,
35
+ # which is the only unit a sweep has
36
36
  # @return [Array<Row>]
37
- def rows(progress: nil)
37
+ def rows(progress: Progress::SILENT)
38
38
  gems = locked_gems
39
39
  queue = Thread::Queue.new
40
40
  gems.each { |g| queue << g }
@@ -42,17 +42,19 @@ module Audition
42
42
 
43
43
  collected = []
44
44
  mutex = Thread::Mutex.new
45
- @concurrency.times.map do
46
- Thread.new do
47
- while (name, version = queue.pop)
48
- row = audit_gem(name, version)
49
- mutex.synchronize do
50
- collected << row
51
- progress&.call(row, collected.size, gems.size)
45
+ progress.phase("sweep", total: gems.size, unit: "gems") do
46
+ @concurrency.times.map do
47
+ Thread.new do
48
+ while (name, version = queue.pop)
49
+ row = audit_gem(name, version)
50
+ mutex.synchronize do
51
+ collected << row
52
+ progress.item(row.name)
53
+ end
52
54
  end
53
55
  end
54
- end
55
- end.each(&:join)
56
+ end.each(&:join)
57
+ end
56
58
 
57
59
  collected.sort_by do |row|
58
60
  [VERDICT_ORDER.fetch(row.verdict), -row.errors, row.name]
@@ -115,8 +117,10 @@ module Audition
115
117
  ruby_files: spec.require_paths.flat_map do |rp|
116
118
  ruby_files_under(File.join(root, rp))
117
119
  end,
118
- entry: {mode: :require, feature: spec.name, root: root},
119
- compiled_files: Target.compiled_for(spec)
120
+ entry: {mode: :require, feature: spec.name,
121
+ load_paths: spec.full_require_paths, root: root},
122
+ compiled_files: Target.compiled_for(spec),
123
+ stub_files: Target.stubs(root)
120
124
  )
121
125
  end
122
126
 
@@ -148,7 +152,12 @@ module Audition
148
152
  end
149
153
  per_file = Static::Analyzer.new
150
154
  .analyze_paths(files, workers: 1)
151
- per_file + Static::GraphAudit.new.analyze_paths(files) +
155
+ gem_calls = Static::GemCalls
156
+ .new(root: target.root, stubs: target.stub_files)
157
+ .analyze_paths(files)
158
+ per_file + gem_calls + Static::GraphAudit.new
159
+ .analyze_paths(files, constant_findings: per_file + gem_calls,
160
+ workers: 1) +
152
161
  Static::NativeExtensions.new.analyze(target,
153
162
  compiled_files: compiled)
154
163
  end