Skip to content

Commit 200b5ad

Browse files
committed
RBS: Add support for generics
- Support generics for classes and modules - Add a new syntax in RDoc comments for generating generics docs in Ruby
1 parent 8309caa commit 200b5ad

File tree

9 files changed

+214
-8
lines changed

9 files changed

+214
-8
lines changed

.ruby-version

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.1.0

lib/rdoc.rb

+1
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ def self.home
143143

144144
autoload :RDoc, "#{__dir__}/rdoc/rdoc"
145145

146+
autoload :TypeParameter, "#{__dir__}/rdoc/type_parameter"
146147
autoload :CrossReference, "#{__dir__}/rdoc/cross_reference"
147148
autoload :ERBIO, "#{__dir__}/rdoc/erbio"
148149
autoload :ERBPartial, "#{__dir__}/rdoc/erb_partial"

lib/rdoc/class_module.rb

+10-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class RDoc::ClassModule < RDoc::Context
4141

4242
attr_accessor :is_alias_for
4343

44+
attr_accessor :type_parameters
45+
4446
##
4547
# Return a RDoc::ClassModule of class +class_type+ that is a copy
4648
# of module +module+. Used to promote modules to classes.
@@ -108,13 +110,14 @@ def self.from_module class_type, mod
108110
#
109111
# This is a constructor for subclasses, and must never be called directly.
110112

111-
def initialize(name, superclass = nil)
113+
def initialize(name, superclass = nil, type_parameters = [])
112114
@constant_aliases = []
113115
@diagram = nil
114116
@is_alias_for = nil
115117
@name = name
116118
@superclass = superclass
117119
@comment_location = [] # [[comment, location]]
120+
@type_parameters = type_parameters
118121

119122
super()
120123
end
@@ -725,6 +728,12 @@ def type
725728
module? ? 'module' : 'class'
726729
end
727730

731+
def type_parameters_to_s
732+
return nil if type_parameters.empty?
733+
734+
"[" + type_parameters.map(&:to_s).join(", ") + "]"
735+
end
736+
728737
##
729738
# Updates the child modules & classes by replacing the ones that are
730739
# aliases through a constant.

lib/rdoc/context.rb

+9-6
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ def add_attribute attribute
285285
# unless it later sees <tt>class Container</tt>. +add_class+ automatically
286286
# upgrades +given_name+ to a class in this case.
287287

288-
def add_class class_type, given_name, superclass = '::Object'
288+
def add_class class_type, given_name, superclass = '::Object', type_parameters = []
289289
# superclass +nil+ is passed by the C parser in the following cases:
290290
# - registering Object in 1.8 (correct)
291291
# - registering BasicObject in 1.9 (correct)
@@ -373,6 +373,7 @@ def add_class class_type, given_name, superclass = '::Object'
373373
klass.superclass = superclass
374374
end
375375
end
376+
klass.type_parameters = type_parameters
376377
else
377378
# this is a new class
378379
mod = @store.modules_hash.delete full_name
@@ -382,10 +383,10 @@ def add_class class_type, given_name, superclass = '::Object'
382383

383384
klass.superclass = superclass unless superclass.nil?
384385
else
385-
klass = class_type.new name, superclass
386+
klass = class_type.new name, superclass, type_parameters
386387

387388
enclosing.add_class_or_module(klass, enclosing.classes_hash,
388-
@store.classes_hash)
389+
@store.classes_hash, type_parameters)
389390
end
390391
end
391392

@@ -401,12 +402,13 @@ def add_class class_type, given_name, superclass = '::Object'
401402
# unless #done_documenting is +true+. Sets the #parent of +mod+
402403
# to +self+, and its #section to #current_section. Returns +mod+.
403404

404-
def add_class_or_module mod, self_hash, all_hash
405+
def add_class_or_module mod, self_hash, all_hash, type_parameters = []
405406
mod.section = current_section # TODO declaring context? something is
406407
# wrong here...
407408
mod.parent = self
408409
mod.full_name = nil
409410
mod.store = @store
411+
mod.type_parameters = type_parameters
410412

411413
unless @done_documenting then
412414
self_hash[mod.name] = mod
@@ -503,14 +505,15 @@ def add_method method
503505
# Adds a module named +name+. If RDoc already knows +name+ is a class then
504506
# that class is returned instead. See also #add_class.
505507

506-
def add_module(class_type, name)
508+
def add_module(class_type, name, type_parameters = [])
507509
mod = @classes[name] || @modules[name]
510+
mod.type_parameters = type_parameters if mod
508511
return mod if mod
509512

510513
full_name = child_name name
511514
mod = @store.modules_hash[full_name] || class_type.new(name)
512515

513-
add_class_or_module mod, @modules, @store.modules_hash
516+
add_class_or_module mod, @modules, @store.modules_hash, type_parameters
514517
end
515518

516519
##

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
<main role="main" aria-labelledby="<%=h klass.aref %>">
2020
<h1 id="<%=h klass.aref %>" class="<%= klass.type %>">
21-
<%= klass.type %> <%= klass.full_name %>
21+
<%= klass.type %> <%= klass.full_name + (klass.type_parameters_to_s ? " <code>#{klass.type_parameters_to_s}</code>" : "") %>
2222
</h1>
2323

2424
<section class="description">

lib/rdoc/parser/ruby.rb

+55
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,33 @@ def parse_class_regular container, declaration_context, single, # :nodoc:
904904
read_documentation_modifiers cls, RDoc::CLASS_MODIFIERS
905905
record_location cls
906906

907+
if comment.text =~ /^#(\W)*:type-params:$/
908+
all_lines = comment.text.lines
909+
non_param_lines = all_lines.take_while { |line| line !~ /^#(\W)*:type-params:$/ }
910+
param_lines = all_lines.drop(non_param_lines.size).drop(1).take_while { |line| line !~ /^#\W*$/ }
911+
comment.text = non_param_lines.join("\n")
912+
cls.type_parameters = param_lines.map do |type_param_line|
913+
type_params = type_param_line.gsub(/^#/, '').gsub(/\n$/, '').lstrip.split(" ")
914+
type_param_hash = { name: nil, variance: :invariant, unchecked: false, upper_bound: nil }
915+
type_params.each_with_index do |type_param, i|
916+
case type_param
917+
when "unchecked"
918+
type_param_hash[:unchecked] = true
919+
when "in"
920+
type_param_hash[:variance] = :contravariant
921+
when "out"
922+
type_param_hash[:variance] = :covariant
923+
when "<"
924+
type_param_hash[:upper_bound] = type_params[i + 1]
925+
break
926+
else
927+
type_param_hash[:name] = type_param
928+
end
929+
end
930+
RDoc::TypeParameter.new(*type_param_hash.values)
931+
end
932+
end
933+
907934
cls.add_comment comment, @top_level
908935

909936
@top_level.add_to_classes_or_modules cls
@@ -1710,6 +1737,34 @@ def parse_module container, single, tk, comment
17101737
record_location mod
17111738

17121739
read_documentation_modifiers mod, RDoc::CLASS_MODIFIERS
1740+
1741+
if comment.text =~ /^#(\W)*:type-params:$/
1742+
all_lines = comment.text.lines
1743+
non_param_lines = all_lines.take_while { |line| line !~ /^#(\W)*:type-params:$/ }
1744+
param_lines = all_lines.drop(non_param_lines.size).drop(1).take_while { |line| line !~ /^#\W*$/ }
1745+
comment.text = non_param_lines.join("\n")
1746+
mod.type_parameters = param_lines.map do |type_param_line|
1747+
type_params = type_param_line.gsub(/^#/, '').gsub(/\n$/, '').lstrip.split(" ")
1748+
type_param_hash = { name: nil, variance: :invariant, unchecked: false, upper_bound: nil }
1749+
type_params.each_with_index do |type_param, i|
1750+
case type_param
1751+
when "unchecked"
1752+
type_param_hash[:unchecked] = true
1753+
when "in"
1754+
type_param_hash[:variance] = :contravariant
1755+
when "out"
1756+
type_param_hash[:variance] = :covariant
1757+
when "<"
1758+
type_param_hash[:upper_bound] = type_params[i + 1]
1759+
break
1760+
else
1761+
type_param_hash[:name] = type_param
1762+
end
1763+
end
1764+
RDoc::TypeParameter.new(*type_param_hash.values)
1765+
end
1766+
end
1767+
17131768
mod.add_comment comment, @top_level
17141769
parse_statements mod
17151770

lib/rdoc/type_parameter.rb

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
module RDoc
2+
class TypeParameter < CodeObject
3+
attr_reader :name, :variance, :unchecked, :upper_bound
4+
5+
def initialize(name, variance, unchecked = false, upper_bound = nil)
6+
@name = name
7+
@variance = variance
8+
@unchecked = unchecked
9+
@upper_bound = upper_bound
10+
end
11+
12+
def ==(other)
13+
other.is_a?(TypeParameter) &&
14+
self.name == other.name &&
15+
self.variance == other.variance &&
16+
self.unchecked == other.unchecked &&
17+
self.upper_bound == other.upper_bound
18+
end
19+
20+
alias eql? ==
21+
22+
def unchecked?
23+
unchecked
24+
end
25+
26+
def to_s
27+
s = ""
28+
29+
if unchecked?
30+
s << "unchecked "
31+
end
32+
33+
case variance
34+
when :invariant
35+
# nop
36+
when :covariant
37+
s << "out "
38+
when :contravariant
39+
s << "in "
40+
end
41+
42+
s << name.to_s
43+
44+
if type = upper_bound
45+
s << " < #{type}"
46+
end
47+
48+
s
49+
end
50+
end
51+
end

test/rdoc/test_rdoc_generator_darkfish.rb

+30
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,36 @@ def test_template_stylesheets
248248
assert_include File.read('index.html'), %Q[href="./#{base}"]
249249
end
250250

251+
def test_generate_type_param
252+
top_level = @store.add_file 'file.rb'
253+
type_parameters = [
254+
RDoc::TypeParameter.new("Elem", :invariant, true, "Integer")
255+
]
256+
top_level.add_class @klass.class, @klass.name, nil, type_parameters
257+
258+
@g.generate
259+
260+
assert_file @klass.name + ".html"
261+
262+
assert_include File.read(@klass.name + ".html"), %Q[<code>\[unchecked Elem < Integer\]</code>]
263+
end
264+
265+
def test_generate_type_params
266+
top_level = @store.add_file 'file.rb'
267+
type_parameters = [
268+
RDoc::TypeParameter.new("Elem", :invariant, true, "Integer"),
269+
RDoc::TypeParameter.new("T", :covariant, false, "String"),
270+
RDoc::TypeParameter.new("A", :contravariant, true, "Object")
271+
]
272+
top_level.add_class @klass.class, @klass.name, nil, type_parameters
273+
274+
@g.generate
275+
276+
assert_file @klass.name + ".html"
277+
278+
assert_include File.read(@klass.name + ".html"), %Q[<code>\[unchecked Elem < Integer, out T < String, unchecked in A < Object\]</code>]
279+
end
280+
251281
##
252282
# Asserts that +filename+ has a link count greater than 1 if hard links to
253283
# @tmpdir are supported.

test/rdoc/test_rdoc_parser_ruby.rb

+56
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,34 @@ def test_parse_class
739739
assert_equal 1, foo.line
740740
end
741741

742+
def test_parse_class_generic
743+
comment = RDoc::Comment.new <<-COMMENT, @top_level, :ruby
744+
##
745+
# my class
746+
# :type-params:
747+
# out KEY < Integer
748+
# unchecked in VALUE < String
749+
# X
750+
#
751+
COMMENT
752+
753+
util_parser "class Foo\nend"
754+
755+
tk = @parser.get_tk
756+
757+
@parser.parse_class @top_level, RDoc::Parser::Ruby::NORMAL, tk, comment
758+
759+
type_parameters = [
760+
RDoc::TypeParameter.new("KEY", :covariant, false, "Integer"),
761+
RDoc::TypeParameter.new("VALUE", :contravariant, true, "String"),
762+
RDoc::TypeParameter.new("X", :invariant, false)
763+
]
764+
foo = @top_level.classes.first
765+
assert_equal 'Foo', foo.full_name
766+
assert_equal 'my class', foo.comment.text
767+
assert_equal type_parameters, foo.type_parameters
768+
end
769+
742770
def test_parse_class_singleton
743771
comment = RDoc::Comment.new "##\n# my class\n", @top_level
744772

@@ -1027,6 +1055,34 @@ def test_parse_module
10271055
assert_equal 'my module', foo.comment.text
10281056
end
10291057

1058+
def test_parse_module_generic
1059+
comment = RDoc::Comment.new <<-COMMENT, @top_level, :ruby
1060+
##
1061+
# my module
1062+
# :type-params:
1063+
# out KEY < Integer
1064+
# unchecked in VALUE < String
1065+
# X
1066+
#
1067+
COMMENT
1068+
1069+
util_parser "module Foo\nend"
1070+
1071+
tk = @parser.get_tk
1072+
1073+
@parser.parse_module @top_level, RDoc::Parser::Ruby::NORMAL, tk, comment
1074+
1075+
type_parameters = [
1076+
RDoc::TypeParameter.new("KEY", :covariant, false, "Integer"),
1077+
RDoc::TypeParameter.new("VALUE", :contravariant, true, "String"),
1078+
RDoc::TypeParameter.new("X", :invariant, false)
1079+
]
1080+
foo = @top_level.modules.first
1081+
assert_equal 'Foo', foo.full_name
1082+
assert_equal 'my module', foo.comment.text
1083+
assert_equal type_parameters, foo.type_parameters
1084+
end
1085+
10301086
def test_parse_module_nodoc
10311087
@top_level.stop_doc
10321088

0 commit comments

Comments
 (0)