Skip to content

Foreign constant reflection breaks with non-standard singleton class inspect #1097

Description

@michaelherold

For classes that redefine their singleton class #inspect method (e.g. ActiveRecord::Base, ActiveRecord::SchemaMigration, and ActiveRecord::InternalMetadata all override with connection information), Tapioca cannot constantize the string into a module, leading to a TypeError down the chain when attempting to handle mixins for those constants.

This pops up in cases where gems monkey-patch those classes. When you run tapioca gem <affected_gem>, you end up with the following style of stacktrace.

Stacktrace of error
/Users/herold/.gem/ruby/3.1.2/gems/tapioca-0.9.2/lib/tapioca/runtime/reflection.rb:61:in `bind_call': bind argument must be an instance of Module (TypeError)
        from /Users/herold/.gem/ruby/3.1.2/gems/tapioca-0.9.2/lib/tapioca/runtime/reflection.rb:61:in `ancestors_of'
        from /Users/herold/.gem/ruby/3.1.2/gems/tapioca-0.9.2/lib/tapioca/gem/listeners/mixins.rb:115:in `interesting_ancestors_of'
        from /Users/herold/.gem/ruby/3.1.2/gems/tapioca-0.9.2/lib/tapioca/gem/listeners/mixins.rb:19:in `on_scope'
        from /Users/herold/.gem/ruby/3.1.2/gems/tapioca-0.9.2/lib/tapioca/gem/listeners/base.rb:26:in `dispatch'
        from /Users/herold/.gem/ruby/3.1.2/gems/tapioca-0.9.2/lib/tapioca/gem/pipeline.rb:188:in `block in on_node'
        from /Users/herold/.gem/ruby/3.1.2/gems/tapioca-0.9.2/lib/tapioca/gem/pipeline.rb:188:in `each'
        from /Users/herold/.gem/ruby/3.1.2/gems/tapioca-0.9.2/lib/tapioca/gem/pipeline.rb:188:in `on_node'
        from /Users/herold/.gem/ruby/3.1.2/gems/tapioca-0.9.2/lib/tapioca/gem/pipeline.rb:153:in `dispatch'
        from /Users/herold/.gem/ruby/3.1.2/gems/tapioca-0.9.2/lib/tapioca/gem/pipeline.rb:49:in `compile'
        from /Users/herold/.gem/ruby/3.1.2/gems/sorbet-runtime-0.5.10263/lib/types/private/methods/_methods.rb:272:in `bind_call'
        from /Users/herold/.gem/ruby/3.1.2/gems/sorbet-runtime-0.5.10263/lib/types/private/methods/_methods.rb:272:in `block in _on_method_added'
        from /Users/herold/.gem/ruby/3.1.2/gems/tapioca-0.9.2/lib/tapioca/commands/gem.rb:188:in `compile_gem_rbi'
        from /Users/herold/.gem/ruby/3.1.2/gems/sorbet-runtime-0.5.10263/lib/types/private/methods/_methods.rb:272:in `bind_call'
        from /Users/herold/.gem/ruby/3.1.2/gems/sorbet-runtime-0.5.10263/lib/types/private/methods/_methods.rb:272:in `block in _on_method_added'
        from /Users/herold/.gem/ruby/3.1.2/gems/tapioca-0.9.2/lib/tapioca/commands/gem.rb:79:in `block (2 levels) in execute'
        from /Users/herold/.gem/ruby/3.1.2/gems/thor-1.2.1/lib/thor/shell/basic.rb:44:in `indent'
        from /Users/herold/.gem/ruby/3.1.2/gems/tapioca-0.9.2/lib/tapioca/commands/gem.rb:78:in `block in execute'
        from /Users/herold/.gem/ruby/3.1.2/gems/parallel-1.22.1/lib/parallel.rb:587:in `call_with_index'
        from /Users/herold/.gem/ruby/3.1.2/gems/parallel-1.22.1/lib/parallel.rb:354:in `block in work_direct'
        from /Users/herold/.gem/ruby/3.1.2/gems/parallel-1.22.1/lib/parallel.rb:597:in `with_instrumentation'
        from /Users/herold/.gem/ruby/3.1.2/gems/parallel-1.22.1/lib/parallel.rb:353:in `work_direct'
        from /Users/herold/.gem/ruby/3.1.2/gems/parallel-1.22.1/lib/parallel.rb:288:in `map'
        from /Users/herold/.gem/ruby/3.1.2/gems/tapioca-0.9.2/lib/tapioca/executor.rb:31:in `run_in_parallel'
        from /Users/herold/.gem/ruby/3.1.2/gems/sorbet-runtime-0.5.10263/lib/types/private/methods/_methods.rb:272:in `bind_call'
        from /Users/herold/.gem/ruby/3.1.2/gems/sorbet-runtime-0.5.10263/lib/types/private/methods/_methods.rb:272:in `block in _on_method_added'
        from /Users/herold/.gem/ruby/3.1.2/gems/tapioca-0.9.2/lib/tapioca/commands/gem.rb:77:in `execute'
        from /Users/herold/.gem/ruby/3.1.2/gems/sorbet-runtime-0.5.10263/lib/types/private/methods/_methods.rb:272:in `bind_call'
        from /Users/herold/.gem/ruby/3.1.2/gems/sorbet-runtime-0.5.10263/lib/types/private/methods/_methods.rb:272:in `block in _on_method_added'
        from /Users/herold/.gem/ruby/3.1.2/gems/tapioca-0.9.2/lib/tapioca/cli.rb:265:in `block in gem'
        from /Users/herold/.gem/ruby/3.1.2/gems/tapioca-0.9.2/lib/tapioca.rb:18:in `block in silence_warnings'
        from /opt/rubies/3.1.2/lib/ruby/3.1.0/rubygems/user_interaction.rb:46:in `use_ui'
        from /Users/herold/.gem/ruby/3.1.2/gems/tapioca-0.9.2/lib/tapioca.rb:17:in `silence_warnings'
        from /Users/herold/.gem/ruby/3.1.2/gems/sorbet-runtime-0.5.10263/lib/types/private/methods/_methods.rb:272:in `bind_call'
        from /Users/herold/.gem/ruby/3.1.2/gems/sorbet-runtime-0.5.10263/lib/types/private/methods/_methods.rb:272:in `block in _on_method_added'
        from /Users/herold/.gem/ruby/3.1.2/gems/tapioca-0.9.2/lib/tapioca/cli.rb:225:in `gem'
        from /Users/herold/.gem/ruby/3.1.2/gems/thor-1.2.1/lib/thor/command.rb:27:in `run'
        from /Users/herold/.gem/ruby/3.1.2/gems/thor-1.2.1/lib/thor/invocation.rb:127:in `invoke_command'
        from /Users/herold/.gem/ruby/3.1.2/gems/thor-1.2.1/lib/thor.rb:392:in `dispatch'
        from /Users/herold/.gem/ruby/3.1.2/gems/thor-1.2.1/lib/thor/base.rb:485:in `start'
        from /Users/herold/.gem/ruby/3.1.2/gems/tapioca-0.9.2/exe/tapioca:23:in `<top (required)>'
        from bin/tapioca:29:in `load'
        from bin/tapioca:29:in `<main>'

In this example, Tapioca::Runtime::Trackers::Mixin ends up with a constants_with_mixin hash with ActiveRecord::SchemaMigration (call 'ActiveRecord::SchemaMigration.connection' to establish a connection) as the key. This, in turn, gets fed through constantize and can't be converted into the correct class, thus ending up with a nil as the constant in the Tapioca::Gem::Event::ForeignConstantFound event.

I attempted a naive patch to remove /\s+.*/ from the name, but that ended up creating syntax errors in the generated RBI file since the original name gets output to the file. Maybe if we sanitize it higher in the stack?

I have a minimal reproduction project available that uses an internal Shopify gem to reproduce the bug if you'd like me to upload that.

cc @vahe

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions