Class: RuboCop::Cop::Sorbet::ForbidTStruct::Property

Inherits:
Object
  • Object
show all
Defined in:
lib/rubocop/cop/sorbet/forbid_t_struct.rb

Constant Summary collapse

GENERIC_BASE_NAMES =

Sorbet T::X[...] generics that have a direct RBS equivalent.

[:Array, :Hash, :Set, :Range].freeze
T_CONST_MAP =

Sorbet bare T::X constants that map to an RBS built-in.

{ Boolean: "bool" }.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(node, kind, name, type, default:, factory:, style: "sig") ⇒ Property

Returns a new instance of Property.



140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 140

def initialize(node, kind, name, type, default:, factory:, style: "sig")
  @node = node
  @kind = kind
  @name = name
  @type = type
  @type_node = node.arguments[1]
  @default = default
  @factory = factory
  @style = style

  # A T::Struct should have both a default and a factory, if we find one let's raise an error
  raise if @default && @factory
end

Instance Attribute Details

#defaultObject (readonly)

Returns the value of attribute default.



138
139
140
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 138

def default
  @default
end

#factoryObject (readonly)

Returns the value of attribute factory.



138
139
140
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 138

def factory
  @factory
end

#kindObject (readonly)

Returns the value of attribute kind.



138
139
140
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 138

def kind
  @kind
end

#nameObject (readonly)

Returns the value of attribute name.



138
139
140
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 138

def name
  @name
end

#nodeObject (readonly)

Returns the value of attribute node.



138
139
140
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 138

def node
  @node
end

Instance Method Details

#attr_accessorObject



162
163
164
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 162

def attr_accessor
  "#{kind} :#{name}"
end

#attr_sigObject



154
155
156
157
158
159
160
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 154

def attr_sig
  if rbs?
    "#: #{rbs_type}"
  else
    "sig { returns(#{type}) }"
  end
end

#generic_base(node) ⇒ Object

T::Array[X], T::Hash[K, V], and T::Set[X] become the RBS generics Array[X], Hash[K, V], Set[X]. Other const receivers keep their class name (custom generics). Non-const receivers return nil so the caller falls back to untyped instead of untyped[X].



295
296
297
298
299
300
301
302
303
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 295

def generic_base(node)
  return unless node&.const_type?

  if t_const?(node.children[0]) && GENERIC_BASE_NAMES.include?(node.children[1])
    node.children[1].to_s
  elsif !t_const?(node.children[0])
    node.source
  end
end

#initialize_assignObject



186
187
188
189
190
191
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 186

def initialize_assign
  rb = String.new
  rb << "@#{name} = #{name}"
  rb << ".call" if factory
  rb
end

#initialize_paramObject



173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 173

def initialize_param
  rb = String.new
  rb << "#{name}:"
  if default
    rb << " #{default}"
  elsif factory
    rb << " #{factory}"
  elsif nilable?
    rb << " nil"
  end
  rb
end

#initialize_sig_paramObject



166
167
168
169
170
171
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 166

def initialize_sig_param
  type_str = rbs? ? rbs_type : type
  return "?#{name}: #{type_str}" if rbs? && optional?

  "#{name}: #{type_str}"
end

#nilable?Boolean

Returns:

  • (Boolean)


193
194
195
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 193

def nilable?
  type.start_with?("T.nilable(")
end

#optional?Boolean

A prop is optional when it declares a default:, a factory:, or is nilable (which gives it an implicit nil default). RBS marks optional keyword parameters with ?name:, mirroring the default value that initialize_param emits.

Returns:

  • (Boolean)


201
202
203
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 201

def optional?
  !!(default || factory || nilable?)
end

#rbs?Boolean

Returns:

  • (Boolean)


210
211
212
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 210

def rbs?
  @style == "rbs"
end

#rbs_typeObject



214
215
216
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 214

def rbs_type
  sorbet_type_to_rbs(@type_node)
end

#sorbet_type_to_rbs(node) ⇒ Object

Translate a Sorbet type expression AST node into RBS syntax. Handles the constructs most commonly found on T::Struct props: T.nilable, T.any, T.all, T.untyped, T.class_of, and generics like T::Array[X]. Any unrecognized node falls back to the valid RBS untyped rather than emitting malformed Sorbet syntax (e.g. T.proc...) into an RBS annotation.



224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 224

def sorbet_type_to_rbs(node)
  return "untyped" if node.nil?

  case node.type
  when :const
    translate_const(node)
  when :send
    translate_send_type(node)
  else
    "untyped"
  end
end

#t_const?(node) ⇒ Boolean

Returns:

  • (Boolean)


274
275
276
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 274

def t_const?(node)
  node&.const_type? && node.children[1] == :T
end

#translate_const(node) ⇒ Object

Bare class constants are valid RBS class-instance types, except for Sorbet's T::Boolean (-> bool) and other T::X constants which have no RBS equivalent and fall back to untyped.



281
282
283
284
285
286
287
288
289
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 281

def translate_const(node)
  return "untyped" if t_const?(node)

  if t_const?(node.children[0])
    T_CONST_MAP.fetch(node.children[1]) { "untyped" }
  else
    node.source
  end
end

#translate_send_type(node) ⇒ Object



237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 237

def translate_send_type(node)
  receiver = node.receiver
  method = node.method_name
  args = node.arguments

  if t_const?(receiver)
    translate_t_method(method, args)
  elsif method == :[] && (base = generic_base(receiver))
    "#{base}[#{args.map { |arg| sorbet_type_to_rbs(arg) }.join(", ")}]"
  else
    "untyped"
  end
end

#translate_t_method(method, args) ⇒ Object

Maps T.xxx(...) type constructors to RBS. Unknown T.xxx sends become untyped so the annotation stays valid RBS.



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 253

def translate_t_method(method, args)
  case method
  when :nilable
    inner = sorbet_type_to_rbs(args.first)
    inner = "(#{inner})" if inner.include?(" | ") || inner.include?(" & ")
    "#{inner}?"
  when :any
    args.map { |arg| sorbet_type_to_rbs(arg) }.join(" | ")
  when :all
    args.map { |arg| sorbet_type_to_rbs(arg) }.join(" & ")
  when :untyped
    "untyped"
  when :noreturn
    "bot"
  when :class_of
    "singleton(#{sorbet_type_to_rbs(args.first)})"
  else
    "untyped"
  end
end

#typeObject



205
206
207
208
# File 'lib/rubocop/cop/sorbet/forbid_t_struct.rb', line 205

def type
  copy = @type.gsub(/[[:space:]]+/, "").strip # Remove newlines and spaces
  copy.gsub(",", ", ") # Add a space after each comma
end