diff --git a/docs/inline.md b/docs/inline.md index 027a2204e..47c520773 100644 --- a/docs/inline.md +++ b/docs/inline.md @@ -142,7 +142,9 @@ It detects method definitions and allows you to add annotation comments to descr ### Unannotated method definition -Methods defined with `def` syntax are detected, but they are untyped. +Methods defined with `def` syntax are detected, but their inferred type depends on whether a super method exists. + +If there is no super method, the inferred type is `(?) -> untyped` -- it accepts any arguments without type checking and returns an `untyped` object. ```ruby class Calculator @@ -150,7 +152,22 @@ class Calculator end ``` -The type of the `Calculator#add` method is `(?) -> untyped` -- it accepts any arguments without type checking and returns an `untyped` object. +The type of `Calculator#add` is `(?) -> untyped`. + +If the super class (or an included module) defines a method with the same name, the unannotated method inherits that type. + +```ruby +class Calculator + # @rbs (Integer, Integer) -> Integer + def add(x, y) = x + y +end + +class ScientificCalculator < Calculator + def add(x, y) = x + y # No annotation +end +``` + +The type of `ScientificCalculator#add` is `(Integer, Integer) -> Integer`, inherited from `Calculator#add`. ### Method type annotation syntax diff --git a/lib/rbs/definition_builder.rb b/lib/rbs/definition_builder.rb index 5c158011d..b4d36b721 100644 --- a/lib/rbs/definition_builder.rb +++ b/lib/rbs/definition_builder.rb @@ -878,27 +878,40 @@ def define_method(methods, definition, method, subst, self_type_methods, defined ) end - defs = original.overloads.map do |overload| - Definition::Method::TypeDef.new( - type: subst.empty? ? overload.method_type : overload.method_type.sub(subst), - member: original, - defined_in: defined_in, - implemented_in: implemented_in - ).tap do |type_def| - # Keep the original annotations given to overloads. - type_def.overload_annotations.replace(overload.annotations) + if original.method_type.empty? && existing_method + # Unannotated method with a parent definition → inherit from the parent, like @rbs ... + method_definition = Definition::Method.new( + super_method: existing_method, + defs: existing_method.defs.map { |defn| defn.update(implemented_in: implemented_in) }, + accessibility: :public, + alias_of: existing_method.alias_of, + alias_member: nil + ) + + method_definition.annotations.replace(existing_method.annotations) + else + defs = original.overloads.map do |overload| + Definition::Method::TypeDef.new( + type: subst.empty? ? overload.method_type : overload.method_type.sub(subst), + member: original, + defined_in: defined_in, + implemented_in: implemented_in + ).tap do |type_def| + # Keep the original annotations given to overloads. + type_def.overload_annotations.replace(overload.annotations) + end end - end - method_definition = Definition::Method.new( - super_method: existing_method, - defs: defs, - accessibility: :public, - alias_of: nil, - alias_member: nil - ) + method_definition = Definition::Method.new( + super_method: existing_method, + defs: defs, + accessibility: :public, + alias_of: nil, + alias_member: nil + ) - method_definition.annotations.replace([]) + method_definition.annotations.replace([]) + end when nil # Overloading method definition only diff --git a/test/rbs/definition_builder_test.rb b/test/rbs/definition_builder_test.rb index c34ae027a..04dabddf4 100644 --- a/test/rbs/definition_builder_test.rb +++ b/test/rbs/definition_builder_test.rb @@ -3805,4 +3805,43 @@ def bar = "" end end end + + def test_inline_method_unannotated_overriding + SignatureManager.new do |manager| + manager.files[Pathname("base.rbs")] = <<~RBS + class Base + def foo: () -> String + end + RBS + + manager.add_ruby_file("child.rb", <<~RUBY) + class Child < Base + def foo = "" + end + + class Orphan + def bar = "" + end + RUBY + + manager.build do |env| + builder = DefinitionBuilder.new(env: env) + + # Unannotated method with parent definition → behaves like @rbs ... + builder.build_instance(type_name("::Child")).tap do |definition| + definition.methods[:foo].tap do |method| + assert_equal ["() -> ::String"], method.method_types.map(&:to_s) + assert_equal type_name("::Base"), method.defs[0].defined_in + end + end + + # Unannotated method without parent definition → (?) -> untyped + builder.build_instance(type_name("::Orphan")).tap do |definition| + definition.methods[:bar].tap do |method| + assert_equal ["(?) -> untyped"], method.method_types.map(&:to_s) + end + end + end + end + end end