Skip to content

Commit bfcd437

Browse files
committed
Extract and display RBS type signatures in documentation
Add `type_signature` accessor to `MethodAttr`. During Prism parsing, extract `#:` annotation lines from comment blocks and store them on methods and attributes. Bump Marshal to v4 for RI serialization. Render type signatures in the aliki theme with server-side type name linking via `RbsSupport.signature_to_html`. Uses the RBS parser to extract type name locations precisely for linking to documentation pages. Validate annotations through `RBS::Parser` — invalid sigs emit a warning but are still displayed. Load additional signatures from `sig/` directories and RBS core types via `RBS::EnvironmentLoader`. Display type signatures in `ri` terminal output.
1 parent a5d0e1b commit bfcd437

File tree

18 files changed

+722
-12
lines changed

18 files changed

+722
-12
lines changed

lib/rdoc/code_object/any_method.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class RDoc::AnyMethod < RDoc::MethodAttr
1414
# RDoc 4.1
1515
# Added is_alias_for
1616

17-
MARSHAL_VERSION = 3 # :nodoc:
17+
MARSHAL_VERSION = 4 # :nodoc:
1818

1919
##
2020
# Don't rename \#initialize to \::new
@@ -166,6 +166,7 @@ def marshal_dump
166166
@parent.class,
167167
@section.title,
168168
is_alias_for,
169+
@type_signature,
169170
]
170171
end
171172

@@ -204,6 +205,7 @@ def marshal_load(array)
204205
@parent_title = array[13]
205206
@section_title = array[14]
206207
@is_alias_for = array[15]
208+
@type_signature = array[16]
207209

208210
array[8].each do |new_name, document|
209211
add_alias RDoc::Alias.new(nil, @name, new_name, RDoc::Comment.from_document(document), singleton: @singleton)

lib/rdoc/code_object/attr.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class RDoc::Attr < RDoc::MethodAttr
1111
# Added parent name and class
1212
# Added section title
1313

14-
MARSHAL_VERSION = 3 # :nodoc:
14+
MARSHAL_VERSION = 4 # :nodoc:
1515

1616
##
1717
# Is the attribute readable ('R'), writable ('W') or both ('RW')?
@@ -108,7 +108,8 @@ def marshal_dump
108108
@file.relative_name,
109109
@parent.full_name,
110110
@parent.class,
111-
@section.title
111+
@section.title,
112+
@type_signature,
112113
]
113114
end
114115

@@ -140,6 +141,7 @@ def marshal_load(array)
140141
@parent_name = array[8]
141142
@parent_class = array[9]
142143
@section_title = array[10]
144+
@type_signature = array[11]
143145

144146
@file = RDoc::TopLevel.new array[7] if version > 1
145147

lib/rdoc/code_object/method_attr.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ class RDoc::MethodAttr < RDoc::CodeObject
5858

5959
attr_accessor :call_seq
6060

61+
##
62+
# RBS type signature from inline #: annotations
63+
64+
attr_accessor :type_signature
65+
6166
##
6267
# The call_seq or the param_seq with method name, if there is no call_seq.
6368

@@ -86,6 +91,7 @@ def initialize(text, name, singleton: false)
8691
@block_params = nil
8792
@call_seq = nil
8893
@params = nil
94+
@type_signature = nil
8995
end
9096

9197
##

lib/rdoc/generator/markup.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,26 @@ def markup_code
141141
src
142142
end
143143

144+
##
145+
# Returns the type signature as HTML with type names linked to their
146+
# documentation pages. Delegates to RDoc::RbsSupport for RBS-aware parsing.
147+
#
148+
# Falls back to escaped HTML without links when the store or parent
149+
# path is unavailable (e.g. in ri mode).
150+
151+
def type_signature_html
152+
return unless @type_signature
153+
154+
store = @store || parent&.store
155+
from_path = parent&.path
156+
lookup = store&.type_name_lookup
157+
158+
RDoc::RbsSupport.signature_to_html(@type_signature, lookup: lookup) do |name, target_path|
159+
href = RDoc::Markup::ToHtml.gen_relative_url(from_path, target_path)
160+
"<a href=\"#{href}\" class=\"rbs-type\">#{ERB::Util.html_escape(name)}</a>"
161+
end
162+
end
163+
144164
end
145165

146166
class RDoc::ClassModule

lib/rdoc/generator/template/aliki/class.rhtml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,9 @@
9191
<div class="method-heading attribute-method-heading">
9292
<a href="#<%= attrib.aref %>" title="Link to this attribute">
9393
<span class="method-name"><%= h attrib.name %></span>
94-
<span class="attribute-access-type">[<%= attrib.rw %>]</span>
95-
</a>
94+
<span class="attribute-access-type">[<%= attrib.rw %>]</span></a><%- if attrib.type_signature %>
95+
<span class="method-type-signature"><code><%= attrib.type_signature_html %></code></span>
96+
<%- end %>
9697
</div>
9798

9899
<div class="method-description">
@@ -150,6 +151,10 @@
150151
</a>
151152
</div>
152153
<%- end %>
154+
155+
<%- if method.type_signature %>
156+
<pre class="method-type-signature"><code><%= method.type_signature_html %></code></pre>
157+
<%- end %>
153158
</div>
154159

155160
<%- if method.token_stream %>

lib/rdoc/generator/template/aliki/css/rdoc.css

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1075,6 +1075,20 @@ main h6 a:hover {
10751075
font-style: italic;
10761076
}
10771077

1078+
/* RBS Type Signature Links — linked types get subtle underline */
1079+
a.rbs-type {
1080+
color: inherit;
1081+
text-decoration: underline;
1082+
text-decoration-color: var(--color-border-default);
1083+
text-underline-offset: 0.2em;
1084+
transition: text-decoration-color var(--transition-fast), color var(--transition-fast);
1085+
}
1086+
1087+
a.rbs-type:hover {
1088+
color: var(--color-link-hover);
1089+
text-decoration-color: var(--color-link-hover);
1090+
}
1091+
10781092
/* Emphasis */
10791093
em {
10801094
text-decoration-color: var(--color-emphasis-decoration);
@@ -1334,6 +1348,49 @@ main .method-heading .method-args {
13341348
font-weight: var(--font-weight-normal);
13351349
}
13361350

1351+
/* Type signatures — overloads stack as a code block under the method name */
1352+
pre.method-type-signature {
1353+
position: relative;
1354+
margin: var(--space-2) 0 0;
1355+
padding: var(--space-2) 0 0;
1356+
background: transparent;
1357+
border: none;
1358+
border-radius: 0;
1359+
overflow: visible;
1360+
font-family: var(--font-code);
1361+
font-size: var(--font-size-sm);
1362+
color: var(--color-text-tertiary);
1363+
line-height: var(--line-height-tight);
1364+
white-space: pre-wrap;
1365+
overflow-wrap: break-word;
1366+
}
1367+
1368+
pre.method-type-signature::before {
1369+
content: '';
1370+
position: absolute;
1371+
top: 0;
1372+
left: 0;
1373+
right: 0;
1374+
border-top: 1px dotted var(--color-border-default);
1375+
}
1376+
1377+
pre.method-type-signature code {
1378+
font-family: inherit;
1379+
font-size: inherit;
1380+
color: inherit;
1381+
background: transparent;
1382+
padding: 0;
1383+
}
1384+
1385+
/* Attribute type sigs render inline after the [RW] badge */
1386+
main .method-heading > .method-type-signature {
1387+
display: inline;
1388+
margin-left: var(--space-2);
1389+
font-family: var(--font-code);
1390+
font-size: var(--font-size-sm);
1391+
color: var(--color-text-secondary);
1392+
}
1393+
13371394
main .method-controls {
13381395
position: absolute;
13391396
top: var(--space-3);
@@ -1440,6 +1497,10 @@ main .attribute-access-type {
14401497
font-size: var(--font-size-base);
14411498
}
14421499

1500+
pre.method-type-signature {
1501+
font-size: var(--font-size-xs);
1502+
}
1503+
14431504
main .method-header {
14441505
padding: var(--space-2);
14451506
}

lib/rdoc/generator/template/aliki/js/aliki.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -435,8 +435,8 @@ function wrapCodeBlocksWithCopyButton() {
435435
// not directly in rhtml templates
436436
// - Modifying the formatter would require extending RDoc's core internals
437437

438-
// Find all pre elements that are not already wrapped
439-
const preElements = document.querySelectorAll('main pre:not(.code-block-wrapper pre)');
438+
// Target code examples and source code; skip type signature blocks
439+
const preElements = document.querySelectorAll('main pre:not(.method-type-signature)');
440440

441441
preElements.forEach((pre) => {
442442
// Skip if already wrapped

lib/rdoc/parser/prism_ruby.rb

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,37 @@ def skip_comments_until(line_no_until)
461461
def consecutive_comment(line_no)
462462
return unless @unprocessed_comments.first&.first == line_no
463463
_line_no, start_line, text = @unprocessed_comments.shift
464-
parse_comment_text_to_directives(text, start_line)
464+
type_signature = extract_type_signature!(text)
465+
result = parse_comment_text_to_directives(text, start_line)
466+
return unless result
467+
comment, directives = result
468+
[comment, directives, type_signature]
469+
end
470+
471+
# Extracts RBS type signature lines (#: ...) from raw comment text.
472+
# Mutates the input text to remove the extracted lines.
473+
# Returns the type signature string, or nil if none found.
474+
private def extract_type_signature!(text)
475+
return nil unless text.include?('#:')
476+
477+
lines = text.lines
478+
sig_lines, doc_lines = lines.partition { |l| l.match?(/\A#:\s/) }
479+
return nil if sig_lines.empty?
480+
481+
text.replace(doc_lines.join)
482+
type_sig = sig_lines.map { |l| l.sub(/\A#:\s?/, '').chomp }.join("\n")
483+
validate_type_signature(type_sig)
484+
type_sig
485+
end
486+
487+
private def validate_type_signature(sig)
488+
sig.split("\n").each do |line|
489+
method_error = RDoc::RbsSupport.validate_method_type(line)
490+
next unless method_error
491+
type_error = RDoc::RbsSupport.validate_type(line)
492+
next unless type_error
493+
@options.warn "Invalid RBS type signature: #{line.inspect}"
494+
end
465495
end
466496

467497
# Parses comment text and retuns a pair of RDoc::Comment and directives
@@ -594,14 +624,15 @@ def add_alias_method(old_name, new_name, line_no)
594624
# Handles `attr :a, :b`, `attr_reader :a, :b`, `attr_writer :a, :b` and `attr_accessor :a, :b`
595625

596626
def add_attributes(names, rw, line_no)
597-
comment, directives = consecutive_comment(line_no)
627+
comment, directives, type_signature = consecutive_comment(line_no)
598628
handle_code_object_directives(@container, directives) if directives
599629
return unless @container.document_children
600630

601631
names.each do |symbol|
602632
a = RDoc::Attr.new(nil, symbol.to_s, rw, comment, singleton: @singleton)
603633
a.store = @store
604634
a.line = line_no
635+
a.type_signature = type_signature
605636
record_location(a)
606637
handle_modifier_directive(a, line_no)
607638
@container.add_attribute(a) if should_document?(a)
@@ -640,7 +671,7 @@ def add_extends(names, line_no) # :nodoc:
640671

641672
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:)
642673
receiver = receiver_name ? find_or_create_module_path(receiver_name, receiver_fallback_type) : @container
643-
comment, directives = consecutive_comment(start_line)
674+
comment, directives, type_signature = consecutive_comment(start_line)
644675
handle_code_object_directives(@container, directives) if directives
645676

646677
internal_add_method(
@@ -655,11 +686,12 @@ def add_method(method_name, receiver_name:, receiver_fallback_type:, visibility:
655686
params: params,
656687
calls_super: calls_super,
657688
block_params: block_params,
658-
tokens: tokens
689+
tokens: tokens,
690+
type_signature: type_signature
659691
)
660692
end
661693

662-
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:
694+
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:
663695
meth = RDoc::AnyMethod.new(nil, method_name, singleton: singleton)
664696
meth.comment = comment
665697
handle_code_object_directives(meth, directives) if directives
@@ -680,6 +712,7 @@ def add_method(method_name, receiver_name:, receiver_fallback_type:, visibility:
680712
meth.params ||= params || '()'
681713
meth.calls_super = calls_super
682714
meth.block_params ||= block_params if block_params
715+
meth.type_signature = type_signature
683716
record_location(meth)
684717
meth.start_collecting_tokens(:ruby)
685718
tokens.each do |token|

0 commit comments

Comments
 (0)