diff --git a/lib/rdoc.rb b/lib/rdoc.rb
index 9dc4595324..8f39af0753 100644
--- a/lib/rdoc.rb
+++ b/lib/rdoc.rb
@@ -155,6 +155,7 @@ def self.home
autoload :RDoc, "#{__dir__}/rdoc/rdoc"
+ autoload :TypeParameter, "#{__dir__}/rdoc/type_parameter"
autoload :CrossReference, "#{__dir__}/rdoc/cross_reference"
autoload :ERBIO, "#{__dir__}/rdoc/erbio"
autoload :ERBPartial, "#{__dir__}/rdoc/erb_partial"
diff --git a/lib/rdoc/class_module.rb b/lib/rdoc/class_module.rb
index c69e14b5e4..fde4c92250 100644
--- a/lib/rdoc/class_module.rb
+++ b/lib/rdoc/class_module.rb
@@ -41,6 +41,8 @@ class RDoc::ClassModule < RDoc::Context
attr_accessor :is_alias_for
+ attr_accessor :type_parameters
+
##
# Return a RDoc::ClassModule of class +class_type+ that is a copy
# of module +module+. Used to promote modules to classes.
@@ -108,13 +110,14 @@ def self.from_module class_type, mod
#
# This is a constructor for subclasses, and must never be called directly.
- def initialize(name, superclass = nil)
+ def initialize(name, superclass = nil, type_parameters = [])
@constant_aliases = []
@diagram = nil
@is_alias_for = nil
@name = name
@superclass = superclass
@comment_location = [] # [[comment, location]]
+ @type_parameters = type_parameters
super()
end
@@ -339,8 +342,8 @@ def marshal_dump # :nodoc:
tl.relative_name
end,
parent.full_name,
- parent.class,
- ]
+ parent.class
+ ].concat(type_parameters)
end
def marshal_load array # :nodoc:
@@ -425,6 +428,7 @@ def marshal_load array # :nodoc:
@parent_name = array[12]
@parent_class = array[13]
+ @type_parameters = array[14] if array[14]
end
##
@@ -725,6 +729,12 @@ def type
module? ? 'module' : 'class'
end
+ def type_parameters_to_s
+ return nil if type_parameters.empty?
+
+ "[" + type_parameters.map(&:to_s).join(", ") + "]"
+ end
+
##
# Updates the child modules & classes by replacing the ones that are
# aliases through a constant.
diff --git a/lib/rdoc/context.rb b/lib/rdoc/context.rb
index c6edfb473c..d63907d97a 100644
--- a/lib/rdoc/context.rb
+++ b/lib/rdoc/context.rb
@@ -285,7 +285,7 @@ def add_attribute attribute
# unless it later sees class Container. +add_class+ automatically
# upgrades +given_name+ to a class in this case.
- def add_class class_type, given_name, superclass = '::Object'
+ def add_class class_type, given_name, superclass = '::Object', type_parameters = []
# superclass +nil+ is passed by the C parser in the following cases:
# - registering Object in 1.8 (correct)
# - registering BasicObject in 1.9 (correct)
@@ -373,6 +373,7 @@ def add_class class_type, given_name, superclass = '::Object'
klass.superclass = superclass
end
end
+ klass.type_parameters = type_parameters
else
# this is a new class
mod = @store.modules_hash.delete full_name
@@ -382,10 +383,10 @@ def add_class class_type, given_name, superclass = '::Object'
klass.superclass = superclass unless superclass.nil?
else
- klass = class_type.new name, superclass
+ klass = class_type.new name, superclass, type_parameters
enclosing.add_class_or_module(klass, enclosing.classes_hash,
- @store.classes_hash)
+ @store.classes_hash, type_parameters)
end
end
@@ -401,12 +402,13 @@ def add_class class_type, given_name, superclass = '::Object'
# unless #done_documenting is +true+. Sets the #parent of +mod+
# to +self+, and its #section to #current_section. Returns +mod+.
- def add_class_or_module mod, self_hash, all_hash
+ def add_class_or_module mod, self_hash, all_hash, type_parameters = []
mod.section = current_section # TODO declaring context? something is
# wrong here...
mod.parent = self
mod.full_name = nil
mod.store = @store
+ mod.type_parameters = type_parameters
unless @done_documenting then
self_hash[mod.name] = mod
@@ -503,14 +505,15 @@ def add_method method
# Adds a module named +name+. If RDoc already knows +name+ is a class then
# that class is returned instead. See also #add_class.
- def add_module(class_type, name)
+ def add_module(class_type, name, type_parameters = [])
mod = @classes[name] || @modules[name]
+ mod.type_parameters = type_parameters if mod
return mod if mod
full_name = child_name name
mod = @store.modules_hash[full_name] || class_type.new(name)
- add_class_or_module mod, @modules, @store.modules_hash
+ add_class_or_module mod, @modules, @store.modules_hash, type_parameters
end
##
diff --git a/lib/rdoc/generator/template/darkfish/class.rhtml b/lib/rdoc/generator/template/darkfish/class.rhtml
index d6510336df..c7d1eee7da 100644
--- a/lib/rdoc/generator/template/darkfish/class.rhtml
+++ b/lib/rdoc/generator/template/darkfish/class.rhtml
@@ -18,7 +18,7 @@
- <%= klass.type %> <%= klass.full_name %>
+ <%= klass.type %> <%= klass.full_name + (klass.type_parameters_to_s ? " #{klass.type_parameters_to_s}
" : "") %>
diff --git a/lib/rdoc/parser/ruby.rb b/lib/rdoc/parser/ruby.rb
index 85f1cd0391..97dea96f85 100644
--- a/lib/rdoc/parser/ruby.rb
+++ b/lib/rdoc/parser/ruby.rb
@@ -901,6 +901,33 @@ def parse_class_regular container, declaration_context, single, # :nodoc:
read_documentation_modifiers cls, RDoc::CLASS_MODIFIERS
record_location cls
+ if comment.text =~ /^#(\W)*:type-params:$/
+ all_lines = comment.text.lines
+ non_param_lines = all_lines.take_while { |line| line !~ /^#(\W)*:type-params:$/ }
+ param_lines = all_lines.drop(non_param_lines.size).drop(1).take_while { |line| line !~ /^#\W*$/ }
+ comment.text = non_param_lines.join("\n")
+ cls.type_parameters = param_lines.map do |type_param_line|
+ type_params = type_param_line.gsub(/^#/, '').gsub(/\n$/, '').lstrip.split(" ")
+ type_param_hash = { name: nil, variance: :invariant, unchecked: false, upper_bound: nil }
+ type_params.each_with_index do |type_param, i|
+ case type_param
+ when "unchecked"
+ type_param_hash[:unchecked] = true
+ when "in"
+ type_param_hash[:variance] = :contravariant
+ when "out"
+ type_param_hash[:variance] = :covariant
+ when "<"
+ type_param_hash[:upper_bound] = type_params[i + 1]
+ break
+ else
+ type_param_hash[:name] = type_param
+ end
+ end
+ RDoc::TypeParameter.new(*type_param_hash.values)
+ end
+ end
+
cls.add_comment comment, @top_level
@top_level.add_to_classes_or_modules cls
@@ -1713,6 +1740,34 @@ def parse_module container, single, tk, comment
record_location mod
read_documentation_modifiers mod, RDoc::CLASS_MODIFIERS
+
+ if comment.text =~ /^#(\W)*:type-params:$/
+ all_lines = comment.text.lines
+ non_param_lines = all_lines.take_while { |line| line !~ /^#(\W)*:type-params:$/ }
+ param_lines = all_lines.drop(non_param_lines.size).drop(1).take_while { |line| line !~ /^#\W*$/ }
+ comment.text = non_param_lines.join("\n")
+ mod.type_parameters = param_lines.map do |type_param_line|
+ type_params = type_param_line.gsub(/^#/, '').gsub(/\n$/, '').lstrip.split(" ")
+ type_param_hash = { name: nil, variance: :invariant, unchecked: false, upper_bound: nil }
+ type_params.each_with_index do |type_param, i|
+ case type_param
+ when "unchecked"
+ type_param_hash[:unchecked] = true
+ when "in"
+ type_param_hash[:variance] = :contravariant
+ when "out"
+ type_param_hash[:variance] = :covariant
+ when "<"
+ type_param_hash[:upper_bound] = type_params[i + 1]
+ break
+ else
+ type_param_hash[:name] = type_param
+ end
+ end
+ RDoc::TypeParameter.new(*type_param_hash.values)
+ end
+ end
+
mod.add_comment comment, @top_level
parse_statements mod
diff --git a/lib/rdoc/type_parameter.rb b/lib/rdoc/type_parameter.rb
new file mode 100644
index 0000000000..d0583e5c92
--- /dev/null
+++ b/lib/rdoc/type_parameter.rb
@@ -0,0 +1,70 @@
+module RDoc
+ class TypeParameter < CodeObject
+ attr_reader :name, :variance, :unchecked, :upper_bound
+
+ MARSHAL_VERSION = 0 # :nodoc:
+
+ def initialize(name, variance, unchecked = false, upper_bound = nil)
+ @name = name
+ @variance = variance
+ @unchecked = unchecked
+ @upper_bound = upper_bound
+ end
+
+ def marshal_load(array)
+ @name = array[1]
+ @variance = array[2]
+ @unchecked = array[3]
+ @upper_bound = array[4]
+ end
+
+ def marshal_dump
+ [
+ MARSHAL_VERSION,
+ @name,
+ @variance,
+ @unchecked,
+ @upper_bound
+ ]
+ end
+
+ def ==(other)
+ other.is_a?(TypeParameter) &&
+ self.name == other.name &&
+ self.variance == other.variance &&
+ self.unchecked == other.unchecked &&
+ self.upper_bound == other.upper_bound
+ end
+
+ alias eql? ==
+
+ def unchecked?
+ unchecked
+ end
+
+ def to_s
+ s = ""
+
+ if unchecked?
+ s << "unchecked "
+ end
+
+ case variance
+ when :invariant
+ # nop
+ when :covariant
+ s << "out "
+ when :contravariant
+ s << "in "
+ end
+
+ s << name.to_s
+
+ if type = upper_bound
+ s << " < #{type}"
+ end
+
+ s
+ end
+ end
+end
diff --git a/test/rdoc/test_rdoc_generator_darkfish.rb b/test/rdoc/test_rdoc_generator_darkfish.rb
index 96319bb4f7..8bb64a59a8 100644
--- a/test/rdoc/test_rdoc_generator_darkfish.rb
+++ b/test/rdoc/test_rdoc_generator_darkfish.rb
@@ -322,6 +322,36 @@ def test_title_escape
assert_main_title(File.binread('index.html'), title)
end
+ def test_generate_type_param
+ top_level = @store.add_file 'file.rb'
+ type_parameters = [
+ RDoc::TypeParameter.new("Elem", :invariant, true, "Integer")
+ ]
+ top_level.add_class @klass.class, @klass.name, nil, type_parameters
+
+ @g.generate
+
+ assert_file @klass.name + ".html"
+
+ assert_include File.read(@klass.name + ".html"), %Q[\[unchecked Elem < Integer\]
]
+ end
+
+ def test_generate_type_params
+ top_level = @store.add_file 'file.rb'
+ type_parameters = [
+ RDoc::TypeParameter.new("Elem", :invariant, true, "Integer"),
+ RDoc::TypeParameter.new("T", :covariant, false, "String"),
+ RDoc::TypeParameter.new("A", :contravariant, true, "Object")
+ ]
+ top_level.add_class @klass.class, @klass.name, nil, type_parameters
+
+ @g.generate
+
+ assert_file @klass.name + ".html"
+
+ assert_include File.read(@klass.name + ".html"), %Q[\[unchecked Elem < Integer, out T < String, unchecked in A < Object\]
]
+ end
+
##
# Asserts that +filename+ has a link count greater than 1 if hard links to
# @tmpdir are supported.
diff --git a/test/rdoc/test_rdoc_parser_ruby.rb b/test/rdoc/test_rdoc_parser_ruby.rb
index 3e2a85ffba..19ee4a37d9 100644
--- a/test/rdoc/test_rdoc_parser_ruby.rb
+++ b/test/rdoc/test_rdoc_parser_ruby.rb
@@ -739,6 +739,34 @@ def test_parse_class
assert_equal 1, foo.line
end
+ def test_parse_class_generic
+ comment = RDoc::Comment.new <<-COMMENT, @top_level, :ruby
+##
+# my class
+# :type-params:
+# out KEY < Integer
+# unchecked in VALUE < String
+# X
+#
+ COMMENT
+
+ util_parser "class Foo\nend"
+
+ tk = @parser.get_tk
+
+ @parser.parse_class @top_level, RDoc::Parser::Ruby::NORMAL, tk, comment
+
+ type_parameters = [
+ RDoc::TypeParameter.new("KEY", :covariant, false, "Integer"),
+ RDoc::TypeParameter.new("VALUE", :contravariant, true, "String"),
+ RDoc::TypeParameter.new("X", :invariant, false)
+ ]
+ foo = @top_level.classes.first
+ assert_equal 'Foo', foo.full_name
+ assert_equal 'my class', foo.comment.text
+ assert_equal type_parameters, foo.type_parameters
+ end
+
def test_parse_class_singleton
comment = RDoc::Comment.new "##\n# my class\n", @top_level
@@ -1027,6 +1055,34 @@ def test_parse_module
assert_equal 'my module', foo.comment.text
end
+ def test_parse_module_generic
+ comment = RDoc::Comment.new <<-COMMENT, @top_level, :ruby
+##
+# my module
+# :type-params:
+# out KEY < Integer
+# unchecked in VALUE < String
+# X
+#
+ COMMENT
+
+ util_parser "module Foo\nend"
+
+ tk = @parser.get_tk
+
+ @parser.parse_module @top_level, RDoc::Parser::Ruby::NORMAL, tk, comment
+
+ type_parameters = [
+ RDoc::TypeParameter.new("KEY", :covariant, false, "Integer"),
+ RDoc::TypeParameter.new("VALUE", :contravariant, true, "String"),
+ RDoc::TypeParameter.new("X", :invariant, false)
+ ]
+ foo = @top_level.modules.first
+ assert_equal 'Foo', foo.full_name
+ assert_equal 'my module', foo.comment.text
+ assert_equal type_parameters, foo.type_parameters
+ end
+
def test_parse_module_nodoc
@top_level.stop_doc