yaroslav/audition

GitHub: yaroslav/audition

Audition 是一款 Ruby Ractor 兼容性检测与修复工具,通过静态分析和动态探测判断代码能否在 Ractor 中运行并给出修正方案。

Stars: 9 | Forks: 0

# Audition 将它指向一个 Ruby 脚本、一个 gem、一个 Rack 应用或一个 Rails 根目录,它会告诉你该代码是否能在 Ractors 中运行,为什么不能,以及如何修复。与 linter 不同,audition 并不停留在对你的源码进行模式匹配:它还会将目标加载到一个沙箱子进程中,并在实际的活动对象图上观察真实的 `Ractor::IsolationError`。 [![GitHub Release](https://img.shields.io/github/v/release/yaroslav/audition)](https://github.com/yaroslav/audition/releases) [![Docs](https://img.shields.io/badge/yard-docs-blue.svg)](https://rubydoc.info/gems/audition) - **三种探测,一个结论。** 包含基于文件的 Prism AST 检查,在 [rubydex](https://github.com/Shopify/rubydex) 图谱上进行的全程序语义分析(类级别的状态可跨文件和重新打开进行解析),以及对实际目标的动态 Ractor 内执行。 - **解释原因,而非仅仅标记。** 每个发现都包含一个 `why`(它违反了 Ractor 模型的哪条规则)和一个 `fix`(应该改写成什么)。 - **类似 RuboCop 的 `--fix`,分为两个级别。** 安全修正:为字符串常量添加 `.freeze`,对可变、浅冻结的容器和 Proc 常量使用 `Ractor.make_shareable(...)`,以及在启动时提升方法体内的 `require`。`--fix-unsafe` 增加了影响语义的重写:插入魔法注释,将类缓存改为 `Ractor.store_if_absent`,将 `autoload` 改为 `require`,以及将一次写入的 globals/类变量改为冻结的常量。`--dry-run` 以 diff 形式预览所有更改。 - **感知依赖。** 运行时发现会通过 `const_source_location` 归因于其源码;当你自己的代码很干净但依赖项不干净时,结论将呈现为明确的 `blocked` 状态,因此 `globalid` 不会因为 ActiveSupport 的状态而背锅。 - **终端原生输出。** 支持颜色、字形和 OSC 8 超链接;`path:line` 在支持的终端中可点击。支持用于 CI 的 JSON 输出。 ``` $ audition worker.rb * audition 0.1.0 ruby 4.0.6 · script at . worker.rb x raises inside a Ractor: Ractor::IsolationError: can not access global variable $jobs from non-main Ractor why: The script ran fine on the main Ractor but failed under Ractor.new; the static findings usually pinpoint the line. x worker.rb:1 write to global variable $jobs why: Non-main Ractors cannot access global variables; this raises Ractor::IsolationError the moment the line executes in a Ractor (verified on Ruby 4.0). fix: Pass the value into the Ractor explicitly (Ractor.new(value) { |v| ... }) or over a Ractor::Port; for per-Ractor state use Ractor.current[:key]. x worker.rb:4 read of global variable $jobs ... dynamic probes x script probe failed (details above) summary: 3 errors verdict: x not ractor-ready $ echo $? 1 ``` 以及整个 bundle 的视图: ``` $ audition Gemfile.lock --static-only ╭───────────────┬─────────┬───────────┬────────┬──────────┬─────────╮ │ gem │ version │ verdict │ errors │ warnings │ fixable │ ├───────────────┼─────────┼───────────┼────────┼──────────┼─────────┤ │ activesupport │ 8.1.0 │ not ready │ 157 │ 97 │ 77 │ │ i18n │ 1.14.7 │ not ready │ 48 │ 40 │ 45 │ │ mail │ 2.9.1 │ not ready │ 27 │ 4 │ 13 │ │ rack │ 3.2.6 │ not ready │ 23 │ 45 │ 60 │ │ ... │ │ │ │ │ │ ╰───────────────┴─────────┴───────────┴────────┴──────────┴─────────╯ 0 of 11 gems ractor-ready ``` **严格要求 Ruby 4.0 或更高版本**:该工具的目标是现代 Ractor API(`Ractor::Port`、`Ractor#value`、主 Ractor `require` 代理)及其经过验证的语义。 ## 目录 - [安装](#installation) - [用法](#usage) - [渐进式采用](#adopting-incrementally) - [它能检测到什么](#what-it-catches) - [实战笔记](#field-notes) - [扩展](#extending) - [开发](#development) - [许可证](#license) ## 安装 ``` gem install audition ``` 或者在 Gemfile 中: ``` gem "audition", require: false ``` ## 用法 ``` audition worker.rb # a script: static + run inside Ractor audition my_gem # an installed gem, by name audition path/to/gem-checkout # a gem working copy (*.gemspec) audition path/to/rack-app # a config.ru directory audition path/to/rails-root # a Rails application audition lib # any directory, static-only audition Gemfile.lock # sweep every gem in the bundle audition path/to/app --deps # same, from the app root ``` 有用的 flags: | Flag | 效果 | | --- | --- | | `--deps` | 逐个扫描目标的 Gemfile.lock gem | | `--write-baseline` / `--no-baseline` | 记录 / 忽略已知发现 | | `--fix` | 应用安全修正,然后重新检查 | | `--fix-unsafe` | 同时应用影响语义的修正 | | `--dry-run` | 搭配 fix flag 使用:预览编辑,不进行任何修改 | | `--format json` | 用于 CI 的机器可读报告 | | `--format github` | 在 PR diff 上显示 GitHub Actions 批注 | | `--compare old.json` | 与先前报告的差异:已修复/已引入 | | `--static-only` / `--dynamic-only` | 选择一个探测层 | | `--fail-on warning` | 更严格的 CI 门禁(默认:error) | | `--capabilities` | 当前 Ruby 在 Ractors 中允许的操作列表 | | `--timeout 60` | 动态探测预算(以秒为单位) | | `--plain` | 无颜色或超链接(也可通过 NO_COLOR、管道触发) | 退出码:`0` 干净,`1` 存在达到或超过 `--fail-on` 阈值的发现(或动态探测失败),`2` 用法错误。 ## 渐进式采用 没有人能在一个 commit 中将 150 个发现减少到零。有三个工具可以让门禁从第一天起就发挥作用: **Baseline。** 记录今天的发现,然后仅在有新发现时让 CI 失败: ``` audition . --write-baseline # writes .audition-baseline.json audition . # exit 0; summary shows "N baselined" ``` 记录会存储每个文件中每项检查的计数,因此行号偏移永远不会使其失效。`--no-baseline` 会再次显示所有内容。 **内联 pragma。** 静默单行,rubocop 风格: ``` $legacy_flag = true # audition:disable global-variables risky_call # audition:disable ``` **项目配置。** 位于目标根目录的 `.audition.yml`(CLI flags 始终具有最高优先级): ``` fail_on: warning timeout: 60 exclude: - legacy/** - db/schema.rb checks: disable: - at-exit ``` ## 它能检测到什么 静态检查,具有 file:line 级别的精度: - **全局变量**,带有经过验证的白名单:`$stdout`、`$~`、`$!`、`$VERBOSE` 写入及相关操作仍然是合法的。 - **类变量**,在 rubydex 图谱上解析。 - **类级别的实例变量**,在类体、`def self.` 和 `class << self` 之间统一,且跨文件;经典的 `@cache ||= {}` 缓存。 - **非深度可共享的常量**:裸可变字面量、插值字符串以及微妙的浅冻结(`[[1], [2]].freeze` 仍然会报错;audition 会解释原因)。遵循 `# frozen_string_literal:` 和 `# shareable_constant_value:` 魔法注释。 - **常量中的同步原语和 Procs**(Mutex、Queue、lambdas)。 - **运行时 require 和 autoload**(通过主 Ractor 代理序列化所有 Ractors)。 - **捕获了外部局部变量的 `Ractor.new` 代码块**(创建时的 ArgumentError),通过 Prism 精确的作用域深度进行解析。 - **有敌意或已移除的 API**:`Ractor.yield`/`take`(在 4.0 中已移除)、ActiveSupport 的 `class_attribute`/`cattr_*`/`mattr_*`、`include Singleton`、`fork`、`ObjectSpace._id2ref`、ENV 变更。 动态检查,在活动的 object graph 上: - 在真实的 Ractor 内运行脚本(通过 `load`,它不会被代理)并报告实际的异常。 - require 一个库,然后用 `Ractor.shareable?` 扫描它引入的每个常量,并检查每个类和模块中的类级别 ivars 和类变量,同时通过 `const_source_location` 进行归因。 - 启动 `config.ru` 并完全在 Ractor 内部响应一个 GET / 请求,即 Ractor Web 服务器的单 worker 模型;然后从 4 个 Ractors x 25 个请求对其进行压测,以发现在并发情况下才会出现的失败。 - 启动 Rails(`config/environment.rb`),eager-load 所有文件,并扫描应用程序的 namespace。 ## 实战笔记 对流行 gems 运行 audition 的发现(2026 年 7 月,Ruby 4.0.6): - **rack 3.2**:`Rack::Builder.parse_file` 完全无法在 Ractor 内运行;`Rack::BUILDER_TOPLEVEL_BINDING` 持有一个不可共享的 Binding。audition 自身的 rack 探测会使用 `Rack::Builder.new` + `instance_eval` 重建应用来绕过这个问题。 - **mail 2.9**:28 个硬性发现,包括 `@@maximum_amount`、`@@autoloads`,以及未冻结的表常量如 `FIELDS_MAP`。 - **globalid 1.3**:自身仅有 6 个发现;报告的其余部分是 ActiveSupport 的状态,在摘要中被归因为依赖错误。 ## 扩展 检查是通过一个小巧的声明式 DSL 编写的,并且可以从 gem 外部注册: ``` class NoSleep < Audition::Static::Checks::Base check_name "no-sleep" explain :sleepy, severity: :warning, message: "sleep inside potential Ractor code", why: "Blocking one Ractor blocks its whole OS thread.", fix: "Prefer Ractor::Port#receive with a timeout." on :call_node do |node| flag(node, :sleepy) if node.name == :sleep && !node.receiver end end Audition::Static::Checks.register(NoSleep) ``` `on` 生成 Prism visitor 并始终继续遍历;`explain` 条目是一个带有 `%{placeholders}` 的消息目录。 ## 开发 ``` bundle install bundle exec rake spec # RSpec suite bundle exec rake standard # standardrb lint lefthook install # pre-commit lint hook bundle exec exe/audition --capabilities ``` 静态扫描在大型目标上会进行 Ractor 并行处理(每个核心一个 worker,为主 Ractor 减去一个);audition 自身的 `lib/` 可以通过 `audition lib` 干净地通过检查。 `docs/design.md` 中的设计说明包含了经过经验验证的 Ruby 4.0 Ractor 语义表,各项检查正是根据该校准的。 ## 协助 Claude Fable 5. ## 许可证 MIT。详见 LICENSE.txt。
标签:Ractor并发模型, Ruby, 云安全监控, 代码自动修复, 代码质量检测, 批量扫描, 知识库, 静态分析