# typed: true # frozen_string_literal: true require_relative "test_case" module RubyIndexer class RBSIndexerTest < TestCase def test_index_core_classes entries = @index["Array"] refute_nil(entries) # Array is a class but also an instance method on Kernel assert_equal(2, entries.length) entry = entries.find { |entry| entry.is_a?(RubyIndexer::Entry::Class) } assert_match(%r{/gems/rbs-.*/core/array.rbs}, entry.file_path) assert_equal("array.rbs", entry.file_name) assert_equal("Object", entry.parent_class) assert_equal(1, entry.mixin_operations.length) enumerable_include = entry.mixin_operations.first assert_equal("Enumerable", enumerable_include.module_name) # Using fixed positions would be fragile, so let's just check some basics. assert_operator(entry.location.start_line, :>, 0) assert_operator(entry.location.end_line, :>, entry.location.start_line) assert_equal(0, entry.location.start_column) assert_operator(entry.location.end_column, :>, 0) end def test_index_core_modules entries = @index["Kernel"] refute_nil(entries) assert_equal(1, entries.length) entry = entries.first assert_match(%r{/gems/rbs-.*/core/kernel.rbs}, entry.file_path) assert_equal("kernel.rbs", entry.file_name) # Using fixed positions would be fragile, so let's just check some basics. assert_operator(entry.location.start_line, :>, 0) assert_operator(entry.location.end_line, :>, entry.location.start_line) assert_equal(0, entry.location.start_column) assert_operator(entry.location.end_column, :>, 0) end def test_index_core_constants entries = @index["RUBY_VERSION"] refute_nil(entries) assert_equal(1, entries.length) # Complex::I is defined as `Complex::I = ...` entries = @index["Complex::I"] refute_nil(entries) assert_equal(1, entries.length) # Encoding::US_ASCII is defined as # ``` # module Encoding # US_ASCII = ... # ... # ```` entries = @index["Encoding::US_ASCII"] refute_nil(entries) assert_equal(1, entries.length) end def test_index_methods entries = @index["initialize"] refute_nil(entries) entry = entries.find { |entry| entry.owner.name == "Array" } assert_match(%r{/gems/rbs-.*/core/array.rbs}, entry.file_path) assert_equal("array.rbs", entry.file_name) assert_equal(Entry::Visibility::PUBLIC, entry.visibility) # Using fixed positions would be fragile, so let's just check some basics. assert_operator(entry.location.start_line, :>, 0) assert_operator(entry.location.end_line, :>, entry.location.start_line) assert_equal(2, entry.location.start_column) assert_operator(entry.location.end_column, :>, 0) end def test_index_global_declaration entries = @index["$DEBUG"] refute_nil(entries) assert_equal(1, entries.length) entry = entries.first assert_instance_of(Entry::GlobalVariable, entry) assert_equal("$DEBUG", entry.name) assert_match(%r{/gems/rbs-.*/core/global_variables.rbs}, entry.file_path) assert_operator(entry.location.start_column, :<, entry.location.end_column) assert_equal(entry.location.start_line, entry.location.end_line) end def test_attaches_correct_owner_to_singleton_methods entries = @index["basename"] refute_nil(entries) owner = entries.first.owner assert_instance_of(Entry::SingletonClass, owner) assert_equal("File::", owner.name) end def test_location_and_name_location_are_the_same # NOTE: RBS does not store the name location for classes, modules or methods. This behavior is not exactly what # we would like, but for now we assign the same location to both entries = @index["Array"] refute_nil(entries) entry = entries.find { |entry| entry.is_a?(Entry::Class) } assert_same(entry.location, entry.name_location) end def test_rbs_method_with_required_positionals entries = @index["crypt"] assert_equal(1, entries.length) entry = entries.first signatures = entry.signatures assert_equal(1, signatures.length) first_signature = signatures.first # (::string salt_str) -> ::String assert_equal(1, first_signature.parameters.length) assert_kind_of(Entry::RequiredParameter, first_signature.parameters[0]) assert_equal(:salt_str, first_signature.parameters[0].name) end def test_rbs_method_with_unnamed_required_positionals entries = @index["try_convert"] entry = entries.find { |entry| entry.owner.name == "Array::" } parameters = entry.signatures[0].parameters assert_equal([:arg0], parameters.map(&:name)) assert_kind_of(Entry::RequiredParameter, parameters[0]) end def test_rbs_method_with_optional_positionals entries = @index["polar"] entry = entries.find { |entry| entry.owner.name == "Complex::" } # def self.polar: (Numeric, ?Numeric) -> Complex parameters = entry.signatures[0].parameters assert_equal([:arg0, :arg1], parameters.map(&:name)) assert_kind_of(Entry::RequiredParameter, parameters[0]) assert_kind_of(Entry::OptionalParameter, parameters[1]) end def test_rbs_method_with_optional_parameter entries = @index["chomp"] assert_equal(1, entries.length) entry = entries.first signatures = entry.signatures assert_equal(1, signatures.length) first_signature = signatures.first # (?::string? separator) -> ::String assert_equal(1, first_signature.parameters.length) assert_kind_of(Entry::OptionalParameter, first_signature.parameters[0]) assert_equal(:separator, first_signature.parameters[0].name) end def test_rbs_method_with_required_and_optional_parameters entries = @index["gsub"] assert_equal(1, entries.length) entry = entries.first signatures = entry.signatures assert_equal(3, signatures.length) # (::Regexp | ::string pattern, ::string | ::hash[::String, ::_ToS] replacement) -> ::String # | (::Regexp | ::string pattern) -> ::Enumerator[::String, ::String] # | (::Regexp | ::string pattern) { (::String match) -> ::_ToS } -> ::String parameters = signatures[0].parameters assert_equal([:pattern, :replacement], parameters.map(&:name)) assert_kind_of(Entry::RequiredParameter, parameters[0]) assert_kind_of(Entry::RequiredParameter, parameters[1]) parameters = signatures[1].parameters assert_equal([:pattern], parameters.map(&:name)) assert_kind_of(Entry::RequiredParameter, parameters[0]) parameters = signatures[2].parameters assert_equal([:pattern, :""], parameters.map(&:name)) assert_kind_of(Entry::RequiredParameter, parameters[0]) assert_kind_of(Entry::BlockParameter, parameters[1]) end def test_rbs_anonymous_block_parameter entries = @index["open"] entry = entries.find { |entry| entry.owner.name == "File::" } assert_equal(2, entry.signatures.length) # (::String name, ?::String mode, ?::Integer perm) -> ::IO? # | [T] (::String name, ?::String mode, ?::Integer perm) { (::IO) -> T } -> T parameters = entry.signatures[0].parameters assert_equal([:file_name, :mode, :perm], parameters.map(&:name)) assert_kind_of(Entry::RequiredParameter, parameters[0]) assert_kind_of(Entry::OptionalParameter, parameters[1]) assert_kind_of(Entry::OptionalParameter, parameters[2]) parameters = entry.signatures[1].parameters assert_equal([:file_name, :mode, :perm, :""], parameters.map(&:name)) assert_kind_of(Entry::RequiredParameter, parameters[0]) assert_kind_of(Entry::OptionalParameter, parameters[1]) assert_kind_of(Entry::OptionalParameter, parameters[2]) assert_kind_of(Entry::BlockParameter, parameters[3]) end def test_rbs_method_with_rest_positionals entries = @index["count"] entry = entries.find { |entry| entry.owner.name == "String" } parameters = entry.signatures.first.parameters assert_equal(1, entry.signatures.length) # (::String::selector selector_0, *::String::selector more_selectors) -> ::Integer assert_equal([:selector_0, :more_selectors], parameters.map(&:name)) assert_kind_of(RubyIndexer::Entry::RequiredParameter, parameters[0]) assert_kind_of(RubyIndexer::Entry::RestParameter, parameters[1]) end def test_rbs_method_with_trailing_positionals entries = @index["select"] # https://ruby-doc.org/3.3.3/IO.html#method-c-select entry = entries.find { |entry| entry.owner.name == "IO::" } signatures = entry.signatures assert_equal(2, signatures.length) # def self.select: [X, Y, Z] (::Array[X & io]? read_array, ?::Array[Y & io]? write_array, ?::Array[Z & io]? error_array) -> [ Array[X], Array[Y], Array[Z] ] # rubocop:disable Layout/LineLength # | [X, Y, Z] (::Array[X & io]? read_array, ?::Array[Y & io]? write_array, ?::Array[Z & io]? error_array, Time::_Timeout? timeout) -> [ Array[X], Array[Y], Array[Z] ]? # rubocop:disable Layout/LineLength parameters = signatures[0].parameters assert_equal([:read_array, :write_array, :error_array], parameters.map(&:name)) assert_kind_of(Entry::RequiredParameter, parameters[0]) assert_kind_of(Entry::OptionalParameter, parameters[1]) assert_kind_of(Entry::OptionalParameter, parameters[2]) parameters = signatures[1].parameters assert_equal([:read_array, :write_array, :error_array, :timeout], parameters.map(&:name)) assert_kind_of(Entry::RequiredParameter, parameters[0]) assert_kind_of(Entry::OptionalParameter, parameters[1]) assert_kind_of(Entry::OptionalParameter, parameters[2]) assert_kind_of(Entry::OptionalParameter, parameters[3]) end def test_rbs_method_with_optional_keywords entries = @index["step"] entry = entries.find { |entry| entry.owner.name == "Numeric" } signatures = entry.signatures assert_equal(4, signatures.length) # (?::Numeric limit, ?::Numeric step) { (::Numeric) -> void } -> self # | (?::Numeric limit, ?::Numeric step) -> ::Enumerator[::Numeric, self] # | (?by: ::Numeric, ?to: ::Numeric) { (::Numeric) -> void } -> self # | (?by: ::Numeric, ?to: ::Numeric) -> ::Enumerator[::Numeric, self] parameters = signatures[0].parameters assert_equal([:limit, :step, :""], parameters.map(&:name)) assert_kind_of(Entry::OptionalParameter, parameters[0]) assert_kind_of(Entry::OptionalParameter, parameters[1]) assert_kind_of(Entry::BlockParameter, parameters[2]) parameters = signatures[1].parameters assert_equal([:limit, :step], parameters.map(&:name)) assert_kind_of(Entry::OptionalParameter, parameters[0]) assert_kind_of(Entry::OptionalParameter, parameters[1]) parameters = signatures[2].parameters assert_equal([:by, :to, :""], parameters.map(&:name)) assert_kind_of(Entry::OptionalKeywordParameter, parameters[0]) assert_kind_of(Entry::OptionalKeywordParameter, parameters[1]) assert_kind_of(Entry::BlockParameter, parameters[2]) parameters = signatures[3].parameters assert_equal([:by, :to], parameters.map(&:name)) assert_kind_of(Entry::OptionalKeywordParameter, parameters[0]) assert_kind_of(Entry::OptionalKeywordParameter, parameters[1]) end def test_rbs_method_with_required_keywords # There are no methods in Core that have required keyword arguments, # so we test against RBS directly rbs = <<~RBS class File def foo: (a: ::Numeric sz, b: ::Numeric) -> void end RBS signatures = parse_rbs_methods(rbs, "foo") parameters = signatures[0].parameters assert_equal([:a, :b], parameters.map(&:name)) assert_kind_of(Entry::KeywordParameter, parameters[0]) assert_kind_of(Entry::KeywordParameter, parameters[1]) end def test_rbs_method_with_rest_keywords entries = @index["method_missing"] entry = entries.find { |entry| entry.owner.name == "BasicObject" } signatures = entry.signatures assert_equal(1, signatures.length) # (Symbol, *untyped, **untyped) ?{ (*untyped, **untyped) -> untyped } -> untyped parameters = signatures[0].parameters assert_equal([:arg0, :"", :""], parameters.map(&:name)) assert_kind_of(Entry::RequiredParameter, parameters[0]) assert_kind_of(Entry::RestParameter, parameters[1]) assert_kind_of(Entry::KeywordRestParameter, parameters[2]) end def test_parse_simple_rbs rbs = <<~RBS class File def self?.open: (String name, ?String mode, ?Integer perm) -> IO? | [T] (String name, ?String mode, ?Integer perm) { (IO) -> T } -> T end RBS signatures = parse_rbs_methods(rbs, "open") assert_equal(2, signatures.length) parameters = signatures[0].parameters assert_equal([:name, :mode, :perm], parameters.map(&:name)) assert_kind_of(Entry::RequiredParameter, parameters[0]) assert_kind_of(Entry::OptionalParameter, parameters[1]) assert_kind_of(Entry::OptionalParameter, parameters[2]) parameters = signatures[1].parameters assert_equal([:name, :mode, :perm, :""], parameters.map(&:name)) assert_kind_of(Entry::RequiredParameter, parameters[0]) assert_kind_of(Entry::OptionalParameter, parameters[1]) assert_kind_of(Entry::OptionalParameter, parameters[2]) assert_kind_of(Entry::BlockParameter, parameters[3]) end def test_signature_alias # In RBS, an alias means that two methods have the same signature. # It does not mean the same thing as a Ruby alias. any_entries = @index["any?"] assert_equal(["Array", "Enumerable", "Hash"], any_entries.map { _1.owner.name }) entry = any_entries.find { |entry| entry.owner.name == "Array" } assert_kind_of(RubyIndexer::Entry::UnresolvedMethodAlias, entry) assert_equal("any?", entry.name) assert_equal("all?", entry.old_name) assert_equal("Array", entry.owner.name) assert(entry.file_path.end_with?("core/array.rbs")) assert_includes(entry.comments, "Returns `true` if any element of `self` meets a given criterion.") end def test_indexing_untyped_functions entries = @index.resolve_method("call", "Method") parameters = entries.first.signatures.first.parameters assert_equal(1, parameters.length) assert_instance_of(Entry::ForwardingParameter, parameters.first) end private def parse_rbs_methods(rbs, method_name) buffer = RBS::Buffer.new(content: rbs, name: "") _, _, declarations = RBS::Parser.parse_signature(buffer) index = RubyIndexer::Index.new indexer = RubyIndexer::RBSIndexer.new(index) pathname = Pathname.new("file.rbs") indexer.process_signature(pathname, declarations) entry = T.must(index[method_name]).first T.cast(entry, Entry::Method).signatures end end end