diff --git a/lib/rbs/ast/ruby/members.rb b/lib/rbs/ast/ruby/members.rb index 2cbb22a35..c80dcc261 100644 --- a/lib/rbs/ast/ruby/members.rb +++ b/lib/rbs/ast/ruby/members.rb @@ -549,17 +549,27 @@ class DefMember < Base attr_reader :name attr_reader :node + attr_reader :kind attr_reader :method_type attr_reader :leading_comment - def initialize(buffer, name, node, method_type, leading_comment) + def initialize(buffer, name, node, method_type, leading_comment, kind: :instance) super(buffer) @name = name @node = node + @kind = kind @method_type = method_type @leading_comment = leading_comment end + def singleton? + kind == :singleton + end + + def instance? + kind == :instance + end + def location rbs_location(node.location) end @@ -583,6 +593,7 @@ def name_location def type_fingerprint [ "members/def", + kind.to_s, name.to_s, method_type.type_fingerprint, leading_comment&.as_comment&.string diff --git a/lib/rbs/definition_builder/method_builder.rb b/lib/rbs/definition_builder/method_builder.rb index aadda0ecc..a4f47e405 100644 --- a/lib/rbs/definition_builder/method_builder.rb +++ b/lib/rbs/definition_builder/method_builder.rb @@ -143,12 +143,14 @@ def build_instance(type_name) decl.members.each do |member| case member when AST::Ruby::Members::DefMember - build_method( - methods, - type, - member: member, - accessibility: :public - ) + if member.instance? + build_method( + methods, + type, + member: member, + accessibility: :public + ) + end when AST::Ruby::Members::AttrReaderMember, AST::Ruby::Members::AttrWriterMember, AST::Ruby::Members::AttrAccessorMember build_ruby_attribute(methods, type, member: member, accessibility: :public) end @@ -181,6 +183,10 @@ def build_singleton(type_name) if member.kind == :singleton build_alias(methods, type, member: member) end + when AST::Ruby::Members::DefMember + if member.singleton? + build_method(methods, type, member: member, accessibility: :public) + end end end end diff --git a/lib/rbs/environment.rb b/lib/rbs/environment.rb index ad06efea6..ddbf33fd6 100644 --- a/lib/rbs/environment.rb +++ b/lib/rbs/environment.rb @@ -795,7 +795,8 @@ def resolve_ruby_member(resolver, member, context:) member.name, member.node, member.method_type.map_type_name {|name, _, _| absolute_type_name(resolver, nil, name, context: context) }, - member.leading_comment + member.leading_comment, + kind: member.kind ) when AST::Ruby::Members::IncludeMember resolved_annotation = member.annotation&.map_type_name {|name, _, _| absolute_type_name(resolver, nil, name, context: context) } diff --git a/lib/rbs/inline_parser.rb b/lib/rbs/inline_parser.rb index 0bb1cf013..ed38f22d9 100644 --- a/lib/rbs/inline_parser.rb +++ b/lib/rbs/inline_parser.rb @@ -202,14 +202,16 @@ def visit_class_or_module_body(decl, node) def visit_def_node(node) return if skip_node?(node) - if node.receiver + if node.receiver && !node.receiver.is_a?(Prism::SelfNode) diagnostics << Diagnostic::NotImplementedYet.new( rbs_location(node.receiver.location), - "Singleton method definition is not supported yet" + "Method definition with non-self receiver is not supported" ) return end + kind = node.receiver ? :singleton : :instance #: :singleton | :instance + case current = current_module when AST::Ruby::Declarations::ClassDecl, AST::Ruby::Declarations::ModuleDecl leading_block = comments.leading_block!(node) @@ -223,7 +225,7 @@ def visit_def_node(node) method_type, leading_unuseds, trailing_unused = AST::Ruby::Members::MethodTypeAnnotation.build(leading_block, trailing_block, [], node) report_unused_annotation(trailing_unused, *leading_unuseds) - defn = AST::Ruby::Members::DefMember.new(buffer, node.name, node, method_type, leading_block) + defn = AST::Ruby::Members::DefMember.new(buffer, node.name, node, method_type, leading_block, kind: kind) current.members << defn # Skip other comments in `def` node diff --git a/sig/ast/ruby/members.rbs b/sig/ast/ruby/members.rbs index 19d8b35d6..253e117c7 100644 --- a/sig/ast/ruby/members.rbs +++ b/sig/ast/ruby/members.rbs @@ -84,10 +84,15 @@ module RBS attr_reader name: Symbol attr_reader node: Prism::DefNode + attr_reader kind: :instance | :singleton attr_reader method_type: MethodTypeAnnotation attr_reader leading_comment: CommentBlock? - def initialize: (Buffer, Symbol name, Prism::DefNode node, MethodTypeAnnotation, CommentBlock? leading_comment) -> void + def initialize: (Buffer, Symbol name, Prism::DefNode node, MethodTypeAnnotation, CommentBlock? leading_comment, ?kind: :instance | :singleton) -> void + + def singleton?: () -> bool + + def instance?: () -> bool def location: () -> Location diff --git a/test/rbs/inline_parser_test.rb b/test/rbs/inline_parser_test.rb index 2cb3db53d..befd885f3 100644 --- a/test/rbs/inline_parser_test.rb +++ b/test/rbs/inline_parser_test.rb @@ -125,18 +125,195 @@ def foo; end assert_empty result.declarations end - def test_error__def__singleton + def test_parse__def__singleton result = parse(<<~RUBY) module Foo def self.foo; end end RUBY + assert_empty result.diagnostics + + result.declarations[0].tap do |decl| + decl.members[0].tap do |member| + assert_instance_of RBS::AST::Ruby::Members::DefMember, member + assert_equal :foo, member.name + assert_predicate member, :singleton? + refute_predicate member, :instance? + assert_equal :singleton, member.kind + end + end + end + + def test_parse__def__singleton__colon_type + result = parse(<<~RUBY) + class Foo + #: () -> void + def self.hello + end + end + RUBY + + assert_empty result.diagnostics + + result.declarations[0].tap do |decl| + decl.members[0].tap do |member| + assert_instance_of RBS::AST::Ruby::Members::DefMember, member + assert_equal :hello, member.name + assert_predicate member, :singleton? + assert_equal ["() -> void"], member.overloads.map { _1.method_type.to_s } + end + end + end + + def test_parse__def__singleton__rbs_annotation + result = parse(<<~RUBY) + class Foo + # @rbs () -> void + def self.hello + end + end + RUBY + + assert_empty result.diagnostics + + result.declarations[0].tap do |decl| + decl.members[0].tap do |member| + assert_instance_of RBS::AST::Ruby::Members::DefMember, member + assert_equal :hello, member.name + assert_predicate member, :singleton? + assert_equal ["() -> void"], member.overloads.map { _1.method_type.to_s } + end + end + end + + def test_parse__def__singleton__with_params + result = parse(<<~RUBY) + class Foo + # @rbs n: Integer + # @rbs return: Integer + def self.double(n) + n * 2 + end + end + RUBY + + assert_empty result.diagnostics + + result.declarations[0].tap do |decl| + decl.members[0].tap do |member| + assert_instance_of RBS::AST::Ruby::Members::DefMember, member + assert_equal :double, member.name + assert_predicate member, :singleton? + assert_equal ["(Integer n) -> Integer"], member.overloads.map { _1.method_type.to_s } + end + end + end + + def test_parse__def__singleton__mixed_with_instance + result = parse(<<~RUBY) + class Foo + #: () -> void + def self.class_method + end + + #: () -> String + def instance_method + "" + end + end + RUBY + + assert_empty result.diagnostics + + result.declarations[0].tap do |decl| + assert_equal 2, decl.members.size + + decl.members[0].tap do |member| + assert_instance_of RBS::AST::Ruby::Members::DefMember, member + assert_equal :class_method, member.name + assert_predicate member, :singleton? + assert_equal ["() -> void"], member.overloads.map { _1.method_type.to_s } + end + + decl.members[1].tap do |member| + assert_instance_of RBS::AST::Ruby::Members::DefMember, member + assert_equal :instance_method, member.name + assert_predicate member, :instance? + assert_equal ["() -> String"], member.overloads.map { _1.method_type.to_s } + end + end + end + + def test_parse__def__singleton__skip + result = parse(<<~RUBY) + class Foo + # @rbs skip + def self.skipped_method + end + end + RUBY + + assert_empty result.diagnostics + + result.declarations[0].tap do |decl| + assert_empty decl.members + end + end + + def test_error__def__singleton__non_self_receiver + result = parse(<<~RUBY) + module Foo + def other_obj.foo; end + end + RUBY + assert_equal 1, result.diagnostics.size assert_any!(result.diagnostics) do |diagnostic| assert_instance_of RBS::InlineParser::Diagnostic::NotImplementedYet, diagnostic - assert_equal "self", diagnostic.location.source - assert_equal "Singleton method definition is not supported yet", diagnostic.message + assert_equal "other_obj", diagnostic.location.source + assert_equal "Method definition with non-self receiver is not supported", diagnostic.message + end + end + + def test_parse__def__singleton__in_module + result = parse(<<~RUBY) + module Foo + #: () -> void + def self.module_method + end + end + RUBY + + assert_empty result.diagnostics + + result.declarations[0].tap do |decl| + decl.members[0].tap do |member| + assert_instance_of RBS::AST::Ruby::Members::DefMember, member + assert_equal :module_method, member.name + assert_predicate member, :singleton? + assert_equal ["() -> void"], member.overloads.map { _1.method_type.to_s } + end + end + end + + def test_parse__def__instance_method_kind + result = parse(<<~RUBY) + class Foo + def instance_method; end + end + RUBY + + assert_empty result.diagnostics + + result.declarations[0].tap do |decl| + decl.members[0].tap do |member| + assert_instance_of RBS::AST::Ruby::Members::DefMember, member + assert_equal :instance_method, member.name + assert_predicate member, :instance? + refute_predicate member, :singleton? + assert_equal :instance, member.kind + end end end