Skip to content

Remove T.let from frozen constants Sorbet can now infer#22949

Merged
dduugg merged 1 commit into
mainfrom
t-let-freeze-inference
Jul 4, 2026
Merged

Remove T.let from frozen constants Sorbet can now infer#22949
dduugg merged 1 commit into
mainfrom
t-let-freeze-inference

Conversation

@dduugg

@dduugg dduugg commented Jul 4, 2026

Copy link
Copy Markdown
Member

Removes T.let type annotations from frozen constants that Sorbet can now infer, following up on #22883 (comment: #22883 (comment)): since 0.6.13304, Sorbet infers X = A.new.freeze the same as X = A.new. This trims boilerplate and lets the annotations that remain carry more signal.

Two groups of constants qualify:

  • Direct .new(...).freeze calls on non-generic classes, which infer the exact class previously declared. Pathname(...) kernel-method calls are not inferred, so those are converted to Pathname.new(...) instead.
  • Pure-literal arrays (symbol/string/nil literals, including nested literal arrays), which infer as fixed-size tuple types. Tuples are subtypes of the previously declared T::Array types, and there is existing precedent for tuple-typed constants (e.g. MACOS_MODULE_NAMES in rubocops/shared/on_system_conditionals_helper.rb).

Shapes that are still not inferred through .freeze (verified against Sorbet 0.6.13316 with T.reveal_type probes) keep their T.let: plain method calls (e.g. ENV.fetch(...)), interpolated strings, hash literals, generic .new (e.g. Set.new(...) infers T::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 Pathname construction), covered by existing typechecking and tests.


  • Have you followed our Contributing guidelines?
  • Have you checked for other open Pull Requests for the same change?
  • Have you explained what your changes do? Performance claims (e.g. "this is faster") must include Hyperfine benchmarks.
  • Have you explained why you'd like these changes included, not just what they do?
  • For bug fixes, have you given step-by-step brew commands to reproduce the bug?
  • Have you written new tests (excluding integration tests)? Here's an example.
  • Have you successfully run brew lgtm (style, typechecking and tests) locally?

  • AI was used to generate or assist with generating this PR.

Claude Code found the candidate constants, probed which expression shapes Sorbet infers through .freeze (using temporary typed: strict files with T.reveal_type) and applied the edits. Verified locally with brew typecheck, brew style --fix --changed and brew tests --online --changed, plus manual review of the full diff.


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.
Copilot AI review requested due to automatic review settings July 4, 2026 16:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.let from frozen constants where Sorbet infers the intended type (e.g., X = Klass.new.freeze, literal arrays).
  • Convert Pathname("...")/Pathname(ENV.fetch(...)) to Pathname.new(...) in constants so inference works without T.let.
  • Keep existing explicit T.let annotations 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.

@dduugg dduugg enabled auto-merge July 4, 2026 16:32
@dduugg dduugg added this pull request to the merge queue Jul 4, 2026
Merged via the queue into main with commit f14a18a Jul 4, 2026
52 checks passed
@dduugg dduugg deleted the t-let-freeze-inference branch July 4, 2026 17:00
@MikeMcQuaid

Copy link
Copy Markdown
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.
@dduugg dduugg mentioned this pull request Jul 6, 2026
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants