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..f968543bdd 100644 --- a/lib/rdoc/code_object/method_attr.rb +++ b/lib/rdoc/code_object/method_attr.rb @@ -58,6 +58,11 @@ class RDoc::MethodAttr < RDoc::CodeObject attr_accessor :call_seq + ## + # RBS type signature from inline #: annotations + + attr_accessor :type_signature + ## # The call_seq or the param_seq with method name, if there is no call_seq. @@ -86,6 +91,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/markup.rb b/lib/rdoc/generator/markup.rb index dc4556c019..aa5857413d 100644 --- a/lib/rdoc/generator/markup.rb +++ b/lib/rdoc/generator/markup.rb @@ -141,6 +141,26 @@ def markup_code src end + ## + # Returns the type signature as HTML with type names linked to their + # documentation pages. Delegates to RDoc::RbsSupport for RBS-aware parsing. + # + # Falls back to escaped HTML without links when the store or parent + # path is unavailable (e.g. in ri mode). + + def type_signature_html + return unless @type_signature + + store = @store || parent&.store + from_path = parent&.path + lookup = store&.type_name_lookup + + RDoc::RbsSupport.signature_to_html(@type_signature, lookup: lookup) do |name, target_path| + href = RDoc::Markup::ToHtml.gen_relative_url(from_path, target_path) + "#{ERB::Util.html_escape(name)}" + end + end + end class RDoc::ClassModule diff --git a/lib/rdoc/generator/template/aliki/class.rhtml b/lib/rdoc/generator/template/aliki/class.rhtml index ba1238b9e9..eed247f072 100644 --- a/lib/rdoc/generator/template/aliki/class.rhtml +++ b/lib/rdoc/generator/template/aliki/class.rhtml @@ -91,8 +91,9 @@
<%= attrib.type_signature_html %>
+ <%- end %>
<%= method.type_signature_html %>
+ <%- 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 0ebfa344bf..bcc333300c 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);
@@ -1334,6 +1348,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);
@@ -1440,6 +1497,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);
}
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/parser/prism_ruby.rb b/lib/rdoc/parser/prism_ruby.rb
index e8dedd5c18..9a853e8748 100644
--- a/lib/rdoc/parser/prism_ruby.rb
+++ b/lib/rdoc/parser/prism_ruby.rb
@@ -461,7 +461,37 @@ 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)
+ result = parse_comment_text_to_directives(text, start_line)
+ return unless result
+ comment, directives = result
+ [comment, directives, type_signature]
+ end
+
+ # 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.
+ private def extract_type_signature!(text)
+ return nil unless text.include?('#:')
+
+ lines = text.lines
+ sig_lines, doc_lines = lines.partition { |l| l.match?(/\A#:\s/) }
+ return nil if sig_lines.empty?
+
+ text.replace(doc_lines.join)
+ type_sig = sig_lines.map { |l| l.sub(/\A#:\s?/, '').chomp }.join("\n")
+ validate_type_signature(type_sig)
+ type_sig
+ end
+
+ private def validate_type_signature(sig)
+ sig.split("\n").each do |line|
+ method_error = RDoc::RbsSupport.validate_method_type(line)
+ next unless method_error
+ type_error = RDoc::RbsSupport.validate_type(line)
+ next unless type_error
+ @options.warn "Invalid RBS type signature: #{line.inspect}"
+ end
end
# Parses comment text and retuns a pair of RDoc::Comment and directives
@@ -594,7 +624,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 +632,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 +671,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 +686,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 +712,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|
diff --git a/lib/rdoc/rbs_support.rb b/lib/rdoc/rbs_support.rb
new file mode 100644
index 0000000000..2999349d6f
--- /dev/null
+++ b/lib/rdoc/rbs_support.rb
@@ -0,0 +1,205 @@
+# frozen_string_literal: true
+
+require 'erb'
+require 'rbs'
+
+##
+# 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 RbsSupport
+ 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
+
+ ##
+ # Merges loaded RBS signatures into the store's code objects.
+ # Inline #: annotations take priority and are not overwritten.
+
+ def merge_into_store(store, signatures)
+ store.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
+
+ ##
+ # Converts a type signature string to HTML with type names linked
+ # to their documentation pages. Uses the RBS parser to extract type
+ # name locations precisely.
+ #
+ # +lookup+ is a Hash mapping type names to their doc paths.
+ # +&resolve_link+ is a block that receives (name, target_path) and
+ # returns an HTML string for the link. This decouples RbsSupport
+ # from the generator's URL resolution.
+ #
+ # Returns escaped HTML with +->+ replaced by +→+.
+
+ def signature_to_html(signature, lookup: nil, &resolve_link)
+ signature.split("\n").map { |line|
+ html = link_type_names_in_line(line, lookup, &resolve_link)
+ html.gsub('->', '→')
+ }.join("\n")
+ end
+
+ private
+
+ ##
+ # Links type names in a single signature line using the RBS parser.
+ # Falls back to plain HTML escaping if no lookup or block is given.
+
+ def link_type_names_in_line(line, lookup, &resolve_link)
+ escaped = ERB::Util.html_escape(line)
+ return escaped unless lookup && resolve_link
+
+ locs = collect_type_name_locations(line)
+ return escaped if locs.empty?
+
+ result = escaped.dup
+
+ # Replace type names with links, working backwards to preserve positions
+ locs.sort_by { |l| -l[:start] }.each do |loc|
+ name = loc[:name]
+ next unless lookup[name]
+
+ # Map original string positions to escaped string positions.
+ # HTML escaping (e.g. -> becomes ->) shifts positions, so we
+ # re-escape the prefix to find the correct offset in the result.
+ 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
+
+ link = resolve_link.call(name, lookup[name])
+ result[start_in_escaped...end_in_escaped] = link
+ 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, stop: name_loc.end_pos }
+ end
+ type.args.each { |a| collect_from_type(a, locs) }
+ when RBS::Types::Union, RBS::Types::Intersection
+ type.types.each { |t| collect_from_type(t, locs) }
+ when RBS::Types::Optional
+ collect_from_type(type.type, locs)
+ when RBS::Types::Tuple
+ type.types.each { |t| collect_from_type(t, 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..f1d097752d 100644
--- a/lib/rdoc/rdoc.rb
+++ b/lib/rdoc/rdoc.rb
@@ -5,6 +5,7 @@
require 'fileutils'
require 'pathname'
require 'time'
+require_relative 'rbs_support'
##
# 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 core types and 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::RbsSupport.load_signatures(*sig_dirs)
+ RDoc::RbsSupport.merge_into_store(@store, signatures) unless signatures.empty?
+ rescue => 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..670be70783 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
render_method_superclass out, method
if method.is_alias_for
al = method.is_alias_for
@@ -1452,6 +1453,14 @@ def render_method_comment(out, method, alias_for = nil)# :nodoc:
end
end
+ def render_method_type_signature(out, type_signature) # :nodoc:
+ return unless type_signature
+
+ sigs = type_signature.split("\n").map { |s| s + "\n" }
+ out << RDoc::Markup::Verbatim.new(*sigs)
+ out << RDoc::Markup::BlankLine.new
+ 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 bcba7ddabb..cb373095de 100644
--- a/lib/rdoc/store.rb
+++ b/lib/rdoc/store.rb
@@ -343,6 +343,22 @@ 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 full names (Foo::Bar) and
+ # short names (Bar). Cached after first call.
+
+ def type_name_lookup
+ @type_name_lookup ||= begin
+ lookup = {}
+ all_classes_and_modules.each do |cm|
+ lookup[cm.full_name] = cm.path
+ lookup[cm.name] = cm.path unless lookup.key?(cm.name)
+ end
+ lookup
+ end
+ end
+
##
# All TopLevels known to RDoc
@@ -443,6 +459,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/code_object/method_attr_test.rb b/test/rdoc/code_object/method_attr_test.rb
index bffcb799d3..ac3ab2d092 100644
--- a/test/rdoc/code_object/method_attr_test.rb
+++ b/test/rdoc/code_object/method_attr_test.rb
@@ -207,4 +207,53 @@ def test_to_s
assert_equal 'RDoc::AnyMethod: C1::m', @c1__m.to_s
end
+ def test_type_signature_html_links_known_types
+ @c1_m.type_signature = "(C2) -> C1"
+
+ result = @c1_m.type_signature_html
+ assert_includes result, 'C2'
+ assert_includes result, '>C1'
+ assert_includes result, '→'
+ end
+
+ def test_type_signature_html_leaves_unknown_types_plain
+ @c1_m.type_signature = "(UnknownType) -> void"
+
+ result = @c1_m.type_signature_html
+ refute_includes result, ' void"
+
+ result = @c1_m.type_signature_html
+ assert_includes result, '>C2::C3'
+ end
+
+ def test_type_signature_html_nil_without_signature
+ assert_nil @c1_m.type_signature_html
+ end
+
+ def test_type_signature_html_multiline
+ @c1_m.type_signature = "(C1) -> C2\n(C2) -> C1"
+
+ result = @c1_m.type_signature_html
+ assert_includes result, '>C1'
+ assert_includes result, '>C2'
+ assert_includes result, "\n"
+ end
+
+ def test_type_signature_html_escapes_html
+ @c1_m.type_signature = "(Array[String]) -> void"
+
+ result = @c1_m.type_signature_html
+ refute_includes result, ' 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_support_test.rb b/test/rdoc/rbs_support_test.rb
new file mode 100644
index 0000000000..b5545dd933
--- /dev/null
+++ b/test/rdoc/rbs_support_test.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require_relative 'helper'
+require 'rdoc/rbs_support'
+
+class RDocRbsSupportTest < RDoc::TestCase
+ def test_validate_method_type_valid
+ assert_nil RDoc::RbsSupport.validate_method_type('(String) -> void')
+ assert_nil RDoc::RbsSupport.validate_method_type('(Integer, ?String) -> bool')
+ assert_nil RDoc::RbsSupport.validate_method_type('() -> Array[String]')
+ end
+
+ def test_validate_method_type_invalid
+ error = RDoc::RbsSupport.validate_method_type('(String ->')
+ assert_kind_of String, error
+ end
+
+ def test_validate_type_valid
+ assert_nil RDoc::RbsSupport.validate_type('String')
+ assert_nil RDoc::RbsSupport.validate_type('Array[Integer]')
+ assert_nil RDoc::RbsSupport.validate_type('String?')
+ end
+
+ def test_validate_type_invalid
+ error = RDoc::RbsSupport.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::RbsSupport.load_signatures(dir)
+ assert_equal '(String name) -> void', sigs['Greeter#greet']
+ assert_equal 'String', sigs['Greeter.language']
+ end
+ end
+
+ def test_merge_into_store
+ top_level = @store.add_file 'test.rb'
+ cm = top_level.add_class RDoc::NormalClass, 'Greeter'
+
+ m = RDoc::AnyMethod.new(nil, 'greet')
+ m.params = '(name)'
+ cm.add_method m
+
+ a = RDoc::Attr.new(nil, 'language', 'R', '')
+ cm.add_attribute a
+
+ signatures = {
+ 'Greeter#greet' => '(String name) -> void',
+ 'Greeter.language' => 'String'
+ }
+
+ RDoc::RbsSupport.merge_into_store(@store, signatures)
+
+ assert_equal '(String name) -> void', m.type_signature
+ assert_equal 'String', a.type_signature
+ end
+
+ def test_merge_does_not_overwrite_inline_annotations
+ top_level = @store.add_file 'test.rb'
+ cm = top_level.add_class RDoc::NormalClass, 'Greeter'
+
+ m = RDoc::AnyMethod.new(nil, 'greet')
+ m.params = '(name)'
+ m.type_signature = '(String) -> void'
+ cm.add_method m
+
+ signatures = {
+ 'Greeter#greet' => '(String name, ?Integer count) -> void'
+ }
+
+ RDoc::RbsSupport.merge_into_store(@store, signatures)
+
+ 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