Skip to content

Commit bace941

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 fd5ca53 commit bace941

File tree

18 files changed

+817
-11
lines changed

18 files changed

+817
-11
lines changed

lib/rdoc/code_object/any_method.rb

Lines changed: 4 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
@@ -60,6 +60,7 @@ def add_alias(an_alias, context = nil)
6060
method.visibility = self.visibility
6161
method.comment = an_alias.comment
6262
method.is_alias_for = self
63+
method.type_signature = self.type_signature
6364
@aliases << method
6465
context.add_method method if context
6566
method
@@ -166,6 +167,7 @@ def marshal_dump
166167
@parent.class,
167168
@section.title,
168169
is_alias_for,
170+
@type_signature,
169171
]
170172
end
171173

@@ -204,6 +206,7 @@ def marshal_load(array)
204206
@parent_title = array[13]
205207
@section_title = array[14]
206208
@is_alias_for = array[15]
209+
@type_signature = array[16]
207210

208211
array[8].each do |new_name, document|
209212
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: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require 'prism'
44
require_relative 'ripper_state_lex'
5+
require_relative '../rbs_helper'
56

67
# Unlike lib/rdoc/parser/ripper_ruby.rb, this file is not based on rtags and does not contain code from
78
# rtags.rb -
@@ -133,6 +134,9 @@ class RDoc::Parser::PrismRuby < RDoc::Parser
133134

134135
parse_files_matching(/\.rbw?$/) unless ENV['RDOC_USE_RIPPER_PARSER']
135136

137+
# Matches an RBS inline type annotation line: #: followed by whitespace
138+
RBS_SIG_LINE = /\A#:\s/ # :nodoc:
139+
136140
attr_accessor :visibility
137141
attr_reader :container, :singleton, :in_proc_block
138142

@@ -461,10 +465,14 @@ def skip_comments_until(line_no_until)
461465
def consecutive_comment(line_no)
462466
return unless @unprocessed_comments.first&.first == line_no
463467
_line_no, start_line, text = @unprocessed_comments.shift
464-
parse_comment_text_to_directives(text, start_line)
468+
type_signature = extract_type_signature!(text, start_line)
469+
result = parse_comment_text_to_directives(text, start_line)
470+
return unless result
471+
comment, directives = result
472+
[comment, directives, type_signature]
465473
end
466474

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

469477
def parse_comment_text_to_directives(comment_text, start_line) # :nodoc:
470478
comment_text, directives = @preprocess.parse_comment(comment_text, start_line, :ruby)
@@ -594,14 +602,15 @@ def add_alias_method(old_name, new_name, line_no)
594602
# Handles `attr :a, :b`, `attr_reader :a, :b`, `attr_writer :a, :b` and `attr_accessor :a, :b`
595603

596604
def add_attributes(names, rw, line_no)
597-
comment, directives = consecutive_comment(line_no)
605+
comment, directives, type_signature = consecutive_comment(line_no)
598606
handle_code_object_directives(@container, directives) if directives
599607
return unless @container.document_children
600608

601609
names.each do |symbol|
602610
a = RDoc::Attr.new(nil, symbol.to_s, rw, comment, singleton: @singleton)
603611
a.store = @store
604612
a.line = line_no
613+
a.type_signature = type_signature
605614
record_location(a)
606615
handle_modifier_directive(a, line_no)
607616
@container.add_attribute(a) if should_document?(a)
@@ -640,7 +649,7 @@ def add_extends(names, line_no) # :nodoc:
640649

641650
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:)
642651
receiver = receiver_name ? find_or_create_module_path(receiver_name, receiver_fallback_type) : @container
643-
comment, directives = consecutive_comment(start_line)
652+
comment, directives, type_signature = consecutive_comment(start_line)
644653
handle_code_object_directives(@container, directives) if directives
645654

646655
internal_add_method(
@@ -655,11 +664,12 @@ def add_method(method_name, receiver_name:, receiver_fallback_type:, visibility:
655664
params: params,
656665
calls_super: calls_super,
657666
block_params: block_params,
658-
tokens: tokens
667+
tokens: tokens,
668+
type_signature: type_signature
659669
)
660670
end
661671

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:
672+
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:
663673
meth = RDoc::AnyMethod.new(nil, method_name, singleton: singleton)
664674
meth.comment = comment
665675
handle_code_object_directives(meth, directives) if directives
@@ -680,6 +690,7 @@ def add_method(method_name, receiver_name:, receiver_fallback_type:, visibility:
680690
meth.params ||= params || '()'
681691
meth.calls_super = calls_super
682692
meth.block_params ||= block_params if block_params
693+
meth.type_signature = type_signature
683694
record_location(meth)
684695
meth.start_collecting_tokens(:ruby)
685696
tokens.each do |token|
@@ -836,6 +847,36 @@ def add_module_or_class(module_name, start_line, end_line, is_class: false, supe
836847
mod
837848
end
838849

850+
private
851+
852+
# Extracts RBS type signature lines (#: ...) from raw comment text.
853+
# Mutates the input text to remove the extracted lines.
854+
# Returns the type signature string, or nil if none found.
855+
856+
def extract_type_signature!(text, start_line)
857+
return nil unless text.include?('#:')
858+
859+
lines = text.lines
860+
sig_lines, doc_lines = lines.partition { |l| l.match?(RBS_SIG_LINE) }
861+
return nil if sig_lines.empty?
862+
863+
first_sig_line = start_line + lines.index(sig_lines.first)
864+
text.replace(doc_lines.join)
865+
type_sig = sig_lines.map { |l| l.sub(RBS_SIG_LINE, '') }.join.chomp
866+
return nil if type_sig.strip.empty?
867+
868+
warn_invalid_type_signature(type_sig, first_sig_line)
869+
type_sig
870+
end
871+
872+
def warn_invalid_type_signature(sig, line_no)
873+
sig.each_line(chomp: true).with_index do |line, i|
874+
next if RDoc::RbsHelper.valid_method_type?(line)
875+
next if RDoc::RbsHelper.valid_type?(line)
876+
@options.warn "#{@top_level.relative_name}:#{line_no + i}: invalid RBS type signature: #{line.inspect}"
877+
end
878+
end
879+
839880
class RDocVisitor < Prism::Visitor # :nodoc:
840881
def initialize(scanner, top_level, store)
841882
@scanner = scanner

0 commit comments

Comments
 (0)