Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RBS: Add support for generics #925

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/rdoc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
16 changes: 13 additions & 3 deletions lib/rdoc/class_module.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

##
Expand Down Expand Up @@ -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.
Expand Down
15 changes: 9 additions & 6 deletions lib/rdoc/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ def add_attribute attribute
# unless it later sees <tt>class Container</tt>. +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)
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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

##
Expand Down
2 changes: 1 addition & 1 deletion lib/rdoc/generator/template/darkfish/class.rhtml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

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

<section class="description">
Expand Down
55 changes: 55 additions & 0 deletions lib/rdoc/parser/ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
70 changes: 70 additions & 0 deletions lib/rdoc/type_parameter.rb
Original file line number Diff line number Diff line change
@@ -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
30 changes: 30 additions & 0 deletions test/rdoc/test_rdoc_generator_darkfish.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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[<code>\[unchecked Elem < Integer\]</code>]
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[<code>\[unchecked Elem < Integer, out T < String, unchecked in A < Object\]</code>]
end

##
# Asserts that +filename+ has a link count greater than 1 if hard links to
# @tmpdir are supported.
Expand Down
56 changes: 56 additions & 0 deletions test/rdoc/test_rdoc_parser_ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
Loading