Reflecting on Ruby Reflection for Rendering RBIs, by Ufuk Kayserilioglu

Abstract

As part of our adoption process of Sorbet at Shopify, we needed an automated way to teach Sorbet about our ~400 gem dependencies. We decided to tackle this problem by generating interface files (RBI) for each gem via runtime reflection.

However, this turns out to not be as simple as it sounds; the flexible nature of Ruby allows gem authors to do many wild things that make this Hard. Come and hear about all the lessons that we learnt about runtime reflection in Ruby while building tapioca.

Details

  • Intro: The need for generating RBI files
    • Static analysis and Sorbet’s limitation
    • Gems are black boxes as far as static analyzer is concerned
    • RBI format
  • Reflection: Ruby reflection APIs
    • How to do runtime reflection in Ruby
    • Module#const_get, Module#constants, Module#instance_methods and friends
    • Method#source_location
    • How to find a constant’s location? (Ruby 2.7 adds const_source_location)
  • Bootstrapping: Which constants do we start from?
    • Run Sorbet over gem source files to create a list of exported “constants” as seen statically.
    • Walk over the list of constants
  • Reopening core classes: Active Support and friends
    • How to detect if a gem is adding methods to core types?
    • How to tell which methods were added by which gem?
  • Pitfalls and Solutions: What to watch out for?
    • Pitfall: Gems overriding various base methods Module#name, Kernel#class, etc
    • Solution: .bind(obj).call idiom (Ruby 2.7 adds bind_call)
    • Pitfall: Private superclasses
    • Solution: Check superclasses for visibility and walk up the chain
    • Pitfall: Classes that end up being their own superclasses
    • Solution: Compare the resolved name of superclass and walk up the chain
    • Pitfall: Methods with invalid names (define_method(“123”) {})
    • Solution: Enforce proper naming for generated methods
    • Surprise: What if a gem deletes a top-level type!?
  • Shortcomings
    • Cannot isolate changes to types to a particular gem
    • Cannot represent some Ruby constructs faithfully (like Forwardable)
  • Conclusions and take aways

Pitch

Ruby has one of the nicest reflection APIs across all languages, but the dynamic nature of the Ruby language allows gem authors to do wild things. This makes proper reflection and, in turn, interface generation very hard.

This talk will present an overview of the Ruby reflection API and talk about what makes it so hard to do reflection correctly in the wild for arbitrary types exported from gems. For every challenge, solutions will be presented and discussed. The talk will also mention how Ruby is improving its APIs to make doing this kind of work easier, with the addition of const_source_location and bind_call in 2.7, for example.

The speaker is the primary author of the open-source tapioca gem at Shopify and has had to deal with all this complexity to get everything working both correctly and with reasonable coverage.

Edit proposal

Submissions

RubyKaigi 2020 - Accepted [Edit]

Add submission