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 +4 -4
- data/README.md +77 -33
- data/lib/audition/bundle_sweep.rb +24 -15
- data/lib/audition/cli.rb +127 -140
- data/lib/audition/config.rb +30 -4
- data/lib/audition/dynamic/harness.rb +219 -20
- data/lib/audition/dynamic/prober.rb +150 -44
- data/lib/audition/finding.rb +10 -2
- data/lib/audition/progress.rb +418 -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 +4 -1
- data/lib/audition/static/analyzer.rb +86 -17
- data/lib/audition/static/checks/dependency_class_state.rb +160 -0
- data/lib/audition/static/checks/mutable_constants.rb +71 -0
- data/lib/audition/static/checks/unshareable_reads.rb +259 -0
- data/lib/audition/static/checks.rb +5 -2
- data/lib/audition/static/gem_calls.rb +1770 -0
- data/lib/audition/static/graph_audit.rb +1050 -7
- data/lib/audition/static/literal_classifier.rb +118 -18
- 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 +2 -0
- metadata +10 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 52c12c94d193fa6946553c81ab22b8c21272f269ad4f6f758e99fa4a1c05217e
|
|
4
|
+
data.tar.gz: 858d3ef4e5742ca430337656b9ecea865a89c3f3525ef990073499c8eb438bca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,
|
|
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
|
|
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.
|
|
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
|
-
*
|
|
66
|
+
* Audition 0.3.0 ruby 4.0.6 · script at .
|
|
67
67
|
|
|
68
68
|
worker.rb
|
|
69
|
-
x
|
|
70
|
-
access global
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
│
|
|
98
|
-
|
|
99
|
-
│
|
|
100
|
-
|
|
101
|
-
│
|
|
102
|
-
│
|
|
103
|
-
│
|
|
104
|
-
|
|
105
|
-
0
|
|
94
|
+
╭───────────────────────────────────────────────────────────────────────────────────────╮
|
|
95
|
+
│ Audition bundle sweep │
|
|
96
|
+
├──────────┬──────────┬─────────────┬────────┬────────────┬──────────┬─────────┬────────┤
|
|
97
|
+
│ gem │ version │ verdict │ errors │ dep errors │ warnings │ fixable │ status │
|
|
98
|
+
├──────────┼──────────┼─────────────┼────────┼────────────┼──────────┼─────────┼────────┤
|
|
99
|
+
│ rubocop │ 1.88.2 │ x not ready │ 194 │ - │ 203 │ 28 │ ok │
|
|
100
|
+
│ parser │ 3.3.12.0 │ x not ready │ 154 │ - │ 26 │ 26 │ 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:
|
|
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;
|
|
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 (
|
|
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
|
|
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);
|
|
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:
|
|
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 [
|
|
35
|
-
#
|
|
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:
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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,
|
|
119
|
-
|
|
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
|
-
|
|
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
|