Skip to content

Commit fd4b149

Browse files
committed
Extract and display RBS type signatures in documentation
Add support for displaying RBS type signatures in both HTML and RI output. Type information is sourced from inline `#:` annotations (parsed by the Prism parser) and from `.rbs` files in the project's `sig/` directory plus RBS stdlib declarations. Implementation: - `RDoc::RbsHelper` module: loads RBS signatures, validates types, renders type signatures as HTML with linked type names - Parser extracts `#:` annotation lines via `RBS_SIG_LINE` constant, validates them, and attaches to `MethodAttr#type_signature` - `Store#merge_rbs_signatures` fills in signatures from `.rbs` files where inline annotations are absent - `Store#type_name_lookup` maps qualified and unambiguous unqualified names to documentation paths for type linking - Aliki theme: type signatures render as styled `<pre>` blocks under method headings, with linked type names using `a.rbs-type` class - RI driver: type signatures display as verbatim blocks - `MARSHAL_VERSION` bumped to 4 for `AnyMethod` and `Attr`
1 parent e8b7ec5 commit fd4b149

File tree

18 files changed

+805
-11
lines changed

18 files changed

+805
-11
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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,18 @@ class RDoc::MethodAttr < RDoc::CodeObject
5858

5959
attr_accessor :call_seq
6060

61+
##
62+
# RBS type signature from inline annotations or loaded .rbs files
63+
64+
attr_accessor :type_signature
65+
66+
##
67+
# Returns the type signature split into individual lines.
68+
69+
def type_signature_lines
70+
@type_signature&.split("\n")
71+
end
72+
6173
##
6274
# The call_seq or the param_seq with method name, if there is no call_seq.
6375

@@ -86,6 +98,7 @@ def initialize(text, name, singleton: false)
8698
@block_params = nil
8799
@call_seq = nil
88100
@params = nil
101+
@type_signature = nil
89102
end
90103

91104
##

lib/rdoc/generator/aliki.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,21 @@ def write_search_index
117117
File.write search_index_path, "var search_data = #{JSON.generate(data)};"
118118
end
119119

120+
##
121+
# Returns the type signature of +method_attr+ as HTML with linked type names.
122+
# Returns nil if no type signature is present.
123+
124+
def type_signature_html(method_attr, from_path)
125+
lines = method_attr.type_signature_lines
126+
return unless lines
127+
128+
RDoc::RbsHelper.signature_to_html(
129+
lines,
130+
lookup: @store.type_name_lookup,
131+
from_path: from_path
132+
)
133+
end
134+
120135
##
121136
# Resolves a URL for use in templates. Absolute URLs are returned unchanged.
122137
# Relative URLs are prefixed with rel_prefix to ensure they resolve correctly from any page.

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@
9393
<span class="method-name"><%= h attrib.name %></span>
9494
<span class="attribute-access-type">[<%= attrib.rw %>]</span>
9595
</a>
96+
<%- if attrib.type_signature %>
97+
<span class="method-type-signature"><code><%= type_signature_html(attrib, klass.path) %></code></span>
98+
<%- end %>
9699
</div>
97100

98101
<div class="method-description">
@@ -150,6 +153,10 @@
150153
</a>
151154
</div>
152155
<%- end %>
156+
157+
<%- if method.type_signature %>
158+
<pre class="method-type-signature"><code><%= type_signature_html(method, klass.path) %></code></pre>
159+
<%- end %>
153160
</div>
154161

155162
<%- 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);
@@ -1335,6 +1349,49 @@ main .method-heading .method-args {
13351349
font-weight: var(--font-weight-normal);
13361350
}
13371351

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

1504+
pre.method-type-signature {
1505+
font-size: var(--font-size-xs);
1506+
}
1507+
14471508
main .method-header {
14481509
padding: var(--space-2);
14491510
padding-right: var(--space-2);

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: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ class RDoc::Parser::PrismRuby < RDoc::Parser
133133

134134
parse_files_matching(/\.rbw?$/) unless ENV['RDOC_USE_RIPPER_PARSER']
135135

136+
# Matches an RBS inline type annotation line: #: followed by whitespace
137+
RBS_SIG_LINE = /\A#:\s/ # :nodoc:
138+
136139
attr_accessor :visibility
137140
attr_reader :container, :singleton, :in_proc_block
138141

@@ -461,10 +464,14 @@ def skip_comments_until(line_no_until)
461464
def consecutive_comment(line_no)
462465
return unless @unprocessed_comments.first&.first == line_no
463466
_line_no, start_line, text = @unprocessed_comments.shift
464-
parse_comment_text_to_directives(text, start_line)
467+
type_signature = extract_type_signature!(text, start_line)
468+
result = parse_comment_text_to_directives(text, start_line)
469+
return unless result
470+
comment, directives = result
471+
[comment, directives, type_signature]
465472
end
466473

467-
# Parses comment text and retuns a pair of RDoc::Comment and directives
474+
# Parses comment text and returns a pair of RDoc::Comment and directives
468475

469476
def parse_comment_text_to_directives(comment_text, start_line) # :nodoc:
470477
comment_text, directives = @preprocess.parse_comment(comment_text, start_line, :ruby)
@@ -594,14 +601,15 @@ def add_alias_method(old_name, new_name, line_no)
594601
# Handles `attr :a, :b`, `attr_reader :a, :b`, `attr_writer :a, :b` and `attr_accessor :a, :b`
595602

596603
def add_attributes(names, rw, line_no)
597-
comment, directives = consecutive_comment(line_no)
604+
comment, directives, type_signature = consecutive_comment(line_no)
598605
handle_code_object_directives(@container, directives) if directives
599606
return unless @container.document_children
600607

601608
names.each do |symbol|
602609
a = RDoc::Attr.new(nil, symbol.to_s, rw, comment, singleton: @singleton)
603610
a.store = @store
604611
a.line = line_no
612+
a.type_signature = type_signature
605613
record_location(a)
606614
handle_modifier_directive(a, line_no)
607615
@container.add_attribute(a) if should_document?(a)
@@ -640,7 +648,7 @@ def add_extends(names, line_no) # :nodoc:
640648

641649
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:)
642650
receiver = receiver_name ? find_or_create_module_path(receiver_name, receiver_fallback_type) : @container
643-
comment, directives = consecutive_comment(start_line)
651+
comment, directives, type_signature = consecutive_comment(start_line)
644652
handle_code_object_directives(@container, directives) if directives
645653

646654
internal_add_method(
@@ -655,11 +663,12 @@ def add_method(method_name, receiver_name:, receiver_fallback_type:, visibility:
655663
params: params,
656664
calls_super: calls_super,
657665
block_params: block_params,
658-
tokens: tokens
666+
tokens: tokens,
667+
type_signature: type_signature
659668
)
660669
end
661670

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:
671+
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:
663672
meth = RDoc::AnyMethod.new(nil, method_name, singleton: singleton)
664673
meth.comment = comment
665674
handle_code_object_directives(meth, directives) if directives
@@ -680,6 +689,7 @@ def add_method(method_name, receiver_name:, receiver_fallback_type:, visibility:
680689
meth.params ||= params || '()'
681690
meth.calls_super = calls_super
682691
meth.block_params ||= block_params if block_params
692+
meth.type_signature = type_signature
683693
record_location(meth)
684694
meth.start_collecting_tokens(:ruby)
685695
tokens.each do |token|
@@ -836,6 +846,35 @@ def add_module_or_class(module_name, start_line, end_line, is_class: false, supe
836846
mod
837847
end
838848

849+
private
850+
851+
# Extracts RBS type signature lines (#: ...) from raw comment text.
852+
# Mutates the input text to remove the extracted lines.
853+
# Returns the type signature string, or nil if none found.
854+
855+
def extract_type_signature!(text, start_line)
856+
return nil unless text.include?('#:')
857+
858+
lines = text.lines
859+
sig_lines, doc_lines = lines.partition { |l| l.match?(RBS_SIG_LINE) }
860+
return nil if sig_lines.empty?
861+
862+
text.replace(doc_lines.join)
863+
type_sig = sig_lines.map { |l| l.sub(RBS_SIG_LINE, '').chomp }.join("\n")
864+
validate_type_signature(type_sig, start_line + doc_lines.size)
865+
type_sig
866+
end
867+
868+
def validate_type_signature(sig, line_no)
869+
sig.split("\n").each_with_index do |line, i|
870+
method_error = RDoc::RbsHelper.validate_method_type(line)
871+
next unless method_error
872+
type_error = RDoc::RbsHelper.validate_type(line)
873+
next unless type_error
874+
@options.warn "#{@top_level.relative_name}:#{line_no + i}: invalid RBS type signature: #{line.inspect}"
875+
end
876+
end
877+
839878
class RDocVisitor < Prism::Visitor # :nodoc:
840879
def initialize(scanner, top_level, store)
841880
@scanner = scanner

0 commit comments

Comments
 (0)