Repository navigation
Generate RBI methods inherited from Struct and other anonymous superclasses - #2737
DaneHarrison wants to merge 10 commits into
Conversation
`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.
| # 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`. |
There was a problem hiding this comment.
-
The RBI's superclass already brings in its ancestors, so they're dropped:
Reflection#inherited_ancestors_of,Mixins#interesting_ancestors_of -
class Foo < Struct.new(:a)is written asclass Foo < ::Struct:Pipeline#compile_superclass -
Only
Struct's ancestors are dropped, since it's the superclass of the last (topmost) anonymous superclass:Pipeline#anonymous_superclasses_of,Mixins#interesting_ancestors_of -
Mixins added inside
Struct.new/Data.defineare kept: tested by "compiles mixins of Struct and Data anonymous superclasses" -
The anonymous superclasses themselves are dropped, otherwise named structs show up as
include ::Struct::Foo:Mixins#interesting_ancestors_of, tested by "compiles Structs, Classes, and Modules", "compiles named Struct superclasses like aws-sdk-core's EmptyStructure", and "compiles named Struct superclasses defined in other gems"
57e7779 to
464edbc
Compare
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 getverify?but notverify_hostname/verify_hostname=. Same thing happens withData.defineandClass.new { }.Implementation
Methods from the anonymous superclass are now compiled onto the named class, so
class Foo < Struct.new(:bar)becomesclass Foo < ::Structwithbarandbar=.There are ~800 lines of tests; commits highlight the tests that turn green after the changes.
New
Pipeline#anonymous_superclasses_ofcaches and returns the unnamed superclassescompile_superclassskips.self.methods work.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.
class Foo < LibA.Base(:x)), this gem gets the members and class methods but not the methods defined in its block (def helperinStruct.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 asShared = Struct.method_defined_by_constantnow accepts methods owned by those superclasses, through the newPipeline#anonymous_superclass_owning(also used bySourceLocation) after the constant itself is checked.Method definitions are tracked against the anonymous class, not the named one, so
method_definitions_forfalls back tosource_location. That returned the sig wrapper or a prepended module's method, so those methods were dropped.fallback_source_method:passes in the methodcompile_methodalready found on the anonymous superclass.method_definition_in_gemwith the anonymous superclass as the owner instead (likeSourceLocationdoes), but that broke testsVisibility falls back to the anonymous superclass, so a prepended module's
privatedoesn't override the original.For loc comments (
--include-loc),SourceLocationlooks the method up under the anonymous superclass, so the comment points to the right line.Mixinsonly drops ancestors from the first named superclass, so aninclude/extendinside theStruct.newblock 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, sincename_ofwrites them asStruct.named_struct?also checks that the class really inherits fromStruct.Mixinsalso skips the superclasses themselves so they don't show up as aninclude ::Struct::Foo.Further Considerations:
methods.rbused==/!=, which a class can override. I switched them to are_equal?, which the rest of Tapioca uses to compare modules by identity.Tests
S1andS4expectations in the existing Structs, Classes, and Modules specSSLOptionsfrom the issue matches theStruct.newblock formData.definesubclass matches theData.defineblock formclass EmptyStructure < Struct.new('AwsEmptyStructure')private :y=Struct.newblock (plain,sig(:final)andattr_accessor)Struct.newblock, and from another gemprivatedoesn't change the Struct method's visibilityinclude/extendinside aStruct.newblock, andincludeinside aData.defineblockinclude/prepend/extendon the anonymous superclass from another gem are skippedClass.newsuperclasses: plain, with a named parent, and nestedinitializefrom Struct, Data andClass.newsuperclassespublic/protected/privateandprivate_class_methodin the blockDelegateClassmethods are still left outdelegateandclass_attributein the block