diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e4d1c295e2..33eaafb49c 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -14,9 +14,9 @@ jobs:
ruby-versions:
uses: ruby/actions/.github/workflows/ruby_versions.yml@master
with:
- # 2.7 breaks `test_parse_statements_nodoc_identifier_alias_method`
- min_version: 3.0
+ min_version: 3.2
versions: '["mswin"]'
+ engine: cruby
test:
needs: ruby-versions
@@ -26,14 +26,6 @@ jobs:
ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }}
os: [ubuntu-latest, macos-latest, windows-latest]
exclude:
- - os: windows-latest
- ruby: truffleruby
- - os: windows-latest
- ruby: truffleruby-head
- - os: windows-latest
- ruby: jruby
- - os: windows-latest
- ruby: jruby-head
- os: macos-latest
ruby: mswin
- os: ubuntu-latest
@@ -68,7 +60,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- prism_version: ['1.0.0', '1.3.0', '1.7.0', 'head']
+ prism_version: ['1.6.0', '1.7.0', 'head']
runs-on: ubuntu-latest
env:
RUBYOPT: --enable-frozen_string_literal
diff --git a/Gemfile b/Gemfile
index 317623101b..4b2be8f5d5 100644
--- a/Gemfile
+++ b/Gemfile
@@ -18,7 +18,5 @@ elsif ENV['PRISM_VERSION']
end
platforms :ruby do
- if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.2')
- gem 'mini_racer' # For testing the searcher.js file
- end
+ gem 'mini_racer' # For testing the searcher.js file
end
diff --git a/lib/rdoc/code_object/any_method.rb b/lib/rdoc/code_object/any_method.rb
index f56110ea11..26b42c07cb 100644
--- a/lib/rdoc/code_object/any_method.rb
+++ b/lib/rdoc/code_object/any_method.rb
@@ -14,7 +14,7 @@ class RDoc::AnyMethod < RDoc::MethodAttr
# RDoc 4.1
# Added is_alias_for
- MARSHAL_VERSION = 3 # :nodoc:
+ MARSHAL_VERSION = 4 # :nodoc:
##
# Don't rename \#initialize to \::new
@@ -166,6 +166,7 @@ def marshal_dump
@parent.class,
@section.title,
is_alias_for,
+ @type_signature,
]
end
@@ -204,6 +205,7 @@ def marshal_load(array)
@parent_title = array[13]
@section_title = array[14]
@is_alias_for = array[15]
+ @type_signature = array[16]
array[8].each do |new_name, document|
add_alias RDoc::Alias.new(nil, @name, new_name, RDoc::Comment.from_document(document), singleton: @singleton)
diff --git a/lib/rdoc/code_object/attr.rb b/lib/rdoc/code_object/attr.rb
index bfc981f7e8..3895121854 100644
--- a/lib/rdoc/code_object/attr.rb
+++ b/lib/rdoc/code_object/attr.rb
@@ -11,7 +11,7 @@ class RDoc::Attr < RDoc::MethodAttr
# Added parent name and class
# Added section title
- MARSHAL_VERSION = 3 # :nodoc:
+ MARSHAL_VERSION = 4 # :nodoc:
##
# Is the attribute readable ('R'), writable ('W') or both ('RW')?
@@ -108,7 +108,8 @@ def marshal_dump
@file.relative_name,
@parent.full_name,
@parent.class,
- @section.title
+ @section.title,
+ @type_signature,
]
end
@@ -140,6 +141,7 @@ def marshal_load(array)
@parent_name = array[8]
@parent_class = array[9]
@section_title = array[10]
+ @type_signature = array[11]
@file = RDoc::TopLevel.new array[7] if version > 1
diff --git a/lib/rdoc/code_object/method_attr.rb b/lib/rdoc/code_object/method_attr.rb
index 3169640982..0f0dc0b1a4 100644
--- a/lib/rdoc/code_object/method_attr.rb
+++ b/lib/rdoc/code_object/method_attr.rb
@@ -58,6 +58,18 @@ class RDoc::MethodAttr < RDoc::CodeObject
attr_accessor :call_seq
+ ##
+ # RBS type signature from inline annotations or loaded .rbs files
+
+ attr_accessor :type_signature
+
+ ##
+ # Returns the type signature split into individual lines.
+
+ def type_signature_lines
+ @type_signature&.split("\n")
+ end
+
##
# The call_seq or the param_seq with method name, if there is no call_seq.
@@ -86,6 +98,7 @@ def initialize(text, name, singleton: false)
@block_params = nil
@call_seq = nil
@params = nil
+ @type_signature = nil
end
##
diff --git a/lib/rdoc/generator/aliki.rb b/lib/rdoc/generator/aliki.rb
index d8314196f9..650961c228 100644
--- a/lib/rdoc/generator/aliki.rb
+++ b/lib/rdoc/generator/aliki.rb
@@ -117,6 +117,21 @@ def write_search_index
File.write search_index_path, "var search_data = #{JSON.generate(data)};"
end
+ ##
+ # Returns the type signature of +method_attr+ as HTML with linked type names.
+ # Returns nil if no type signature is present.
+
+ def type_signature_html(method_attr, from_path)
+ lines = method_attr.type_signature_lines
+ return unless lines
+
+ RDoc::RbsHelper.signature_to_html(
+ lines,
+ lookup: @store.type_name_lookup,
+ from_path: from_path
+ )
+ end
+
##
# Resolves a URL for use in templates. Absolute URLs are returned unchanged.
# Relative URLs are prefixed with rel_prefix to ensure they resolve correctly from any page.
diff --git a/lib/rdoc/generator/template/aliki/class.rhtml b/lib/rdoc/generator/template/aliki/class.rhtml
index ba1238b9e9..5cd7e71b32 100644
--- a/lib/rdoc/generator/template/aliki/class.rhtml
+++ b/lib/rdoc/generator/template/aliki/class.rhtml
@@ -93,6 +93,9 @@
<%= h attrib.name %>
[<%= attrib.rw %>]
+ <%- if attrib.type_signature %>
+ <%= type_signature_html(attrib, klass.path) %>
+ <%- end %>
<%= type_signature_html(method, klass.path) %>
+ <%- end %>
<%- if method.token_stream %>
diff --git a/lib/rdoc/generator/template/aliki/css/rdoc.css b/lib/rdoc/generator/template/aliki/css/rdoc.css
index 8341a4ff37..6c4f474428 100644
--- a/lib/rdoc/generator/template/aliki/css/rdoc.css
+++ b/lib/rdoc/generator/template/aliki/css/rdoc.css
@@ -1075,6 +1075,20 @@ main h6 a:hover {
font-style: italic;
}
+/* RBS Type Signature Links — linked types get subtle underline */
+a.rbs-type {
+ color: inherit;
+ text-decoration: underline;
+ text-decoration-color: var(--color-border-default);
+ text-underline-offset: 0.2em;
+ transition: text-decoration-color var(--transition-fast), color var(--transition-fast);
+}
+
+a.rbs-type:hover {
+ color: var(--color-link-hover);
+ text-decoration-color: var(--color-link-hover);
+}
+
/* Emphasis */
em {
text-decoration-color: var(--color-emphasis-decoration);
@@ -1335,6 +1349,49 @@ main .method-heading .method-args {
font-weight: var(--font-weight-normal);
}
+/* Type signatures — overloads stack as a code block under the method name */
+pre.method-type-signature {
+ position: relative;
+ margin: var(--space-2) 0 0;
+ padding: var(--space-2) 0 0;
+ background: transparent;
+ border: none;
+ border-radius: 0;
+ overflow: visible;
+ font-family: var(--font-code);
+ font-size: var(--font-size-sm);
+ color: var(--color-text-tertiary);
+ line-height: var(--line-height-tight);
+ white-space: pre-wrap;
+ overflow-wrap: break-word;
+}
+
+pre.method-type-signature::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ border-top: 1px dotted var(--color-border-default);
+}
+
+pre.method-type-signature code {
+ font-family: inherit;
+ font-size: inherit;
+ color: inherit;
+ background: transparent;
+ padding: 0;
+}
+
+/* Attribute type sigs render inline after the [RW] badge */
+main .method-heading > .method-type-signature {
+ display: inline;
+ margin-left: var(--space-2);
+ font-family: var(--font-code);
+ font-size: var(--font-size-sm);
+ color: var(--color-text-secondary);
+}
+
main .method-controls {
position: absolute;
top: var(--space-3);
@@ -1444,6 +1501,10 @@ main .attribute-access-type {
font-size: var(--font-size-base);
}
+ pre.method-type-signature {
+ font-size: var(--font-size-xs);
+ }
+
main .method-header {
padding: var(--space-2);
padding-right: var(--space-2);
diff --git a/lib/rdoc/generator/template/aliki/js/aliki.js b/lib/rdoc/generator/template/aliki/js/aliki.js
index 7883132b00..4dcfa826fc 100644
--- a/lib/rdoc/generator/template/aliki/js/aliki.js
+++ b/lib/rdoc/generator/template/aliki/js/aliki.js
@@ -435,8 +435,8 @@ function wrapCodeBlocksWithCopyButton() {
// not directly in rhtml templates
// - Modifying the formatter would require extending RDoc's core internals
- // Find all pre elements that are not already wrapped
- const preElements = document.querySelectorAll('main pre:not(.code-block-wrapper pre)');
+ // Target code examples and source code; skip type signature blocks
+ const preElements = document.querySelectorAll('main pre:not(.method-type-signature)');
preElements.forEach((pre) => {
// Skip if already wrapped
diff --git a/lib/rdoc/markdown.kpeg b/lib/rdoc/markdown.kpeg
index d95a88a823..8d24cc097f 100644
--- a/lib/rdoc/markdown.kpeg
+++ b/lib/rdoc/markdown.kpeg
@@ -1249,7 +1249,7 @@ RawNoteBlock = @StartList:a
# Markdown extensions added by RDoc follow
CodeFence = &{ github? }
- Ticks3 (@Sp StrChunk:format)? Spnl < (
+ Ticks3 (@Sp StrChunk:format)? @Sp @Newline? < (
( !"`" Nonspacechar )+ |
!Ticks3 /`+/ |
Spacechar |
diff --git a/lib/rdoc/markdown.rb b/lib/rdoc/markdown.rb
index e4d0ae9ff6..45954fd1cc 100644
--- a/lib/rdoc/markdown.rb
+++ b/lib/rdoc/markdown.rb
@@ -15570,7 +15570,7 @@ def _RawNoteBlock
return _tmp
end
- # CodeFence = &{ github? } Ticks3 (@Sp StrChunk:format)? Spnl < ((!"`" Nonspacechar)+ | !Ticks3 /`+/ | Spacechar | @Newline)+ > Ticks3 @Sp @Newline* { verbatim = RDoc::Markup::Verbatim.new text verbatim.format = format.intern if format.instance_of?(String) verbatim }
+ # CodeFence = &{ github? } Ticks3 (@Sp StrChunk:format)? @Sp @Newline? < ((!"`" Nonspacechar)+ | !Ticks3 /`+/ | Spacechar | @Newline)+ > Ticks3 @Sp @Newline* { verbatim = RDoc::Markup::Verbatim.new text verbatim.format = format.intern if format.instance_of?(String) verbatim }
def _CodeFence
_save = self.pos
@@ -15612,31 +15612,41 @@ def _CodeFence
self.pos = _save
break
end
- _tmp = apply(:_Spnl)
+ _tmp = _Sp()
unless _tmp
self.pos = _save
break
end
- _text_start = self.pos
_save4 = self.pos
-
+ _tmp = _Newline()
+ unless _tmp
+ _tmp = true
+ self.pos = _save4
+ end
+ unless _tmp
+ self.pos = _save
+ break
+ end
+ _text_start = self.pos
_save5 = self.pos
- while true # choice
- _save6 = self.pos
+ _save6 = self.pos
+ while true # choice
_save7 = self.pos
+
+ _save8 = self.pos
while true # sequence
- _save8 = self.pos
+ _save9 = self.pos
_tmp = match_string("`")
_tmp = _tmp ? nil : true
- self.pos = _save8
+ self.pos = _save9
unless _tmp
- self.pos = _save7
+ self.pos = _save8
break
end
_tmp = apply(:_Nonspacechar)
unless _tmp
- self.pos = _save7
+ self.pos = _save8
end
break
end # end sequence
@@ -15644,19 +15654,19 @@ def _CodeFence
if _tmp
while true
- _save9 = self.pos
+ _save10 = self.pos
while true # sequence
- _save10 = self.pos
+ _save11 = self.pos
_tmp = match_string("`")
_tmp = _tmp ? nil : true
- self.pos = _save10
+ self.pos = _save11
unless _tmp
- self.pos = _save9
+ self.pos = _save10
break
end
_tmp = apply(:_Nonspacechar)
unless _tmp
- self.pos = _save9
+ self.pos = _save10
end
break
end # end sequence
@@ -15665,59 +15675,59 @@ def _CodeFence
end
_tmp = true
else
- self.pos = _save6
+ self.pos = _save7
end
break if _tmp
- self.pos = _save5
+ self.pos = _save6
- _save11 = self.pos
+ _save12 = self.pos
while true # sequence
- _save12 = self.pos
+ _save13 = self.pos
_tmp = apply(:_Ticks3)
_tmp = _tmp ? nil : true
- self.pos = _save12
+ self.pos = _save13
unless _tmp
- self.pos = _save11
+ self.pos = _save12
break
end
_tmp = scan(/\G(?-mix:`+)/)
unless _tmp
- self.pos = _save11
+ self.pos = _save12
end
break
end # end sequence
break if _tmp
- self.pos = _save5
+ self.pos = _save6
_tmp = apply(:_Spacechar)
break if _tmp
- self.pos = _save5
+ self.pos = _save6
_tmp = _Newline()
break if _tmp
- self.pos = _save5
+ self.pos = _save6
break
end # end choice
if _tmp
while true
- _save13 = self.pos
+ _save14 = self.pos
while true # choice
- _save14 = self.pos
-
_save15 = self.pos
+
+ _save16 = self.pos
while true # sequence
- _save16 = self.pos
+ _save17 = self.pos
_tmp = match_string("`")
_tmp = _tmp ? nil : true
- self.pos = _save16
+ self.pos = _save17
unless _tmp
- self.pos = _save15
+ self.pos = _save16
break
end
_tmp = apply(:_Nonspacechar)
unless _tmp
- self.pos = _save15
+ self.pos = _save16
end
break
end # end sequence
@@ -15725,19 +15735,19 @@ def _CodeFence
if _tmp
while true
- _save17 = self.pos
+ _save18 = self.pos
while true # sequence
- _save18 = self.pos
+ _save19 = self.pos
_tmp = match_string("`")
_tmp = _tmp ? nil : true
- self.pos = _save18
+ self.pos = _save19
unless _tmp
- self.pos = _save17
+ self.pos = _save18
break
end
_tmp = apply(:_Nonspacechar)
unless _tmp
- self.pos = _save17
+ self.pos = _save18
end
break
end # end sequence
@@ -15746,36 +15756,36 @@ def _CodeFence
end
_tmp = true
else
- self.pos = _save14
+ self.pos = _save15
end
break if _tmp
- self.pos = _save13
+ self.pos = _save14
- _save19 = self.pos
+ _save20 = self.pos
while true # sequence
- _save20 = self.pos
+ _save21 = self.pos
_tmp = apply(:_Ticks3)
_tmp = _tmp ? nil : true
- self.pos = _save20
+ self.pos = _save21
unless _tmp
- self.pos = _save19
+ self.pos = _save20
break
end
_tmp = scan(/\G(?-mix:`+)/)
unless _tmp
- self.pos = _save19
+ self.pos = _save20
end
break
end # end sequence
break if _tmp
- self.pos = _save13
+ self.pos = _save14
_tmp = apply(:_Spacechar)
break if _tmp
- self.pos = _save13
+ self.pos = _save14
_tmp = _Newline()
break if _tmp
- self.pos = _save13
+ self.pos = _save14
break
end # end choice
@@ -15783,7 +15793,7 @@ def _CodeFence
end
_tmp = true
else
- self.pos = _save4
+ self.pos = _save5
end
if _tmp
text = get_text(_text_start)
@@ -16677,7 +16687,7 @@ def _DefinitionListDefinition
Rules[:_InlineNote] = rule_info("InlineNote", "&{ notes? } \"^[\" @StartList:a (!\"]\" Inline:l { a << l })+ \"]\" { ref = [:inline, @note_order.length] @footnotes[ref] = paragraph a note_for ref }")
Rules[:_Notes] = rule_info("Notes", "(Note | SkipBlock)*")
Rules[:_RawNoteBlock] = rule_info("RawNoteBlock", "@StartList:a (!@BlankLine !RawNoteReference OptionallyIndentedLine:l { a << l })+ < @BlankLine* > { a << text } { a }")
- Rules[:_CodeFence] = rule_info("CodeFence", "&{ github? } Ticks3 (@Sp StrChunk:format)? Spnl < ((!\"`\" Nonspacechar)+ | !Ticks3 /`+/ | Spacechar | @Newline)+ > Ticks3 @Sp @Newline* { verbatim = RDoc::Markup::Verbatim.new text verbatim.format = format.intern if format.instance_of?(String) verbatim }")
+ Rules[:_CodeFence] = rule_info("CodeFence", "&{ github? } Ticks3 (@Sp StrChunk:format)? @Sp @Newline? < ((!\"`\" Nonspacechar)+ | !Ticks3 /`+/ | Spacechar | @Newline)+ > Ticks3 @Sp @Newline* { verbatim = RDoc::Markup::Verbatim.new text verbatim.format = format.intern if format.instance_of?(String) verbatim }")
Rules[:_Table] = rule_info("Table", "&{ github? } TableHead:header TableLine:line TableRow+:body { table = RDoc::Markup::Table.new(header, line, body) parse_table_cells(table) }")
Rules[:_TableHead] = rule_info("TableHead", "TableItem2+:items \"|\"? @Newline { items }")
Rules[:_TableRow] = rule_info("TableRow", "((TableItem:item1 TableItem2+:items { [item1, *items] }):row | TableItem2+:row) \"|\"? @Newline { row }")
diff --git a/lib/rdoc/parser/prism_ruby.rb b/lib/rdoc/parser/prism_ruby.rb
index e8dedd5c18..5e8efb1e11 100644
--- a/lib/rdoc/parser/prism_ruby.rb
+++ b/lib/rdoc/parser/prism_ruby.rb
@@ -133,6 +133,9 @@ class RDoc::Parser::PrismRuby < RDoc::Parser
parse_files_matching(/\.rbw?$/) unless ENV['RDOC_USE_RIPPER_PARSER']
+ # Matches an RBS inline type annotation line: #: followed by whitespace
+ RBS_SIG_LINE = /\A#:\s/ # :nodoc:
+
attr_accessor :visibility
attr_reader :container, :singleton, :in_proc_block
@@ -461,10 +464,14 @@ def skip_comments_until(line_no_until)
def consecutive_comment(line_no)
return unless @unprocessed_comments.first&.first == line_no
_line_no, start_line, text = @unprocessed_comments.shift
- parse_comment_text_to_directives(text, start_line)
+ type_signature = extract_type_signature!(text, start_line)
+ result = parse_comment_text_to_directives(text, start_line)
+ return unless result
+ comment, directives = result
+ [comment, directives, type_signature]
end
- # Parses comment text and retuns a pair of RDoc::Comment and directives
+ # Parses comment text and returns a pair of RDoc::Comment and directives
def parse_comment_text_to_directives(comment_text, start_line) # :nodoc:
comment_text, directives = @preprocess.parse_comment(comment_text, start_line, :ruby)
@@ -594,7 +601,7 @@ def add_alias_method(old_name, new_name, line_no)
# Handles `attr :a, :b`, `attr_reader :a, :b`, `attr_writer :a, :b` and `attr_accessor :a, :b`
def add_attributes(names, rw, line_no)
- comment, directives = consecutive_comment(line_no)
+ comment, directives, type_signature = consecutive_comment(line_no)
handle_code_object_directives(@container, directives) if directives
return unless @container.document_children
@@ -602,6 +609,7 @@ def add_attributes(names, rw, line_no)
a = RDoc::Attr.new(nil, symbol.to_s, rw, comment, singleton: @singleton)
a.store = @store
a.line = line_no
+ a.type_signature = type_signature
record_location(a)
handle_modifier_directive(a, line_no)
@container.add_attribute(a) if should_document?(a)
@@ -640,7 +648,7 @@ def add_extends(names, line_no) # :nodoc:
def add_method(method_name, receiver_name:, receiver_fallback_type:, visibility:, singleton:, params:, calls_super:, block_params:, tokens:, start_line:, args_end_line:, end_line:)
receiver = receiver_name ? find_or_create_module_path(receiver_name, receiver_fallback_type) : @container
- comment, directives = consecutive_comment(start_line)
+ comment, directives, type_signature = consecutive_comment(start_line)
handle_code_object_directives(@container, directives) if directives
internal_add_method(
@@ -655,11 +663,12 @@ def add_method(method_name, receiver_name:, receiver_fallback_type:, visibility:
params: params,
calls_super: calls_super,
block_params: block_params,
- tokens: tokens
+ tokens: tokens,
+ type_signature: type_signature
)
end
- private def internal_add_method(method_name, container, comment:, dont_rename_initialize: false, directives:, modifier_comment_lines: nil, line_no:, visibility:, singleton:, params:, calls_super:, block_params:, tokens:) # :nodoc:
+ private def internal_add_method(method_name, container, comment:, dont_rename_initialize: false, directives:, modifier_comment_lines: nil, line_no:, visibility:, singleton:, params:, calls_super:, block_params:, tokens:, type_signature: nil) # :nodoc:
meth = RDoc::AnyMethod.new(nil, method_name, singleton: singleton)
meth.comment = comment
handle_code_object_directives(meth, directives) if directives
@@ -680,6 +689,7 @@ def add_method(method_name, receiver_name:, receiver_fallback_type:, visibility:
meth.params ||= params || '()'
meth.calls_super = calls_super
meth.block_params ||= block_params if block_params
+ meth.type_signature = type_signature
record_location(meth)
meth.start_collecting_tokens(:ruby)
tokens.each do |token|
@@ -836,6 +846,35 @@ def add_module_or_class(module_name, start_line, end_line, is_class: false, supe
mod
end
+ private
+
+ # Extracts RBS type signature lines (#: ...) from raw comment text.
+ # Mutates the input text to remove the extracted lines.
+ # Returns the type signature string, or nil if none found.
+
+ def extract_type_signature!(text, start_line)
+ return nil unless text.include?('#:')
+
+ lines = text.lines
+ sig_lines, doc_lines = lines.partition { |l| l.match?(RBS_SIG_LINE) }
+ return nil if sig_lines.empty?
+
+ text.replace(doc_lines.join)
+ type_sig = sig_lines.map { |l| l.sub(RBS_SIG_LINE, '').chomp }.join("\n")
+ validate_type_signature(type_sig, start_line + doc_lines.size)
+ type_sig
+ end
+
+ def validate_type_signature(sig, line_no)
+ sig.split("\n").each_with_index do |line, i|
+ method_error = RDoc::RbsHelper.validate_method_type(line)
+ next unless method_error
+ type_error = RDoc::RbsHelper.validate_type(line)
+ next unless type_error
+ @options.warn "#{@top_level.relative_name}:#{line_no + i}: invalid RBS type signature: #{line.inspect}"
+ end
+ end
+
class RDocVisitor < Prism::Visitor # :nodoc:
def initialize(scanner, top_level, store)
@scanner = scanner
diff --git a/lib/rdoc/rbs_helper.rb b/lib/rdoc/rbs_helper.rb
new file mode 100644
index 0000000000..3072ab99e1
--- /dev/null
+++ b/lib/rdoc/rbs_helper.rb
@@ -0,0 +1,173 @@
+# frozen_string_literal: true
+
+require 'erb'
+require 'pathname'
+require 'rbs'
+require 'rdoc/markup/formatter'
+
+##
+# RBS type signature support.
+# Loads type information from .rbs files, validates inline annotations,
+# and converts type signatures to HTML with linked type names.
+
+module RDoc
+ module RbsHelper
+ class << self
+
+ ##
+ # Validates an RBS method type signature string.
+ # Returns nil if valid, or an error message string if invalid.
+
+ def validate_method_type(sig)
+ RBS::Parser.parse_method_type(sig, require_eof: true)
+ nil
+ rescue RBS::ParsingError => e
+ e.message
+ end
+
+ ##
+ # Validates an RBS type signature string.
+ # Returns nil if valid, or an error message string if invalid.
+
+ def validate_type(sig)
+ RBS::Parser.parse_type(sig, require_eof: true)
+ nil
+ rescue RBS::ParsingError => e
+ e.message
+ end
+
+ ##
+ # Loads RBS signatures from the given directories.
+ # Returns a Hash mapping "ClassName#method_name" => "type sig string".
+
+ def load_signatures(*dirs)
+ loader = RBS::EnvironmentLoader.new
+ dirs.each { |dir| loader.add(path: Pathname(dir)) }
+
+ env = RBS::Environment.new
+ loader.load(env: env)
+
+ signatures = {}
+
+ env.class_decls.each do |type_name, entry|
+ class_name = type_name.to_s.delete_prefix('::')
+
+ entry.each_decl do |decl|
+ decl.members.each do |member|
+ case member
+ when RBS::AST::Members::MethodDefinition
+ key = member.singleton? ? "#{class_name}::#{member.name}" : "#{class_name}##{member.name}"
+ sigs = member.overloads.map { |o| o.method_type.to_s }
+ signatures[key] = sigs.join("\n")
+ when RBS::AST::Members::AttrReader, RBS::AST::Members::AttrWriter, RBS::AST::Members::AttrAccessor
+ key = "#{class_name}.#{member.name}"
+ signatures[key] = member.type.to_s
+ end
+ end
+ end
+ end
+
+ signatures
+ end
+
+ ##
+ # Converts type signature lines to HTML with type names linked to
+ # their documentation pages. Uses the RBS parser to extract type
+ # name locations precisely.
+ #
+ # +lines+ is an Array of signature line strings.
+ # +lookup+ is a Hash mapping type names to their doc paths.
+ # +from_path+ is the current page path for generating relative URLs.
+ #
+ # Returns escaped HTML with +->+ replaced by +→+.
+
+ def signature_to_html(lines, lookup:, from_path:)
+ lines.map { |line|
+ link_type_names_in_line(line, lookup, from_path).gsub('->', '→')
+ }.join("\n")
+ end
+
+ private
+
+ def link_type_names_in_line(line, lookup, from_path)
+ escaped = ERB::Util.html_escape(line)
+
+ locs = collect_type_name_locations(line)
+ return escaped if locs.empty?
+
+ result = escaped.dup
+
+ # Replace type names with links, working backwards to preserve positions.
+ # HTML escaping (e.g. -> becomes ->) shifts positions, so we
+ # re-escape the prefix to find the correct offset in the result.
+ locs.sort_by { |l| -l[:start] }.each do |loc|
+ name = loc[:name]
+ next unless (target_path = lookup[name])
+
+ prefix = ERB::Util.html_escape(line[0...loc[:start]])
+ escaped_name = ERB::Util.html_escape(name)
+ start_in_escaped = prefix.length
+ end_in_escaped = start_in_escaped + escaped_name.length
+
+ href = ::RDoc::Markup::Formatter.gen_relative_url(from_path, target_path)
+ result[start_in_escaped...end_in_escaped] =
+ "#{escaped_name}"
+ end
+
+ result
+ end
+
+ ##
+ # Extracts type name locations from a signature line using the RBS parser.
+
+ def collect_type_name_locations(line)
+ locs = []
+
+ begin
+ mt = RBS::Parser.parse_method_type(line, require_eof: true)
+ rescue RBS::ParsingError
+ begin
+ type = RBS::Parser.parse_type(line, require_eof: true)
+ collect_from_type(type, locs)
+ return locs
+ rescue RBS::ParsingError
+ return locs
+ end
+ end
+
+ mt.type.each_param { |p| collect_from_type(p.type, locs) }
+ if mt.block
+ mt.block.type.each_param { |p| collect_from_type(p.type, locs) }
+ collect_from_type(mt.block.type.return_type, locs)
+ end
+ collect_from_type(mt.type.return_type, locs)
+
+ locs
+ end
+
+ ##
+ # Recursively collects type name locations from an RBS type AST node.
+
+ def collect_from_type(type, locs)
+ case type
+ when RBS::Types::ClassInstance
+ name = type.name.to_s.delete_prefix('::')
+ if type.location
+ name_loc = type.location[:name] || type.location
+ locs << { name: name, start: name_loc.end_pos - name.length }
+ end
+ type.args.each { |a| collect_from_type(a, locs) }
+ when RBS::Types::Union, RBS::Types::Intersection, RBS::Types::Tuple
+ type.types.each { |t| collect_from_type(t, locs) }
+ when RBS::Types::Optional
+ collect_from_type(type.type, locs)
+ when RBS::Types::Record
+ type.all_fields.each_value { |t| collect_from_type(t, locs) }
+ when RBS::Types::Proc
+ type.type.each_param { |p| collect_from_type(p.type, locs) }
+ collect_from_type(type.type.return_type, locs)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/rdoc/rdoc.rb b/lib/rdoc/rdoc.rb
index 195bd21421..723163399f 100644
--- a/lib/rdoc/rdoc.rb
+++ b/lib/rdoc/rdoc.rb
@@ -5,6 +5,7 @@
require 'fileutils'
require 'pathname'
require 'time'
+require_relative 'rbs_helper'
##
# This is the driver for generating RDoc output. It handles file parsing and
@@ -492,6 +493,17 @@ def document(options)
@store.complete @options.visibility
+ # Load RBS type signatures from sig/ directory
+ begin
+ sig_dirs = []
+ sig_dir = File.join(@options.root.to_s, 'sig')
+ sig_dirs << sig_dir if File.directory?(sig_dir)
+ signatures = RDoc::RbsHelper.load_signatures(*sig_dirs)
+ @store.merge_rbs_signatures(signatures) unless signatures.empty?
+ rescue RBS::ParsingError, Errno::ENOENT, LoadError => e
+ @options.warn "Failed to load RBS type signatures: #{e.message}"
+ end
+
@stats.coverage_level = @options.coverage_report
if @options.coverage_report then
diff --git a/lib/rdoc/ri/driver.rb b/lib/rdoc/ri/driver.rb
index 014c5be4fb..9b8c36e742 100644
--- a/lib/rdoc/ri/driver.rb
+++ b/lib/rdoc/ri/driver.rb
@@ -1415,6 +1415,7 @@ def render_method(out, store, method, name) # :nodoc:
out << RDoc::Markup::Rule.new(1)
render_method_arguments out, method.arglists
+ render_method_type_signature out, method.type_signature_lines if method.type_signature
render_method_superclass out, method
if method.is_alias_for
al = method.is_alias_for
@@ -1452,6 +1453,10 @@ def render_method_comment(out, method, alias_for = nil)# :nodoc:
end
end
+ def render_method_type_signature(out, lines) # :nodoc:
+ out << RDoc::Markup::Verbatim.new(*lines.map { |s| s + "\n" })
+ end
+
def render_method_superclass(out, method) # :nodoc:
return unless
method.respond_to?(:superclass_method) and method.superclass_method
diff --git a/lib/rdoc/store.rb b/lib/rdoc/store.rb
index 379d2f2246..adcc26081e 100644
--- a/lib/rdoc/store.rb
+++ b/lib/rdoc/store.rb
@@ -343,6 +343,61 @@ def all_classes_and_modules
@classes_hash.values + @modules_hash.values
end
+ ##
+ # Returns a hash mapping class/module names to their paths, for use
+ # by type signature linking. Maps both qualified names (Foo::Bar) and
+ # unambiguous unqualified names (Bar). Ambiguous unqualified names
+ # (where multiple classes share the same name) are excluded to avoid
+ # wrong links. Cached after first call.
+
+ def type_name_lookup
+ @type_name_lookup ||= begin
+ lookup = {}
+ unqualified_names = {}
+ ambiguous_names = {}
+ all_classes_and_modules.each do |cm|
+ lookup[cm.full_name] = cm.path
+ unqualified_name = cm.name
+ next if unqualified_name == cm.full_name
+
+ if ambiguous_names[unqualified_name]
+ # already known ambiguous, skip
+ elsif unqualified_names.key?(unqualified_name)
+ unqualified_names.delete(unqualified_name)
+ ambiguous_names[unqualified_name] = true
+ else
+ unqualified_names[unqualified_name] = cm.path
+ end
+ end
+ lookup.merge!(unqualified_names)
+ end
+ end
+
+ ##
+ # Merges RBS type signatures into code objects.
+ # Inline #: annotations take priority and are not overwritten.
+
+ def merge_rbs_signatures(signatures)
+ all_classes_and_modules.each do |cm|
+ cm.method_list.each do |method|
+ next if method.type_signature
+
+ key = method.singleton ? "#{cm.full_name}::#{method.name}" : "#{cm.full_name}##{method.name}"
+ if (sig = signatures[key])
+ method.type_signature = sig
+ end
+ end
+
+ cm.attributes.each do |attr|
+ next if attr.type_signature
+
+ if (sig = signatures["#{cm.full_name}.#{attr.name}"])
+ attr.type_signature = sig
+ end
+ end
+ end
+ end
+
##
# All TopLevels known to RDoc
@@ -443,6 +498,7 @@ def clean_cache_collection(collection) # :nodoc:
# See also RDoc::Context#remove_from_documentation?
def complete(min_visibility)
+ @type_name_lookup = nil
fix_basic_object_inheritance
# cache included modules before they are removed from the documentation
diff --git a/rdoc.gemspec b/rdoc.gemspec
index 1afe52f7b6..294255d7a1 100644
--- a/rdoc.gemspec
+++ b/rdoc.gemspec
@@ -63,11 +63,12 @@ RDoc includes the +rdoc+ and +ri+ tools for generating and displaying documentat
s.rdoc_options = ["--main", "README.md"]
s.extra_rdoc_files += s.files.grep(%r[\A[^\/]+\.(?:rdoc|md)\z])
- s.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
+ s.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
s.required_rubygems_version = Gem::Requirement.new(">= 2.2")
s.add_dependency 'psych', '>= 4.0.0'
s.add_dependency 'erb'
s.add_dependency 'tsort'
- s.add_dependency 'prism', '>= 1.0.0'
+ s.add_dependency 'prism', '>= 1.6.0'
+ s.add_dependency 'rbs', '>= 4.0.0'
end
diff --git a/test/rdoc/code_object/any_method_test.rb b/test/rdoc/code_object/any_method_test.rb
index 43dc679d95..eb97257d5a 100644
--- a/test/rdoc/code_object/any_method_test.rb
+++ b/test/rdoc/code_object/any_method_test.rb
@@ -242,6 +242,27 @@ def test_marshal_dump
assert_equal section, loaded.section
end
+ def test_marshal_dump_with_type_signature
+ @store.path = Dir.tmpdir
+ top_level = @store.add_file 'file.rb'
+
+ m = RDoc::AnyMethod.new nil, 'method'
+ m.block_params = 'some_block'
+ m.call_seq = 'call_seq'
+ m.comment = 'this is a comment'
+ m.params = 'param'
+ m.type_signature = '(String) -> Integer'
+ m.record_location top_level
+
+ cm = top_level.add_class RDoc::ClassModule, 'Klass'
+ cm.add_method m
+
+ loaded = Marshal.load Marshal.dump m
+ loaded.store = @store
+
+ assert_equal '(String) -> Integer', loaded.type_signature
+ end
+
def test_marshal_load_aliased_method
aliased_method = Marshal.load Marshal.dump(@c2_a)
diff --git a/test/rdoc/code_object/attr_test.rb b/test/rdoc/code_object/attr_test.rb
index 3588743694..a226ffa5cd 100644
--- a/test/rdoc/code_object/attr_test.rb
+++ b/test/rdoc/code_object/attr_test.rb
@@ -74,6 +74,23 @@ def test_marshal_dump
assert_equal section, loaded.section
end
+ def test_marshal_dump_with_type_signature
+ @store.path = Dir.tmpdir
+ top_level = @store.add_file 'file.rb'
+
+ a = RDoc::Attr.new nil, 'name', 'R', 'a comment'
+ a.type_signature = 'String'
+ a.record_location top_level
+
+ cm = top_level.add_class RDoc::ClassModule, 'Klass'
+ cm.add_attribute a
+
+ loaded = Marshal.load Marshal.dump a
+ loaded.store = @store
+
+ assert_equal 'String', loaded.type_signature
+ end
+
def test_marshal_dump_singleton
tl = @store.add_file 'file.rb'
diff --git a/test/rdoc/parser/prism_ruby_test.rb b/test/rdoc/parser/prism_ruby_test.rb
index f018123f7b..dca28c2bd8 100644
--- a/test/rdoc/parser/prism_ruby_test.rb
+++ b/test/rdoc/parser/prism_ruby_test.rb
@@ -2374,6 +2374,160 @@ def m2; end
assert_equal "foo1\nbar1", m1.call_seq.chomp
assert_equal "ARGF.readlines(a)\nARGF.readlines(b)\nARGF.readlines(c)\nARGF.readlines(d)", m2.call_seq.chomp
end
+
+ # Type signature tests — Prism parser only (Ripper doesn't extract #: annotations)
+
+ def test_method_type_signature
+ omit 'Prism parser only' if accept_legacy_bug?
+ util_parser <<~RUBY
+ class Foo
+ # A greeting method
+ #: (String, Integer) -> void
+ def greet(name, count); end
+ end
+ RUBY
+
+ klass = @store.find_class_named 'Foo'
+ greet = klass.method_list.first
+ assert_equal 'greet', greet.name
+ assert_equal '(String, Integer) -> void', greet.type_signature
+ assert_equal 'A greeting method', greet.comment.text.strip
+ end
+
+ def test_attribute_type_signature
+ omit 'Prism parser only' if accept_legacy_bug?
+ util_parser <<~RUBY
+ class Foo
+ #: String
+ attr_reader :name
+
+ #: Integer
+ attr_accessor :count
+ end
+ RUBY
+
+ klass = @store.find_class_named 'Foo'
+ attrs = klass.attributes.sort_by(&:name)
+ assert_equal 'count', attrs[0].name
+ assert_equal 'Integer', attrs[0].type_signature
+ assert_equal 'name', attrs[1].name
+ assert_equal 'String', attrs[1].type_signature
+ end
+
+ def test_method_type_signature_multiple_overloads
+ omit 'Prism parser only' if accept_legacy_bug?
+ util_parser <<~RUBY
+ class Foo
+ # Convert a value
+ #: (String) -> Integer
+ #: (Integer) -> String
+ def convert(value); end
+ end
+ RUBY
+
+ klass = @store.find_class_named 'Foo'
+ convert = klass.method_list.first
+ assert_equal "(String) -> Integer\n(Integer) -> String", convert.type_signature
+ assert_equal 'Convert a value', convert.comment.text.strip
+ end
+
+ def test_method_without_type_signature
+ util_parser <<~RUBY
+ class Foo
+ # A plain method
+ def plain; end
+ end
+ RUBY
+
+ klass = @store.find_class_named 'Foo'
+ plain = klass.method_list.first
+ assert_nil plain.type_signature
+ assert_equal 'A plain method', plain.comment.text.strip
+ end
+
+ def test_method_type_signature_only
+ omit 'Prism parser only' if accept_legacy_bug?
+ util_parser <<~RUBY
+ class Foo
+ #: (Integer) -> void
+ def bar(x); end
+ end
+ RUBY
+
+ klass = @store.find_class_named 'Foo'
+ bar = klass.method_list.first
+ assert_equal '(Integer) -> void', bar.type_signature
+ end
+
+ def test_method_type_signature_with_blank_line_separation
+ omit 'Prism parser only' if accept_legacy_bug?
+ util_parser <<~RUBY
+ class Foo
+ # Documentation here
+ #
+ #: (String) -> void
+ def bar(x); end
+ end
+ RUBY
+
+ klass = @store.find_class_named 'Foo'
+ bar = klass.method_list.first
+ assert_equal '(String) -> void', bar.type_signature
+ assert_includes bar.comment.text, 'Documentation here'
+ end
+
+ def test_method_type_signature_with_override_comment
+ omit 'Prism parser only' if accept_legacy_bug?
+ util_parser <<~RUBY
+ class Foo
+ # @override
+ #: (untyped) -> void
+ def accept(visitor); end
+ end
+ RUBY
+
+ klass = @store.find_class_named 'Foo'
+ accept = klass.method_list.first
+ assert_equal '(untyped) -> void', accept.type_signature
+ assert_includes accept.comment.text, '@override'
+ end
+
+ def test_type_signature_real_file
+ content = File.read(File.expand_path('../../../lib/rdoc/markup/heading.rb', __dir__))
+ top_level = @store.add_file 'lib/rdoc/markup/heading.rb'
+ parser = RDoc::Parser::PrismRuby.new(top_level, content, @options, @stats)
+ parser.scan
+
+ heading_class = @store.find_class_named('RDoc::Markup::Heading')
+ assert heading_class, 'RDoc::Markup::Heading should be found'
+
+ label_method = heading_class.method_list.find { |m| m.name == 'label' }
+ assert label_method, 'label method should be found'
+ assert_equal '(RDoc::Context?) -> String', label_method.type_signature
+
+ aref_method = heading_class.method_list.find { |m| m.name == 'aref' }
+ assert aref_method, 'aref method should be found'
+ assert_equal '() -> String', aref_method.type_signature
+
+ accept_method = heading_class.method_list.find { |m| m.name == 'accept' }
+ assert accept_method, 'accept method should be found'
+ assert_equal '(untyped) -> void', accept_method.type_signature
+ end
+
+ def test_type_signature_invalid_still_stored
+ omit 'Prism parser only' if accept_legacy_bug?
+ util_parser <<~RUBY
+ class Foo
+ #: (String ->
+ def bar(x); end
+ end
+ RUBY
+
+ klass = @store.find_class_named 'Foo'
+ bar = klass.method_list.first
+ # Invalid sigs are still stored (don't block display)
+ assert_equal '(String ->', bar.type_signature
+ end
end
class RDocParserPrismRubyTest < RDoc::TestCase
diff --git a/test/rdoc/rbs_helper_test.rb b/test/rdoc/rbs_helper_test.rb
new file mode 100644
index 0000000000..fb011ccabf
--- /dev/null
+++ b/test/rdoc/rbs_helper_test.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require_relative 'helper'
+require 'rdoc/rbs_helper'
+require 'rdoc/markup/formatter'
+
+class RDocRbsHelperTest < RDoc::TestCase
+ def test_validate_method_type_valid
+ assert_nil RDoc::RbsHelper.validate_method_type('(String) -> void')
+ assert_nil RDoc::RbsHelper.validate_method_type('(Integer, ?String) -> bool')
+ assert_nil RDoc::RbsHelper.validate_method_type('() -> Array[String]')
+ end
+
+ def test_validate_method_type_invalid
+ error = RDoc::RbsHelper.validate_method_type('(String ->')
+ assert_kind_of String, error
+ end
+
+ def test_validate_type_valid
+ assert_nil RDoc::RbsHelper.validate_type('String')
+ assert_nil RDoc::RbsHelper.validate_type('Array[Integer]')
+ assert_nil RDoc::RbsHelper.validate_type('String?')
+ end
+
+ def test_validate_type_invalid
+ error = RDoc::RbsHelper.validate_type('String[')
+ assert_kind_of String, error
+ end
+
+ def test_load_signatures_from_directory
+ Dir.mktmpdir do |dir|
+ File.write(File.join(dir, 'test.rbs'), <<~RBS)
+ class Greeter
+ def greet: (String name) -> void
+ attr_reader language: String
+ end
+ RBS
+
+ sigs = RDoc::RbsHelper.load_signatures(dir)
+ assert_equal '(String name) -> void', sigs['Greeter#greet']
+ assert_equal 'String', sigs['Greeter.language']
+ end
+ end
+
+ def test_signature_to_html_links_known_types
+ lookup = { 'String' => 'String.html', 'Integer' => 'Integer.html' }
+ result = RDoc::RbsHelper.signature_to_html(["(String) -> Integer"], lookup: lookup, from_path: 'Test.html')
+
+ assert_includes result, 'String'
+ assert_includes result, 'Integer'
+ assert_includes result, '→'
+ end
+
+ def test_signature_to_html_leaves_unknown_types_plain
+ result = RDoc::RbsHelper.signature_to_html(["(UnknownType) -> void"], lookup: {}, from_path: 'Test.html')
+
+ refute_includes result, ' 'Foo/Bar.html' }
+ result = RDoc::RbsHelper.signature_to_html(["(::Foo::Bar) -> void"], lookup: lookup, from_path: 'Test.html')
+
+ assert_includes result, 'Foo::Bar'
+ end
+
+ def test_signature_to_html_multiline
+ lookup = { 'String' => 'String.html', 'Integer' => 'Integer.html' }
+ result = RDoc::RbsHelper.signature_to_html(["(String) -> Integer", "(Integer) -> String"], lookup: lookup, from_path: 'Test.html')
+
+ assert_includes result, 'String'
+ assert_includes result, 'Integer'
+ assert_includes result, "\n"
+ end
+
+ def test_signature_to_html_union_type
+ lookup = { 'String' => 'String.html', 'Integer' => 'Integer.html' }
+ result = RDoc::RbsHelper.signature_to_html(["(String | Integer) -> void"], lookup: lookup, from_path: 'Test.html')
+
+ assert_includes result, 'String'
+ assert_includes result, 'Integer'
+ end
+
+ def test_signature_to_html_optional_type
+ lookup = { 'String' => 'String.html' }
+ result = RDoc::RbsHelper.signature_to_html(["String?"], lookup: lookup, from_path: 'Test.html')
+
+ assert_includes result, 'String'
+ end
+
+ def test_signature_to_html_tuple_type
+ lookup = { 'String' => 'String.html', 'Integer' => 'Integer.html' }
+ result = RDoc::RbsHelper.signature_to_html(["[String, Integer]"], lookup: lookup, from_path: 'Test.html')
+
+ assert_includes result, 'String'
+ assert_includes result, 'Integer'
+ end
+
+ def test_signature_to_html_intersection_type
+ lookup = { 'String' => 'String.html', 'Comparable' => 'Comparable.html' }
+ result = RDoc::RbsHelper.signature_to_html(["(String & Comparable) -> void"], lookup: lookup, from_path: 'Test.html')
+
+ assert_includes result, 'String'
+ assert_includes result, 'Comparable'
+ end
+
+ def test_signature_to_html_proc_type
+ lookup = { 'String' => 'String.html', 'Integer' => 'Integer.html' }
+ result = RDoc::RbsHelper.signature_to_html(["^(String) -> Integer"], lookup: lookup, from_path: 'Test.html')
+
+ assert_includes result, 'String'
+ assert_includes result, 'Integer'
+ end
+
+ def test_signature_to_html_links_block_return_type
+ lookup = { 'String' => 'String.html', 'Integer' => 'Integer.html' }
+ result = RDoc::RbsHelper.signature_to_html(["() { (String) -> Integer } -> void"], lookup: lookup, from_path: 'Test.html')
+
+ assert_includes result, 'String'
+ assert_includes result, 'Integer'
+ end
+end
diff --git a/test/rdoc/rdoc_markdown_test.rb b/test/rdoc/rdoc_markdown_test.rb
index fc96b2985c..5ffed64f78 100644
--- a/test/rdoc/rdoc_markdown_test.rb
+++ b/test/rdoc/rdoc_markdown_test.rb
@@ -1242,6 +1242,21 @@ def test_code_fence_with_unintended_array
assert_equal expected, doc
end
+ def test_code_fence_preserves_text_indentation
+ doc = parse <<~MD
+ ```ruby
+ foo()
+ bar()
+ ```
+ MD
+
+ expected = doc(
+ verb(" foo()\n bar()\n").tap { |v| v.format = :ruby }
+ )
+
+ assert_equal expected, doc
+ end
+
def test_gfm_table
doc = parse <<~MD
| | |compare-ruby|built-ruby|
diff --git a/test/rdoc/rdoc_store_test.rb b/test/rdoc/rdoc_store_test.rb
index d9bb0bbbdb..44d3c15d8b 100644
--- a/test/rdoc/rdoc_store_test.rb
+++ b/test/rdoc/rdoc_store_test.rb
@@ -1311,4 +1311,83 @@ def test_cleanup_stale_contributions_removes_empty_class
assert_not_include @s.classes_hash, 'GoneClass'
end
+ def test_merge_rbs_signatures
+ m = RDoc::AnyMethod.new(nil, 'greet')
+ m.params = '(name)'
+ @klass.add_method m
+
+ a = RDoc::Attr.new(nil, 'language', 'R', '')
+ @klass.add_attribute a
+
+ @s.merge_rbs_signatures(
+ 'Object#greet' => '(String name) -> void',
+ 'Object.language' => 'String'
+ )
+
+ assert_equal '(String name) -> void', m.type_signature
+ assert_equal 'String', a.type_signature
+ end
+
+ def test_merge_rbs_signatures_singleton_method
+ @s.merge_rbs_signatures(
+ 'Object::cmethod' => '() -> String'
+ )
+
+ assert_equal '() -> String', @cmeth.type_signature
+ end
+
+ def test_type_name_lookup_full_names
+ @s.complete :public
+
+ lookup = @s.type_name_lookup
+ assert_equal @klass.path, lookup['Object']
+ assert_equal @nest_klass.path, lookup['Object::SubClass']
+ assert_equal @mod.path, lookup['Mod']
+ end
+
+ def test_type_name_lookup_unqualified_names
+ @s.complete :public
+
+ lookup = @s.type_name_lookup
+ assert_equal @nest_klass.path, lookup['SubClass']
+ assert_equal @mod.path, lookup['Mod']
+ end
+
+ def test_type_name_lookup_ambiguous_unqualified_name_excluded
+ file = @s.add_file 'other.rb'
+ other_klass = file.add_class RDoc::NormalClass, 'Other::SubClass'
+ other_klass.record_location file
+ @s.complete :public
+
+ lookup = @s.type_name_lookup
+
+ # Both qualified names are present
+ assert_equal @nest_klass.path, lookup['Object::SubClass']
+ assert_equal other_klass.path, lookup['Other::SubClass']
+
+ # Ambiguous unqualified name is excluded to avoid wrong links
+ refute lookup.key?('SubClass')
+ end
+
+ def test_type_name_lookup_cached
+ @s.complete :public
+
+ lookup1 = @s.type_name_lookup
+ lookup2 = @s.type_name_lookup
+ assert_same lookup1, lookup2
+ end
+
+ def test_merge_rbs_signatures_does_not_overwrite_inline_annotations
+ m = RDoc::AnyMethod.new(nil, 'greet')
+ m.params = '(name)'
+ m.type_signature = '(String) -> void'
+ @klass.add_method m
+
+ @s.merge_rbs_signatures(
+ 'Object#greet' => '(String name, ?Integer count) -> void'
+ )
+
+ assert_equal '(String) -> void', m.type_signature
+ end
+
end
diff --git a/test/rdoc/ri/driver_test.rb b/test/rdoc/ri/driver_test.rb
index 2d1a2ce741..ec6a1c8475 100644
--- a/test/rdoc/ri/driver_test.rb
+++ b/test/rdoc/ri/driver_test.rb
@@ -737,6 +737,20 @@ def test_display_method
assert_match %r%blah.6%, out
end
+ def test_display_method_with_type_signature
+ util_store
+
+ @blah.type_signature = '(Integer) -> String'
+ @store1.save
+
+ out, = capture_output do
+ @driver.display_method 'Foo::Bar#blah'
+ end
+
+ assert_match %r%Foo::Bar#blah%, out
+ assert_match %r%\(Integer\) -> String%, out
+ end
+
def test_display_method_attribute
util_store