Skip to content

Commit c3a8a62

Browse files
committed
Add RBS support module for sig/ loading and validation
`RDoc::RbsSupport` provides: - `load_signatures` to load types from .rbs files via `RBS::EnvironmentLoader` - `merge_into_store` to attach sig/ types to code objects (inline `#:` annotations take priority) - `validate_method_type`/`validate_type` for syntax checking Auto-detects `sig/` directory during generation and merges types after `store.complete`.
1 parent c7e4cd5 commit c3a8a62

3 files changed

Lines changed: 179 additions & 0 deletions

File tree

lib/rdoc/rbs_support.rb

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# frozen_string_literal: true
2+
3+
require 'rbs'
4+
5+
##
6+
# RBS type signature support.
7+
# Loads type information from .rbs files and validates inline annotations.
8+
9+
module RDoc::RbsSupport
10+
11+
##
12+
# Validates an RBS method type signature string.
13+
# Returns nil if valid, or an error message string if invalid.
14+
15+
def self.validate_method_type(sig)
16+
RBS::Parser.parse_method_type(sig, require_eof: true)
17+
nil
18+
rescue RBS::ParsingError => e
19+
e.message
20+
end
21+
22+
##
23+
# Validates an RBS type signature string.
24+
# Returns nil if valid, or an error message string if invalid.
25+
26+
def self.validate_type(sig)
27+
RBS::Parser.parse_type(sig, require_eof: true)
28+
nil
29+
rescue RBS::ParsingError => e
30+
e.message
31+
end
32+
33+
##
34+
# Loads RBS signatures from the given directories.
35+
# Returns a Hash mapping "ClassName#method_name" => "type sig string".
36+
37+
def self.load_signatures(*dirs)
38+
loader = RBS::EnvironmentLoader.new(core_root: nil)
39+
dirs.each { |dir| loader.add(path: Pathname(dir)) }
40+
41+
env = RBS::Environment.new
42+
loader.load(env: env)
43+
44+
signatures = {}
45+
46+
env.class_decls.each do |type_name, entry|
47+
class_name = type_name.to_s.delete_prefix('::')
48+
49+
entry.each_decl do |decl|
50+
decl.members.each do |member|
51+
case member
52+
when RBS::AST::Members::MethodDefinition
53+
key = member.singleton? ? "#{class_name}::#{member.name}" : "#{class_name}##{member.name}"
54+
sigs = member.overloads.map { |o| o.method_type.to_s }
55+
signatures[key] = sigs.join("\n")
56+
when RBS::AST::Members::AttrReader, RBS::AST::Members::AttrWriter, RBS::AST::Members::AttrAccessor
57+
key = "#{class_name}.#{member.name}"
58+
signatures[key] = member.type.to_s
59+
end
60+
end
61+
end
62+
end
63+
64+
signatures
65+
end
66+
67+
##
68+
# Merges loaded RBS signatures into the store's code objects.
69+
# Inline #: annotations take priority and are not overwritten.
70+
71+
def self.merge_into_store(store, signatures)
72+
store.all_classes_and_modules.each do |cm|
73+
cm.method_list.each do |method|
74+
next if method.type_signature
75+
76+
key = method.singleton ? "#{cm.full_name}::#{method.name}" : "#{cm.full_name}##{method.name}"
77+
method.type_signature = signatures[key] if signatures[key]
78+
end
79+
80+
cm.attributes.each do |attr|
81+
next if attr.type_signature
82+
83+
key = "#{cm.full_name}.#{attr.name}"
84+
attr.type_signature = signatures[key] if signatures[key]
85+
end
86+
end
87+
end
88+
end

lib/rdoc/rdoc.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require 'fileutils'
66
require 'pathname'
77
require 'time'
8+
require_relative 'rbs_support'
89

910
##
1011
# This is the driver for generating RDoc output. It handles file parsing and
@@ -492,6 +493,13 @@ def document(options)
492493

493494
@store.complete @options.visibility
494495

496+
# Load RBS signatures from sig/ directory
497+
sig_dir = File.join(@options.root.to_s, 'sig')
498+
if File.directory?(sig_dir)
499+
signatures = RDoc::RbsSupport.load_signatures(sig_dir)
500+
RDoc::RbsSupport.merge_into_store(@store, signatures) unless signatures.empty?
501+
end
502+
495503
@stats.coverage_level = @options.coverage_report
496504

497505
if @options.coverage_report then

test/rdoc/rbs_support_test.rb

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'helper'
4+
require 'rdoc/rbs_support'
5+
6+
class RDocRbsSupportTest < RDoc::TestCase
7+
def test_validate_method_type_valid
8+
assert_nil RDoc::RbsSupport.validate_method_type('(String) -> void')
9+
assert_nil RDoc::RbsSupport.validate_method_type('(Integer, ?String) -> bool')
10+
assert_nil RDoc::RbsSupport.validate_method_type('() -> Array[String]')
11+
end
12+
13+
def test_validate_method_type_invalid
14+
error = RDoc::RbsSupport.validate_method_type('(String ->')
15+
assert_kind_of String, error
16+
end
17+
18+
def test_validate_type_valid
19+
assert_nil RDoc::RbsSupport.validate_type('String')
20+
assert_nil RDoc::RbsSupport.validate_type('Array[Integer]')
21+
assert_nil RDoc::RbsSupport.validate_type('String?')
22+
end
23+
24+
def test_validate_type_invalid
25+
error = RDoc::RbsSupport.validate_type('String[')
26+
assert_kind_of String, error
27+
end
28+
29+
def test_load_signatures_from_directory
30+
Dir.mktmpdir do |dir|
31+
File.write(File.join(dir, 'test.rbs'), <<~RBS)
32+
class Greeter
33+
def greet: (String name) -> void
34+
attr_reader language: String
35+
end
36+
RBS
37+
38+
sigs = RDoc::RbsSupport.load_signatures(dir)
39+
assert_equal '(String name) -> void', sigs['Greeter#greet']
40+
assert_equal 'String', sigs['Greeter.language']
41+
end
42+
end
43+
44+
def test_merge_into_store
45+
top_level = @store.add_file 'test.rb'
46+
cm = top_level.add_class RDoc::NormalClass, 'Greeter'
47+
48+
m = RDoc::AnyMethod.new(nil, 'greet')
49+
m.params = '(name)'
50+
cm.add_method m
51+
52+
a = RDoc::Attr.new(nil, 'language', 'R', '')
53+
cm.add_attribute a
54+
55+
signatures = {
56+
'Greeter#greet' => '(String name) -> void',
57+
'Greeter.language' => 'String'
58+
}
59+
60+
RDoc::RbsSupport.merge_into_store(@store, signatures)
61+
62+
assert_equal '(String name) -> void', m.type_signature
63+
assert_equal 'String', a.type_signature
64+
end
65+
66+
def test_merge_does_not_overwrite_inline_annotations
67+
top_level = @store.add_file 'test.rb'
68+
cm = top_level.add_class RDoc::NormalClass, 'Greeter'
69+
70+
m = RDoc::AnyMethod.new(nil, 'greet')
71+
m.params = '(name)'
72+
m.type_signature = '(String) -> void'
73+
cm.add_method m
74+
75+
signatures = {
76+
'Greeter#greet' => '(String name, ?Integer count) -> void'
77+
}
78+
79+
RDoc::RbsSupport.merge_into_store(@store, signatures)
80+
81+
assert_equal '(String) -> void', m.type_signature
82+
end
83+
end

0 commit comments

Comments
 (0)