Skip to content

Adopt Sorbet with Tapioca-generated RBIs - #77

Merged
dduugg merged 5 commits into
mainfrom
adopt-sorbet
Sep 29, 2026
Merged

dduugg merged 5 commits into
mainfrom
adopt-sorbet

Conversation

@dduugg

@dduugg dduugg commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Sets up static type checking with Sorbet and Tapioca, and raises every runtime file under lib/ to # typed: strict. Nothing changes at runtime. sorbet-runtime stays out of the gemspec. The # typed: sigils and the RBS comment signatures (#: (String) -> bool) are only comments, and nothing under lib/ references T. sorbet/ isn't part of spec.files.

This undoes the Sorbet: Enabled: false block from #76, which was there because the gem didn't use Sorbet.

Commits

  1. Adopt Sorbet with Tapioca-generated RBIs (29 files, the hand-written part)
    • Gemfile: add sorbet and tapioca, plus railties so Sorbet can resolve Rails and ActionController in lib/singed/railtie.rb. Adding railties moves activesupport and activejob from 8.1.3.1 to 8.1.4.
    • sorbet/config (with --enable-experimental-rbs-comments), sorbet/tapioca/config.yml, bin/tapioca.
    • sorbet/tapioca/require.rb lists the gems and stdlib singed uses, plus sidekiq/api, sidekiq/testing and zeitwerk. The sidekiq and railties annotations and spec/support/sidekiq.rb refer to those.
    • # typed: sigils on every file under lib/ and spec/. spoom srb bump raised the files that type-check to typed: true, and commit 4 raises all of lib/ to strict. The spec files stay at typed: false, because Sorbet doesn't know the RSpec matchers.
    • .rubocop.yml: the Sorbet department is on again.
      • The files below typed: strict are listed in .rubocop_todo.yml under Sorbet/StrictSigil.
      • Sorbet/ConstantsFromStrings is disabled inline where the job class is looked up by the name in a Sidekiq payload. That lookup can only happen at runtime.
    • AGENTS.md: add the type-check and RBI regeneration commands.
  2. Generate gem RBIs and annotations with Tapioca (77 files, about 302k lines). This is the unedited output of bin/tapioca gems --all and bin/tapioca annotations, marked linguist-generated.
  3. List every RuboCop plugin in the Gemfile. rubocop-performance, rubocop-rake, rubocop-rspec and rubocop-sorbet were only in the bundle as dependencies of rubocop-gusto. They are now listed directly. The lockfile change is limited to DEPENDENCIES. .rubocop.yml also lists rubocop-sorbet in plugins now, instead of loading it only through rubocop-gusto's config.
  4. Promote lib/ to typed: strict with RBS comment signatures (18 files)
    • Every method, attribute, and instance variable under lib/ has an RBS comment type. Casts use the comment forms (#: as !nil, #: self as Singed::Railtie, #: as untyped), so nothing needs sorbet-runtime. The style follows sidekiq-gusto.
    • Block-taking methods are generic, so flamegraph { 42 }, Flamegraph#record and the Sidekiq middleware's call return the block's type.
    • sorbet/rbi/shims/ covers what the generated RBIs miss:
      • the ActiveSupport::BacktraceCleaner and StackProf::Report ivars that singed reads;
      • StackProf::Report#frames;
      • a requires_ancestor on Sidekiq::Job.
    • A few method bodies change shape so Sorbet can narrow nilable values. Behavior is unchanged.
      • Singed.silence_line? and filter_line read backtrace_cleaner into a local.
      • CLI#run reads @filename directly.
      • Report#filter! reads @data into a local.
      • Flamegraph#record declares its block.
    • Singed.backtrace_cleaner is typed untyped, not ActiveSupport::BacktraceCleaner. Tapioca rewrites a gem's RBS comments into runtime sigs while it loads an app. In an app without ActiveSupport, that constant can't resolve, and the two methods would lose their sigs in the app's RBI.
    • ControllerExt::ClassMethods#flamegraph accepts a Symbol, a String, or an Array of them, like Rails' only:.
    • README: a controller that Sorbet checks needs an explicit include Singed::ControllerExt. The Railtie's include happens at runtime, so without it Sorbet checks flamegraph :show against Kernel#flamegraph, which is typed now.
    • .rubocop_todo.yml no longer excludes lib/ from Sorbet/StrictSigil, so RuboCop keeps lib/ strict.
    • AGENTS.md documents the conventions, including why these signatures still matter at runtime for apps that use Tapioca.
  5. Run srb tc in CI. A Sorbet workflow, modeled on the RuboCop one, runs bundle exec srb tc on every push and pull request. Its Sorbet check is now a required status check on main.

Verification

  • bundle exec srb tc: no errors
  • bundle exec rspec: 42 examples, 0 failures
  • bundle exec rubocop: no offenses
  • bin/tapioca check-shims: no duplicates
  • bin/tapioca gems --verify: all RBIs up to date
  • Loading every file under lib/ without Bundler (so sorbet-runtime can't load), with and without Rails, Sidekiq and RSpec loaded: T stays undefined.
  • The RBS comments in lib/singed.rb, translated to sigs the way Tapioca does it, evaluate without ActiveSupport loaded.

Not in this PR

  • CI for bin/tapioca gems --verify. Until that exists, nothing stops the RBIs from going stale.
  • Raising the spec files above typed: false.
  • A pre-existing bug: with Singed.enabled = false, flamegraph { } raises NoMethodError from StackProf::Report#frames, because Flamegraph#save builds a report from a nil profile. The StackProf shim's @data type records this; the behavior is unchanged.

Set up static type checking with Sorbet. No runtime dependency is added:
sorbet-runtime is not in the gemspec, and sorbet/config enables RBS
comments so signatures can be written as comments later.

- Gemfile: add sorbet and tapioca, plus railties so Sorbet can resolve
  Rails and ActionController in lib/singed/railtie.rb. Resolving railties
  moves activesupport and activejob from 8.1.3.1 to 8.1.4.
- sorbet/config, sorbet/tapioca/config.yml and bin/tapioca from
  `tapioca init`.
- sorbet/tapioca/require.rb loads the gems and stdlib singed uses, plus
  sidekiq/api, sidekiq/testing and zeitwerk, which the sidekiq and
  railties annotations and spec/support/sidekiq.rb reference.
- `# typed:` sigils on every file under lib/ and spec/. `spoom srb bump`
  raised the files that type-check to `typed: true`. The rest stay at
  `typed: false`: the spec files, because Sorbet does not know the RSpec
  matchers, and cli.rb, controller_ext.rb, railtie.rb and sidekiq.rb,
  which have type errors at `true`.
- .rubocop.yml: stop disabling the Sorbet department. The 15 files below
  `typed: strict` go in .rubocop_todo.yml under Sorbet/StrictSigil.
  Sorbet/ConstantsFromStrings is disabled inline where a job class is
  resolved from the name in a Sidekiq payload, since that can only happen
  at runtime.
- AGENTS.md: list the type-check and RBI regeneration commands.
Output of `bin/tapioca gems --all` and `bin/tapioca annotations`. No
hand edits. `bin/tapioca gems --verify` reports all RBIs up to date.
@dduugg
dduugg requested a review from a team as a code owner September 28, 2026 21:23
rubocop-performance, rubocop-rake, rubocop-rspec and rubocop-sorbet were
only in the bundle as dependencies of rubocop-gusto. They are now listed
directly, like rubocop-gusto, so the Gemfile shows every plugin
.rubocop.yml loads. The lockfile change is limited to DEPENDENCIES.

.rubocop.yml now lists rubocop-sorbet in plugins. Until now it was loaded
only through rubocop-gusto's config/default.yml.
Every file under lib/ is now `# typed: strict`. Signatures and type
annotations are RBS comments, so sorbet-runtime is still not a
dependency and nothing under lib/ references T at runtime.

- sorbet/rbi/shims/ declares what the generated RBIs miss: the
  ActiveSupport::BacktraceCleaner and StackProf::Report ivars that
  singed reads, StackProf::Report#frames, and a requires_ancestor on
  Sidekiq::Job.
- A few method bodies change shape so Sorbet can narrow nilable values.
  Singed.silence_line? and filter_line read backtrace_cleaner into a
  local, CLI#run reads @filename directly, Report#filter! reads @DaTa
  into a local, and Flamegraph#record declares its block. Behavior is
  unchanged.
- Singed.backtrace_cleaner is typed untyped rather than
  ActiveSupport::BacktraceCleaner. Tapioca turns a gem's RBS comments
  into runtime sigs while it loads an app, and apps without
  ActiveSupport would fail to resolve the constant.
- README: typed controllers need an explicit
  `include Singed::ControllerExt` so that Sorbet resolves
  `flamegraph :show` to the controller macro rather than
  Kernel#flamegraph.
- .rubocop_todo.yml no longer excludes lib/ from Sorbet/StrictSigil.
- AGENTS.md documents these conventions.
Adds a Sorbet workflow, modeled on the RuboCop one, that type-checks
every push and pull request. Its Sorbet check becomes a required status
check on main.
@dduugg
dduugg merged commit 3d40fbc into main Sep 29, 2026
13 checks passed
@dduugg
dduugg deleted the adopt-sorbet branch September 29, 2026 17:22
dduugg added a commit that referenced this pull request Sep 29, 2026
AGENTS.md conflicted, since #77 added a Types section where this branch
added a Pull requests section; both are kept. #77 also makes everything
under lib/ `# typed: strict`, so lib/singed/version.rb gets the sigil too.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant