diff --git a/lib/tapioca/gem/listeners/methods.rb b/lib/tapioca/gem/listeners/methods.rb index 92f74aea0..db80f90a5 100644 --- a/lib/tapioca/gem/listeners/methods.rb +++ b/lib/tapioca/gem/listeners/methods.rb @@ -46,9 +46,12 @@ def compile_directly_owned_methods( method = mod.instance_method(name) method_visibility = visibility - if method.owner != mod - # Use the visibility of the method `mod` itself defines, ignoring any modules prepended to it - method_visibility = visibility_defined_by_constant(name, mod) || visibility + unless are_equal?(method.owner, mod) + # Use the visibility of the method `mod` itself defines, ignoring any modules prepended to it. + # Methods from anonymous superclasses aren't defined by `mod`, so check those superclasses next. + method_visibility = visibility_defined_by_constant(name, mod) || + visibility_defined_by_anonymous_superclasses(name, mod) || + visibility end vis = case method_visibility @@ -81,8 +84,12 @@ def compile_method(tree, symbol_name, constant, method, visibility = RBI::Public signature = signature_defined_by_constant(method, constant) signature ||= inferred_attr_writer_signature(method, constant) method = signature.method if signature #: UnboundMethod + # Methods from anonymous superclasses aren't tracked for the constant, so use the method's source location + # instead of `constant.instance_method`, which can return a prepended module's method or a `sig` wrapper. + fallback_source_method = method if @pipeline.anonymous_superclass_owning(method, constant) + definition = @pipeline.method_definition_in_gem(method.name, constant, fallback_source_method: fallback_source_method) - case @pipeline.method_definition_in_gem(method.name, constant) + case definition when Pipeline::MethodUnknown # This means that this is a C-method. Thus, we want to # skip it only if the constant is an ignored one, since @@ -186,13 +193,17 @@ def compile_method(tree, symbol_name, constant, method, visibility = RBI::Public # It walks up the ancestor tree via the `super_method` method; if any of the super # methods are owned by the constant, it means that the constant declares the method, # and that super method is returned. + # + # Methods owned by the constant's anonymous superclasses also count, since those + # superclasses are not part of the RBI and their methods would otherwise be lost. #: (UnboundMethod method, Module[top] constant) -> UnboundMethod? def method_defined_by_constant(method, constant) # Widen the type of `method` to be nilable method = method #: UnboundMethod? while method - return method if method.owner == constant + return method if are_equal?(method.owner, constant) + return method if @pipeline.anonymous_superclass_owning(method, constant) method = method.super_method end @@ -210,14 +221,14 @@ def method_defined_by_constant(method, constant) #: (UnboundMethod method, Module[top] constant) -> untyped def signature_defined_by_constant(method, constant) signature = signature_of!(method) - return signature if signature && signature.method.owner == method.owner + return signature if signature && are_equal?(signature.method.owner, method.owner) # Widen the type of `prepended_method` to be nilable prepended_method = constant.instance_method(method.name) #: UnboundMethod? - while prepended_method && prepended_method.owner != method.owner + while prepended_method && !are_equal?(prepended_method.owner, method.owner) signature = signature_of(prepended_method) - return signature if signature && signature.method.owner == method.owner + return signature if signature && are_equal?(signature.method.owner, method.owner) prepended_method = prepended_method.super_method end @@ -246,6 +257,18 @@ def visibility_defined_by_constant(name, constant) end end + # Return the visibility of the method the constant's anonymous superclasses define, ignoring any modules + # prepended to them. + #: (Symbol name, Module[top] constant) -> Symbol? + def visibility_defined_by_anonymous_superclasses(name, constant) + @pipeline.anonymous_superclasses_of(constant).each do |superclass| + visibility = visibility_defined_by_constant(name, superclass) + return visibility if visibility + end + + nil + end + #: (UnboundMethod method, Module[top] constant) -> untyped def inferred_attr_writer_signature(method, constant) reader_method = attr_reader_for_writer(method, constant) diff --git a/lib/tapioca/gem/listeners/mixins.rb b/lib/tapioca/gem/listeners/mixins.rb index 9dbe30313..85eeb0a49 100644 --- a/lib/tapioca/gem/listeners/mixins.rb +++ b/lib/tapioca/gem/listeners/mixins.rb @@ -60,12 +60,34 @@ def add_mixins(tree, constant, mods, mixin_type) #: (Module[top] constant, Module[top] mixin, Runtime::Trackers::Mixin::Type mixin_type) -> bool def mixed_in_by_gem?(constant, mixin, mixin_type) mixin_location = Runtime::Trackers::Mixin.mixin_location(mixin, mixin_type, constant) + mixin_location ||= anonymous_superclass_mixin_location(constant, mixin, mixin_type) return true if mixin_location.nil? @pipeline.gem.contains_path?(mixin_location) end + # Mixins added inside an anonymous superclass are tracked under that superclass, not the constant, so look + # for them there. Modules prepended into an anonymous superclass come after the constant in its ancestors + # and are listed as includes, so includes are looked up as either an include or a prepend. + #: (Module[top] constant, Module[top] mixin, Runtime::Trackers::Mixin::Type mixin_type) -> String? + def anonymous_superclass_mixin_location(constant, mixin, mixin_type) + mixin_types = if mixin_type == Runtime::Trackers::Mixin::Type::Extend + [mixin_type] + else + [Runtime::Trackers::Mixin::Type::Include, Runtime::Trackers::Mixin::Type::Prepend] + end + + @pipeline.anonymous_superclasses_of(constant).each do |superclass| + mixin_types.each do |type| + location = Runtime::Trackers::Mixin.mixin_location(mixin, type, superclass) + return location if location + end + end + + nil + end + #: (String mixin_name) -> bool def filtered_mixin?(mixin_name) # filter T:: namespace mixins that aren't T::Props @@ -75,7 +97,14 @@ def filtered_mixin?(mixin_name) #: (Module[top] constant) -> Array[Module[top]] def interesting_ancestors_of(constant) - inherited_ancestors = Set.new.compare_by_identity.merge(inherited_ancestors_of(constant)) + # Drop the ancestors the superclass in the RBI already brings in. `class Foo < Struct.new(:a)` is written as + # `class Foo < ::Struct`, so only `Struct`'s ancestors are dropped (it's the superclass of the last anonymous + # superclass), and mixins added inside `Struct.new` are kept. Also drop the anonymous superclasses themselves, + # or a named struct like `Struct::Foo` shows up as `include ::Struct::Foo`. + anonymous_superclasses = @pipeline.anonymous_superclasses_of(constant) + inherited_ancestors = Set.new.compare_by_identity.merge( + inherited_ancestors_of(anonymous_superclasses.last || constant), + ).merge(anonymous_superclasses) # TODO: There is actually a bug here where this will drop modules that # may be included twice. For example: diff --git a/lib/tapioca/gem/listeners/source_location.rb b/lib/tapioca/gem/listeners/source_location.rb index 88f46ca50..74ef13d2c 100644 --- a/lib/tapioca/gem/listeners/source_location.rb +++ b/lib/tapioca/gem/listeners/source_location.rb @@ -34,7 +34,12 @@ def on_scope(event) # @override #: (MethodNodeAdded event) -> void def on_method(event) - definition = @pipeline.method_definition_in_gem(event.method.name, event.constant) + anonymous_superclass = @pipeline.anonymous_superclass_owning(event.method, event.constant) + + # Methods from anonymous superclasses are tracked under the anonymous superclass, not the constant, + # so use it if it owns the method. Otherwise, fall back to the constant, which is always there. + tracked_owner = anonymous_superclass || event.constant + definition = @pipeline.method_definition_in_gem(event.method.name, tracked_owner) if Pipeline::MethodInGemWithLocation === definition loc = definition.location diff --git a/lib/tapioca/gem/pipeline.rb b/lib/tapioca/gem/pipeline.rb index ebb35faee..021f3b5c7 100644 --- a/lib/tapioca/gem/pipeline.rb +++ b/lib/tapioca/gem/pipeline.rb @@ -26,6 +26,7 @@ def initialize( @gem = gem @seen = Set.new #: Set[String] @alias_namespace = Set.new #: Set[String] + @anonymous_superclasses = {}.compare_by_identity #: Hash[Module[top], Array[Module[top]]] @error_handler = error_handler @events = [] #: Array[Gem::Event] @@ -150,9 +151,13 @@ def initialize(location) end end - #: (Symbol method_name, Module[top] owner) -> MethodDefinitionLookupResult - def method_definition_in_gem(method_name, owner) - definitions = Tapioca::Runtime::Trackers::MethodDefinition.method_definitions_for(method_name, owner) + #: (Symbol method_name, Module[top] owner, ?fallback_source_method: UnboundMethod?) -> MethodDefinitionLookupResult + def method_definition_in_gem(method_name, owner, fallback_source_method: nil) + definitions = Tapioca::Runtime::Trackers::MethodDefinition.method_definitions_for( + method_name, + owner, + fallback_source_method: fallback_source_method, + ) # If the source location of the method isn't available, signal that by returning nil. return MethodUnknown.new if definitions.empty? @@ -189,8 +194,90 @@ def name_of(constant) name end + # Return the superclasses that `compile_superclass` skips because they can't be written to the RBI, like the + # unnamed ones created by `class Foo < Struct.new(:bar)`, `Data.define` or `Class.new`, and named structs like + # `Struct.new("Bar", :baz)`, which are written as `Struct`. Abstract and generic superclasses are not included + # (see `fold_into_constant?`). The methods and mixins of these superclasses are folded into the constant. + # + # Only the gem that first defines the constant picks its superclass, so other gems that reopen the constant get + # no superclasses. Otherwise, they would repeat the superclass methods in their own RBI. + #: (Module[top] constant) -> Array[Module[top]] + def anonymous_superclasses_of(constant) + @anonymous_superclasses[constant] ||= begin + superclasses = [] #: Array[Module[top]] + + if constant.singleton_class? + # Singleton classes have no names, so find the anonymous superclasses through the attached class. + # Since `Foo.singleton_class.superclass == Foo.superclass.singleton_class`, return the anonymous + # superclasses' singleton classes, which define class methods like `new`. + singleton_class = constant #: as Class[top] + attached_class = attached_class_of(singleton_class) + + if attached_class + superclasses = anonymous_superclasses_of(attached_class).map { |klass| singleton_class_of(klass) } + end + elsif Class === constant + superclass = superclass_of(constant) #: Class[top]? + + while superclass && fold_into_constant?(superclass) + superclasses << superclass + superclass = superclass_of(superclass) + end + + # Drop the superclasses if another gem first defined the constant and this gem only reopens it. Most classes + # have none, so check that first to skip looking up where the constant was defined. + superclasses = [] unless superclasses.empty? || superclass_picked_by_gem?(constant) + end + + superclasses + end + end + + # Return the anonymous superclass of the constant that owns the method, or nil if none does + #: (UnboundMethod method, Module[top] constant) -> Module[top]? + def anonymous_superclass_owning(method, constant) + owner = method.owner + + anonymous_superclasses_of(constant).find { |superclass| are_equal?(superclass, owner) } + end + private + # Whether the gem first defines the constant. Ruby only sets the superclass the first time the class is defined + # (`class Foo < Bar`), so that gem picks it, and reopening the class elsewhere can't change it. + #: (Module[top] constant) -> bool + def superclass_picked_by_gem?(constant) + name = name_of(constant) + # Without a name, there's no way to tell where the constant was defined, so assume it's this gem + return true if name.nil? + + constant_in_gem?(name) + end + + # Whether the superclass is missing from the RBI, so its methods and mixins should be folded into the constant + #: (Class[top] superclass) -> bool + def fold_into_constant?(superclass) + # Most superclasses are named and written to the RBI, so check the name first + name = name_of(superclass) + return false unless name.nil? || named_struct?(superclass, name) + + # Abstract methods and type variables can't be written on the constant, so stop at these superclasses + return false if abstract_type_of(superclass) + return false if Runtime::GenericTypeRegistry.lookup_type_variables(superclass) + + true + end + + # Whether the superclass is a named struct like `Struct::Bar`, which `name_of` writes as `Struct` + #: (Class[top] superclass, String name) -> bool + def named_struct?(superclass, name) + return false unless name == "Struct" + return false if are_equal?(superclass, ::Struct) + + # Use `ancestors` instead of `<`, which classes can override + ancestors_of(superclass).any? { |ancestor| are_equal?(ancestor, ::Struct) } + end + #: (Gemfile::GemSpec gem) -> Set[String] def load_bootstrap_symbols(gem) engine_symbols = Static::SymbolLoader.engine_symbols(gem) diff --git a/lib/tapioca/runtime/trackers/method_definition.rb b/lib/tapioca/runtime/trackers/method_definition.rb index a5772ae3a..95c62db92 100644 --- a/lib/tapioca/runtime/trackers/method_definition.rb +++ b/lib/tapioca/runtime/trackers/method_definition.rb @@ -24,12 +24,15 @@ def register(method_name, owner, locations) registrations_for(method_name, owner) << loc end - #: (Symbol method_name, Module[top] owner) -> Array[SourceLocation] - def method_definitions_for(method_name, owner) + # Return where `method_name` was defined on `owner`. If no definitions were recorded for `owner` (for example, + # the method was defined on an anonymous superclass), fall back to the source location of `fallback_source_method`, + # or of the method `owner` resolves to. + #: (Symbol method_name, Module[top] owner, ?fallback_source_method: UnboundMethod?) -> Array[SourceLocation] + def method_definitions_for(method_name, owner, fallback_source_method: nil) definitions = registrations_for(method_name, owner) if definitions.empty? - source_loc = owner.instance_method(method_name).source_location + source_loc = (fallback_source_method || owner.instance_method(method_name)).source_location definitions = [SourceLocation.from_loc(source_loc)].compact end diff --git a/spec/tapioca/gem/pipeline_spec.rb b/spec/tapioca/gem/pipeline_spec.rb index 43a52ed94..65dade0e9 100644 --- a/spec/tapioca/gem/pipeline_spec.rb +++ b/spec/tapioca/gem/pipeline_spec.rb @@ -1678,7 +1678,19 @@ def foo; end end module M3; end - class S1 < ::Struct; end + + class S1 < ::Struct + def foo; end + def foo=(_); end + + class << self + def [](*_arg0); end + def inspect; end + def keyword_init?; end + def members; end + def new(*_arg0); end + end + end class S2 < ::Struct def foo; end @@ -1706,7 +1718,806 @@ def new(*_arg0); end end end - class S4 < ::Struct; end + class S4 < ::Struct + def foo; end + def foo=(_); end + + class << self + def [](*_arg0); end + def inspect; end + def keyword_init?; end + def members; end + def new(*_arg0); end + end + end + RBI + + assert_equal(output, compile) + end + + it "compiles methods of Struct anonymous superclasses the same as the block form" do + add_ruby_file("ssl_options.rb", <<~RUBY) + class SSLOptions < Struct.new(:verify_hostname) + def verify?; end + end + + BlockSSLOptions = Struct.new(:verify_hostname) do + def verify?; end + end + RUBY + + output = template(<<~RBI) + class BlockSSLOptions < ::Struct + def verify?; end + def verify_hostname; end + def verify_hostname=(_); end + + class << self + def [](*_arg0); end + def inspect; end + def keyword_init?; end + def members; end + def new(*_arg0); end + end + end + + class SSLOptions < ::Struct + def verify?; end + def verify_hostname; end + def verify_hostname=(_); end + + class << self + def [](*_arg0); end + def inspect; end + def keyword_init?; end + def members; end + def new(*_arg0); end + end + end + RBI + + assert_equal(output, compile) + end + + it "compiles named Struct superclasses like aws-sdk-core's EmptyStructure" do + add_ruby_file("structure.rb", <<~RUBY) + module Aws + module Structure + def initialize(values = {}) + values.each do |k, v| + self[k] = v + end + end + + def key?(member_name) + !self[member_name].nil? + end + end + + class EmptyStructure < Struct.new("AwsEmptyStructure") + include(Aws::Structure) + end + end + RUBY + + output = template(<<~RBI) + module Aws; end + + class Aws::EmptyStructure < ::Struct + include ::Aws::Structure + + class << self + def [](*_arg0); end + def inspect; end + def keyword_init?; end + def members; end + def new(*_arg0); end + end + end + + module Aws::Structure + def initialize(values = T.unsafe(nil)); end + + def key?(member_name); end + end + RBI + + assert_equal(output, compile) + end + + it "compiles named Struct superclasses defined in other gems" do + mock_gem("aws") do + add_ruby_file("lib/aws.rb", <<~RUBY) + EmptyStructure = Struct.new("EmptyStructure") + Credentials = Struct.new("Credentials", :key) + RUBY + end + + mock_gem("foo") do + add_ruby_file("lib/foo.rb", <<~RUBY) + class EmptyA < EmptyStructure; end + + class FooCredentials < Credentials + def secret; end + end + RUBY + end + + output = <<~RBI + class EmptyA < ::Struct + class << self + def [](*_arg0); end + def inspect; end + def keyword_init?; end + def members; end + def new(*_arg0); end + end + end + + class FooCredentials < ::Struct + def key; end + def key=(_); end + def secret; end + + class << self + def [](*_arg0); end + def inspect; end + def keyword_init?; end + def members; end + def new(*_arg0); end + end + end + RBI + + assert_equal(output, compile("foo")) + end + + it "does not compile Struct anonymous superclass methods in other gems that reopen the class" do + mock_gem("foo") do + add_ruby_file("lib/foo.rb", <<~RUBY) + class Foo < Struct.new(:x) + end + RUBY + end + + mock_gem("bar") do + add_ruby_file("lib/bar.rb", <<~RUBY) + class Foo + def bar; end + end + RUBY + end + + output = <<~RBI + class Foo < ::Struct + def bar; end + end + RBI + + assert_equal(output, compile("bar")) + end + + it "compiles Struct anonymous superclass methods overridden or hidden by the subclass" do + add_ruby_file("point.rb", <<~RUBY) + class Point < Struct.new(:x, :y, keyword_init: true) + def x + super.to_i + end + + private :y= + end + RUBY + + output = template(<<~RBI) + class Point < ::Struct + def x; end + def x=(_); end + def y; end + + private + + def y=(_); end + + class << self + def [](*_arg0); end + def inspect; end + def keyword_init?; end + def members; end + def new(*_arg0); end + end + end + RBI + + assert_equal(output, compile) + end + + it "keeps signatures of methods defined in the block of an anonymous superclass" do + add_ruby_file("counter.rb", <<~RUBY) + class Counter < Struct.new(:x) do + extend T::Sig + + sig { returns(Integer) } + attr_accessor :count + + sig { returns(Integer) } + def total; 1; end + + sig(:final) { returns(String) } + def label; ""; end + end + end + RUBY + + output = template(<<~RBI) + class Counter < ::Struct + sig { returns(::Integer) } + def count; end + + sig { params(count: ::Integer).returns(::Integer) } + def count=(count); end + + sig(:final) { returns(::String) } + def label; end + + sig { returns(::Integer) } + def total; end + + def x; end + def x=(_); end + + class << self + def [](*_arg0); end + def inspect; end + def keyword_init?; end + def members; end + def new(*_arg0); end + end + end + RBI + + assert_equal(output, compile) + end + + it "compiles source locations of methods from an anonymous superclass" do + add_ruby_file("point.rb", <<~RUBY) + class Point < Struct.new(:x) do + extend T::Sig + + sig { returns(Integer) } + def double; x * 2; end + end + end + RUBY + + output = template(<<~RBI) + # pkg:gem/#{DEFAULT_GEM_NAME}#lib/point.rb:6 + class Point < ::Struct + # pkg:gem/#{DEFAULT_GEM_NAME}#lib/point.rb:5 + sig { returns(::Integer) } + def double; end + + # pkg:gem/#{DEFAULT_GEM_NAME}#lib/point.rb:1 + def x; end + + # pkg:gem/#{DEFAULT_GEM_NAME}#lib/point.rb:1 + def x=(_); end + + class << self + # pkg:gem/#{DEFAULT_GEM_NAME}#lib/point.rb:1 + def [](*_arg0); end + + # pkg:gem/#{DEFAULT_GEM_NAME}#lib/point.rb:1 + def inspect; end + + # pkg:gem/#{DEFAULT_GEM_NAME}#lib/point.rb:1 + def keyword_init?; end + + # pkg:gem/#{DEFAULT_GEM_NAME}#lib/point.rb:1 + def members; end + + # pkg:gem/#{DEFAULT_GEM_NAME}#lib/point.rb:1 + def new(*_arg0); end + end + end + RBI + + assert_equal(output, compile(include_loc: true)) + end + + it "compiles Struct anonymous superclass methods behind a module prepended into the anonymous superclass" do + add_ruby_file("point.rb", <<~RUBY) + module Rounded + def x + super.round + end + end + + class Point < Struct.new(:x) { prepend Rounded } + end + RUBY + + output = template(<<~RBI) + class Point < ::Struct + include ::Rounded + + def x; end + def x=(_); end + + class << self + def [](*_arg0); end + def inspect; end + def keyword_init?; end + def members; end + def new(*_arg0); end + end + end + + module Rounded + def x; end + end + RBI + + assert_equal(output, compile) + end + + it "compiles Struct anonymous superclass methods overridden by a module prepended from another gem" do + mock_gem("foo") do + add_ruby_file("lib/foo.rb", <<~RUBY) + class Point < Struct.new(:x) do + def double; x * 2; end + end + end + RUBY + end + + mock_gem("bar") do + add_ruby_file("lib/bar.rb", <<~RUBY) + module Patch + def double; super; end + def x; super; end + end + + Point.prepend(Patch) + RUBY + end + + output = <<~RBI + class Point < ::Struct + def double; end + def x; end + def x=(_); end + + class << self + def [](*_arg0); end + def inspect; end + def keyword_init?; end + def members; end + def new(*_arg0); end + end + end + RBI + + assert_equal(output, compile("foo")) + end + + it "uses the anonymous superclass method visibility instead of the prepended method's visibility" do + add_ruby_file("point.rb", <<~RUBY) + module Hidden + private + + def x + super + end + end + + class Point < Struct.new(:x) + prepend Hidden + end + RUBY + + output = template(<<~RBI) + module Hidden + private + + def x; end + end + + class Point < ::Struct + include ::Hidden + + def x; end + def x=(_); end + + class << self + def [](*_arg0); end + def inspect; end + def keyword_init?; end + def members; end + def new(*_arg0); end + end + end + RBI + + assert_equal(output, compile) + end + + it "compiles mixins of Struct and Data anonymous superclasses" do + add_ruby_file("point.rb", <<~RUBY) + module Helpers; end + module ClassHelpers; end + + class Point < Struct.new(:x) do + include Helpers + extend ClassHelpers + end + end + + class Coordinate < Data.define(:x) { include Comparable } + end + RUBY + + output = template(<<~RBI) + module ClassHelpers; end + + class Coordinate < ::Data + include ::Comparable + + def x; end + + class << self + def [](*_arg0); end + def inspect; end + def members; end + def new(*_arg0); end + end + end + + module Helpers; end + + class Point < ::Struct + include ::Helpers + extend ::ClassHelpers + + def x; end + def x=(_); end + + class << self + def [](*_arg0); end + def inspect; end + def keyword_init?; end + def members; end + def new(*_arg0); end + end + end + RBI + + assert_equal(output, compile) + end + + it "does not compile mixins of anonymous superclasses performed in other gems" do + mock_gem("foo") do + add_ruby_file("lib/foo.rb", <<~RUBY) + class Foo < Struct.new(:x) + end + RUBY + end + + mock_gem("bar") do + add_ruby_file("lib/bar.rb", <<~RUBY) + module Bar; end + module BarPrepend; end + module BarExtend; end + + Foo.superclass.include(Bar) + Foo.superclass.prepend(BarPrepend) + Foo.superclass.extend(BarExtend) + RUBY + end + + output = <<~RBI + class Foo < ::Struct + def x; end + def x=(_); end + + class << self + def [](*_arg0); end + def inspect; end + def keyword_init?; end + def members; end + def new(*_arg0); end + end + end + RBI + + assert_equal(output, compile("foo")) + end + + it "does not repeat Struct anonymous superclass methods on grandchild classes" do + add_ruby_file("point.rb", <<~RUBY) + class Point < Struct.new(:x) + end + + class Point3D < Point + def z; end + end + RUBY + + output = template(<<~RBI) + class Point < ::Struct + def x; end + def x=(_); end + + class << self + def [](*_arg0); end + def inspect; end + def keyword_init?; end + def members; end + def new(*_arg0); end + end + end + + class Point3D < ::Point + def z; end + end + RBI + + assert_equal(output, compile) + end + + it "compiles methods of Data anonymous superclasses the same as the block form" do + add_ruby_file("point.rb", <<~RUBY) + class Point < Data.define(:x, :y) + def sum + x + y + end + end + + BlockPoint = Data.define(:x, :y) do + def sum + x + y + end + end + RUBY + + output = template(<<~RBI) + class BlockPoint < ::Data + def sum; end + def x; end + def y; end + + class << self + def [](*_arg0); end + def inspect; end + def members; end + def new(*_arg0); end + end + end + + class Point < ::Data + def sum; end + def x; end + def y; end + + class << self + def [](*_arg0); end + def inspect; end + def members; end + def new(*_arg0); end + end + end + RBI + + assert_equal(output, compile) + end + + it "compiles methods of Class.new anonymous superclasses" do + add_ruby_file("foo.rb", <<~RUBY) + class Base + def base; end + end + + class Foo < Class.new { def bar; end; def self.baz; end } + end + + class Qux < Class.new(Base) { def quux; end } + end + + class Deep < Class.new(Class.new { def deep; end }) { def shallow; end } + end + RUBY + + output = template(<<~RBI) + class Base + def base; end + end + + class Deep + def deep; end + def shallow; end + end + + class Foo + def bar; end + + class << self + def baz; end + end + end + + class Qux < ::Base + def quux; end + end + RBI + + assert_equal(output, compile) + end + + it "does not compile methods of abstract or generic anonymous superclasses" do + add_ruby_file("foo.rb", <<~RUBY) + class Impl < Class.new { + extend T::Sig + extend T::Helpers + + abstract! + + sig { abstract.returns(Integer) } + def value; end + } + end + + class IntList < Class.new { + extend T::Sig + extend T::Generic + + Elem = type_member + + sig { returns(T.nilable(Elem)) } + def first; end + } + end + RUBY + + output = template(<<~RBI) + class Impl; end + class IntList; end + RBI + + assert_equal(output, compile) + end + + it "compiles initialize methods of anonymous superclasses" do + add_ruby_file("point.rb", <<~RUBY) + class Point < Struct.new(:x) do + def initialize(x = 1) + super + end + end + end + + class Coordinate < Data.define(:x, :y) do + def initialize(x:, y: 0) = super + end + end + + class Pair < Class.new { def initialize(a, b: 1); end } + end + RUBY + + output = template(<<~RBI) + class Coordinate < ::Data + def initialize(x:, y: T.unsafe(nil)); end + + def x; end + def y; end + + class << self + def [](*_arg0); end + def inspect; end + def members; end + def new(*_arg0); end + end + end + + class Pair + def initialize(a, b: T.unsafe(nil)); end + end + + class Point < ::Struct + def initialize(x = T.unsafe(nil)); end + + def x; end + def x=(_); end + + class << self + def [](*_arg0); end + def inspect; end + def keyword_init?; end + def members; end + def new(*_arg0); end + end + end + RBI + + assert_equal(output, compile) + end + + it "compiles the visibility of methods defined in an anonymous superclass" do + add_ruby_file("point.rb", <<~RUBY) + class Point < Struct.new(:x) do + def pub; end + protected def prot; end + private def priv; end + + def self.build; end + private_class_method :build + end + end + RUBY + + output = template(<<~RBI) + class Point < ::Struct + def pub; end + def x; end + def x=(_); end + + protected + + def prot; end + + private + + def priv; end + + class << self + def [](*_arg0); end + def inspect; end + def keyword_init?; end + def members; end + def new(*_arg0); end + + private + + def build; end + end + end + RBI + + assert_equal(output, compile) + end + + it "compiles public methods ActiveSupport defines in an anonymous superclass" do + add_ruby_file("list.rb", <<~RUBY) + require "active_support" + require "active_support/core_ext/module/delegation" + require "active_support/core_ext/class/attribute" + + class List < Struct.new(:items) do + delegate :size, to: :items + class_attribute :setting + end + end + RUBY + + output = template(<<~RBI) + class List < ::Struct + def items; end + def items=(_); end + def setting; end + def setting=(_arg0); end + def setting?; end + def size(*, **, &); end + + class << self + def [](*_arg0); end + def inspect; end + def keyword_init?; end + def members; end + def new(*_arg0); end + def setting; end + def setting=(value); end + def setting?; end + end + end RBI assert_equal(output, compile)