decode 0.24.3 → 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 +4 -1
  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 +5 -1
  13. data/lib/decode/comment/rbs.rb +8 -8
  14. data/lib/decode/comment/tag.rb +13 -1
  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 -68
  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 +501 -113
  47. data.tar.gz.sig +0 -0
  48. metadata +4 -1
  49. metadata.gz.sig +0 -0
@@ -25,7 +25,7 @@ module Decode
25
25
  # The Ruby source code parser.
26
26
  class Parser
27
27
  # Initialize a new Ruby parser.
28
- # @parameter language [Language] The language instance.
28
+ # @parameter language [Language::Generic] The language instance.
29
29
  def initialize(language)
30
30
  @language = language
31
31
 
@@ -33,7 +33,26 @@ module Decode
33
33
  @definitions = Hash.new.compare_by_identity
34
34
  end
35
35
 
36
+ # @attribute [Language::Generic] The language instance.
37
+ attr :language
38
+
39
+ # @attribute [Symbol] The current visibility mode.
40
+ attr :visibility
41
+
42
+ # @attribute [Hash] Cache for definition lookups.
43
+ attr :definitions
44
+
45
+ # Parse the source code using Prism.
46
+ # @parameter source [Source] The source to parse.
47
+ # @returns [untyped] The parsed syntax tree.
48
+ def parse_source(source)
49
+ text = source.read
50
+ return ::Prism.parse(text)
51
+ end
52
+
36
53
  # Extract definitions from the given input file.
54
+ # @parameter source [Source] The source file to parse.
55
+ # @returns [Enumerator[Definition]] An enumerator of definitions.
37
56
  def definitions_for(source, &block)
38
57
  return enum_for(:definitions_for, source) unless block_given?
39
58
 
@@ -46,6 +65,9 @@ module Decode
46
65
  end
47
66
 
48
67
  # Walk over the syntax tree and extract relevant definitions with their associated comments.
68
+ # @parameter node [untyped] The syntax tree node to walk.
69
+ # @parameter parent [Definition?] The parent definition.
70
+ # @parameter source [Source?] The source file for location tracking.
49
71
  def walk_definitions(node, parent = nil, source = nil, &block)
50
72
  # Check for scope definitions from comments
51
73
  if node.comments.any?
@@ -71,13 +93,13 @@ module Decode
71
93
  path = nested_path_for(node.constant_path)
72
94
 
73
95
  definition = Module.new(path,
74
- visibility: :public,
75
- comments: comments_for(node),
76
- parent: parent,
77
- node: node,
78
- language: @language,
79
- source: source,
80
- )
96
+ visibility: :public,
97
+ comments: comments_for(node),
98
+ parent: parent,
99
+ node: node,
100
+ language: @language,
101
+ source: source,
102
+ )
81
103
 
82
104
  store_definition(parent, path.last.to_sym, definition)
83
105
  yield definition
@@ -92,14 +114,14 @@ module Decode
92
114
  super_class = nested_name_for(node.superclass)
93
115
 
94
116
  definition = Class.new(path,
95
- super_class: super_class,
96
- visibility: :public,
97
- comments: comments_for(node),
98
- parent: parent,
99
- node: node,
100
- language: @language,
101
- source: source,
102
- )
117
+ super_class: super_class,
118
+ visibility: :public,
119
+ comments: comments_for(node),
120
+ parent: parent,
121
+ node: node,
122
+ language: @language,
123
+ source: source,
124
+ )
103
125
 
104
126
  store_definition(parent, path.last.to_sym, definition)
105
127
  yield definition
@@ -112,13 +134,13 @@ module Decode
112
134
  when :singleton_class_node
113
135
  if name = singleton_name_for(node)
114
136
  definition = Singleton.new(name,
115
- comments: comments_for(node),
116
- parent: parent,
117
- node: node,
118
- language: @language,
119
- visibility: :public,
120
- source: source
121
- )
137
+ comments: comments_for(node),
138
+ parent: parent,
139
+ node: node,
140
+ language: @language,
141
+ visibility: :public,
142
+ source: source
143
+ )
122
144
 
123
145
  yield definition
124
146
 
@@ -130,23 +152,23 @@ module Decode
130
152
  receiver = receiver_for(node.receiver)
131
153
 
132
154
  definition = Method.new(node.name,
133
- visibility: @visibility,
134
- comments: comments_for(node),
135
- parent: parent,
136
- node: node,
137
- language: @language,
138
- receiver: receiver,
139
- source: source,
140
- )
155
+ visibility: @visibility,
156
+ comments: comments_for(node),
157
+ parent: parent,
158
+ node: node,
159
+ language: @language,
160
+ receiver: receiver,
161
+ source: source,
162
+ )
141
163
 
142
164
  yield definition
143
165
  when :constant_write_node
144
166
  definition = Constant.new(node.name,
145
- comments: comments_for(node),
146
- parent: parent,
147
- node: node,
148
- language: @language,
149
- )
167
+ comments: comments_for(node),
168
+ parent: parent,
169
+ node: node,
170
+ language: @language,
171
+ )
150
172
 
151
173
  store_definition(parent, node.name, definition)
152
174
  yield definition
@@ -165,13 +187,13 @@ module Decode
165
187
  receiver = receiver_for(argument_node.receiver)
166
188
 
167
189
  definition = Method.new(argument_node.name,
168
- visibility: name,
169
- comments: comments_for(argument_node),
170
- parent: parent,
171
- node: argument_node,
172
- language: @language,
173
- receiver: receiver,
174
- )
190
+ visibility: name,
191
+ comments: comments_for(argument_node),
192
+ parent: parent,
193
+ node: argument_node,
194
+ language: @language,
195
+ receiver: receiver,
196
+ )
175
197
 
176
198
  yield definition
177
199
  end
@@ -195,9 +217,9 @@ module Decode
195
217
  end
196
218
  when :attr, :attr_reader, :attr_writer, :attr_accessor
197
219
  definition = Attribute.new(attribute_name_for(node),
198
- comments: comments_for(node),
199
- parent: parent, language: @language, node: node
200
- )
220
+ comments: comments_for(node),
221
+ parent: parent, language: @language, node: node
222
+ )
201
223
 
202
224
  yield definition
203
225
  when :alias_method
@@ -211,13 +233,13 @@ module Decode
211
233
  old_name = symbol_name_for(old_name_arg)
212
234
 
213
235
  definition = Alias.new(new_name.to_sym, old_name.to_sym,
214
- comments: comments_for(node),
215
- parent: parent,
216
- node: node,
217
- language: @language,
218
- visibility: @visibility,
219
- source: source,
220
- )
236
+ comments: comments_for(node),
237
+ parent: parent,
238
+ node: node,
239
+ language: @language,
240
+ visibility: @visibility,
241
+ source: source,
242
+ )
221
243
 
222
244
  yield definition
223
245
  end
@@ -230,10 +252,10 @@ module Decode
230
252
 
231
253
  if has_name_comment || has_attribute_comment || has_block
232
254
  definition = Call.new(
233
- attribute_name_for(node),
234
- comments: comments_for(node),
235
- parent: parent, language: @language, node: node
236
- )
255
+ attribute_name_for(node),
256
+ comments: comments_for(node),
257
+ parent: parent, language: @language, node: node
258
+ )
237
259
 
238
260
  yield definition
239
261
 
@@ -249,13 +271,13 @@ module Decode
249
271
  old_name = node.old_name.unescaped
250
272
 
251
273
  definition = Alias.new(new_name.to_sym, old_name.to_sym,
252
- comments: comments_for(node),
253
- parent: parent,
254
- node: node,
255
- language: @language,
256
- visibility: @visibility,
257
- source: source,
258
- )
274
+ comments: comments_for(node),
275
+ parent: parent,
276
+ node: node,
277
+ language: @language,
278
+ visibility: @visibility,
279
+ source: source,
280
+ )
259
281
 
260
282
  yield definition
261
283
  when :if_node
@@ -286,13 +308,15 @@ module Decode
286
308
  end
287
309
 
288
310
  # Extract segments from the given input file.
311
+ # @parameter source [Source] The source file to parse.
312
+ # @returns [Enumerator[Segment]] An enumerator of segments.
289
313
  def segments_for(source, &block)
290
314
  result = self.parse_source(source)
291
315
  comments = result.comments.reject do |comment|
292
316
  comment.location.slice.start_with?("#!/") ||
293
- comment.location.slice.start_with?("# frozen_string_literal:") ||
294
- comment.location.slice.start_with?("# Released under the MIT License.") ||
295
- comment.location.slice.start_with?("# Copyright,")
317
+ comment.location.slice.start_with?("# frozen_string_literal:") ||
318
+ comment.location.slice.start_with?("# Released under the MIT License.") ||
319
+ comment.location.slice.start_with?("# Copyright,")
296
320
  end
297
321
 
298
322
  # Now we iterate over the syntax tree and generate segments:
@@ -304,14 +328,16 @@ module Decode
304
328
  # Extract clean comment text from a node by removing leading # symbols and whitespace.
305
329
  # Only returns comments that directly precede the node (i.e., are adjacent to it).
306
330
  # @parameter node [Node] The AST node with comments.
307
- # @returns [Array] Array of cleaned comment strings.
331
+ # Extract comments that directly precede a node.
332
+ # @parameter node [untyped] The syntax tree node.
333
+ # @returns [Array[String]] Array of cleaned comment strings.
308
334
  def comments_for(node)
309
335
  # Find the node's starting line
310
336
  node_start_line = node.location.start_line
311
337
 
312
338
  # Filter comments to only include those that directly precede the node
313
339
  # We work backwards from the line before the node to find consecutive comments
314
- adjacent_comments = []
340
+ adjacent_comments = [] #: Array[String]
315
341
  expected_line = node_start_line - 1
316
342
 
317
343
  # Process comments in reverse order to work backwards from the node
@@ -337,16 +363,31 @@ module Decode
337
363
  end
338
364
  end
339
365
 
366
+ # Assign a definition to a parent scope.
367
+ # @parameter parent [Definition?] The parent definition.
368
+ # @parameter definition [Definition] The definition to assign.
340
369
  def assign_definition(parent, definition)
341
- (@definitions[parent] ||= {})[definition.name] = definition
370
+ parent_definitions = @definitions[parent] ||= {}
371
+ parent_definitions[definition.name] = definition
342
372
  end
343
373
 
374
+ # Look up a definition in the parent scope.
375
+ # @parameter parent [Definition?] The parent definition.
376
+ # @parameter name [Symbol] The definition name to look up.
377
+ # @returns [Definition?] The found definition, if any.
344
378
  def lookup_definition(parent, name)
345
- (@definitions[parent] ||= {})[name]
379
+ if parent_definitions = @definitions[parent]
380
+ return parent_definitions[name]
381
+ end
346
382
  end
347
383
 
384
+ # Store a definition in the parent scope.
385
+ # @parameter parent [Definition?] The parent definition.
386
+ # @parameter name [Symbol] The definition name.
387
+ # @parameter definition [Definition] The definition to store.
348
388
  def store_definition(parent, name, definition)
349
- (@definitions[parent] ||= {})[name] = definition
389
+ parent_definitions = @definitions[parent] ||= {}
390
+ parent_definitions[name] = definition
350
391
  end
351
392
 
352
393
  # Parse the given source object, can be a string or a Source instance.
@@ -363,8 +404,8 @@ module Decode
363
404
  saved_visibility = @visibility
364
405
  @visibility = visibility
365
406
  yield
366
- ensure
367
- @visibility = saved_visibility
407
+ ensure
408
+ @visibility = saved_visibility
368
409
  end
369
410
 
370
411
  NAME_ATTRIBUTE = /\A@name\s+(?<value>.*?)\Z/
@@ -441,9 +482,9 @@ module Decode
441
482
  end
442
483
 
443
484
  KIND_ATTRIBUTE = /\A
444
- (@(?<kind>attribute)\s+(?<value>.*?))|
445
- (@define\s+(?<kind>)\s+(?<value>.*?))
446
- \Z/x
485
+ (@(?<kind>attribute)\s+(?<value>.*?))|
486
+ (@define\s+(?<kind>)\s+(?<value>.*?))
487
+ \Z/x
447
488
 
448
489
  def kind_for(node, comments = nil)
449
490
  comments&.each do |comment|
@@ -456,8 +497,8 @@ module Decode
456
497
  end
457
498
 
458
499
  SCOPE_ATTRIBUTE = /\A
459
- @scope\s+(?<names>.*?)
460
- \Z/x
500
+ @scope\s+(?<names>.*?)
501
+ \Z/x
461
502
 
462
503
  def scope_for(comments, parent = nil, &block)
463
504
  comments&.each do |comment|
@@ -509,20 +550,20 @@ module Decode
509
550
  # Start a new segment with these comments
510
551
  yield current_segment if current_segment
511
552
  current_segment = Segment.new(
512
- preceding_comments.map{|comment| comment.location.slice.sub(/^#[\s\t]?/, "")},
513
- @language,
514
- statement
515
- )
553
+ preceding_comments.map{|comment| comment.location.slice.sub(/^#[\s\t]?/, "")},
554
+ @language,
555
+ statement
556
+ )
516
557
  elsif current_segment
517
558
  # Extend current segment with this statement
518
559
  current_segment.expand(statement)
519
560
  else
520
561
  # Start a new segment without comments
521
562
  current_segment = Segment.new(
522
- [],
523
- @language,
524
- statement
525
- )
563
+ [],
564
+ @language,
565
+ statement
566
+ )
526
567
  end
527
568
  end
528
569
 
@@ -530,10 +571,10 @@ module Decode
530
571
  else
531
572
  # One top level segment:
532
573
  segment = Segment.new(
533
- [],
534
- @language,
535
- node
536
- )
574
+ [],
575
+ @language,
576
+ node
577
+ )
537
578
 
538
579
  yield segment
539
580
  end
@@ -12,7 +12,8 @@ module Decode
12
12
  class Reference < Language::Reference
13
13
  # Create a reference from a constant node.
14
14
  # @parameter node [Prism::Node] The constant node.
15
- # @parameter language [Language] The language instance.
15
+ # @parameter language [Language::Generic] The language instance.
16
+ # @returns [Reference] A new reference instance.
16
17
  def self.from_const(node, language)
17
18
  lexical_path = append_const(node)
18
19
 
@@ -22,6 +23,7 @@ module Decode
22
23
  # Append a constant node to the path.
23
24
  # @parameter node [Prism::Node] The constant node.
24
25
  # @parameter path [Array] The path to append to.
26
+ # @returns [Array] The path with constant information appended.
25
27
  def self.append_const(node, path = [])
26
28
  case node.type
27
29
  when :constant_read_node
@@ -50,6 +52,7 @@ module Decode
50
52
 
51
53
  # Split a Ruby identifier into prefix and name components.
52
54
  # @parameter text [String] The text to split.
55
+ # @returns [Array] Array of prefix and name pairs.
53
56
  def split(text)
54
57
  text.scan(/(::|\.|#|:)?([^:.#]+)/)
55
58
  end
@@ -12,7 +12,7 @@ module Decode
12
12
  class Segment < Decode::Segment
13
13
  # Initialize a new Ruby segment.
14
14
  # @parameter comments [Array(String)] The comments for this segment.
15
- # @parameter language [Language] The language instance.
15
+ # @parameter language [Generic] The language instance.
16
16
  # @parameter node [Prism::Node] The syntax tree node.
17
17
  # @parameter options [Hash] Additional options.
18
18
  def initialize(comments, language, node, **options)
@@ -32,7 +32,7 @@ module Decode
32
32
  end
33
33
 
34
34
  # The source code trailing the comments.
35
- # @returns [String | Nil]
35
+ # @returns [String?]
36
36
  def code
37
37
  @expression.slice
38
38
  end
@@ -23,6 +23,12 @@ module Decode
23
23
  @extensions = {}
24
24
  end
25
25
 
26
+ # @attribute [Hash[String, Language::Generic]] The named languages.
27
+ attr :named
28
+
29
+ # @attribute [Hash[String, Language::Generic]] The languages by extension.
30
+ attr :extensions
31
+
26
32
  # Freeze the languages context to prevent further modifications.
27
33
  def freeze
28
34
  return unless frozen?
@@ -35,6 +41,7 @@ module Decode
35
41
 
36
42
  # Add a language to this context.
37
43
  # @parameter language [Language::Generic] The language to add.
44
+ # @returns [self]
38
45
  def add(language)
39
46
  # Register by name:
40
47
  language.names.each do |name|
@@ -45,22 +52,26 @@ module Decode
45
52
  language.extensions.each do |extension|
46
53
  @extensions[extension] = language
47
54
  end
55
+
56
+ return self
48
57
  end
49
58
 
50
59
  # Fetch a language by name, creating a generic language if needed.
51
60
  # @parameter name [String] The name of the language to fetch.
52
- # @returns [Language::Generic] The language instance for the given name.
61
+ # @returns [Language::Generic?] The language instance for the given name, or nil if frozen and not found.
53
62
  def fetch(name)
54
63
  @named.fetch(name) do
55
64
  unless @named.frozen?
56
65
  @named[name] = Language::Generic.new(name)
66
+ else
67
+ nil
57
68
  end
58
69
  end
59
70
  end
60
71
 
61
72
  # Create a source object for the given file path.
62
73
  # @parameter path [String] The file system path to create a source for.
63
- # @returns [Source | Nil] A source object if the file extension is supported, nil otherwise.
74
+ # @returns [Source?] A source object if the file extension is supported, nil otherwise.
64
75
  def source_for(path)
65
76
  extension = File.extname(path)
66
77
 
@@ -69,17 +80,27 @@ module Decode
69
80
  end
70
81
  end
71
82
 
83
+ # @constant [Regexp] Regular expression for parsing language references.
72
84
  REFERENCE = /\A(?<name>[a-z]+)?\s+(?<identifier>.*?)\z/
73
85
 
74
86
  # Parse a language agnostic reference.
75
87
  # @parameter text [String] The text to parse (e.g., "ruby MyModule::MyClass").
76
- # @parameter default_language [Language::Generic] The default language to use if none specified.
77
- # @returns [Language::Reference | Nil] The parsed reference, or nil if parsing fails.
88
+ # @parameter default_language [Language::Generic?] The default language to use if none specified.
89
+ # @returns [Language::Reference?] The parsed reference, or nil if parsing fails.
78
90
  def parse_reference(text, default_language: nil)
79
91
  if match = REFERENCE.match(text)
80
- language = self.fetch(match[:name]) || default_language
92
+ name = match[:name]
93
+ identifier = match[:identifier]
81
94
 
82
- return language.reference_for(match[:identifier])
95
+ if name
96
+ language = self.fetch(name) || default_language
97
+ else
98
+ language = default_language
99
+ end
100
+
101
+ if language && identifier
102
+ return language.reference_for(identifier)
103
+ end
83
104
  elsif default_language
84
105
  return default_language.reference_for(text)
85
106
  end
@@ -88,9 +109,9 @@ module Decode
88
109
  # Create a reference for the given language and identifier.
89
110
  # @parameter name [String] The name of the language.
90
111
  # @parameter identifier [String] The identifier to create a reference for.
91
- # @returns [Language::Reference] The created reference.
112
+ # @returns [Language::Reference?] The created reference, or nil if language not found.
92
113
  def reference_for(name, identifier)
93
- self.fetch(name).reference_for(identifier)
114
+ self.fetch(name)&.reference_for(identifier)
94
115
  end
95
116
  end
96
117
  end
@@ -5,7 +5,18 @@
5
5
 
6
6
  module Decode
7
7
  # Represents a location in a source file.
8
- class Location < Struct.new(:path, :line)
8
+ class Location
9
+ def initialize(path, line)
10
+ @path = path
11
+ @line = line
12
+ end
13
+
14
+ # @attribute [String] The path to the source file.
15
+ attr :path
16
+
17
+ # @attribute [Integer] The line number in the source file.
18
+ attr :line
19
+
9
20
  # Generate a string representation of the location.
10
21
  def to_s
11
22
  "#{path}:#{line}"