# typed: strict
# frozen_string_literal: true
module RubyIndexer
class RBSIndexer
HAS_UNTYPED_FUNCTION = !!defined?(RBS::Types::UntypedFunction) #: bool
#: (Index index) -> void
def initialize(index)
@index = index
end
#: -> void
def index_ruby_core
loader = RBS::EnvironmentLoader.new
RBS::Environment.from_loader(loader).resolve_type_names
loader.each_signature do |_source, pathname, _buffer, declarations, _directives|
process_signature(pathname, declarations)
end
end
#: (Pathname pathname, Array[RBS::AST::Declarations::Base] declarations) -> void
def process_signature(pathname, declarations)
declarations.each do |declaration|
process_declaration(declaration, pathname)
end
end
private
#: (RBS::AST::Declarations::Base declaration, Pathname pathname) -> void
def process_declaration(declaration, pathname)
case declaration
when RBS::AST::Declarations::Class, RBS::AST::Declarations::Module
handle_class_or_module_declaration(declaration, pathname)
when RBS::AST::Declarations::Constant
namespace_nesting = declaration.name.namespace.path.map(&:to_s)
handle_constant(declaration, namespace_nesting, URI::Generic.from_path(path: pathname.to_s))
when RBS::AST::Declarations::Global
handle_global_variable(declaration, pathname)
else # rubocop:disable Style/EmptyElse
# Other kinds not yet handled
end
end
#: ((RBS::AST::Declarations::Class | RBS::AST::Declarations::Module) declaration, Pathname pathname) -> void
def handle_class_or_module_declaration(declaration, pathname)
nesting = [declaration.name.name.to_s]
uri = URI::Generic.from_path(path: pathname.to_s)
location = to_ruby_indexer_location(declaration.location)
comments = comments_to_string(declaration)
entry = if declaration.is_a?(RBS::AST::Declarations::Class)
parent_class = declaration.super_class&.name&.name&.to_s
Entry::Class.new(nesting, uri, location, location, comments, parent_class)
else
Entry::Module.new(nesting, uri, location, location, comments)
end
add_declaration_mixins_to_entry(declaration, entry)
@index.add(entry)
declaration.members.each do |member|
case member
when RBS::AST::Members::MethodDefinition
handle_method(member, entry)
when RBS::AST::Declarations::Constant
handle_constant(member, nesting, uri)
when RBS::AST::Members::Alias
# In RBS, an alias means that two methods have the same signature.
# It does not mean the same thing as a Ruby alias.
handle_signature_alias(member, entry)
end
end
end
#: (RBS::Location rbs_location) -> RubyIndexer::Location
def to_ruby_indexer_location(rbs_location)
RubyIndexer::Location.new(
rbs_location.start_line,
rbs_location.end_line,
rbs_location.start_column,
rbs_location.end_column,
)
end
#: ((RBS::AST::Declarations::Class | RBS::AST::Declarations::Module) declaration, Entry::Namespace entry) -> void
def add_declaration_mixins_to_entry(declaration, entry)
declaration.each_mixin do |mixin|
name = mixin.name.name.to_s
case mixin
when RBS::AST::Members::Include
entry.mixin_operations << Entry::Include.new(name)
when RBS::AST::Members::Prepend
entry.mixin_operations << Entry::Prepend.new(name)
when RBS::AST::Members::Extend
singleton = @index.existing_or_new_singleton_class(entry.name)
singleton.mixin_operations << Entry::Include.new(name)
end
end
end
#: (RBS::AST::Members::MethodDefinition member, Entry::Namespace owner) -> void
def handle_method(member, owner)
name = member.name.name
uri = URI::Generic.from_path(path: member.location.buffer.name.to_s)
location = to_ruby_indexer_location(member.location)
comments = comments_to_string(member)
real_owner = member.singleton? ? @index.existing_or_new_singleton_class(owner.name) : owner
signatures = signatures(member)
@index.add(Entry::Method.new(
name,
uri,
location,
location,
comments,
signatures,
member.visibility || :public,
real_owner,
))
end
#: (RBS::AST::Members::MethodDefinition member) -> Array[Entry::Signature]
def signatures(member)
member.overloads.map do |overload|
parameters = process_overload(overload)
Entry::Signature.new(parameters)
end
end
#: (RBS::AST::Members::MethodDefinition::Overload overload) -> Array[Entry::Parameter]
def process_overload(overload)
function = overload.method_type.type
if function.is_a?(RBS::Types::Function)
parameters = parse_arguments(function)
block = overload.method_type.block
parameters << Entry::BlockParameter.anonymous if block&.required
return parameters
end
# Untyped functions are a new RBS feature (since v3.6.0) to declare methods that accept any parameters. For our
# purposes, accepting any argument is equivalent to `...`
if HAS_UNTYPED_FUNCTION && function.is_a?(RBS::Types::UntypedFunction)
[Entry::ForwardingParameter.new]
else
[]
end
end
#: (RBS::Types::Function function) -> Array[Entry::Parameter]
def parse_arguments(function)
parameters = []
parameters.concat(process_required_and_optional_positionals(function))
parameters.concat(process_trailing_positionals(function)) if function.trailing_positionals
parameters << process_rest_positionals(function) if function.rest_positionals
parameters.concat(process_required_keywords(function)) if function.required_keywords
parameters.concat(process_optional_keywords(function)) if function.optional_keywords
parameters << process_rest_keywords(function) if function.rest_keywords
parameters
end
#: (RBS::Types::Function function) -> Array[Entry::RequiredParameter]
def process_required_and_optional_positionals(function)
argument_offset = 0
required = function.required_positionals.map.with_index(argument_offset) do |param, i|
# Some parameters don't have names, e.g.
# def self.try_convert: [U] (untyped) -> ::Array[U]?
name = param.name || :"arg#{i}"
argument_offset += 1
Entry::RequiredParameter.new(name: name)
end
optional = function.optional_positionals.map.with_index(argument_offset) do |param, i|
# Optional positionals may be unnamed, e.g.
# def self.polar: (Numeric, ?Numeric) -> Complex
name = param.name || :"arg#{i}"
Entry::OptionalParameter.new(name: name)
end
required + optional
end
#: (RBS::Types::Function function) -> Array[Entry::OptionalParameter]
def process_trailing_positionals(function)
function.trailing_positionals.map do |param|
Entry::OptionalParameter.new(name: param.name)
end
end
#: (RBS::Types::Function function) -> Entry::RestParameter
def process_rest_positionals(function)
rest = function.rest_positionals
rest_name = rest.name || Entry::RestParameter::DEFAULT_NAME
Entry::RestParameter.new(name: rest_name)
end
#: (RBS::Types::Function function) -> Array[Entry::KeywordParameter]
def process_required_keywords(function)
function.required_keywords.map do |name, _param|
Entry::KeywordParameter.new(name: name)
end
end
#: (RBS::Types::Function function) -> Array[Entry::OptionalKeywordParameter]
def process_optional_keywords(function)
function.optional_keywords.map do |name, _param|
Entry::OptionalKeywordParameter.new(name: name)
end
end
#: (RBS::Types::Function function) -> Entry::KeywordRestParameter
def process_rest_keywords(function)
param = function.rest_keywords
name = param.name || Entry::KeywordRestParameter::DEFAULT_NAME
Entry::KeywordRestParameter.new(name: name)
end
# RBS treats constant definitions differently depend on where they are defined.
# When constants' rbs are defined inside a class/module block, they are treated as
# members of the class/module.
#
# module Encoding
# US_ASCII = ... # US_ASCII is a member of Encoding
# end
#
# When constants' rbs are defined outside a class/module block, they are treated as
# top-level constants.
#
# Complex::I = ... # Complex::I is a top-level constant
#
# And we need to handle their nesting differently.
#: (RBS::AST::Declarations::Constant declaration, Array[String] nesting, URI::Generic uri) -> void
def handle_constant(declaration, nesting, uri)
fully_qualified_name = [*nesting, declaration.name.name.to_s].join("::")
@index.add(Entry::Constant.new(
fully_qualified_name,
uri,
to_ruby_indexer_location(declaration.location),
comments_to_string(declaration),
))
end
#: (RBS::AST::Declarations::Global declaration, Pathname pathname) -> void
def handle_global_variable(declaration, pathname)
name = declaration.name.to_s
uri = URI::Generic.from_path(path: pathname.to_s)
location = to_ruby_indexer_location(declaration.location)
comments = comments_to_string(declaration)
@index.add(Entry::GlobalVariable.new(
name,
uri,
location,
comments,
))
end
#: (RBS::AST::Members::Alias member, Entry::Namespace owner_entry) -> void
def handle_signature_alias(member, owner_entry)
uri = URI::Generic.from_path(path: member.location.buffer.name.to_s)
comments = comments_to_string(member)
entry = Entry::UnresolvedMethodAlias.new(
member.new_name.to_s,
member.old_name.to_s,
owner_entry,
uri,
to_ruby_indexer_location(member.location),
comments,
)
@index.add(entry)
end
#: ((RBS::AST::Declarations::Class | RBS::AST::Declarations::Module | RBS::AST::Declarations::Constant | RBS::AST::Declarations::Global | RBS::AST::Members::MethodDefinition | RBS::AST::Members::Alias) declaration) -> String?
def comments_to_string(declaration)
declaration.comment&.string
end
end
end