decode 0.24.2 → 0.24.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/bake/decode/rbs.rb +1 -1
  4. data/context/coverage.md +1 -1
  5. data/context/getting-started.md +1 -1
  6. data/context/ruby-documentation.md +3 -3
  7. data/context/types.md +127 -0
  8. data/lib/decode/comment/attribute.rb +5 -2
  9. data/lib/decode/comment/constant.rb +47 -0
  10. data/lib/decode/comment/node.rb +32 -12
  11. data/lib/decode/comment/option.rb +1 -1
  12. data/lib/decode/comment/parameter.rb +6 -2
  13. data/lib/decode/comment/rbs.rb +8 -8
  14. data/lib/decode/comment/tag.rb +19 -0
  15. data/lib/decode/comment/tags.rb +16 -5
  16. data/lib/decode/comment/text.rb +1 -0
  17. data/lib/decode/comment/yields.rb +5 -1
  18. data/lib/decode/definition.rb +33 -31
  19. data/lib/decode/documentation.rb +10 -5
  20. data/lib/decode/index.rb +12 -7
  21. data/lib/decode/language/generic.rb +10 -1
  22. data/lib/decode/language/reference.rb +7 -4
  23. data/lib/decode/language/ruby/class.rb +2 -2
  24. data/lib/decode/language/ruby/code.rb +21 -3
  25. data/lib/decode/language/ruby/definition.rb +15 -3
  26. data/lib/decode/language/ruby/generic.rb +2 -1
  27. data/lib/decode/language/ruby/parser.rb +132 -91
  28. data/lib/decode/language/ruby/reference.rb +4 -1
  29. data/lib/decode/language/ruby/segment.rb +2 -2
  30. data/lib/decode/languages.rb +29 -8
  31. data/lib/decode/location.rb +12 -1
  32. data/lib/decode/rbs/class.rb +91 -14
  33. data/lib/decode/rbs/generator.rb +67 -11
  34. data/lib/decode/rbs/method.rb +394 -65
  35. data/lib/decode/rbs/module.rb +81 -5
  36. data/lib/decode/rbs/type.rb +51 -0
  37. data/lib/decode/rbs/wrapper.rb +10 -3
  38. data/lib/decode/scope.rb +2 -2
  39. data/lib/decode/segment.rb +3 -2
  40. data/lib/decode/source.rb +5 -14
  41. data/lib/decode/syntax/rewriter.rb +4 -1
  42. data/lib/decode/trie.rb +29 -21
  43. data/lib/decode/version.rb +2 -1
  44. data/readme.md +6 -0
  45. data/releases.md +6 -0
  46. data/sig/decode.rbs +1189 -0
  47. data.tar.gz.sig +0 -0
  48. metadata +5 -15
  49. metadata.gz.sig +0 -0
@@ -5,19 +5,21 @@
5
5
 
6
6
  require "rbs"
7
7
  require "console"
8
- require "types"
9
8
  require_relative "wrapper"
9
+ require_relative "type"
10
10
 
11
11
  module Decode
12
12
  module RBS
13
13
  # Represents a Ruby method definition wrapper for RBS generation.
14
14
  class Method < Wrapper
15
-
16
15
  # Initialize a new method wrapper.
17
16
  # @parameter definition [Decode::Definition] The method definition to wrap.
18
17
  def initialize(definition)
19
18
  super
20
19
  @signatures = nil
20
+ @keyword_arguments = nil
21
+ @return_type = nil
22
+ @parameters = nil
21
23
  end
22
24
 
23
25
  # Extract method signatures from the method definition.
@@ -26,10 +28,28 @@ module Decode
26
28
  @signatures ||= extract_signatures
27
29
  end
28
30
 
31
+ # Extract keyword arguments from the method definition.
32
+ # @returns [Hash] Hash with :required and :optional keys.
33
+ def keyword_arguments
34
+ @keyword_arguments ||= extract_keyword_arguments(@definition, nil)
35
+ end
36
+
37
+ # Extract return type from the method definition.
38
+ # @returns [::RBS::Types::t] The RBS return type.
39
+ def return_type
40
+ @return_type ||= extract_return_type(@definition, nil) || ::RBS::Parser.parse_type("untyped")
41
+ end
42
+
43
+ # Extract parameters from the method definition.
44
+ # @returns [Array] Array of RBS parameter objects.
45
+ def parameters
46
+ @parameters ||= extract_parameters(@definition, nil)
47
+ end
48
+
29
49
  # Convert the method definition to RBS AST
30
50
  def to_rbs_ast(index = nil)
31
51
  method_name = @definition.name
32
- comment = extract_comment(@definition)
52
+ comment = self.comment
33
53
 
34
54
  overloads = []
35
55
  if signatures.any?
@@ -41,25 +61,38 @@ module Decode
41
61
  )
42
62
  end
43
63
  else
44
- return_type = extract_return_type(@definition, index) || ::RBS::Parser.parse_type("untyped")
45
- parameters = extract_parameters(@definition, index)
46
- block_type = extract_block_type(@definition, index)
64
+ return_type = self.return_type
47
65
 
48
- method_type = ::RBS::MethodType.new(
49
- type_params: [],
50
- type: ::RBS::Types::Function.new(
51
- required_positionals: parameters,
52
- optional_positionals: [],
53
- rest_positionals: nil,
54
- trailing_positionals: [],
55
- required_keywords: {},
56
- optional_keywords: {},
57
- rest_keywords: nil,
58
- return_type: return_type
59
- ),
60
- block: block_type,
61
- location: nil
62
- )
66
+ # Get parameters using AST-based detection
67
+ if ast_function = build_function_type_from_ast(@definition, index)
68
+ method_type = ::RBS::MethodType.new(
69
+ type_params: [],
70
+ type: ast_function,
71
+ block: extract_block_type(@definition, index),
72
+ location: nil
73
+ )
74
+ else
75
+ # Fall back to documentation-based approach
76
+ parameters = self.parameters
77
+ keywords = self.keyword_arguments
78
+ block_type = extract_block_type(@definition, index)
79
+
80
+ method_type = ::RBS::MethodType.new(
81
+ type_params: [],
82
+ type: ::RBS::Types::Function.new(
83
+ required_positionals: parameters,
84
+ optional_positionals: [],
85
+ rest_positionals: nil,
86
+ trailing_positionals: [],
87
+ required_keywords: keywords[:required],
88
+ optional_keywords: keywords[:optional],
89
+ rest_keywords: nil,
90
+ return_type: return_type
91
+ ),
92
+ block: block_type,
93
+ location: nil
94
+ )
95
+ end
63
96
 
64
97
  overloads << ::RBS::AST::Members::MethodDefinition::Overload.new(
65
98
  method_type: method_type,
@@ -70,14 +103,118 @@ module Decode
70
103
  kind = @definition.receiver ? :singleton : :instance
71
104
 
72
105
  ::RBS::AST::Members::MethodDefinition.new(
73
- name: method_name.to_sym,
74
- kind: kind,
75
- overloads: overloads,
76
- annotations: [],
77
- location: nil,
78
- comment: comment,
79
- overloading: false,
80
- visibility: :public
106
+ name: method_name.to_sym,
107
+ kind: kind,
108
+ overloads: overloads,
109
+ annotations: [],
110
+ location: nil,
111
+ comment: comment,
112
+ overloading: false,
113
+ visibility: @definition.visibility || :public
114
+ )
115
+ end
116
+
117
+ # Build a complete RBS function type from AST information.
118
+ # @parameter definition [Definition] The method definition.
119
+ # @parameter index [Index] The index for context.
120
+ # @returns [RBS::Types::Function] The complete function type, or nil if no AST.
121
+ def build_function_type_from_ast(definition, index)
122
+ node = definition.node
123
+ # Only return nil if we don't have an AST node at all
124
+ return nil unless node&.respond_to?(:parameters)
125
+
126
+ doc_types = extract_documented_parameter_types(definition)
127
+
128
+ required_positionals = []
129
+ optional_positionals = []
130
+ rest_positionals = nil
131
+ required_keywords = {}
132
+ optional_keywords = {}
133
+ keyword_rest = nil
134
+
135
+ # Only process parameters if the node actually has them:
136
+ if node.parameters
137
+ # Handle required positional parameters:
138
+ if node.parameters.respond_to?(:requireds) && node.parameters.requireds
139
+ node.parameters.requireds.each do |param|
140
+ name = param.name
141
+ type = doc_types[name.to_s] || ::RBS::Parser.parse_type("untyped")
142
+
143
+ required_positionals << ::RBS::Types::Function::Param.new(
144
+ type: type,
145
+ name: name.to_sym
146
+ )
147
+ end
148
+ end
149
+
150
+ # Handle optional positional parameters (with defaults):
151
+ if node.parameters.respond_to?(:optionals) && node.parameters.optionals
152
+ node.parameters.optionals.each do |param|
153
+ name = param.name
154
+ # For optional parameters, use the documented type as-is (don't make it nullable):
155
+ type = doc_types[name.to_s] || ::RBS::Parser.parse_type("untyped")
156
+
157
+ optional_positionals << ::RBS::Types::Function::Param.new(
158
+ type: type,
159
+ name: name.to_sym
160
+ )
161
+ end
162
+ end
163
+
164
+ # Handle rest parameter (*args):
165
+ if node.parameters.respond_to?(:rest) && node.parameters.rest
166
+ rest_param = node.parameters.rest
167
+ name = rest_param.name || :args
168
+ base_type = doc_types[name.to_s] || ::RBS::Parser.parse_type("untyped")
169
+
170
+ rest_positionals = ::RBS::Types::Function::Param.new(
171
+ type: base_type,
172
+ name: name.to_sym
173
+ )
174
+ end
175
+
176
+ # Handle keyword parameters:
177
+ if node.parameters.respond_to?(:keywords) && node.parameters.keywords
178
+ node.parameters.keywords.each do |param|
179
+ name = param.name
180
+ type = doc_types[name.to_s] || ::RBS::Parser.parse_type("untyped")
181
+
182
+ if param.respond_to?(:value) && param.value
183
+ # Has default value - optional keyword:
184
+ optional_keywords[name.to_sym] = type
185
+ else
186
+ # No default value - required keyword:
187
+ required_keywords[name.to_sym] = type
188
+ end
189
+ end
190
+ end
191
+
192
+ # Handle keyword rest parameter (**kwargs):
193
+ if node.parameters.respond_to?(:keyword_rest) && node.parameters.keyword_rest
194
+ rest_param = node.parameters.keyword_rest
195
+ if rest_param.name
196
+ name = rest_param.name
197
+ base_type = doc_types[name.to_s] || ::RBS::Parser.parse_type("untyped")
198
+
199
+ keyword_rest = ::RBS::Types::Function::Param.new(
200
+ type: base_type,
201
+ name: name.to_sym
202
+ )
203
+ end
204
+ end
205
+ end
206
+
207
+ return_type = extract_return_type(@definition, index) || ::RBS::Parser.parse_type("untyped")
208
+
209
+ ::RBS::Types::Function.new(
210
+ required_positionals: required_positionals,
211
+ optional_positionals: optional_positionals,
212
+ rest_positionals: rest_positionals,
213
+ trailing_positionals: [],
214
+ required_keywords: required_keywords,
215
+ optional_keywords: optional_keywords,
216
+ rest_keywords: keyword_rest,
217
+ return_type: return_type
81
218
  )
82
219
  end
83
220
 
@@ -87,37 +224,54 @@ module Decode
87
224
  extract_tags.select(&:method_signature?).map(&:method_signature)
88
225
  end
89
226
 
90
- # Extract return type from method documentation
227
+ # Extract return type from method documentation.
91
228
  def extract_return_type(definition, index)
92
- # Look for @returns tags in the method's documentation
229
+ # Look for @returns tags in the method's documentation:
93
230
  documentation = definition.documentation
94
231
 
95
- # Find @returns tag
96
- returns_tag = documentation&.filter(Decode::Comment::Returns)&.first
232
+ # Find all @returns tags:
233
+ returns_tags = documentation&.filter(Decode::Comment::Returns)&.to_a
97
234
 
98
- if returns_tag
99
- # Parse the type from the tag
100
- type_string = returns_tag.type.strip
101
- parse_type_string(type_string)
235
+ if returns_tags&.any?
236
+ if returns_tags.length == 1
237
+ # Single return type:
238
+ type_string = returns_tags.first.type.strip
239
+ Type.parse(type_string)
240
+ else
241
+ # Multiple return types - create union:
242
+ types = returns_tags.map do |tag|
243
+ type_string = tag.type.strip
244
+ Type.parse(type_string)
245
+ end
246
+
247
+ ::RBS::Types::Union.new(types: types, location: nil)
248
+ end
102
249
  else
103
- # Infer return type based on method name patterns
250
+ # Infer return type based on method name patterns:
104
251
  infer_return_type(definition)
105
252
  end
106
253
  end
107
254
 
108
- # Extract parameter types from method documentation
255
+ # Extract parameter types from method documentation.
109
256
  def extract_parameters(definition, index)
257
+ # Try AST-based extraction first:
258
+ if ast_params = extract_parameters_from_ast(definition)
259
+ return ast_params unless ast_params.empty?
260
+ end
261
+
262
+ # Fall back to documentation-based extraction:
110
263
  documentation = definition.documentation
111
264
  return [] unless documentation
112
265
 
113
- # Find @parameter tags
266
+ # Find @parameter tags (but not @option tags, which are handled separately):
114
267
  param_tags = documentation.filter(Decode::Comment::Parameter).to_a
268
+ param_tags = param_tags.reject {|tag| tag.is_a?(Decode::Comment::Option)}
115
269
  return [] if param_tags.empty?
116
270
 
117
271
  param_tags.map do |tag|
118
272
  name = tag.name
119
273
  type_string = tag.type.strip
120
- type = parse_type_string(type_string)
274
+ type = Type.parse(type_string)
121
275
 
122
276
  ::RBS::Types::Function::Param.new(
123
277
  type: type,
@@ -126,20 +280,205 @@ module Decode
126
280
  end
127
281
  end
128
282
 
129
- # Extract block type from method documentation
283
+ # Extract parameter information from the Prism AST node.
284
+ # @parameter definition [Definition] The method definition with AST node.
285
+ # @returns [Array] Array of RBS parameter objects, or nil if no AST available.
286
+ def extract_parameters_from_ast(definition)
287
+ node = definition.node
288
+ return nil unless node&.respond_to?(:parameters) && node.parameters
289
+
290
+ params = []
291
+ doc_types = extract_documented_parameter_types(definition)
292
+
293
+ # Handle required positional parameters:
294
+ if node.parameters.respond_to?(:requireds) && node.parameters.requireds
295
+ node.parameters.requireds.each do |param|
296
+ name = param.name
297
+ type = doc_types[name.to_s] || ::RBS::Parser.parse_type("untyped")
298
+
299
+ params << ::RBS::Types::Function::Param.new(
300
+ type: type,
301
+ name: name.to_sym
302
+ )
303
+ end
304
+ end
305
+
306
+ # Handle optional positional parameters (with defaults):
307
+ if node.parameters.respond_to?(:optionals) && node.parameters.optionals
308
+ node.parameters.optionals.each do |param|
309
+ name = param.name
310
+ # For optional parameters, make the documented type optional if not already:
311
+ base_type = doc_types[name.to_s] || ::RBS::Parser.parse_type("untyped")
312
+ type = make_type_optional_if_needed(base_type)
313
+
314
+ params << ::RBS::Types::Function::Param.new(
315
+ type: type,
316
+ name: name.to_sym
317
+ )
318
+ end
319
+ end
320
+
321
+ # Handle rest parameter (*args):
322
+ if node.parameters.respond_to?(:rest) && node.parameters.rest
323
+ rest_param = node.parameters.rest
324
+ name = rest_param.name || :args
325
+ base_type = doc_types[name.to_s] || ::RBS::Parser.parse_type("untyped")
326
+ # Rest parameters should be `Array[T]`:
327
+ array_type = ::RBS::Types::ClassInstance.new(
328
+ name: ::RBS::TypeName.new(name: :Array, namespace: ::RBS::Namespace.empty),
329
+ args: [base_type],
330
+ location: nil
331
+ )
332
+
333
+ params << ::RBS::Types::Function::Param.new(
334
+ type: array_type,
335
+ name: name.to_sym
336
+ )
337
+ end
338
+
339
+ params
340
+ end
341
+
342
+ # Extract keyword arguments from @option tags and AST.
343
+ def extract_keyword_arguments(definition, index)
344
+ # Try AST-based extraction first:
345
+ if ast_keywords = extract_keyword_arguments_from_ast(definition)
346
+ return ast_keywords
347
+ end
348
+
349
+ # Fall back to documentation-based extraction:
350
+ documentation = definition.documentation
351
+ return { required: {}, optional: {} } unless documentation
352
+
353
+ # Find @option tags:
354
+ option_tags = documentation.filter(Decode::Comment::Option).to_a
355
+ return { required: {}, optional: {} } if option_tags.empty?
356
+
357
+ keywords = { required: {}, optional: {} }
358
+
359
+ option_tags.each do |tag|
360
+ name = tag.name.to_s
361
+ # Remove leading colon if present (e.g., ":cached" -> "cached"):
362
+ name = name.sub(/\A:/, "")
363
+
364
+ type_string = tag.type.strip
365
+ type = Type.parse(type_string)
366
+
367
+ # Determine if the keyword is optional based on the type annotation.
368
+ # If the type is nullable (contains nil or ends with ?), make it optional:
369
+ if Type.nullable?(type)
370
+ keywords[:optional][name.to_sym] = type
371
+ else
372
+ keywords[:required][name.to_sym] = type
373
+ end
374
+ end
375
+
376
+ keywords
377
+ end
378
+
379
+ # Extract keyword arguments from the Prism AST node.
380
+ # @parameter definition [Definition] The method definition with AST node.
381
+ # @returns [Hash] Hash with :required and :optional keyword arguments, or nil if no AST.
382
+ def extract_keyword_arguments_from_ast(definition)
383
+ node = definition.node
384
+ return nil unless node&.respond_to?(:parameters) && node.parameters
385
+
386
+ required = {}
387
+ optional = {}
388
+ doc_types = extract_documented_parameter_types(definition)
389
+
390
+ # Handle keyword parameters:
391
+ if node.parameters.respond_to?(:keywords) && node.parameters.keywords
392
+ node.parameters.keywords.each do |param|
393
+ name = param.name
394
+ base_type = doc_types[name.to_s] || ::RBS::Parser.parse_type("untyped")
395
+
396
+ if param.respond_to?(:value) && param.value
397
+ # Has default value - optional keyword:
398
+ type = make_type_optional_if_needed(base_type)
399
+ optional[name.to_sym] = type
400
+ else
401
+ # No default value - required keyword:
402
+ required[name.to_sym] = base_type
403
+ end
404
+ end
405
+ end
406
+
407
+ # Handle keyword rest parameter (**kwargs):
408
+ if node.parameters.respond_to?(:keyword_rest) && node.parameters.keyword_rest
409
+ rest_param = node.parameters.keyword_rest
410
+ if rest_param.name
411
+ name = rest_param.name
412
+ base_type = doc_types[name.to_s] || ::RBS::Parser.parse_type("untyped")
413
+ # Keyword rest should be `Hash[Symbol, T]`:
414
+ hash_type = ::RBS::Types::ClassInstance.new(
415
+ name: ::RBS::TypeName.new(name: :Hash, namespace: ::RBS::Namespace.empty),
416
+ args: [
417
+ ::RBS::Types::ClassInstance.new(name: ::RBS::TypeName.new(name: :Symbol, namespace: ::RBS::Namespace.empty), args: [], location: nil),
418
+ base_type
419
+ ],
420
+ location: nil
421
+ )
422
+ optional[name.to_sym] = hash_type
423
+ end
424
+ end
425
+
426
+ { required: required, optional: optional }
427
+ end
428
+
429
+ # Extract documented parameter types into a hash for lookup.
430
+ # @parameter definition [Definition] The method definition.
431
+ # @returns [Hash] Map of parameter name to RBS type.
432
+ def extract_documented_parameter_types(definition)
433
+ doc_types = {}
434
+ documentation = definition.documentation
435
+ return doc_types unless documentation
436
+
437
+ # Extract types from @parameter tags:
438
+ param_tags = documentation.filter(Decode::Comment::Parameter).to_a
439
+ param_tags.each do |tag|
440
+ doc_types[tag.name] = Type.parse(tag.type.strip)
441
+ end
442
+
443
+ # Extract types from @option tags
444
+ option_tags = documentation.filter(Decode::Comment::Option).to_a
445
+ option_tags.each do |tag|
446
+ # Remove leading colon:
447
+ name = tag.name.sub(/\A:/, "")
448
+ doc_types[name] = Type.parse(tag.type.strip)
449
+ end
450
+
451
+ doc_types
452
+ end
453
+
454
+ # Make a type optional if it's not already nullable.
455
+ # @parameter type [RBS::Types::t] The base type.
456
+ # @returns [RBS::Types::t] The optionally-nullable type.
457
+ def make_type_optional_if_needed(type)
458
+ return type if Type.nullable?(type)
459
+
460
+ # Create a union with nil to make it optional:
461
+ ::RBS::Types::Union.new(
462
+ types: [type, ::RBS::Types::Bases::Nil.new(location: nil)],
463
+ location: nil
464
+ )
465
+ end
466
+
467
+
468
+ # Extract block type from method documentation.
130
469
  def extract_block_type(definition, index)
131
470
  documentation = definition.documentation
132
471
  return nil unless documentation
133
472
 
134
- # Find @yields tags
473
+ # Find `@yields` tags:
135
474
  yields_tag = documentation.filter(Decode::Comment::Yields).first
136
475
  return nil unless yields_tag
137
476
 
138
- # Extract block parameters from nested @parameter tags
477
+ # Extract block parameters from nested `@parameter` tags:
139
478
  block_params = yields_tag.filter(Decode::Comment::Parameter).map do |param_tag|
140
479
  name = param_tag.name
141
480
  type_string = param_tag.type.strip
142
- type = parse_type_string(type_string)
481
+ type = Type.parse(type_string)
143
482
 
144
483
  ::RBS::Types::Function::Param.new(
145
484
  type: type,
@@ -147,16 +486,16 @@ module Decode
147
486
  )
148
487
  end
149
488
 
150
- # Parse the block signature to determine if it's required
151
- # Check both the directive name and the block signature
489
+ # Parse the block signature to determine if it's required.
490
+ # Check both the directive name and the block signature:
152
491
  block_signature = yields_tag.block
153
492
  directive_name = yields_tag.directive
154
493
  required = !directive_name.include?("?") && !block_signature.include?("?") && !block_signature.include?("optional")
155
494
 
156
- # Determine block return type (default to void if not specified)
495
+ # Determine block return type (default to `void` if not specified):
157
496
  block_return_type = ::RBS::Parser.parse_type("void")
158
497
 
159
- # Create the block function type
498
+ # Create the block function type:
160
499
  block_function = ::RBS::Types::Function.new(
161
500
  required_positionals: block_params,
162
501
  optional_positionals: [],
@@ -168,7 +507,7 @@ module Decode
168
507
  return_type: block_return_type
169
508
  )
170
509
 
171
- # Create and return the block type
510
+ # Create and return the block type:
172
511
  ::RBS::Types::Block.new(
173
512
  type: block_function,
174
513
  required: required,
@@ -176,39 +515,29 @@ module Decode
176
515
  )
177
516
  end
178
517
 
179
- # Infer return type based on method patterns and heuristics
518
+ # Infer return type based on method patterns and heuristics.
180
519
  def infer_return_type(definition)
181
520
  method_name = definition.name
182
521
  method_name_str = method_name.to_s
183
522
 
184
- # Methods ending with ? are typically boolean
523
+ # Methods ending with `?` are typically boolean:
185
524
  if method_name_str.end_with?("?")
186
525
  return ::RBS::Parser.parse_type("bool")
187
526
  end
188
527
 
189
- # Methods named initialize return void
528
+ # Methods named `initialize` return `void`:
190
529
  if method_name == :initialize
191
530
  return ::RBS::Parser.parse_type("void")
192
531
  end
193
532
 
194
- # Methods with names that suggest they return self
533
+ # Methods with names that suggest they return `self`:
195
534
  if method_name_str.match?(/^(add|append|prepend|push|<<|concat|merge!|sort!|reverse!|clear|delete|remove)/)
196
535
  return ::RBS::Parser.parse_type("self")
197
536
  end
198
537
 
199
- # Default to untyped
538
+ # Default to `untyped`:
200
539
  ::RBS::Parser.parse_type("untyped")
201
540
  end
202
-
203
- # Parse a type string and convert it to RBS type
204
- def parse_type_string(type_string)
205
- type = Types.parse(type_string)
206
- return ::RBS::Parser.parse_type(type.to_rbs)
207
- rescue => error
208
- Console.warn(self, "Failed to parse type string: #{type_string}", error)
209
- return ::RBS::Parser.parse_type("untyped")
210
- end
211
-
212
541
  end
213
542
  end
214
543
  end
@@ -5,6 +5,8 @@
5
5
 
6
6
  require "rbs"
7
7
  require_relative "wrapper"
8
+ require_relative "method"
9
+ require_relative "type"
8
10
 
9
11
  module Decode
10
12
  module RBS
@@ -18,18 +20,29 @@ module Decode
18
20
  end
19
21
 
20
22
  # Convert the module definition to RBS AST
21
- def to_rbs_ast(method_definitions = [], index = nil)
23
+ # @parameter method_definitions [Array(Method)] The method definitions to convert.
24
+ # @parameter constant_definitions [Array(Constant)] The constant definitions to convert.
25
+ # @parameter attribute_definitions [Array(Attribute)] The attribute definitions to convert.
26
+ # @parameter index [Index?] The index for resolving references.
27
+ # @returns [RBS::AST::Declarations::Module] The RBS AST for the module.
28
+ def to_rbs_ast(method_definitions = [], constant_definitions = [], attribute_definitions = [], index = nil)
22
29
  name = simple_name_to_rbs(@definition.name)
23
- comment = extract_comment(@definition)
30
+ comment = self.comment
24
31
 
25
32
  # Build method definitions
26
33
  methods = method_definitions.map{|method_def| Method.new(method_def).to_rbs_ast(index)}.compact
27
34
 
35
+ # Build constant definitions:
36
+ constants = constant_definitions.map{|const_def| build_constant_rbs(const_def)}.compact
37
+
38
+ # Build attribute definitions and infer instance variable types:
39
+ attributes, instance_variables = build_attributes_rbs(attribute_definitions)
40
+
28
41
  ::RBS::AST::Declarations::Module.new(
29
42
  name: name,
30
43
  type_params: [],
31
44
  self_types: [],
32
- members: methods,
45
+ members: constants + attributes + instance_variables + methods,
33
46
  annotations: [],
34
47
  location: nil,
35
48
  comment: comment
@@ -38,11 +51,74 @@ module Decode
38
51
 
39
52
  private
40
53
 
41
- # Convert a simple name to RBS TypeName (not qualified)
54
+ # Build a constant RBS declaration.
55
+ def build_constant_rbs(constant_definition)
56
+ # Look for @constant tags in the constant's documentation:
57
+ documentation = constant_definition.documentation
58
+ constant_tags = documentation&.filter(Decode::Comment::Constant)&.to_a
59
+
60
+ if constant_tags&.any?
61
+ type_string = constant_tags.first.type.strip
62
+ type = ::Decode::RBS::Type.parse(type_string)
63
+
64
+ ::RBS::AST::Declarations::Constant.new(
65
+ name: constant_definition.name.to_sym,
66
+ type: type,
67
+ location: nil,
68
+ comment: nil
69
+ )
70
+ end
71
+ end
72
+
73
+ # Convert a simple name to RBS TypeName (not qualified).
42
74
  def simple_name_to_rbs(name)
43
75
  ::RBS::TypeName.new(name: name.to_sym, namespace: ::RBS::Namespace.empty)
44
76
  end
45
77
 
78
+ # Build attribute RBS declarations and infer instance variable types.
79
+ # @parameter attribute_definitions [Array] Array of Attribute definition objects
80
+ # @returns [Array] A tuple of [attribute_declarations, instance_variable_declarations]
81
+ def build_attributes_rbs(attribute_definitions)
82
+ attributes = []
83
+ instance_variables = []
84
+
85
+ # Create a mapping from attribute names to their types:
86
+ attribute_types = {}
87
+
88
+ attribute_definitions.each do |attribute_definition|
89
+ # Extract @attribute type annotation from documentation:
90
+ documentation = attribute_definition.documentation
91
+ attribute_tags = documentation&.filter(Decode::Comment::Attribute)&.to_a
92
+
93
+ if attribute_tags&.any?
94
+ type_string = attribute_tags.first.type.strip
95
+ type = ::Decode::RBS::Type.parse(type_string)
96
+
97
+ attribute_types[attribute_definition.name] = type
98
+
99
+ # Generate attr_reader RBS declaration:
100
+ attributes << ::RBS::AST::Members::AttrReader.new(
101
+ name: attribute_definition.name.to_sym,
102
+ type: type,
103
+ ivar_name: :"@#{attribute_definition.name}",
104
+ kind: :instance,
105
+ annotations: [],
106
+ location: nil,
107
+ comment: nil
108
+ )
109
+
110
+ # Generate instance variable declaration:
111
+ instance_variables << ::RBS::AST::Members::InstanceVariable.new(
112
+ name: :"@#{attribute_definition.name}",
113
+ type: type,
114
+ location: nil,
115
+ comment: nil
116
+ )
117
+ end
118
+ end
119
+
120
+ [attributes, instance_variables]
121
+ end
46
122
  end
47
123
  end
48
- end
124
+ end