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:
- Add
rubocop-sorbet to the Gemfile (alongside the existing rubocop-shopify) and regenerate gem RBIs with dev rbi.
- 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.
- 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.
- Add sigils to the two stragglers in
test/ (test_helper.rb, test_loader.rb).
- 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.)
Enforce the presence of a
typed:sigil in Ruby filesEnforce the presence of a
typed:sigil in every Ruby file, defaulting totyped: strictfor all production files. Test files might have to use a weaker sigil to avoid annoying type pinnings in setup methods — today they sit attyped: 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 astyped: false, so even though sorbet/config includeslib/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.ymlAllCops/Exclude) because the scripts are polyglot sh/ruby bootstraps that don't parse as plain Ruby. They stay out of scope.# typed: true.CI already runs both gates (.github/workflows/ci.yml):
bundle exec rubocopand./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:rubocop-sorbetto the Gemfile (alongside the existingrubocop-shopify) and regenerate gem RBIs withdev rbi.Sorbet/ValidSigilwithRequireSigilOnAllFiles: true— every Ruby file must declare a sigil (bin/ remains excluded via the existingAllCops/Exclude).Sorbet/StrictSigilforsrc/**/*andlib/**/*— production files must betyped: strict.Sorbet/EnforceSigilOrder— keep sigils in the canonical position relative to# frozen_string_literal: true.test/**/*exempted fromSorbet/StrictSigil; sigil presence still required there.lib/up totyped: strict. This is the bulk of the work: strict requires asigon every method, andlib/modules (deps, plan, cd, learnings, credentials, …) have none today. Ratchet per module: start each file at the highest sigil that passesdev tc, then raise to strict module by module, adding sigs as needed. Any file that cannot reach strict yet getstyped: truewith an explicit RuboCop exclusion listing it, so the exceptions stay visible and shrink over time.test/(test_helper.rb,test_loader.rb).dev style,dev tc, anddev test.Test files
Tests are written in the RSpock dialect, and RSpock's
transform!rewrites test-class ASTs (bareThen/Expectcomparisons,Wheretables). The original hope wastyped: true, but the transform plus type pinnings in setup methods may make that impractical — which is presumably why every test file was pinned totyped: false. Keep tests attyped: falsefor this issue (presence enforced, strictness not), and spike separately on whether RSpock-transformed classes survivetyped: true; if they do, ratchet tests in a follow-up.Acceptance criteria
.rbfile outsidebin/andtmp/declares atyped:sigil, enforced by RuboCop in CI.src/andlib/istyped: strict, except an explicitly listed (and ideally empty) set oftyped: trueholdouts.dev style,dev tc, anddev testall pass.Open questions
typed: true, or does the AST rewrite fight Sorbet? (Spike; out of scope for the initial enforcement.)sorbet/tapioca/require.rbstay attyped: true? (Generated-adjacent; probably yes, via the same exclusion list as anylib/holdouts.)