Skip to content

Generate RBI methods inherited from Struct and other anonymous superclasses - #2737

Open
DaneHarrison wants to merge 10 commits into
Shopify:mainfrom
DaneHarrison:task/1384-anonymous-superclass-methods
Open

DaneHarrison wants to merge 10 commits into
Shopify:mainfrom
DaneHarrison:task/1384-anonymous-superclass-methods

Conversation

@DaneHarrison

@DaneHarrison DaneHarrison commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Fixes #1384 - When a class inherits from an anonymous superclass like class SSLOptions < Struct.new(:verify_hostname), Tapioca skips everything defined on the anonymous class. So we get verify? but not verify_hostname/verify_hostname=. Same thing happens with Data.define and Class.new { }.


Implementation

Methods from the anonymous superclass are now compiled onto the named class, so class Foo < Struct.new(:bar) becomes class Foo < ::Struct with bar and bar=.

There are ~800 lines of tests; commits highlight the tests that turn green after the changes.


  • New Pipeline#anonymous_superclasses_of caches and returns the unnamed superclasses compile_superclass skips.

    • Singleton classes map to their attached class so self. methods work.
    • Abstract and generic superclasses stop the walk, since their abstract methods and type members can't go on the subclass.
  • Only the gem that first defines the class gets these superclasses. Otherwise a gem that reopens the class repeats all the Struct methods in its RBI.

    • When the superclass is a named struct defined in another gem, or is built by another gem's method (class Foo < LibA.Base(:x)), this gem gets the members and class methods but not the methods defined in its block (def helper in Struct.new("Shared", :key) { ... }). Those are defined in the other gem, so they're skipped here. That gem doesn't have them either, since it writes a named struct as Shared = Struct.
  • method_defined_by_constant now accepts methods owned by those superclasses, through the new Pipeline#anonymous_superclass_owning (also used by SourceLocation) after the constant itself is checked.

  • Method definitions are tracked against the anonymous class, not the named one, so method_definitions_for falls back to source_location. That returned the sig wrapper or a prepended module's method, so those methods were dropped. fallback_source_method: passes in the method compile_method already found on the anonymous superclass.

    • I tried calling method_definition_in_gem with the anonymous superclass as the owner instead (like SourceLocation does), but that broke tests
      • "compiles named Struct superclasses defined in other gems"
      • "compiles DelegateClass"
      • "does not compile methods a DelegateClass superclass delegates"
      • "compiles public methods ActiveSupport defines in an anonymous superclass"
  • Visibility falls back to the anonymous superclass, so a prepended module's private doesn't override the original.

  • For loc comments (--include-loc), SourceLocation looks the method up under the anonymous superclass, so the comment points to the right line.

  • Mixins only drops ancestors from the first named superclass, so an include/extend inside the Struct.new block ends up on the class. Mixin locations are looked up on the anonymous superclass too, so mixins done by other gems still get filtered.

  • Named structs (Struct::Foo) count as anonymous too, since name_of writes them as Struct. named_struct? also checks that the class really inherits from Struct. Mixins also skips the superclasses themselves so they don't show up as an include ::Struct::Foo.


Further Considerations:

  • Unrelated to this issue, I fixed a minor issue in my last PR (Fix method signatures being overwritten by prepended modules #2718): owner checks in methods.rb used ==/!=, which a class can override. I switched them to are_equal?, which the rest of Tapioca uses to compare modules by identity.
  • Since most classes won't have an anonymous superclass, I tried to memoize and short-circuit when possible.

Tests

  • Updated the S1 and S4 expectations in the existing Structs, Classes, and Modules spec
  • SSLOptions from the issue matches the Struct.new block form
  • A Data.define subclass matches the Data.define block form
  • A copy of aws-sdk-core's class EmptyStructure < Struct.new('AwsEmptyStructure')
  • Named structs defined in another gem and subclassed in this one
  • A Struct subclass reopened by another gem doesn't repeat the Struct methods
  • Struct methods overridden by the subclass, or made private with private :y=
  • Sigs defined in the Struct.new block (plain, sig(:final) and attr_accessor)
  • Source locations for methods from the anonymous superclass
  • Modules prepended onto the subclass, inside the Struct.new block, and from another gem
  • A prepended module's private doesn't change the Struct method's visibility
  • include/extend inside a Struct.new block, and include inside a Data.define block
  • include/prepend/extend on the anonymous superclass from another gem are skipped
  • Grandchild of a Struct subclass doesn't repeat the methods
  • Data anonymous superclass matches the block form
  • Class.new superclasses: plain, with a named parent, and nested
  • Abstract and generic anonymous superclasses aren't compiled into the subclass
  • initialize from Struct, Data and Class.new superclasses
  • public/protected/private and private_class_method in the block
  • DelegateClass methods are still left out
  • ActiveSupport delegate and class_attribute in the block

`class Foo < Struct.new(:bar)` is written as `class Foo < ::Struct`, so the
`bar`/`bar=` accessors defined on the anonymous superclass were dropped.
Methods owned by unnamed superclasses are now compiled on the constant.
- compiles Structs, Classes, and Modules: expect methods for a named `Struct.new("Foo")` superclass
- compiles named Struct superclasses like aws-sdk-core's EmptyStructure
- compiles named Struct superclasses defined in other gems
- does not compile Struct anonymous superclass methods in other gems that reopen the class
- keeps signatures of methods defined in the block of an anonymous superclass
- compiles source locations of methods from an anonymous superclass
- compiles Struct anonymous superclass methods behind a module prepended into the anonymous superclass
- compiles Struct anonymous superclass methods overridden by a module prepended from another gem
- uses the anonymous superclass method visibility instead of the prepended method's visibility
- compiles mixins of Struct and Data anonymous superclasses
- does not compile mixins of anonymous superclasses performed in other gems (passes for now, since no mixins are compiled yet)
- does not compile methods of abstract or generic anonymous superclasses (passes for now, since sig-wrapped methods aren't found yet)
Only the gem that first defines the class picks its superclass, so other
gems reopening it shouldn't repeat the superclass methods.

Passing: does not compile Struct anonymous superclass methods in other gems that reopen the class
Methods from anonymous superclasses aren't tracked for the constant, so the
lookup fell back to `constant.instance_method`, which can return a `sig`
wrapper or a prepended module's method. Those aren't defined in the gem, so
the methods were dropped. Use the anonymous superclass's own method instead.
Abstract and generic superclasses are now skipped, since their sigs can't be
written on the constant.

Passing:
- keeps signatures of methods defined in the block of an anonymous superclass
- compiles Struct anonymous superclass methods overridden by a module prepended from another gem
- does not compile methods of abstract or generic anonymous superclasses (fails without the abstract/generic check once these methods are kept)
When a prepended module overrides an anonymous superclass method, take the
visibility from the anonymous superclass, like we already do for methods the
constant defines.

Passing: uses the anonymous superclass method visibility instead of the prepended method's visibility
Methods from anonymous superclasses are tracked under that superclass, not
the constant, so look up their source locations there.

Passing: compiles source locations of methods from an anonymous superclass
Mixins added inside `Struct.new`/`Data.define` blocks were dropped along
with the anonymous superclass. Keep them on the constant, and check where
they were added so mixins from other gems are still skipped.

Passing:
- compiles mixins of Struct and Data anonymous superclasses
- compiles Struct anonymous superclass methods behind a module prepended into the anonymous superclass
- does not compile mixins of anonymous superclasses performed in other gems (fails without the location check)
`Struct.new("Bar", :baz)` creates `Struct::Bar`, which is written as
`Struct`, so its methods were dropped the same way.

Passing:
- compiles Structs, Classes, and Modules
- compiles named Struct superclasses like aws-sdk-core's EmptyStructure
- compiles named Struct superclasses defined in other gems
The prepend handling compared `UnboundMethod#owner` against modules with
`==`/`!=`, which a class can override. Use `are_equal?` instead.
Comment on lines +100 to +103
# 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`.

@DaneHarrison DaneHarrison Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DaneHarrison
DaneHarrison force-pushed the task/1384-anonymous-superclass-methods branch from 57e7779 to 464edbc Compare October 7, 2026 23:37
@DaneHarrison
DaneHarrison marked this pull request as ready for review October 8, 2026 00:01
@DaneHarrison
DaneHarrison requested a review from a team as a code owner October 8, 2026 00:01

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tapioca does not generate RBI methods for Struct subclasses

1 participant