Remove T.let from frozen constants Sorbet can now infer#22949
Merged
Conversation
Sorbet 0.6.13304 infers `X = A.new.freeze` the same as `X = A.new`, so constants assigned a direct `.new` call followed by `.freeze` no longer need explicit `T.let` type annotations. `Pathname(...)` kernel-method calls are not inferred, so those are converted to `Pathname.new(...)` instead. Similarly, pure-literal arrays are inferred through `.freeze` as fixed-size tuple types, which are subtypes of the previously declared `T::Array` types. Arrays containing splats, constant references, method calls or no elements at all are still not inferred and keep their `T.let` annotations.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR reduces Sorbet annotation boilerplate by removing T.let from frozen constants that Sorbet can now infer reliably, keeping explicit annotations only where inference through .freeze is still insufficient. It also replaces Pathname(...) kernel-method calls with Pathname.new(...) in the affected constants to preserve inference.
Changes:
- Remove
T.letfrom frozen constants where Sorbet infers the intended type (e.g.,X = Klass.new.freeze, literal arrays). - Convert
Pathname("...")/Pathname(ENV.fetch(...))toPathname.new(...)in constants so inference works withoutT.let. - Keep existing explicit
T.letannotations for non-inferable shapes (e.g., composed expressions like concatenations).
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| Library/Homebrew/version.rb | Simplifies NULL_TOKEN constant typing by relying on inference. |
| Library/Homebrew/utils/github/api.rb | Removes T.let from simple frozen scope arrays and a frozen regexp constant. |
| Library/Homebrew/tap_constants.rb | Removes T.let from a frozen Regexp.new(...) constant. |
| Library/Homebrew/sorbet/tapioca/compilers/forwardables.rb | Removes T.let from frozen literal arrays of method names. |
| Library/Homebrew/sorbet/tapioca/compilers/cask/config.rb | Removes T.let from a frozen literal symbol array constant. |
| Library/Homebrew/services/subcommand/list.rb | Removes T.let from frozen trigger/field constants (including a nil element tuple). |
| Library/Homebrew/sandbox.rb | Removes T.let from a frozen %w[...] constant. |
| Library/Homebrew/rubocops/shared/install_steps_helper.rb | Removes T.let from several frozen literal symbol arrays used for validation. |
| Library/Homebrew/rubocops/patches.rb | Removes T.let from a frozen patch type symbol array. |
| Library/Homebrew/rubocops/os_depends_on.rb | Removes T.let from a frozen symbol array constant. |
| Library/Homebrew/rubocops/cask/uninstall_methods_order.rb | Removes T.let from a frozen metadata keys symbol array. |
| Library/Homebrew/os/mac/xcode.rb | Replaces Pathname(...) with ::Pathname.new(...) and removes T.let. |
| Library/Homebrew/livecheck/strategy/sparkle.rb | Removes T.let from a frozen literal string array constant. |
| Library/Homebrew/livecheck/strategy/header_match.rb | Removes T.let from a frozen literal string array constant. |
| Library/Homebrew/install_steps.rb | Removes T.let from a frozen %w[...] constant. |
| Library/Homebrew/global.rb | Replaces Pathname(...) with Pathname.new(...) for inference and removes T.let. |
| Library/Homebrew/github_runner_matrix.rb | Removes T.let from frozen platform/arch symbol arrays. |
| Library/Homebrew/extend/kernel.rb | Removes T.let from a frozen mutex constant. |
| Library/Homebrew/env_config.rb | Removes T.let from a frozen %w[...] falsy-values constant. |
| Library/Homebrew/dev-cmd/formula-analytics.rb | Removes T.let from a frozen Date.new(...) constant. |
| Library/Homebrew/dependable.rb | Removes T.let from a frozen reserved-tags symbol array. |
| Library/Homebrew/cask/dsl/caveats.rb | Removes T.let from a frozen conditional-caveats symbol array. |
| Library/Homebrew/cask/cask.rb | Removes T.let from a frozen %w[...] hash-keys constant. |
| Library/Homebrew/cask/artifact/generated_completion.rb | Removes T.let from a frozen supported-shells symbol array. |
| Library/Homebrew/attestation.rb | Removes T.let from a frozen DateTime.new(...) constant. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
carlocab
approved these changes
Jul 4, 2026
Member
|
Nice, thanks! |
dduugg
added a commit
to dduugg/rubocop-sorbet
that referenced
this pull request
Jul 4, 2026
Extends the RedundantTLet cops to cover two more freeze-transparent
inference cases confirmed against Sorbet 0.6.13321:
RedundantTLetForLiteral now flags array literals of simple literals:
- Frozen arrays (`[...].freeze`) infer as a fixed-size tuple, a subtype
of the annotated `T::Array`, so the annotation is redundant. Since a
valid annotation is always a supertype of the elements, the tuple is
always assignable back, making the correction safe.
- Unfrozen arrays infer as `T::Array[<element type>]`; flagged only when
the inferred type matches the annotation exactly, to avoid silently
widening (e.g. `["a", nil]` infers a nilable element type).
Regexp/range elements (which degrade the array to `T.untyped`), empty
arrays, arrays of non-literals, and hash literals are all excluded and
keep their `T.let`.
RedundantTLet now also recognizes block-form constructors
(`Klass.new { ... }.freeze`), which Sorbet infers as the instantiated
class the same as a plain `.new`.
This brings the cops in line with the manual cleanup in
Homebrew/brew#22949, whose bulk was frozen literal-array constants.
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Removes
T.lettype annotations from frozen constants that Sorbet can now infer, following up on #22883 (comment: #22883 (comment)): since 0.6.13304, Sorbet infersX = A.new.freezethe same asX = A.new. This trims boilerplate and lets the annotations that remain carry more signal.Two groups of constants qualify:
.new(...).freezecalls on non-generic classes, which infer the exact class previously declared.Pathname(...)kernel-method calls are not inferred, so those are converted toPathname.new(...)instead.T::Arraytypes, and there is existing precedent for tuple-typed constants (e.g.MACOS_MODULE_NAMESinrubocops/shared/on_system_conditionals_helper.rb).Shapes that are still not inferred through
.freeze(verified against Sorbet 0.6.13316 withT.reveal_typeprobes) keep theirT.let: plain method calls (e.g.ENV.fetch(...)), interpolated strings, hash literals, generic.new(e.g.Set.new(...)infersT::Set[T.untyped]), empty arrays, and arrays containing splats, constant references or method-call elements.No new tests: this is a type-annotation-only change with no runtime behaviour difference (beyond equivalent
Pathnameconstruction), covered by existing typechecking and tests.brewcommands to reproduce the bug?brew lgtm(style, typechecking and tests) locally?Claude Code found the candidate constants, probed which expression shapes Sorbet infers through
.freeze(using temporarytyped: strictfiles withT.reveal_type) and applied the edits. Verified locally withbrew typecheck,brew style --fix --changedandbrew tests --online --changed, plus manual review of the full diff.