decode 0.23.4 → 0.24.0

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: 6d918d6104a7b51fdf463bc40d3c5108dcfbae329064cb25395a988cb45c6aba
4
- data.tar.gz: 7d303793cff2638b2b6c5a0527ab3cb0cfa3a33a0c4c1340436e03f0d7804602
3
+ metadata.gz: 8caef896e4f0541d426216be2ee041cd78ef81642b677a420d9f89f77a719ae0
4
+ data.tar.gz: 26c7090e233385633a99ec80f906f6cc7f429dd3eb02fe0d24a87ab9019b2931
5
5
  SHA512:
6
- metadata.gz: 4fa8f8c71fb84a01414399a3f214d0870ee220225d2059ddb4416cb00bd7e4425ddf4c81c6c5a02ade8d53d9462f0be1544b4dd514b3b57d9dd3e052225fe269
7
- data.tar.gz: 81174c0ff895f8ade0f80ad31216bcfeb13080173728c186fba4d37843ca45c551438a5401d865d32bef97e8416b5ab4bebaa5983949f43b8e22d56591d139ce
6
+ metadata.gz: 9b3ef91c4974a9d9cad7b5918dd885d55838c4a0d1f0eb06e2584bcde500e6bc657ef2bb11c50b6963171336153ae76a8b8d2365ed5444895906b35272e4795f
7
+ data.tar.gz: 4c60126b5604f42b9c8f9e1f10d47ba264d52042a0365881df2957bf77ad0faf6a2f8a9fc7e05c77c430a8376a873b606d9e5112bddee194944323247108e139
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
@@ -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)
@@ -12,7 +12,17 @@ module Decode
12
12
  class Call < Definition
13
13
  # A block can sometimes be a container for other definitions.
14
14
  def container?
15
- @node&.block && @node.block.opening == "do"
15
+ case block = @node&.block
16
+ when nil
17
+ false
18
+ when Prism::BlockArgumentNode
19
+ false
20
+ when Prism::BlockNode
21
+ # Technically, all block nodes are containers, but we prefer to be opinionated about when we consider them containers:
22
+ block.opening == "do"
23
+ else
24
+ false
25
+ end
16
26
  end
17
27
 
18
28
  # The short form of the class.
@@ -33,7 +43,7 @@ module Decode
33
43
  else
34
44
  # For multiline calls, use the actual call name with arguments
35
45
  if @node.arguments && @node.arguments.arguments.any?
36
- argument_text = @node.arguments.arguments.map {|argument| argument.location.slice}.join(", ")
46
+ argument_text = @node.arguments.arguments.map{|argument| argument.location.slice}.join(", ")
37
47
  "#{@node.name}(#{argument_text})"
38
48
  else
39
49
  @node.name.to_s
@@ -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,83 @@
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
+ class Class < Wrapper
12
+
13
+ def initialize(definition)
14
+ super
15
+ @generics = nil
16
+ end
17
+
18
+ def generics
19
+ @generics ||= extract_generics
20
+ end
21
+
22
+ # Convert the class definition to RBS AST
23
+ def to_rbs_ast(method_definitions = [], index = nil)
24
+ name = simple_name_to_rbs(@definition.name)
25
+ comment = extract_comment(@definition)
26
+
27
+ # Extract generics from RBS tags
28
+ type_params = generics.map do |generic|
29
+ ::RBS::AST::TypeParam.new(
30
+ name: generic.to_sym,
31
+ variance: nil,
32
+ upper_bound: nil,
33
+ location: nil
34
+ )
35
+ end
36
+
37
+ # Build method definitions
38
+ methods = method_definitions.map{|method_def| Method.new(method_def).to_rbs_ast(index)}.compact
39
+
40
+ # Extract super class if present
41
+ super_class = if @definition.super_class
42
+ ::RBS::AST::Declarations::Class::Super.new(
43
+ name: qualified_name_to_rbs(@definition.super_class),
44
+ args: [],
45
+ location: nil
46
+ )
47
+ end
48
+
49
+ # Create the class declaration with generics
50
+ ::RBS::AST::Declarations::Class.new(
51
+ name: name,
52
+ type_params: type_params,
53
+ super_class: super_class,
54
+ members: methods,
55
+ annotations: [],
56
+ location: nil,
57
+ comment: comment
58
+ )
59
+ end
60
+
61
+ private
62
+
63
+ def extract_generics
64
+ tags.select(&:generic?).map(&:generic_parameter)
65
+ end
66
+
67
+ # Convert a simple name to RBS TypeName (not qualified)
68
+ def simple_name_to_rbs(name)
69
+ ::RBS::TypeName.new(name: name.to_sym, namespace: ::RBS::Namespace.empty)
70
+ end
71
+
72
+ # Convert a qualified name to RBS TypeName
73
+ def qualified_name_to_rbs(qualified_name)
74
+ parts = qualified_name.split("::")
75
+ name = parts.pop
76
+ namespace = ::RBS::Namespace.new(path: parts.map(&:to_sym), absolute: true)
77
+
78
+ ::RBS::TypeName.new(name: name.to_sym, namespace: namespace)
79
+ end
80
+
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,97 @@
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
+ class Generator
14
+ def initialize
15
+ # Set up RBS environment for type resolution
16
+ @loader = ::RBS::EnvironmentLoader.new()
17
+ @environment = ::RBS::Environment.from_loader(@loader).resolve_type_names
18
+ end
19
+
20
+ # Generate RBS declarations for the given index.
21
+ # @parameter index [Decode::Index] The index containing definitions to generate RBS for.
22
+ # @parameter output [IO] The output stream to write to.
23
+ def generate(index, output: $stdout)
24
+ # Build nested RBS AST structure using a hash for proper ||= behavior
25
+ declarations = {}
26
+
27
+ # Efficiently traverse the trie to find containers and their methods
28
+ index.trie.traverse do |lexical_path, node, descend|
29
+ # Process container definitions at this node
30
+ if node.values
31
+ containers = node.values.select {|definition| definition.container? && definition.public?}
32
+ containers.each do |definition|
33
+ case definition
34
+ when Decode::Language::Ruby::Class, Decode::Language::Ruby::Module
35
+ build_nested_declaration(definition, declarations, index)
36
+ end
37
+ end
38
+ end
39
+
40
+ # Continue traversing children
41
+ descend.call
42
+ end
43
+
44
+ # Write the RBS output
45
+ writer = ::RBS::Writer.new(out: output)
46
+
47
+ unless declarations.empty?
48
+ writer.write(declarations.values)
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ # Build nested RBS declarations preserving the parent hierarchy
55
+ def build_nested_declaration(definition, declarations, index)
56
+ # Create the declaration for this definition using ||= to avoid duplicates
57
+ qualified_name = definition.qualified_name
58
+ declarations[qualified_name] ||= definition_to_rbs(definition, index)
59
+
60
+ # Add this declaration to its parent's members if it has a parent
61
+ if definition.parent
62
+ parent_qualified_name = definition.parent.qualified_name
63
+ parent_container = declarations[parent_qualified_name]
64
+
65
+ # Only add if not already present
66
+ unless parent_container.members.any? {|member|
67
+ member.respond_to?(:name) && member.name.name == definition.name.to_sym
68
+ }
69
+ parent_container.members << declarations[qualified_name]
70
+ end
71
+ end
72
+ end
73
+
74
+ # Convert a definition to RBS AST
75
+ def definition_to_rbs(definition, index)
76
+ case definition
77
+ when Decode::Language::Ruby::Class
78
+ Class.new(definition).to_rbs_ast(get_methods_for_definition(definition, index), index)
79
+ when Decode::Language::Ruby::Module
80
+ Module.new(definition).to_rbs_ast(get_methods_for_definition(definition, index), index)
81
+ end
82
+ end
83
+
84
+ # Get methods for a given definition efficiently using trie lookup
85
+ def get_methods_for_definition(definition, index)
86
+ # Use the trie to efficiently find methods for this definition
87
+ if node = index.trie.lookup(definition.full_path)
88
+ node.children.flat_map do |name, child|
89
+ child.values.select{|symbol| symbol.is_a?(Decode::Language::Ruby::Method) && symbol.public?}
90
+ end
91
+ else
92
+ []
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,209 @@
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
+ class Method < Wrapper
14
+
15
+ def initialize(definition)
16
+ super
17
+ @signatures = nil
18
+ end
19
+
20
+ def signatures
21
+ @signatures ||= extract_signatures
22
+ end
23
+
24
+ # Convert the method definition to RBS AST
25
+ def to_rbs_ast(index = nil)
26
+ method_name = @definition.name
27
+ comment = extract_comment(@definition)
28
+
29
+ overloads = []
30
+ if signatures.any?
31
+ signatures.each do |signature_string|
32
+ method_type = ::RBS::Parser.parse_method_type(signature_string)
33
+ overloads << ::RBS::AST::Members::MethodDefinition::Overload.new(
34
+ method_type: method_type,
35
+ annotations: []
36
+ )
37
+ end
38
+ else
39
+ return_type = extract_return_type(@definition, index) || ::RBS::Parser.parse_type("untyped")
40
+ parameters = extract_parameters(@definition, index)
41
+ block_type = extract_block_type(@definition, index)
42
+
43
+ method_type = ::RBS::MethodType.new(
44
+ type_params: [],
45
+ type: ::RBS::Types::Function.new(
46
+ required_positionals: parameters,
47
+ optional_positionals: [],
48
+ rest_positionals: nil,
49
+ trailing_positionals: [],
50
+ required_keywords: {},
51
+ optional_keywords: {},
52
+ rest_keywords: nil,
53
+ return_type: return_type
54
+ ),
55
+ block: block_type,
56
+ location: nil
57
+ )
58
+
59
+ overloads << ::RBS::AST::Members::MethodDefinition::Overload.new(
60
+ method_type: method_type,
61
+ annotations: []
62
+ )
63
+ end
64
+
65
+ kind = @definition.receiver ? :singleton : :instance
66
+
67
+ ::RBS::AST::Members::MethodDefinition.new(
68
+ name: method_name.to_sym,
69
+ kind: kind,
70
+ overloads: overloads,
71
+ annotations: [],
72
+ location: nil,
73
+ comment: comment,
74
+ overloading: false,
75
+ visibility: :public
76
+ )
77
+ end
78
+
79
+ private
80
+
81
+ def extract_signatures
82
+ extract_tags.select(&:method_signature?).map(&:method_signature)
83
+ end
84
+
85
+ # Extract return type from method documentation
86
+ def extract_return_type(definition, index)
87
+ # Look for @returns tags in the method's documentation
88
+ documentation = definition.documentation
89
+
90
+ # Find @returns tag
91
+ returns_tag = documentation&.filter(Decode::Comment::Returns)&.first
92
+
93
+ if returns_tag
94
+ # Parse the type from the tag
95
+ type_string = returns_tag.type.strip
96
+ parse_type_string(type_string)
97
+ else
98
+ # Infer return type based on method name patterns
99
+ infer_return_type(definition)
100
+ end
101
+ end
102
+
103
+ # Extract parameter types from method documentation
104
+ def extract_parameters(definition, index)
105
+ documentation = definition.documentation
106
+ return [] unless documentation
107
+
108
+ # Find @parameter tags
109
+ param_tags = documentation.filter(Decode::Comment::Parameter).to_a
110
+ return [] if param_tags.empty?
111
+
112
+ param_tags.map do |tag|
113
+ name = tag.name
114
+ type_string = tag.type.strip
115
+ type = parse_type_string(type_string)
116
+
117
+ ::RBS::Types::Function::Param.new(
118
+ type: type,
119
+ name: name.to_sym
120
+ )
121
+ end
122
+ end
123
+
124
+ # Extract block type from method documentation
125
+ def extract_block_type(definition, index)
126
+ documentation = definition.documentation
127
+ return nil unless documentation
128
+
129
+ # Find @yields tags
130
+ yields_tag = documentation.filter(Decode::Comment::Yields).first
131
+ return nil unless yields_tag
132
+
133
+ # Extract block parameters from nested @parameter tags
134
+ block_params = yields_tag.filter(Decode::Comment::Parameter).map do |param_tag|
135
+ name = param_tag.name
136
+ type_string = param_tag.type.strip
137
+ type = parse_type_string(type_string)
138
+
139
+ ::RBS::Types::Function::Param.new(
140
+ type: type,
141
+ name: name.to_sym
142
+ )
143
+ end
144
+
145
+ # Parse the block signature to determine if it's required
146
+ # Check both the directive name and the block signature
147
+ block_signature = yields_tag.block
148
+ directive_name = yields_tag.directive
149
+ required = !directive_name.include?("?") && !block_signature.include?("?") && !block_signature.include?("optional")
150
+
151
+ # Determine block return type (default to void if not specified)
152
+ block_return_type = ::RBS::Parser.parse_type("void")
153
+
154
+ # Create the block function type
155
+ block_function = ::RBS::Types::Function.new(
156
+ required_positionals: block_params,
157
+ optional_positionals: [],
158
+ rest_positionals: nil,
159
+ trailing_positionals: [],
160
+ required_keywords: {},
161
+ optional_keywords: {},
162
+ rest_keywords: nil,
163
+ return_type: block_return_type
164
+ )
165
+
166
+ # Create and return the block type
167
+ ::RBS::Types::Block.new(
168
+ type: block_function,
169
+ required: required,
170
+ self_type: nil
171
+ )
172
+ end
173
+
174
+ # Infer return type based on method patterns and heuristics
175
+ def infer_return_type(definition)
176
+ method_name = definition.name
177
+ method_name_str = method_name.to_s
178
+
179
+ # Methods ending with ? are typically boolean
180
+ if method_name_str.end_with?("?")
181
+ return ::RBS::Parser.parse_type("bool")
182
+ end
183
+
184
+ # Methods named initialize return void
185
+ if method_name == :initialize
186
+ return ::RBS::Parser.parse_type("void")
187
+ end
188
+
189
+ # Methods with names that suggest they return self
190
+ if method_name_str.match?(/^(add|append|prepend|push|<<|concat|merge!|sort!|reverse!|clear|delete|remove)/)
191
+ return ::RBS::Parser.parse_type("self")
192
+ end
193
+
194
+ # Default to untyped
195
+ ::RBS::Parser.parse_type("untyped")
196
+ end
197
+
198
+ # Parse a type string and convert it to RBS type
199
+ def parse_type_string(type_string)
200
+ type = Types.parse(type_string)
201
+ return ::RBS::Parser.parse_type(type.to_rbs)
202
+ rescue => error
203
+ Console.warn(self, "Failed to parse type string: #{type_string}", error)
204
+ return ::RBS::Parser.parse_type("untyped")
205
+ end
206
+
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,45 @@
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
+ class Module < Wrapper
12
+
13
+ def initialize(definition)
14
+ super
15
+ end
16
+
17
+ # Convert the module definition to RBS AST
18
+ def to_rbs_ast(method_definitions = [], index = nil)
19
+ name = simple_name_to_rbs(@definition.name)
20
+ comment = extract_comment(@definition)
21
+
22
+ # Build method definitions
23
+ methods = method_definitions.map{|method_def| Method.new(method_def).to_rbs_ast(index)}.compact
24
+
25
+ ::RBS::AST::Declarations::Module.new(
26
+ name: name,
27
+ type_params: [],
28
+ self_types: [],
29
+ members: methods,
30
+ annotations: [],
31
+ location: nil,
32
+ comment: comment
33
+ )
34
+ end
35
+
36
+ private
37
+
38
+ # Convert a simple name to RBS TypeName (not qualified)
39
+ def simple_name_to_rbs(name)
40
+ ::RBS::TypeName.new(name: name.to_sym, namespace: ::RBS::Namespace.empty)
41
+ end
42
+
43
+ end
44
+ end
45
+ 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,11 @@
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"
@@ -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
@@ -4,5 +4,5 @@
4
4
  # Copyright, 2020-2025, by Samuel Williams.
5
5
 
6
6
  module Decode
7
- VERSION = "0.23.4"
7
+ VERSION = "0.24.0"
8
8
  end
data/readme.md CHANGED
@@ -22,6 +22,14 @@ Please see the [project documentation](https://ioquatix.github.io/decode/) for m
22
22
 
23
23
  Please see the [project releases](https://ioquatix.github.io/decode/releases/index) for all releases.
24
24
 
25
+ ### v0.24.0
26
+
27
+ - [Introduce support for RBS signature generation.](https://ioquatix.github.io/decode/releases/index#introduce-support-for-rbs-signature-generation.)
28
+
29
+ ### v0.23.5
30
+
31
+ - Fix handling of `&block` arguments in call nodes.
32
+
25
33
  ### v0.23.4
26
34
 
27
35
  - Fix handling of definitions nested within `if`/`unless`/`elsif`/`else` blocks.
data/releases.md CHANGED
@@ -1,5 +1,39 @@
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
+
33
+ ## v0.23.5
34
+
35
+ - Fix handling of `&block` arguments in call nodes.
36
+
3
37
  ## v0.23.4
4
38
 
5
39
  - Fix handling of definitions nested within `if`/`unless`/`elsif`/`else` blocks.
data.tar.gz.sig CHANGED
@@ -1,4 +1,2 @@
1
- P�g����^�+�u���<
2
- �&�Z��0�;�*�݀�Zd6u� �(�X��`P�TX��!�w���j� ��n���Œ�ܰ� njX�Vvׇ�D��� ��P� �\�D�AP�5f����@��S1��_���@��w�s�$A3�!��,����3&M\h9q� ex��-W�+N�=��
3
- |��'����1d�YǡQ�d�{ί�LW}7��A���;*Py9��q�ߣ:=���v#���x.���`Y�7��W�GM����Mi+|uG���2ivf
4
- �?{�Sj�1�lT��T@""�u?��T�u���@�
1
+ ��xH��cز�6��;��Z��nC�3����Ʌ#�@���sW��-��A����(ȧ�\�2�~�(�g��ϸwA�^��
2
+ �����C]먩��f~:&t˴����H�= ���+�O/���J�R��.S��FDz��wZIڧ��H�?(jx&y߸�򂳮}M*���E��{��Qf<��4Z�Ʉ���^���
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.4
4
+ version: 0.24.0
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
metadata.gz.sig CHANGED
Binary file