Skip to content

Add RedundantTLet cop - #362

Merged
amomchilov merged 1 commit into
mainfrom
jb-prevent-duplicate-tlet
Feb 19, 2026
Merged

amomchilov merged 1 commit into
mainfrom
jb-prevent-duplicate-tlet

Conversation

@dejmedus

Copy link
Copy Markdown
Contributor

Sorbet can infer the type of initialize method instance variables when they are assigned to parameters already typed by signatures. In cases where a T.let assertion is:

  • top level inside an initialize method
  • assigned to an instance variable
  • typed the same as in sig params

we can DRY up code by removing it

sig { params(foo: String) }
def initialize(foo:)
- @foo = T.let(foo, String)
+ @foo = foo
end

The cop will display Sorbet/RedundantTLet: Unnecessary T.let. The instance variable type is inferred from the signature and optionally autocorrect

Demo
tlet-demo.mov

@dejmedus
dejmedus marked this pull request as ready for review January 20, 2026 18:20
@dejmedus
dejmedus requested a review from a team as a code owner January 20, 2026 18:20
@dejmedus dejmedus self-assigned this Jan 20, 2026

@Morriar Morriar 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.

This is looking good, let's add a few tests to encode current behaviour with more complex type expressions then we can merge it 👍

@a = T.let(a, T::Array[Integer])
^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{MSG}
end
RUBY

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.

Let's add a few example with more complicated type syntax:

sig { params(a: Foo[Bar], b: T.any(A, B), c: T.proc.void).void }
def initialize(proc)
  @a = T.let(a, Foo[Foo]) # error
  @b = T.let(b, T.any(B, A)) # it's equivalent, should it be ok or error?
  @c = T.let(c, T.proc.returns(Integer)) # ok
end

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a few more examples:)

@ b = T.let(b, T.any(B, A)) # it's equivalent, should it be ok or error?

Right now the cop is checking for syntactical equivalence, not semantical, so T.any(A, B) and T.any(B, A) (or T.proc.params(x: A, y: B) and T.proc.params(y: B, x: A)) are treated as different and left alone

I think it would be possible to manually check for parity in these cases but the solutions I looked at felt a bit fragile. I would lean towards not warning/autocorrecting. What do you think?

Remove unnecessary T.let when assigning signature
parameters directly to instance variables in initialize.
Sorbet infers the type automatically

Example:
```ruby
sig { params(foo: String) }
def initialize(foo:)
  # unnecessary T.let:
  @foo = T.let(foo, String)
  # will autocorrect to:
  @foo = foo
end
```
@dejmedus
dejmedus force-pushed the jb-prevent-duplicate-tlet branch from 2848a5b to b0648aa Compare January 20, 2026 23:57
dduugg added a commit to dduugg/rubocop-sorbet that referenced this pull request Feb 9, 2026
Similar to `Sorbet/RedundantTLet` (Shopify#362), which removes redundant
`T.let` in `initialize` methods where instance variable types can be
inferred from the signature, this cop targets a different case:
constant assignments with simple literals.

When a constant is assigned a simple literal (String, Integer, Float,
Symbol, Regexp, Complex, Rational) wrapped in `T.let` with the
matching class as the type, the `T.let` is redundant because Sorbet
can infer the type of simple literals automatically. For example:

  # bad — Sorbet already knows this is Integer
  MAX_RETRIES = T.let(3, Integer)

  # good
  MAX_RETRIES = 3

The cop only fires on constant assignments (`casgn`) with simple
literals. It correctly ignores collections, nilable types, instance
variables, local variables, and cases where the type doesn't match
the literal.

Includes autocorrection support.
dduugg added a commit to dduugg/rubocop-sorbet that referenced this pull request Feb 9, 2026
Similar to `Sorbet/RedundantTLet` (Shopify#362), which removes redundant
`T.let` in `initialize` methods where instance variable types can be
inferred from the signature, this cop targets a different case:
constant assignments with simple literals.

When a constant is assigned a simple literal (String, Integer, Float,
Symbol, Regexp, Complex, Rational) wrapped in `T.let` with the
matching class as the type, the `T.let` is redundant because Sorbet
can infer the type of simple literals automatically. For example:

  # bad — Sorbet already knows this is Integer
  MAX_RETRIES = T.let(3, Integer)

  # good
  MAX_RETRIES = 3

The cop only fires on constant assignments (`casgn`) with simple
literals. It correctly ignores collections, nilable types, instance
variables, local variables, and cases where the type doesn't match
the literal.

Includes autocorrection support.
dduugg added a commit to dduugg/rubocop-sorbet that referenced this pull request Feb 9, 2026
Similar to `Sorbet/RedundantTLet` (Shopify#362), which removes redundant
`T.let` in `initialize` methods where instance variable types can be
inferred from the signature, this cop targets a different case:
constant assignments with simple literals.

When a constant is assigned a simple literal (String, Integer, Float,
Symbol, Regexp) wrapped in `T.let` with the matching class as the
type, the `T.let` is redundant because Sorbet can infer the type
automatically. For example:

  # bad — Sorbet already knows this is Integer
  MAX_RETRIES = T.let(3, Integer)

  # good
  MAX_RETRIES = 3

The cop only fires on constant assignments (`casgn`) with simple
literals. It correctly ignores collections, nilable types, instance
variables, local variables, and cases where the type doesn't match
the literal.

Includes autocorrection support.
dduugg added a commit to dduugg/rubocop-sorbet that referenced this pull request Feb 9, 2026
Similar to `Sorbet/RedundantTLet` (Shopify#362), which removes redundant
`T.let` in `initialize` methods where instance variable types can be
inferred from the signature, this cop targets a different case:
constant assignments with simple literals.

When a constant is assigned a simple literal (Float, Integer, Regexp,
String, Symbol) wrapped in `T.let` with the matching class as the
type, the `T.let` is redundant because Sorbet can infer the type
automatically. For example:

  # bad — Sorbet already knows this is Integer
  MAX_RETRIES = T.let(3, Integer)

  # good
  MAX_RETRIES = 3

The cop only fires on constant assignments (`casgn`) with simple
literals. It correctly ignores collections, nilable types, instance
variables, local variables, and cases where the type doesn't match
the literal.

Includes autocorrection support.
dduugg added a commit to dduugg/rubocop-sorbet that referenced this pull request Feb 9, 2026
Similar to `Sorbet/RedundantTLet` (Shopify#362), which removes redundant
`T.let` in `initialize` methods where instance variable types can be
inferred from the signature, this cop targets a different case:
constant assignments with simple literals.

When a constant is assigned a simple literal (Float, Integer, Regexp,
String, Symbol) wrapped in `T.let` with the matching class as the
type, the `T.let` is redundant because Sorbet can infer the type
automatically. For example:

  # bad — Sorbet already knows this is Integer
  MAX_RETRIES = T.let(3, Integer)

  # good
  MAX_RETRIES = 3

The cop only fires on constant assignments (`casgn`) with simple
literals. It correctly ignores collections, nilable types, instance
variables, local variables, and cases where the type doesn't match
the literal.

Includes autocorrection support.
dduugg added a commit to dduugg/rubocop-sorbet that referenced this pull request Feb 9, 2026
Similar to `Sorbet/RedundantTLet` (Shopify#362), which removes redundant
`T.let` in `initialize` methods where instance variable types can be
inferred from the signature, this cop targets a different case:
constant assignments with simple literals.

When a constant is assigned a simple literal (Float, Integer, Regexp,
String, Symbol) wrapped in `T.let` with the matching class as the
type, the `T.let` is redundant because Sorbet can infer the type
automatically. For example:

  # bad — Sorbet already knows this is Integer
  MAX_RETRIES = T.let(3, Integer)

  # good
  MAX_RETRIES = 3

The cop only fires on constant assignments (`casgn`) with simple
literals. It correctly ignores collections, nilable types, instance
variables, local variables, and cases where the type doesn't match
the literal.

Includes autocorrection support.
dduugg added a commit to dduugg/rubocop-sorbet that referenced this pull request Feb 10, 2026
Similar to `Sorbet/RedundantTLet` (Shopify#362), which removes redundant
`T.let` in `initialize` methods where instance variable types can be
inferred from the signature, this cop targets a different case:
constant assignments with simple literals.

When a constant is assigned a simple literal (Float, Integer, Regexp,
String, Symbol) wrapped in `T.let` with the matching class as the
type, the `T.let` is redundant because Sorbet can infer the type
automatically. For example:

  # bad — Sorbet already knows this is Integer
  MAX_RETRIES = T.let(3, Integer)

  # good
  MAX_RETRIES = 3

The cop only fires on constant assignments (`casgn`) with simple
literals. It correctly ignores collections, nilable types, instance
variables, local variables, and cases where the type doesn't match
the literal.

Includes autocorrection support.
@dejmedus
dejmedus requested a review from Morriar February 10, 2026 16:45

@KaanOzkan KaanOzkan 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.

LGTM

@amomchilov
amomchilov merged commit a271166 into main Feb 19, 2026
12 checks passed
@amomchilov
amomchilov deleted the jb-prevent-duplicate-tlet branch February 19, 2026 22:11
amomchilov pushed a commit that referenced this pull request Feb 20, 2026
Similar to `Sorbet/RedundantTLet` (#362), which removes redundant
`T.let` in `initialize` methods where instance variable types can be
inferred from the signature, this cop targets a different case:
constant assignments with simple literals.

When a constant is assigned a simple literal (Float, Integer, Regexp,
String, Symbol) wrapped in `T.let` with the matching class as the
type, the `T.let` is redundant because Sorbet can infer the type
automatically. For example:

  # bad — Sorbet already knows this is Integer
  MAX_RETRIES = T.let(3, Integer)

  # good
  MAX_RETRIES = 3

The cop only fires on constant assignments (`casgn`) with simple
literals. It correctly ignores collections, nilable types, instance
variables, local variables, and cases where the type doesn't match
the literal.

Includes autocorrection support.

This branch was previously deployed

1 inactive deployment
production — b0648aa9 Deployed Mar 27, 2026 by shopify-shipit[bot]
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.

Suggested cop: remove T.let when its signature matches the param signature in initialize

4 participants