Class: RuboCop::Cop::Sorbet::RedundantTLetForLiteral

Inherits:
Base
  • Object
show all
Extended by:
AutoCorrector
Includes:
ConstantScope, TLetCorrection, TargetSorbetVersion
Defined in:
lib/rubocop/cop/sorbet/redundant_t_let_for_literal.rb

Overview

Checks for redundant T.let declarations where the first argument is a literal whose type Sorbet can infer automatically, so wrapping it in T.let is redundant.

Simple literals (strings, symbols, integers, floats, regexps) infer as their own class. Regexp literals are the only simple literals whose inference survives a .freeze call (Sorbet 0.6.13304+), so T.let(/foo/.freeze, Regexp) is also redundant; other frozen simple literals (e.g. "hello".freeze) are not inferred and still need T.let.

Array literals of simple literals are also inferred:

  • A frozen array ([...].freeze) infers as a fixed-size tuple, which is a subtype of the annotated T::Array, so the annotation is redundant.
  • An unfrozen array infers as T::Array[<element type>]. It is only flagged when that inferred type matches the annotation exactly, to avoid silently widening (e.g. ["a", nil] infers a nilable element).

Hashes are excluded: Sorbet infers hash literals as T.untyped, so the annotation is required.

Examples:

# bad
MAX_RETRIES = T.let(3, Integer)
GREETING = T.let("hello", String)
RATE = T.let(1.5, Float)
PATTERN = T.let(/foo/, Regexp)
FROZEN_PATTERN = T.let(/foo/.freeze, Regexp)
STATUS = T.let(:active, Symbol)
SHELLS = T.let([:bash, :zsh].freeze, T::Array[Symbol])
NAMES = T.let(["alice", "bob"], T::Array[String])

# good
MAX_RETRIES = 3
GREETING = "hello"
RATE = 1.5
PATTERN = /foo/
FROZEN_PATTERN = /foo/.freeze
STATUS = :active
SHELLS = [:bash, :zsh].freeze
NAMES = ["alice", "bob"]

# good — non-regexp frozen simple literals are not inferred
GREETING = T.let("hello".freeze, String)

# good — hash literals are inferred as T.untyped
OPTIONS = T.let({ verbose: true }, T::Hash[Symbol, T::Boolean])

# good — unfrozen array whose annotation is wider than the inferred type
NAMES = T.let(["alice", "bob"], T::Array[T.nilable(String)])

# good — type is not the literal's own class
value = T.let("hello", T.nilable(String))

# good — instance variables need T.let for Sorbet to track their type
@max_retries = T.let(3, Integer)

# good — local variables may need T.let so Sorbet allows reassignment
count = T.let(0, Integer)

Constant Summary collapse

MSG =
"Redundant `T.let` for %{type} literal. Sorbet can infer this type automatically."
LITERAL_TYPE_TO_CLASS =

Simple literal node types Sorbet infers, mapped to the class name. Interpolated strings/symbols (dstr/dsym) infer as String/Symbol.

{
  dstr: :String,
  dsym: :Symbol,
  float: :Float,
  int: :Integer,
  regexp: :Regexp,
  str: :String,
  sym: :Symbol,
}.freeze
ARRAY_ELEMENT_TYPES =

Element node types allowed inside an inferable array literal: the literals whose class Sorbet reflects into the array's element type. Regexps and ranges, by contrast, degrade the array to T.untyped, so they are excluded. Interpolated strings/symbols (dstr/dsym) infer as String/Symbol, the same as their plain literal forms.

[:dstr, :dsym, :str, :sym, :int, :float, :true, :false, :nil].freeze
ELEMENT_TYPE_TO_CLASS =

Element node types whose inferred class is unambiguous, used to derive the element type of an unfrozen array literal.

{
  dstr: "String",
  dsym: "Symbol",
  float: "Float",
  int: "Integer",
  str: "String",
  sym: "Symbol",
}.freeze

Instance Method Summary collapse

Methods included from TargetSorbetVersion

#enabled_for_sorbet_static_version?, included, #read_sorbet_static_version_from_bundler_lock_file, #sorbet_enabled?, #target_sorbet_static_version_from_bundler_lock_file

Instance Method Details

#on_casgn(node) ⇒ Object



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/rubocop/cop/sorbet/redundant_t_let_for_literal.rb', line 123

def on_casgn(node)
  return unless statically_scoped?(node)

  t_let_with_literal_and_class?(node) do |value_node, class_name|
    literal_node = inferable_literal_node(value_node)
    next unless literal_node
    next unless LITERAL_TYPE_TO_CLASS[literal_node.type] == class_name

    register_offense(node, value_node, class_name)
  end

  return unless enabled_for_sorbet_static_version?

  t_let_with_array?(node) do |value_node, type_node|
    frozen = value_node.send_type?
    array_node = frozen ? value_node.receiver : value_node
    next unless inferable_array?(array_node)
    next unless redundant_array_annotation?(array_node, type_node, frozen: frozen)

    register_offense(node, value_node, :Array)
  end
end

#t_let_with_array?(node) ⇒ Object



119
120
121
# File 'lib/rubocop/cop/sorbet/redundant_t_let_for_literal.rb', line 119

def_node_matcher :t_let_with_array?, <<~PATTERN
  (casgn _ _ (send (const {nil? cbase} :T) :let ${array (send array :freeze)} $_))
PATTERN

#t_let_with_literal_and_class?(node) ⇒ Object



114
115
116
# File 'lib/rubocop/cop/sorbet/redundant_t_let_for_literal.rb', line 114

def_node_matcher :t_let_with_literal_and_class?, <<~PATTERN
  (casgn _ _ (send (const {nil? cbase} :T) :let ${literal? (send (regexp ...) :freeze)} (const nil? $_)))
PATTERN