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 and trailing RBS annotations where the assigned value is a literal whose type Sorbet can infer automatically.

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])
RBS_GREETING = "hello" #: String
RBS_NAMES = ["alice", "bob"] #: 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"]
RBS_GREETING = "hello"
RBS_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 %{annotation} 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



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

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

  if 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

  check_rbs_annotation(node)
end

#t_let_with_array?(node) ⇒ Object



121
122
123
# File 'lib/rubocop/cop/sorbet/redundant_t_let_for_literal.rb', line 121

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



116
117
118
# File 'lib/rubocop/cop/sorbet/redundant_t_let_for_literal.rb', line 116

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