Skip to content

Commit

Permalink
RBS: Add support for generics
Browse files Browse the repository at this point in the history
- Support generics for classes and modules
- Add a new syntax in RDoc comments for generating generics docs in Ruby
  • Loading branch information
raosush committed Sep 6, 2022
1 parent d12272b commit 3e30f8b
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 8 deletions.
1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.1.0
1 change: 1 addition & 0 deletions lib/rdoc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,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
11 changes: 10 additions & 1 deletion 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 @@ -725,6 +728,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 @@ -904,6 +904,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 @@ -1710,6 +1737,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
51 changes: 51 additions & 0 deletions lib/rdoc/type_parameter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
module RDoc
class TypeParameter < CodeObject
attr_reader :name, :variance, :unchecked, :upper_bound

def initialize(name, variance, unchecked = false, upper_bound = nil)
@name = name
@variance = variance
@unchecked = unchecked
@upper_bound = 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 @@ -248,6 +248,36 @@ def test_template_stylesheets
assert_include File.read('index.html'), %Q[href="./#{base}"]
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

0 comments on commit 3e30f8b

Please sign in to comment.