decode 0.23.5 → 0.24.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11a86571ec0cc705f2d0250c768572373ae0717a991a50f57b78cd148a0cd021
4
- data.tar.gz: aa180f6bb19db4c6f75c372a3eb43a44067ff7b9a964aa332424fc19b2d85273
3
+ metadata.gz: 16fcc036ea4c0db950c8dccf9b8f7a8a8b646e4375d5aa51dae09aa119269916
4
+ data.tar.gz: 209ee8f76ba71de8f80df11d838f734d36867bc7bde266188731e07e16cfadd2
5
5
  SHA512:
6
- metadata.gz: 8c78fd76970e57222c42100d8e910eedd71e1eeb3de399d06ef5e4c24e74a1acd2027a67bc9a6178bb93b19917fd2595d513ee1ee8729cfb8f6c0ca0b6458156
7
- data.tar.gz: afb977b32a85cf2e44e59b437a54cb88b9ef454db994f595bb97ba3ca23ca24415bcdb8bb12a584378c05ec71e35bf1424f15eb7fa8e7b67e8460c00ceea3518
6
+ metadata.gz: 841791e39cb636b47ed5a2bd9f5974ab8fc77bccf1fb7ab932300aa0c32046afe47a42ebc562c3580bfc53363b89bc5afcb9e1af151f3c414dc367b4a940d02e
7
+ data.tar.gz: 173fa08e9dcac6fa0afdc73ccb9452afb95c4dccf00333e0209692628bb0336e80cd1eea65061458e301980726e94701d08e3e02fccaa30482b1faf97b49331d
checksums.yaml.gz.sig CHANGED
Binary file
data/agent.md CHANGED
@@ -29,3 +29,11 @@ There are two types of mocking in sus: `receive` and `mock`. The `receive` match
29
29
  #### [Shared Test Behaviors and Fixtures](.context/sus/shared.md)
30
30
 
31
31
  Sus provides shared test contexts which can be used to define common behaviours or tests that can be reused across one or more test files.
32
+
33
+ ### types
34
+
35
+ A simple human-readable and Ruby-parsable type library.
36
+
37
+ #### [Usage](.context/types/usage.md)
38
+
39
+ The Types gem provides abstract types for the Ruby programming language that can be used for documentation and evaluation purposes. It offers a simple and Ruby-compatible approach to type signature...
data/bake/decode/index.rb CHANGED
@@ -13,10 +13,7 @@ end
13
13
  # Process the given source root and report on comment coverage.
14
14
  # @parameter root [String] The root path to index.
15
15
  def coverage(root)
16
- paths = Dir.glob(File.join(root, "**/*"))
17
-
18
- index = Decode::Index.new
19
- index.update(paths)
16
+ index = Decode::Index.for(root)
20
17
 
21
18
  documented = Set.new
22
19
  missing = {}
@@ -71,10 +68,7 @@ end
71
68
  # Process the given source root and report on symbols.
72
69
  # @parameter root [String] The root path to index.
73
70
  def symbols(root)
74
- paths = Dir.glob(File.join(root, "**/*"))
75
-
76
- index = Decode::Index.new
77
- index.update(paths)
71
+ index = Decode::Index.for(root)
78
72
 
79
73
  index.trie.traverse do |path, node, descend|
80
74
  level = path.size
@@ -91,10 +85,7 @@ end
91
85
  # Print documentation for all definitions.
92
86
  # @parameter root [String] The root path to index.
93
87
  def documentation(root)
94
- paths = Dir.glob(File.join(root, "**/*"))
95
-
96
- index = Decode::Index.new
97
- index.update(paths)
88
+ index = Decode::Index.for(root)
98
89
 
99
90
  index.definitions.each do |name, definition|
100
91
  comments = definition.comments
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ def initialize(...)
7
+ super
8
+
9
+ require "decode/rbs"
10
+ end
11
+
12
+ # Generate RBS declarations for the given source root.
13
+ # @parameter root [String] The root path to index.
14
+ def generate(root)
15
+ index = Decode::Index.for(root)
16
+ generator = Decode::RBS::Generator.new
17
+ generator.generate(index)
18
+ end
@@ -61,7 +61,7 @@ module Decode
61
61
  #
62
62
  # @yields {|node, descend| descend.call}
63
63
  # @parameter node [Node] The current node which is being traversed.
64
- # @parameter descend [Proc | Nil] The recursive method for traversing children.
64
+ # @parameter descend [Proc] The recursive method for traversing children.
65
65
  def traverse(&block)
66
66
  descend = ->(node){node.traverse(&block)}
67
67
 
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require_relative "tag"
7
+
8
+ module Decode
9
+ module Comment
10
+ # Represents an RBS type annotation following rbs-inline syntax.
11
+ #
12
+ # Examples:
13
+ # - `@rbs generic T` - Declares a generic type parameter for a class
14
+ # - `@rbs [T] () { () -> T } -> Task[T]` - Complete method type signature
15
+ #
16
+ class RBS < Tag
17
+ # Parse an RBS pragma from text.
18
+ # @parameter directive [String] The directive name (should be "rbs").
19
+ # @parameter text [String] The RBS type annotation text.
20
+ # @parameter lines [Array(String)] The remaining lines (not used for RBS).
21
+ # @parameter tags [Array(Tag)] The collection of tags.
22
+ # @parameter level [Integer] The indentation level.
23
+ def self.parse(directive, text, lines, tags, level = 0)
24
+ self.build(directive, text)
25
+ end
26
+
27
+ # Build an RBS pragma from a directive and text.
28
+ # @parameter directive [String] The directive name.
29
+ # @parameter text [String] The RBS type annotation text.
30
+ def self.build(directive, text)
31
+ node = self.new(directive, text)
32
+ return node
33
+ end
34
+
35
+ # Initialize a new RBS pragma.
36
+ # @parameter directive [String] The directive name.
37
+ # @parameter text [String] The RBS type annotation text.
38
+ def initialize(directive, text = nil)
39
+ super(directive)
40
+ @text = text&.strip
41
+ end
42
+
43
+ # The RBS type annotation text.
44
+ # @attribute [String] The raw RBS text.
45
+ attr :text
46
+
47
+ # Check if this is a generic type declaration.
48
+ # @returns [Boolean] True if this is a generic declaration.
49
+ def generic?
50
+ @text&.start_with?("generic ")
51
+ end
52
+
53
+ # Extract the generic type parameter name.
54
+ # @returns [String | Nil] The generic type parameter name, or nil if not a generic.
55
+ def generic_parameter
56
+ if generic?
57
+ # Extract the parameter name from "generic T" or "generic T, U"
58
+ match = @text.match(/^generic\s+([A-Z][A-Za-z0-9_]*(?:\s*,\s*[A-Z][A-Za-z0-9_]*)*)/)
59
+ return match[1] if match
60
+ end
61
+ end
62
+
63
+ # Check if this is a method type signature.
64
+ # @returns [Boolean] True if this is a method signature.
65
+ def method_signature?
66
+ @text && !generic?
67
+ end
68
+
69
+ # Get the method type signature text.
70
+ # @returns [String | Nil] The method signature text, or nil if not a method signature.
71
+ def method_signature
72
+ method_signature? ? @text : nil
73
+ end
74
+ end
75
+ end
76
+ end
@@ -56,7 +56,7 @@ module Decode
56
56
  else
57
57
  # Ignore unknown directive.
58
58
  end
59
-
59
+
60
60
  # Or it's just text:
61
61
  else
62
62
  yield Text.new(line)
@@ -119,14 +119,14 @@ module Decode
119
119
  # A short form of the definition.
120
120
  # e.g. `def short_form`.
121
121
  #
122
- # @returns [String | nil]
122
+ # @returns [String | Nil]
123
123
  def short_form
124
124
  end
125
125
 
126
126
  # A long form of the definition.
127
127
  # e.g. `def initialize(kind, name, comments, **options)`.
128
128
  #
129
- # @returns [String | nil]
129
+ # @returns [String | Nil]
130
130
  def long_form
131
131
  self.short_form
132
132
  end
@@ -134,7 +134,7 @@ module Decode
134
134
  # A long form which uses the qualified name if possible.
135
135
  # Defaults to {long_form}.
136
136
  #
137
- # @returns [String | nil]
137
+ # @returns [String | Nil]
138
138
  def qualified_form
139
139
  self.long_form
140
140
  end
@@ -148,7 +148,7 @@ module Decode
148
148
 
149
149
  # The full text of the definition.
150
150
  #
151
- # @returns [String | nil]
151
+ # @returns [String | Nil]
152
152
  def text
153
153
  end
154
154
 
data/lib/decode/index.rb CHANGED
@@ -11,6 +11,33 @@ require_relative "languages"
11
11
  module Decode
12
12
  # Represents a list of definitions organised for quick lookup and lexical enumeration.
13
13
  class Index
14
+ # Create and populate an index from the given paths.
15
+ # @parameter paths [Array(String)] The paths to index (files, directories, or glob patterns).
16
+ # @parameter languages [Languages] The languages to support in this index.
17
+ # @returns [Index] A new index populated with definitions from the given paths.
18
+ def self.for(*paths, languages: Languages.all)
19
+ # Resolve all paths to actual files:
20
+ resolved_paths = paths.flat_map do |path|
21
+ if File.directory?(path)
22
+ Dir.glob(File.join(path, "**/*"))
23
+ elsif File.file?(path)
24
+ [path]
25
+ else
26
+ # Handle glob patterns or non-existent paths:
27
+ Dir.glob(path)
28
+ end
29
+ end
30
+
31
+ resolved_paths.sort!
32
+ resolved_paths.uniq!
33
+
34
+ # Create and populate the index:
35
+ index = new(languages)
36
+ index.update(resolved_paths)
37
+
38
+ return index
39
+ end
40
+
14
41
  # Initialize an empty index.
15
42
  # @parameter languages [Languages] The languages to support in this index.
16
43
  def initialize(languages = Languages.all)
@@ -30,10 +57,10 @@ module Decode
30
57
  def inspect
31
58
  "#<#{self.class} #{@definitions.size} definition(s)>"
32
59
  end
33
-
60
+
34
61
  # Generate a string representation of the index.
35
62
  alias to_s inspect
36
-
63
+
37
64
  # All supported languages for this index.
38
65
  # @attribute [Languages] The languages this index can parse.
39
66
  attr :languages
@@ -43,7 +43,7 @@ module Decode
43
43
  else
44
44
  # For multiline calls, use the actual call name with arguments
45
45
  if @node.arguments && @node.arguments.arguments.any?
46
- argument_text = @node.arguments.arguments.map {|argument| argument.location.slice}.join(", ")
46
+ argument_text = @node.arguments.arguments.map{|argument| argument.location.slice}.join(", ")
47
47
  "#{@node.name}(#{argument_text})"
48
48
  else
49
49
  @node.name.to_s
@@ -56,7 +56,7 @@ module Decode
56
56
  def nested_name
57
57
  "class"
58
58
  end
59
-
59
+
60
60
  # A singleton class is a container for other definitions.
61
61
  # @returns [Boolean]
62
62
  def container?
@@ -8,6 +8,7 @@ require_relative "parser"
8
8
  require_relative "code"
9
9
 
10
10
  require_relative "../generic"
11
+ require_relative "../../comment/rbs"
11
12
 
12
13
  module Decode
13
14
  module Language
@@ -16,6 +17,25 @@ module Decode
16
17
  class Generic < Language::Generic
17
18
  EXTENSIONS = [".rb", ".ru"]
18
19
 
20
+ TAGS = Comment::Tags.build do |tags|
21
+ tags["attribute"] = Comment::Attribute
22
+ tags["parameter"] = Comment::Parameter
23
+ tags["option"] = Comment::Option
24
+ tags["yields"] = Comment::Yields
25
+ tags["returns"] = Comment::Returns
26
+ tags["raises"] = Comment::Raises
27
+ tags["throws"] = Comment::Throws
28
+
29
+ tags["deprecated"] = Comment::Pragma
30
+
31
+ tags["asynchronous"] = Comment::Pragma
32
+
33
+ tags["public"] = Comment::Pragma
34
+ tags["private"] = Comment::Pragma
35
+
36
+ tags["rbs"] = Comment::RBS
37
+ end
38
+
19
39
  # Get the parser for Ruby source code.
20
40
  # @returns [Parser] The Ruby parser instance.
21
41
  def parser
@@ -509,7 +509,7 @@ module Decode
509
509
  # Start a new segment with these comments
510
510
  yield current_segment if current_segment
511
511
  current_segment = Segment.new(
512
- preceding_comments.map {|comment| comment.location.slice.sub(/^#[\s\t]?/, "")},
512
+ preceding_comments.map{|comment| comment.location.slice.sub(/^#[\s\t]?/, "")},
513
513
  @language,
514
514
  statement
515
515
  )
@@ -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 | Nil]
36
36
  def code
37
37
  @expression.slice
38
38
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "rbs"
7
+ require_relative "wrapper"
8
+ require_relative "method"
9
+ module Decode
10
+ module RBS
11
+ # Represents a Ruby class definition wrapper for RBS generation.
12
+ class Class < Wrapper
13
+
14
+ # Initialize a new class wrapper.
15
+ # @parameter definition [Decode::Definition] The class definition to wrap.
16
+ def initialize(definition)
17
+ super
18
+ @generics = nil
19
+ end
20
+
21
+ # Extract generic type parameters from the class definition.
22
+ # @returns [Array] The generic type parameters for this class.
23
+ def generics
24
+ @generics ||= extract_generics
25
+ end
26
+
27
+ # Convert the class definition to RBS AST
28
+ def to_rbs_ast(method_definitions = [], index = nil)
29
+ name = simple_name_to_rbs(@definition.name)
30
+ comment = extract_comment(@definition)
31
+
32
+ # Extract generics from RBS tags
33
+ type_params = generics.map do |generic|
34
+ ::RBS::AST::TypeParam.new(
35
+ name: generic.to_sym,
36
+ variance: nil,
37
+ upper_bound: nil,
38
+ location: nil
39
+ )
40
+ end
41
+
42
+ # Build method definitions
43
+ methods = method_definitions.map{|method_def| Method.new(method_def).to_rbs_ast(index)}.compact
44
+
45
+ # Extract super class if present
46
+ super_class = if @definition.super_class
47
+ ::RBS::AST::Declarations::Class::Super.new(
48
+ name: qualified_name_to_rbs(@definition.super_class),
49
+ args: [],
50
+ location: nil
51
+ )
52
+ end
53
+
54
+ # Create the class declaration with generics
55
+ ::RBS::AST::Declarations::Class.new(
56
+ name: name,
57
+ type_params: type_params,
58
+ super_class: super_class,
59
+ members: methods,
60
+ annotations: [],
61
+ location: nil,
62
+ comment: comment
63
+ )
64
+ end
65
+
66
+ private
67
+
68
+ def extract_generics
69
+ tags.select(&:generic?).map(&:generic_parameter)
70
+ end
71
+
72
+ # Convert a simple name to RBS TypeName (not qualified)
73
+ def simple_name_to_rbs(name)
74
+ ::RBS::TypeName.new(name: name.to_sym, namespace: ::RBS::Namespace.empty)
75
+ end
76
+
77
+ # Convert a qualified name to RBS TypeName
78
+ def qualified_name_to_rbs(qualified_name)
79
+ parts = qualified_name.split("::")
80
+ name = parts.pop
81
+ namespace = ::RBS::Namespace.new(path: parts.map(&:to_sym), absolute: true)
82
+
83
+ ::RBS::TypeName.new(name: name.to_sym, namespace: namespace)
84
+ end
85
+
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "rbs"
7
+ require_relative "../index"
8
+ require_relative "class"
9
+ require_relative "module"
10
+
11
+ module Decode
12
+ module RBS
13
+ # Represents a generator for RBS type declarations.
14
+ class Generator
15
+ # Initialize a new RBS generator.
16
+ # Sets up the RBS environment for type resolution.
17
+ def initialize
18
+ # Set up RBS environment for type resolution
19
+ @loader = ::RBS::EnvironmentLoader.new()
20
+ @environment = ::RBS::Environment.from_loader(@loader).resolve_type_names
21
+ end
22
+
23
+ # Generate RBS declarations for the given index.
24
+ # @parameter index [Decode::Index] The index containing definitions to generate RBS for.
25
+ # @parameter output [IO] The output stream to write to.
26
+ def generate(index, output: $stdout)
27
+ # Build nested RBS AST structure using a hash for proper ||= behavior
28
+ declarations = {}
29
+ roots = []
30
+
31
+ # Efficiently traverse the trie to find containers and their methods
32
+ index.trie.traverse do |lexical_path, node, descend|
33
+ # Process container definitions at this node
34
+ if node.values
35
+ containers = node.values.select {|definition| definition.container? && definition.public?}
36
+ containers.each do |definition|
37
+ case definition
38
+ when Decode::Language::Ruby::Class, Decode::Language::Ruby::Module
39
+ if declaration = build_nested_declaration(definition, declarations, index)
40
+ roots << declaration
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ # Continue traversing children
47
+ descend.call
48
+ end
49
+
50
+ # Write the RBS output
51
+ writer = ::RBS::Writer.new(out: output)
52
+
53
+ unless roots.empty?
54
+ writer.write(roots)
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ # Build nested RBS declarations preserving the parent hierarchy.
61
+ # @returns [::RBS::AST::Declarations::Class | ::RBS::AST::Declarations::Module] If the definition has no parent, returns the declaration.
62
+ # @returns [Nil] If the definition has a parent, adds to parent's members.
63
+ def build_nested_declaration(definition, declarations, index)
64
+ # Create the declaration for this definition using ||= to avoid duplicates
65
+ qualified_name = definition.qualified_name
66
+ declaration = (declarations[qualified_name] ||= definition_to_rbs(definition, index))
67
+
68
+ # Add this declaration to its parent's members if it has a parent
69
+ if definition.parent
70
+ parent_qualified_name = definition.parent.qualified_name
71
+ parent_container = declarations[parent_qualified_name]
72
+
73
+ # Only add if not already present
74
+ unless parent_container.members.any?{|member| member.respond_to?(:name) && member.name.name == definition.name.to_sym}
75
+ parent_container.members << declarations[qualified_name]
76
+ end
77
+
78
+ return nil
79
+ else
80
+ return declaration
81
+ end
82
+ end
83
+
84
+ # Convert a definition to RBS AST
85
+ def definition_to_rbs(definition, index)
86
+ case definition
87
+ when Decode::Language::Ruby::Class
88
+ Class.new(definition).to_rbs_ast(get_methods_for_definition(definition, index), index)
89
+ when Decode::Language::Ruby::Module
90
+ Module.new(definition).to_rbs_ast(get_methods_for_definition(definition, index), index)
91
+ end
92
+ end
93
+
94
+ # Get methods for a given definition efficiently using trie lookup
95
+ def get_methods_for_definition(definition, index)
96
+ # Use the trie to efficiently find methods for this definition
97
+ if node = index.trie.lookup(definition.full_path)
98
+ node.children.flat_map do |name, child|
99
+ child.values.select{|symbol| symbol.is_a?(Decode::Language::Ruby::Method) && symbol.public?}
100
+ end
101
+ else
102
+ []
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "rbs"
7
+ require "console"
8
+ require "types"
9
+ require_relative "wrapper"
10
+
11
+ module Decode
12
+ module RBS
13
+ # Represents a Ruby method definition wrapper for RBS generation.
14
+ class Method < Wrapper
15
+
16
+ # Initialize a new method wrapper.
17
+ # @parameter definition [Decode::Definition] The method definition to wrap.
18
+ def initialize(definition)
19
+ super
20
+ @signatures = nil
21
+ end
22
+
23
+ # Extract method signatures from the method definition.
24
+ # @returns [Array] The extracted signatures for this method.
25
+ def signatures
26
+ @signatures ||= extract_signatures
27
+ end
28
+
29
+ # Convert the method definition to RBS AST
30
+ def to_rbs_ast(index = nil)
31
+ method_name = @definition.name
32
+ comment = extract_comment(@definition)
33
+
34
+ overloads = []
35
+ if signatures.any?
36
+ signatures.each do |signature_string|
37
+ method_type = ::RBS::Parser.parse_method_type(signature_string)
38
+ overloads << ::RBS::AST::Members::MethodDefinition::Overload.new(
39
+ method_type: method_type,
40
+ annotations: []
41
+ )
42
+ end
43
+ 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)
47
+
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
+ )
63
+
64
+ overloads << ::RBS::AST::Members::MethodDefinition::Overload.new(
65
+ method_type: method_type,
66
+ annotations: []
67
+ )
68
+ end
69
+
70
+ kind = @definition.receiver ? :singleton : :instance
71
+
72
+ ::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
81
+ )
82
+ end
83
+
84
+ private
85
+
86
+ def extract_signatures
87
+ extract_tags.select(&:method_signature?).map(&:method_signature)
88
+ end
89
+
90
+ # Extract return type from method documentation
91
+ def extract_return_type(definition, index)
92
+ # Look for @returns tags in the method's documentation
93
+ documentation = definition.documentation
94
+
95
+ # Find @returns tag
96
+ returns_tag = documentation&.filter(Decode::Comment::Returns)&.first
97
+
98
+ if returns_tag
99
+ # Parse the type from the tag
100
+ type_string = returns_tag.type.strip
101
+ parse_type_string(type_string)
102
+ else
103
+ # Infer return type based on method name patterns
104
+ infer_return_type(definition)
105
+ end
106
+ end
107
+
108
+ # Extract parameter types from method documentation
109
+ def extract_parameters(definition, index)
110
+ documentation = definition.documentation
111
+ return [] unless documentation
112
+
113
+ # Find @parameter tags
114
+ param_tags = documentation.filter(Decode::Comment::Parameter).to_a
115
+ return [] if param_tags.empty?
116
+
117
+ param_tags.map do |tag|
118
+ name = tag.name
119
+ type_string = tag.type.strip
120
+ type = parse_type_string(type_string)
121
+
122
+ ::RBS::Types::Function::Param.new(
123
+ type: type,
124
+ name: name.to_sym
125
+ )
126
+ end
127
+ end
128
+
129
+ # Extract block type from method documentation
130
+ def extract_block_type(definition, index)
131
+ documentation = definition.documentation
132
+ return nil unless documentation
133
+
134
+ # Find @yields tags
135
+ yields_tag = documentation.filter(Decode::Comment::Yields).first
136
+ return nil unless yields_tag
137
+
138
+ # Extract block parameters from nested @parameter tags
139
+ block_params = yields_tag.filter(Decode::Comment::Parameter).map do |param_tag|
140
+ name = param_tag.name
141
+ type_string = param_tag.type.strip
142
+ type = parse_type_string(type_string)
143
+
144
+ ::RBS::Types::Function::Param.new(
145
+ type: type,
146
+ name: name.to_sym
147
+ )
148
+ end
149
+
150
+ # Parse the block signature to determine if it's required
151
+ # Check both the directive name and the block signature
152
+ block_signature = yields_tag.block
153
+ directive_name = yields_tag.directive
154
+ required = !directive_name.include?("?") && !block_signature.include?("?") && !block_signature.include?("optional")
155
+
156
+ # Determine block return type (default to void if not specified)
157
+ block_return_type = ::RBS::Parser.parse_type("void")
158
+
159
+ # Create the block function type
160
+ block_function = ::RBS::Types::Function.new(
161
+ required_positionals: block_params,
162
+ optional_positionals: [],
163
+ rest_positionals: nil,
164
+ trailing_positionals: [],
165
+ required_keywords: {},
166
+ optional_keywords: {},
167
+ rest_keywords: nil,
168
+ return_type: block_return_type
169
+ )
170
+
171
+ # Create and return the block type
172
+ ::RBS::Types::Block.new(
173
+ type: block_function,
174
+ required: required,
175
+ self_type: nil
176
+ )
177
+ end
178
+
179
+ # Infer return type based on method patterns and heuristics
180
+ def infer_return_type(definition)
181
+ method_name = definition.name
182
+ method_name_str = method_name.to_s
183
+
184
+ # Methods ending with ? are typically boolean
185
+ if method_name_str.end_with?("?")
186
+ return ::RBS::Parser.parse_type("bool")
187
+ end
188
+
189
+ # Methods named initialize return void
190
+ if method_name == :initialize
191
+ return ::RBS::Parser.parse_type("void")
192
+ end
193
+
194
+ # Methods with names that suggest they return self
195
+ if method_name_str.match?(/^(add|append|prepend|push|<<|concat|merge!|sort!|reverse!|clear|delete|remove)/)
196
+ return ::RBS::Parser.parse_type("self")
197
+ end
198
+
199
+ # Default to untyped
200
+ ::RBS::Parser.parse_type("untyped")
201
+ 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
+ end
213
+ end
214
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "rbs"
7
+ require_relative "wrapper"
8
+
9
+ module Decode
10
+ module RBS
11
+ # Represents a Ruby module definition wrapper for RBS generation.
12
+ class Module < Wrapper
13
+
14
+ # Initialize a new module wrapper.
15
+ # @parameter definition [Decode::Definition] The module definition to wrap.
16
+ def initialize(definition)
17
+ super
18
+ end
19
+
20
+ # Convert the module definition to RBS AST
21
+ def to_rbs_ast(method_definitions = [], index = nil)
22
+ name = simple_name_to_rbs(@definition.name)
23
+ comment = extract_comment(@definition)
24
+
25
+ # Build method definitions
26
+ methods = method_definitions.map{|method_def| Method.new(method_def).to_rbs_ast(index)}.compact
27
+
28
+ ::RBS::AST::Declarations::Module.new(
29
+ name: name,
30
+ type_params: [],
31
+ self_types: [],
32
+ members: methods,
33
+ annotations: [],
34
+ location: nil,
35
+ comment: comment
36
+ )
37
+ end
38
+
39
+ private
40
+
41
+ # Convert a simple name to RBS TypeName (not qualified)
42
+ def simple_name_to_rbs(name)
43
+ ::RBS::TypeName.new(name: name.to_sym, namespace: ::RBS::Namespace.empty)
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "rbs"
7
+
8
+ module Decode
9
+ module RBS
10
+ # Base wrapper class for RBS generation from definitions.
11
+ class Wrapper
12
+ # Initialize the wrapper instance variables.
13
+ # @parameter definition [Definition] The definition to wrap.
14
+ def initialize(definition)
15
+ @definition = definition
16
+ @tags = nil
17
+ end
18
+
19
+ # Extract RBS tags from the definition's documentation.
20
+ # @returns [Array<Comment::RBS>] The RBS tags found in the documentation.
21
+ def tags
22
+ @tags ||= extract_tags
23
+ end
24
+
25
+ private
26
+
27
+ # Extract RBS tags from the definition's documentation.
28
+ # @returns [Array<Comment::RBS>] The RBS tags found in the documentation.
29
+ def extract_tags
30
+ @definition.documentation&.children&.select do |child|
31
+ child.is_a?(Comment::RBS)
32
+ end || []
33
+ end
34
+
35
+ # Extract comment from definition documentation.
36
+ # @parameter definition [Definition] The definition to extract comment from (defaults to @definition).
37
+ # @returns [RBS::AST::Comment, nil] The extracted comment or nil if no documentation.
38
+ def extract_comment(definition = @definition)
39
+ documentation = definition.documentation
40
+ return nil unless documentation
41
+
42
+ # Extract the main description text (non-tag content)
43
+ comment_lines = []
44
+
45
+ documentation.children&.each do |child|
46
+ if child.is_a?(Decode::Comment::Text)
47
+ comment_lines << child.line.strip
48
+ elsif !child.is_a?(Decode::Comment::Tag)
49
+ # Handle other text-like nodes
50
+ comment_lines << child.to_s.strip if child.respond_to?(:to_s)
51
+ end
52
+ end
53
+
54
+ # Join lines with newlines to preserve markdown formatting
55
+ unless comment_lines.empty?
56
+ comment_text = comment_lines.join("\n").strip
57
+ return ::RBS::AST::Comment.new(string: comment_text, location: nil) unless comment_text.empty?
58
+ end
59
+
60
+ nil
61
+ end
62
+ end
63
+ end
64
+ end
data/lib/decode/rbs.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "rbs"
7
+ require_relative "rbs/wrapper"
8
+ require_relative "rbs/class"
9
+ require_relative "rbs/method"
10
+ require_relative "rbs/module"
11
+ require_relative "rbs/generator"
12
+
13
+ module Decode
14
+ # RBS generation functionality for Ruby type signatures.
15
+ module RBS
16
+ end
17
+ end
@@ -29,7 +29,7 @@ module Decode
29
29
  attr :language
30
30
 
31
31
  # An interface for accsssing the documentation of the definition.
32
- # @returns [Documentation | nil] A {Documentation} instance if this definition has comments.
32
+ # @returns [Documentation | Nil] A {Documentation} instance if this definition has comments.
33
33
  def documentation
34
34
  if @comments&.any?
35
35
  @documentation ||= Documentation.new(@comments, @language)
@@ -37,7 +37,7 @@ module Decode
37
37
  end
38
38
 
39
39
  # The source code trailing the comments.
40
- # @returns [String | nil]
40
+ # @returns [String | Nil]
41
41
  def code
42
42
  end
43
43
  end
data/lib/decode/trie.rb CHANGED
@@ -21,10 +21,10 @@ module Decode
21
21
  def inspect
22
22
  "#<#{self.class} #{@children.size} children>"
23
23
  end
24
-
24
+
25
25
  # Generate a string representation of the node.
26
26
  alias to_s inspect
27
-
27
+
28
28
  # A mutable array of all values that terminate at this node.
29
29
  # @attribute [Array | Nil] The values stored at this node, or nil if no values.
30
30
  attr_accessor :values
@@ -4,5 +4,5 @@
4
4
  # Copyright, 2020-2025, by Samuel Williams.
5
5
 
6
6
  module Decode
7
- VERSION = "0.23.5"
7
+ VERSION = "0.24.1"
8
8
  end
data/readme.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A Ruby code analysis tool and documentation generator.
4
4
 
5
- [![Development Status](https://github.com/ioquatix/decode/workflows/Test/badge.svg)](https://github.com/ioquatix/decode/actions?workflow=Test)
5
+ [![Development Status](https://github.com/socketry/decode/workflows/Test/badge.svg)](https://github.com/socketry/decode/actions?workflow=Test)
6
6
 
7
7
  ## Motivation
8
8
 
@@ -10,17 +10,21 @@ As part of my effort to build [better project documentation](https://github.com/
10
10
 
11
11
  ## Usage
12
12
 
13
- Please see the [project documentation](https://ioquatix.github.io/decode/) for more details.
13
+ Please see the [project documentation](https://socketry.github.io/decode/) for more details.
14
14
 
15
- - [Getting Started](https://ioquatix.github.io/decode/guides/getting-started/index) - This guide explains how to use `decode` for source code analysis.
15
+ - [Getting Started](https://socketry.github.io/decode/guides/getting-started/index) - This guide explains how to use `decode` for source code analysis.
16
16
 
17
- - [Code Coverage](https://ioquatix.github.io/decode/guides/code-coverage/index) - This guide explains how to compute documentation code coverage.
17
+ - [Code Coverage](https://socketry.github.io/decode/guides/code-coverage/index) - This guide explains how to compute documentation code coverage.
18
18
 
19
- - [Extract Symbols](https://ioquatix.github.io/decode/guides/extract-symbols/index) - This example demonstrates how to extract symbols using the index. An instance of <code class="language-ruby">Decode::Index</code> is used for loading symbols from source code files. These symbols are available as a flat list and as a trie structure. You can look up specific symbols using a reference using <code class="language-ruby">Decode::Index\#lookup</code>.
19
+ - [Extract Symbols](https://socketry.github.io/decode/guides/extract-symbols/index) - This example demonstrates how to extract symbols using the index. An instance of <code class="language-ruby">Decode::Index</code> is used for loading symbols from source code files. These symbols are available as a flat list and as a trie structure. You can look up specific symbols using a reference using <code class="language-ruby">Decode::Index\#lookup</code>.
20
20
 
21
21
  ## Releases
22
22
 
23
- Please see the [project releases](https://ioquatix.github.io/decode/releases/index) for all releases.
23
+ Please see the [project releases](https://socketry.github.io/decode/releases/index) for all releases.
24
+
25
+ ### v0.24.0
26
+
27
+ - [Introduce support for RBS signature generation](https://socketry.github.io/decode/releases/index#introduce-support-for-rbs-signature-generation)
24
28
 
25
29
  ### v0.23.5
26
30
 
data/releases.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Releases
2
2
 
3
+ ## v0.24.0
4
+
5
+ ### Introduce support for RBS signature generation
6
+
7
+ Decode now supports generating RBS type signatures from Ruby source code, making it easier to add type annotations to existing Ruby projects. The RBS generator analyzes your Ruby code and documentation to produce type signatures that can be used with tools like Steep, TypeProf, and other RBS-compatible type checkers.
8
+
9
+ To generate RBS signatures for your Ruby code, use the provided bake task:
10
+
11
+ ``` bash
12
+ -- Generate RBS signatures for the current directory
13
+ $ bundle exec bake decode:rbs:generate .
14
+
15
+ -- Generate RBS signatures for a specific directory
16
+ $ bundle exec bake decode:rbs:generate lib/
17
+ ```
18
+
19
+ The generator will output RBS declarations to stdout, which you can redirect to a file:
20
+
21
+ ``` bash
22
+ -- Save RBS signatures to a file
23
+ $ bundle exec bake decode:rbs:generate lib/ > sig/generated.rbs
24
+ ```
25
+
26
+ The RBS generator produces type signatures for:
27
+
28
+ - **Classes and modules** with their inheritance relationships.
29
+ - **Method signatures** with parameter and return types, or explicitly provide `@rbs` method signatures.
30
+ - **Generic type parameters** from `@rbs generic` documentation tags.
31
+ - **Documentation comments** as RBS comments.
32
+
3
33
  ## v0.23.5
4
34
 
5
35
  - Fix handling of `&block` arguments in call nodes.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: decode
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.23.5
4
+ version: 0.24.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -52,12 +52,41 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rbs
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: types
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
55
83
  executables: []
56
84
  extensions: []
57
85
  extra_rdoc_files: []
58
86
  files:
59
87
  - agent.md
60
88
  - bake/decode/index.rb
89
+ - bake/decode/rbs.rb
61
90
  - context/coverage.md
62
91
  - context/getting-started.md
63
92
  - context/ruby-documentation.md
@@ -68,6 +97,7 @@ files:
68
97
  - lib/decode/comment/parameter.rb
69
98
  - lib/decode/comment/pragma.rb
70
99
  - lib/decode/comment/raises.rb
100
+ - lib/decode/comment/rbs.rb
71
101
  - lib/decode/comment/returns.rb
72
102
  - lib/decode/comment/tag.rb
73
103
  - lib/decode/comment/tags.rb
@@ -98,6 +128,12 @@ files:
98
128
  - lib/decode/language/ruby/segment.rb
99
129
  - lib/decode/languages.rb
100
130
  - lib/decode/location.rb
131
+ - lib/decode/rbs.rb
132
+ - lib/decode/rbs/class.rb
133
+ - lib/decode/rbs/generator.rb
134
+ - lib/decode/rbs/method.rb
135
+ - lib/decode/rbs/module.rb
136
+ - lib/decode/rbs/wrapper.rb
101
137
  - lib/decode/scope.rb
102
138
  - lib/decode/segment.rb
103
139
  - lib/decode/source.rb
@@ -109,13 +145,13 @@ files:
109
145
  - license.md
110
146
  - readme.md
111
147
  - releases.md
112
- homepage: https://github.com/ioquatix/decode
148
+ homepage: https://github.com/socketry/decode
113
149
  licenses:
114
150
  - MIT
115
151
  metadata:
116
- documentation_uri: https://ioquatix.github.io/decode/
117
- funding_uri: https://github.com/sponsors/ioquatix/
118
- source_code_uri: https://github.com/ioquatix/decode.git
152
+ documentation_uri: https://socketry.github.io/decode/
153
+ funding_uri: https://github.com/sponsors/socketry/
154
+ source_code_uri: https://github.com/socketry/decode.git
119
155
  rdoc_options: []
120
156
  require_paths:
121
157
  - lib
metadata.gz.sig CHANGED
Binary file