Skip to content

Enforce Sorbet typed sigil #139

Description

@JPDuchesne

Enforce the presence of a typed: sigil in Ruby files

Enforce the presence of a typed: sigil in every Ruby file, defaulting to typed: strict for all production files. Test files might have to use a weaker sigil to avoid annoying type pinnings in setup methods — today they sit at typed: false, likely because of RSpock's AST transform.

Current state

Nothing enforces sigils today; the split is by convention only:

  • src/ — all 39 files carry # typed: strict. This is the typed CLI core and the target state.
  • lib/ — none of the 85 files carry a sigil. Sorbet treats sigil-less files as typed: false, so even though sorbet/config includes lib/ via --dir ., these files are effectively unchecked.
  • test/ — 113 of 115 files carry # typed: false; test/test_helper.rb and test/test_loader.rb have no sigil.
  • bin/ — excluded from both Sorbet (sorbet/config --ignore=bin/) and RuboCop (.rubocop.yml AllCops/Exclude) because the scripts are polyglot sh/ruby bootstraps that don't parse as plain Ruby. They stay out of scope.
  • sorbet/tapioca/require.rb carries # typed: true.

CI already runs both gates (.github/workflows/ci.yml): bundle exec rubocop and ./bin/tc.rb. Enforcement added to RuboCop is therefore enforced on every PR with no CI changes.

Proposal

Enforce sigils with rubocop-sorbet, scoped per directory:

  1. Add rubocop-sorbet to the Gemfile (alongside the existing rubocop-shopify) and regenerate gem RBIs with dev rbi.
  2. Configure the cops in .rubocop.yml:
    • Sorbet/ValidSigil with RequireSigilOnAllFiles: true — every Ruby file must declare a sigil (bin/ remains excluded via the existing AllCops/Exclude).
    • Sorbet/StrictSigil for src/**/* and lib/**/* — production files must be typed: strict.
    • Sorbet/EnforceSigilOrder — keep sigils in the canonical position relative to # frozen_string_literal: true.
    • test/**/* exempted from Sorbet/StrictSigil; sigil presence still required there.
  3. Bring lib/ up to typed: strict. This is the bulk of the work: strict requires a sig on every method, and lib/ modules (deps, plan, cd, learnings, credentials, …) have none today. Ratchet per module: start each file at the highest sigil that passes dev tc, then raise to strict module by module, adding sigs as needed. Any file that cannot reach strict yet gets typed: true with an explicit RuboCop exclusion listing it, so the exceptions stay visible and shrink over time.
  4. Add sigils to the two stragglers in test/ (test_helper.rb, test_loader.rb).
  5. Verify with dev style, dev tc, and dev test.

Test files

Tests are written in the RSpock dialect, and RSpock's transform! rewrites test-class ASTs (bare Then/Expect comparisons, Where tables). The original hope was typed: true, but the transform plus type pinnings in setup methods may make that impractical — which is presumably why every test file was pinned to typed: false. Keep tests at typed: false for this issue (presence enforced, strictness not), and spike separately on whether RSpock-transformed classes survive typed: true; if they do, ratchet tests in a follow-up.

Acceptance criteria

  • Every .rb file outside bin/ and tmp/ declares a typed: sigil, enforced by RuboCop in CI.
  • All of src/ and lib/ is typed: strict, except an explicitly listed (and ideally empty) set of typed: true holdouts.
  • dev style, dev tc, and dev test all pass.

Open questions

  • Can RSpock-transformed test classes type-check at typed: true, or does the AST rewrite fight Sorbet? (Spike; out of scope for the initial enforcement.)
  • Should sorbet/tapioca/require.rb stay at typed: true? (Generated-adjacent; probably yes, via the same exclusion list as any lib/ holdouts.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions