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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/agent.md +8 -0
- data/bake/decode/index.rb +3 -12
- data/bake/decode/rbs.rb +18 -0
- data/lib/decode/comment/node.rb +1 -1
- data/lib/decode/comment/rbs.rb +76 -0
- data/lib/decode/definition.rb +4 -4
- data/lib/decode/index.rb +27 -0
- data/lib/decode/language/ruby/call.rb +12 -2
- data/lib/decode/language/ruby/generic.rb +20 -0
- data/lib/decode/language/ruby/parser.rb +1 -1
- data/lib/decode/language/ruby/segment.rb +1 -1
- data/lib/decode/rbs/class.rb +83 -0
- data/lib/decode/rbs/generator.rb +97 -0
- data/lib/decode/rbs/method.rb +209 -0
- data/lib/decode/rbs/module.rb +45 -0
- data/lib/decode/rbs/wrapper.rb +64 -0
- data/lib/decode/rbs.rb +11 -0
- data/lib/decode/segment.rb +2 -2
- data/lib/decode/version.rb +1 -1
- data/readme.md +8 -0
- data/releases.md +34 -0
- data.tar.gz.sig +2 -4
- metadata +37 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8caef896e4f0541d426216be2ee041cd78ef81642b677a420d9f89f77a719ae0
|
4
|
+
data.tar.gz: 26c7090e233385633a99ec80f906f6cc7f429dd3eb02fe0d24a87ab9019b2931
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/bake/decode/rbs.rb
ADDED
@@ -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
|
data/lib/decode/comment/node.rb
CHANGED
@@ -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
|
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
|
data/lib/decode/definition.rb
CHANGED
@@ -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 |
|
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 |
|
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 |
|
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 |
|
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
|
-
|
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
|
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
|
512
|
+
preceding_comments.map{|comment| comment.location.slice.sub(/^#[\s\t]?/, "")},
|
513
513
|
@language,
|
514
514
|
statement
|
515
515
|
)
|
@@ -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"
|
data/lib/decode/segment.rb
CHANGED
@@ -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 |
|
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 |
|
40
|
+
# @returns [String | Nil]
|
41
41
|
def code
|
42
42
|
end
|
43
43
|
end
|
data/lib/decode/version.rb
CHANGED
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
|
-
|
2
|
-
|
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
|
+
��x�H��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.
|
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
|