Skip to content
39 changes: 32 additions & 7 deletions lib/rdoc/code_object/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -313,13 +313,7 @@ def add_class(class_type, given_name, superclass = '::Object')
@store.modules_hash[given_name]
return enclosing if enclosing
# not found: create the parent(s)
names = ename.split('::')
enclosing = self
names.each do |n|
enclosing = enclosing.classes_hash[n] ||
enclosing.modules_hash[n] ||
enclosing.add_module(RDoc::NormalModule, n)
end
enclosing = find_or_create_module_path ename
end
else
name = full_name
Expand Down Expand Up @@ -500,11 +494,42 @@ def add_method(method)
method
end

##
# Returns the owner context and local name for +constant_path+, creating
# missing namespace modules. A leading +::+ resolves from the top-level.

def find_or_create_constant_owner_name(constant_path) # :nodoc:
constant_path = constant_path.to_s
owner = constant_path.start_with?('::') ? top_level : self
constant_path = constant_path.delete_prefix('::')

owner_path, separator, name = constant_path.rpartition('::')
owner = owner.find_or_create_module_path owner_path unless separator.empty?

[owner, name]
end

##
# Finds or creates the module path under this context.

def find_or_create_module_path(path) # :nodoc:
path.to_s.split('::').inject(self) do |owner, name|
owner.classes_hash[name] ||
owner.modules_hash[name] ||
owner.add_module(RDoc::NormalModule, name)
end
end

##
# 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)
if name.to_s.include?('::')
owner, name = find_or_create_constant_owner_name name
return owner.add_module class_type, name unless owner == self
end

mod = @classes[name] || @modules[name]
return mod if mod

Expand Down
1 change: 1 addition & 0 deletions lib/rdoc/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -293,5 +293,6 @@ def handle_tab_width(body)
require_relative 'parser/changelog'
require_relative 'parser/markdown'
require_relative 'parser/rd'
require_relative 'parser/rbs'
require_relative 'parser/ruby'
require_relative 'parser/ruby_colorizer'
265 changes: 265 additions & 0 deletions lib/rdoc/parser/rbs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
# frozen_string_literal: true

require 'rbs'

##
# Parse RBS signature files as first-class RDoc input.

class RDoc::Parser::RBS < RDoc::Parser
RBS_FILE_EXTENSION = /\.rbs$/

parse_files_matching RBS_FILE_EXTENSION

def scan
_, _, decls = ::RBS::Parser.parse_signature(@content)
decls.each do |decl|
parse_decl decl, @top_level
end
@top_level
end

private

def record_object_location(object, location)
object.line = location.start_line if location

if RDoc::ClassModule === object
@top_level.add_to_classes_or_modules object unless
@top_level.classes_or_modules.include? object
object.record_location @top_level
else
object.record_location @top_level
end

object
end

def rdoc_comment_for(decl, context)
rbs_comment = decl.comment if decl.respond_to?(:comment)
return unless rbs_comment

# TODO: Run RBS comments through RDoc's directive preprocessor so
# directives like :nodoc: affect the documented object.
comment = RDoc::Comment.new rbs_comment.string, context
Comment thread
st0012 marked this conversation as resolved.
Outdated
comment.format = 'markdown'
comment
end

def local_module_name(type_name, namespace)
name = type_name.to_s
return name if name.start_with?('::')

namespace_names = namespace ? namespace.to_s.split('::') : []

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about:

def local_module_name(type_name, context)
  ...
  namespace_names = TopLevel === context ? context.full_name.split('::') : []
  ...
end

Passing context, removing namespace will simplify parse_decl (will be def parse_decl(decl, context))


namespace_names.length.downto(1) do |length|
qualified_name = namespace_names.take(length).join('::')
if module_name = @top_level.find_module_named("#{qualified_name}::#{name}")
return module_name.full_name
end
end

name
end

def merge_attr_rw(existing_rw, new_rw)
rw = +''
rw << 'R' if existing_rw.include?('R') || new_rw.include?('R')
rw << 'W' if existing_rw.include?('W') || new_rw.include?('W')
rw
end

def merge_documentation(object, comment, type_signature_lines)
if comment
object.comment = if object.comment.empty?
comment
else
Comment thread
st0012 marked this conversation as resolved.
"#{object.comment}\n---\n#{comment}"
end
end

# TODO: Track RBS-owned documentation overlays so incremental reparsing can
# replace stale comments and signatures from the previous RBS parse.
object.type_signature_lines ||= type_signature_lines
Comment thread
st0012 marked this conversation as resolved.
end
Comment thread
st0012 marked this conversation as resolved.

def merge_attribute_methods(context, name, rw, singleton, comment, type_signature_lines)
method_names = []
method_names << name if rw.include?('R')
method_names << "#{name}=" if rw.include?('W')

methods = method_names.map { |method_name| context.find_method(method_name, singleton) }
methods.compact.each do |method|
merge_documentation method, comment, type_signature_lines
end

methods.all?
end

def rdoc_method_name(decl)
rbs_constructor_decl?(decl) ? 'new' : decl.name.to_s
end

def rdoc_method_singleton?(decl)
# TODO: RBS `self?` methods are :singleton_instance and should add both a
# singleton method and a private instance method.
rbs_constructor_decl?(decl) || decl.singleton?
end

def rdoc_method_visibility(decl)
rbs_constructor_decl?(decl) ? :public : decl.visibility
end

def rbs_constructor_decl?(decl)
decl.kind == :instance && decl.name == :initialize
end

def parse_attr_decl(decl, context)
rw = case decl
when ::RBS::AST::Members::AttrReader
'R'
when ::RBS::AST::Members::AttrWriter
'W'
when ::RBS::AST::Members::AttrAccessor
'RW'
end

comment = rdoc_comment_for(decl, context)
type_signature_lines = [decl.type.to_s]
name = decl.name.to_s
singleton = decl.kind == :singleton
if attribute = context.find_attribute(name, singleton)
merge_documentation attribute, comment, type_signature_lines
attribute.rw = merge_attr_rw attribute.rw, rw
return
end

if merge_attribute_methods(context, name, rw, singleton, comment, type_signature_lines)
return
end

attribute = RDoc::Attr.new(
name,
rw,
comment,
singleton: singleton
)
record_object_location attribute, decl.location
attribute.type_signature_lines = type_signature_lines
context.add_attribute attribute
attribute.visibility = decl.visibility if decl.visibility
end

def parse_class_decl(decl, context, _namespace)
owner, name = context.find_or_create_constant_owner_name decl.name
superclass = decl.super_class&.name&.to_s || '::Object'
klass = owner.add_class RDoc::NormalClass, name, superclass
record_object_location klass, decl.location
klass.add_comment rdoc_comment_for(decl, @top_level), @top_level if decl.comment

decl.members.each { |member| parse_decl member, klass, klass.full_name }
end

def parse_constant_decl(decl, context)
constant = RDoc::Constant.new decl.name.to_s, decl.type.to_s,
rdoc_comment_for(decl, context)
record_object_location constant, decl.location
context.add_constant constant
end

def parse_decl(decl, context, namespace = nil)
case decl
when ::RBS::AST::Declarations::Class
parse_class_decl decl, context, namespace
when ::RBS::AST::Declarations::Module, ::RBS::AST::Declarations::Interface
parse_module_decl decl, context, namespace
when ::RBS::AST::Declarations::ClassAlias,
::RBS::AST::Declarations::ModuleAlias
# TODO: Add RBS class and module aliases to the RDoc store.
nil
else
parse_member_decl decl, context, namespace
end
end

def parse_extend_decl(decl, context, namespace)
extend_decl = RDoc::Extend.new local_module_name(decl.name, namespace),
rdoc_comment_for(decl, context)
record_object_location extend_decl, decl.location
context.add_extend extend_decl
end

def parse_include_decl(decl, context, namespace)
include_decl = RDoc::Include.new local_module_name(decl.name, namespace),
rdoc_comment_for(decl, context)
record_object_location include_decl, decl.location
context.add_include include_decl
end

def parse_member_decl(decl, context, namespace)
case decl
when ::RBS::AST::Declarations::Constant
parse_constant_decl decl, context
when ::RBS::AST::Members::MethodDefinition
parse_method_decl decl, context
when ::RBS::AST::Members::Alias
parse_method_alias_decl decl, context
when ::RBS::AST::Members::AttrReader,
::RBS::AST::Members::AttrWriter,
::RBS::AST::Members::AttrAccessor
parse_attr_decl decl, context
when ::RBS::AST::Members::Include
parse_include_decl decl, context, namespace
when ::RBS::AST::Members::Extend
parse_extend_decl decl, context, namespace
when ::RBS::AST::Members::Private,
::RBS::AST::Members::Public
# TODO: Track standalone RBS visibility members.
nil
end
end

def parse_method_alias_decl(decl, context)
alias_def = RDoc::Alias.new(
decl.old_name.to_s,
decl.new_name.to_s,
rdoc_comment_for(decl, context),
singleton: decl.kind == :singleton
)
record_object_location alias_def, decl.location
context.add_alias alias_def
end

def parse_method_decl(decl, context)
comment = rdoc_comment_for(decl, context)
type_signature_lines = decl.overloads.map { |overload| overload.method_type.to_s }
method_name = rdoc_method_name(decl)
singleton = rdoc_method_singleton?(decl)
visibility = rdoc_method_visibility(decl)

if method = context.find_method(method_name, singleton)
merge_documentation method, comment, type_signature_lines
return
end

method = RDoc::AnyMethod.new method_name, singleton: singleton
record_object_location method, decl.location
method.type_signature_lines = type_signature_lines

if loc = decl.location
method.start_collecting_tokens :ruby
method.add_token line_no: loc.start_line, char_no: 1, text: loc.source
end

method.comment = comment if decl.comment
context.add_method method
method.visibility = visibility if visibility
end

def parse_module_decl(decl, context, _namespace)
mod = context.add_module RDoc::NormalModule, decl.name.to_s
record_object_location mod, decl.location
mod.add_comment rdoc_comment_for(decl, @top_level), @top_level if decl.comment

decl.members.each { |member| parse_decl member, mod, mod.full_name }
end
end
Loading
Loading