Tapioca Documentation

repository·main·Indexed 21 days ago

https://github.com/shopify/tapioca

Tapioca is a tool that facilitates the use of Sorbet, a static type checker for Ruby, by automatically generating RBI (Ruby Interface) files. It bridges the gap between Sorbet and dynamic Ruby features such as gems, Rails, and various DSLs. The tool provides a CLI for generating gem RBIs, pulling community annotations from sources like rbi-central, and managing type checking configurations.

Tokens
43.4K
Snippets
121
Records
153
Agent score
74%

What's inside Tapioca

  1. Generated RBI structure for Kredis attributes

    main

    When using the Kredis compiler, the generated RBI follows a specific pattern:

    1. A module named GeneratedKredisAttributeMethods is added to the class.
    2. Methods like kredis_list return types like Kredis::Types::List.
    3. Methods like kredis_flag return Kredis::Types::Flag and have a corresponding ? method returning T::Boolean.
    4. kredis_enum generates a private nested class (e.g., PrivateEnum<AttributeName>) inheriting from Kredis::Types::Enum, containing methods for each allowed value.
    # typed: true
    
    class Person
      module GeneratedKredisAttributeMethods
        sig { returns(Kredis::Types::Flag) }
        def awesome; end
    
        sig { returns(T::Boolean) }
        def awesome?; end
    
        sig { returns(PrivateEnumMorning) }
        def morning; end
    
        sig { returns(Kredis::Types::List) }
        def names; end
    
        sig { returns(Kredis::Types::Counter) }
        def steps; end
    
        class PrivateEnumMorning < Kredis::Types::Enum
          sig { void }
          def black!; end
    
          sig { returns(T::Boolean) }
          def black?; end
    
          sig { void }
          def blue!; end
    
          sig { returns(T::Boolean) }
          def blue?; end
    
          sig { void }
          def bright!; end
    
          sig { returns(T::Boolean) }
          def bright?; end
        end
      end
    end
  2. How StateMachines RBI generation works

    main

    When the StateMachines compiler processes a class, it generates an RBI file that includes two specific helper modules to represent the state machine's interface:

    1. StateMachineClassHelperModule: Contains class-level methods for humanizing event names and state names.
    2. StateMachineInstanceHelperModule: Contains instance-level methods for checking states (e.g., alarm_active?), managing the state value (e.g., alarm_state=), triggering events (e.g., enable_alarm!), and querying transitions.

    For a state machine defined with a namespace (e.g., state_machine :alarm_state, namespace: :'alarm'), the generated methods will be prefixed with the namespace (e.g., alarm_state, alarm_active?, enable_alarm).

    class Vehicle
      state_machine :alarm_state, initial: :active, namespace: :'alarm' do
        event :enable do
          transition all => :active
        end
    
        event :disable do
          transition all => :off
        end
    
        state :active, :value => 1
        state :off, :value => 0
      end
    end
    
    # The compiler produces a vehicle.rbi containing:
    # - StateMachineClassHelperModule (class methods)
    # - StateMachineInstanceHelperModule (instance methods like `alarm_active?`, `enable_alarm!`, etc.)
  3. Use RBI shims for missing constants and methods

    main

    If Tapioca cannot statically resolve certain constants or methods (due to optional dependencies, complex metaprogramming, or specific code paths), use shims.

    A shim is a hand-crafted RBI file that manually defines the missing signatures.

    Location: Place shims in sorbet/rbi/shims/. It is conventional to mirror your application's directory structure. For example, a shim for app/models/person.rb should be located at sorbet/rbi/shims/app/models/person.rbi.

    Example Shim:

    # typed: true
    
    class Person
      sig { void }
      def some_method_sorbet_cannot_find; end
    end
  4. Import hand-written signatures from a gem's `rbi/` folder

    main

    If a gem includes an rbi/ folder in its release (via .gemspec), Tapioca will automatically import these signatures and combine them with its generated RBIs.

    To prevent this behavior and ignore exported gem RBIs, use the --no-exported-gem-rbis flag.

  5. How to write a custom DSL compiler

    main

    If you use dynamic methods (e.g., via define_method or included hooks) that Sorbet cannot see statically, you can write a custom DSL compiler.

    A compiler must implement two main parts:

    1. gather_constants: A class method that collects the constants (classes or modules) the compiler should process.
    2. decorate: An instance method that defines how to generate the RBI definitions for those constants.

    Requirements:

    • The compiler must inherit from Tapioca::Dsl::Compiler.
    • You must declare a ConstantType type member so Sorbet can type-check the constant attribute reader used in decorate.
    • To be discovered, place the compiler in sorbet/tapioca/compilers or in a tapioca/dsl/compilers folder on the load path.

    Example: Creating an Encryptable compiler

    module Tapioca
      module Compilers
        class Encryptable < Tapioca::Dsl::Compiler
          extend T::Sig
    
          # Required for Sorbet to understand the 'constant' reader type
          ConstantType = type_member {{ fixed: T.class_of(Encryptable) }}
    
          sig { override.returns(T::Enumerable[T::Module[T.anything]]) }
          def self.gather_constants
            # Collect all the classes that include Encryptable
            all_classes.select { |c| c < ::Encryptable }
          end
    
          sig { override.void }
          def decorate
            # Create a RBI definition for each class that includes Encryptable
            root.create_path(constant) do |klass|
              # For each encrypted attribute we find in the class
              constant.encrypted_attributes.each do |attr_name|
                # Create the RBI definitions for all the missing methods
                klass.create_method(attr_name, return_type: "String")
                klass.create_method("#{attr_name}=", parameters: [ create_param("value", type: "String") ], return_type: "void")
                klass.create_method("#{attr_name}_encrypted", return_type: "String")
                klass.create_method("#{attr_name}_encrypted=", parameters: [ create_param("value", type: "String") ], return_type: "void")
              end
            end
          end
        end
      end
    end
  6. Understand how ActiveRecordColumns generates RBI methods

    main

    When the ActiveRecordColumns compiler runs, it inspects your db/schema.rb and generates a module (typically named GeneratedAttributeMethods) within your model's RBI file. This module includes getter, setter, and predicate (?) methods for each column.

    Example Mapping

    Given a schema definition:

    create_table :posts do |t|
      t.string :title, null: false
      t.string :body
      t.boolean :published
      t.timestamps
    end

    With the default persisted setting, the generated RBI will look like this:

    class Post
      include GeneratedAttributeMethods
    
      module GeneratedAttributeMethods
        # For a non-nullable string
        sig { returns(::String) }
        def title; end
        sig { params(value: ::String).returns(::String) }
        def title=(value); end
        sig { returns(T::Boolean) }
        def title?; end
    
        # For a nullable string
        sig { returns(T.nilable(::String)) }
        def body; end
        sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
        def body=; end
        sig { returns(T::Boolean) }
        def body?; end
    
        # For a boolean
        sig { returns(T::Boolean) }
        def published; end
        sig { params(value: T::Boolean).returns(T::Boolean) }
        def published=; end
        sig { returns(T::Boolean) }
        def published?; end
      end
    end
    # post.rbi
    # typed: true
    class Post
      include GeneratedAttributeMethods
    
      module GeneratedAttributeMethods
        sig { returns(T.nilable(::String)) }
        def body; end
    
        sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
        def body=; end
    
        sig { returns(T::Boolean) }
        def body?; end
    
        sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
        def created_at; end
    
        sig { params(value: ::ActiveSupport::TimeWithZone).returns(::ActiveSupport::TimeWithZone) }
        def created_at=; end
    
        sig { returns(T::Boolean) }
        def created_at?; end
    
        sig { returns(T.nilable(T::Boolean)) }
        def published; end
    
        sig { params(value: T::Boolean).returns(T::Boolean) }
        def published=; end
    
        sig { returns(T::Boolean) }
        def published?; end
    
        sig { returns(::String) }
        def title; end
    
        sig { params(value: ::String).returns(::String) }
        def title=(value); end
    
        sig { returns(T::Boolean) }
        def title?; end
    
        sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
        def updated_at; end
    
        sig { params(value: ::ActiveSupport::TimeWithZone).returns(::ActiveSupport::TimeWithZone) }
        def updated_at=; end
    
        sig { returns(T::Boolean) }
        def updated_at?; end
      end
    end
  7. Generate RBI files for ActiveSupport::Concern hierarchies

    main

    The Tapioca::Dsl::Compilers::ActiveSupportConcern compiler is used to generate RBI files for classes that utilize the ActiveSupport::Concern pattern. Specifically, it targets classes that include another class/module which itself extends ActiveSupport::Concern and contains a ClassMethods module.

    When this pattern is detected, the compiler produces an RBI file that uses the mixes_in_class_methods method to declare the inclusion of those class methods, ensuring Sorbet can track them correctly.

    # Example Ruby hierarchy
    module Foo
     extend ActiveSupport::Concern
     module ClassMethods; end
    end
    
    module Bar
     extend ActiveSupport::Concern
     module ClassMethods; end
     include Foo
    end
    
    class Baz
     include Bar
    end
    
    # Resulting RBI content
    # typed: true
    module Bar
      mixes_in_class_methods(::Foo::ClassMethods)
    end
  8. Automatic RBI generation features in Editor Integration

    main

    When the Tapioca add-on is enabled in your editor, it provides two main automated workflows:

    1. DSL RBI generation: When you edit a Ruby file, Tapioca automatically runs bin/tapioca dsl for the constants found in that file (e.g., bin/tapioca dsl MyClass).
    2. Gem RBI generation: When Gemfile.lock is modified, Tapioca automatically runs bin/tapioca gem for the updated gems (e.g., bin/tapioca gem my_gem).
  9. How ActiveRecordEnum compiler decorates RBI files

    main

    The Tapioca::Dsl::Compilers::ActiveRecordEnum compiler automatically decorates RBI files for subclasses of ActiveRecord::Base that use Rails enum declarations.

    When a model defines an enum, this compiler generates an EnumMethodsModule within the class's RBI file. This module includes Sorbet signatures for:

    • Predicate methods (e.g., all_title?)
    • Bang methods (e.g., all_title!)
    • Class methods to retrieve the enum mapping (e.g., self.title_types)

    This ensures that the dynamically generated methods provided by Rails enums are visible to the Sorbet type checker.

    # Example ActiveRecord model
    class Post < ApplicationRecord
      enum :title_type, %i(book all web), suffix: :title
    end
    
    # Resulting post.rbi content
    # typed: true
    class Post
      include EnumMethodsModule
    
      module EnumMethodsModule
        sig { void }
        def all_title!; end
    
        sig { returns(T::Boolean) }
        def all_title?; end
    
        sig { returns(T::Hash[T.any(String, Symbol), Integer]) }
        def self.title_types; end
    
        sig { void }
        def book_title!; end
    
        sig { returns(T::Boolean) }
        def book_title?; end
    
        sig { void }
        def web_title!; end
    
        sig { returns(T::Boolean) }
        def web_title?; end
      end
    end
  10. How ActiveRecordRelations compiler works

    main

    The Tapioca::Dsl::Compilers::ActiveRecordRelations compiler decorates RBI files for subclasses of ActiveRecord::Base. It adds type signatures for methods related to relation, collection proxy, query, spawn, finder, and calculation.

    To represent ActiveRecord relations accurately in Sorbet, the compiler generates three synthetic classes and three synthetic modules for every model (e.g., Post).

    Synthetic Classes

    1. Model::PrivateRelation: Subclasses ActiveRecord::Relation. Represents a relation on the Model class itself. Methods returning a relation will return this type.
    2. Model::PrivateAssociationRelation: Subclasses ActiveRecord::AssociationRelation. Represents a relation on a singular association (e.g., foo.model). It tracks the resource association.
    3. Model::PrivateCollectionProxy: Subclasses ActiveRecord::Associations::CollectionProxy. Represents a relation on a plural association (e.g., foo.models). It includes methods like build and create.

    Synthetic Modules

    1. Model::GeneratedRelationMethods: Contains methods where the return type is Model::PrivateRelation (e.g., Post.all).
    2. Model::GeneratedAssociationRelationMethods: Contains methods where the return type is Model::PrivateAssociationRelation (e.g., Post.all called on an association).
    3. Model::CommonRelationMethods: Contains methods that return types independent of the relation kind (e.g., find_by!, which returns a model instance). This module is shared to reduce duplication.

    Note: The actual Model class extends both CommonRelationMethods and GeneratedRelationMethods to allow chaining methods like find_by and all directly on the class.

    class Post < ApplicationRecord
    end
  11. Write custom DSL extensions

    main

    When a DSL's implementation doesn't store enough information for a compiler to define signatures (e.g., it doesn't track which attributes were passed to a macro), you can write a DSL Extension.

    An extension is code loaded before the application to override behavior and store metadata.

    Discovery: For Tapioca to find your extensions, place them in:

    1. sorbet/tapioca/extensions within your application.
    2. A tapioca/dsl/extensions folder on the Ruby load path.

    Pattern:

    1. Define a module in Tapioca::Extensions that prepends or overrides the target method.
    2. Use an instance variable (e.g., @__tapioca_metadata) to store the information needed by the compiler.
    3. In your Tapioca::Dsl::Compiler subclass, access this metadata during the decorate phase to create RBI definitions.
    # Example Extension
    module Tapioca
      module Extensions
        module MyExtension
          attr_reader :__tapioca_metadata
    
          def my_dsl_method(arg)
            @__tapioca_metadata ||= []
            @__tapioca_metadata << arg.to_s
            super
          end
        end
      end
    end
    
    # Example Compiler using the extension
    module Tapioca
      module Compilers
        class MyCompiler < Tapioca::Dsl::Compiler
          def decorate
            root.create_path(constant) do |klass|
              klass.__tapioca_metadata.each do |meta|
                klass.create_method(meta, return_type: "String")
              end
            end
          end
        end
      end
    end
  12. How ActiveRecordAssociations compiler refines RBI files

    main

    The Tapioca::Dsl::Compilers::ActiveRecordAssociations compiler is used to refine RBI files for classes that inherit from ActiveRecord::Base.

    Its specific responsibility is to define the Sorbet method signatures for methods automatically generated by ActiveRecord associations (such as belongs_to, has_many, and has_one) and nested attributes (via accepts_nested_attributes_for).

    When this compiler runs, it generates a module (e.g., Post::GeneratedAssociationMethods) containing the type signatures for:

    • Association readers and writers (e.g., category, category=).
    • Association builders (e.g., build_author, create_category!).
    • Nested attribute writers (e.g., author_attributes=).
    • ID collection accessors (e.g., comment_ids).
    • Reload and reset methods (e.g., reload_category, reset_author).
    # Example ActiveRecord Model
    class Post < ActiveRecord::Base
      belongs_to :category
      has_many :comments
      has_one :author, class_name: "User"
    
      accepts_nested_attributes_for :category, :comments, :author
    end
    
    # Resulting RBI output (post.rbi)
    # typed: true
    
    class Post
      include Post::GeneratedAssociationMethods
    
      module Post::GeneratedAssociationMethods
        sig { returns(T.nilable(::User)) }
        def author; end
    
        sig { params(value: T.nilable(::User)).void }
        def author=(value); end
    
        sig { params(attributes: T.untyped).returns(T.untyped) }
        def author_attributes=(attributes); end
    
        sig { params(args: T.untyped, blk: T.untyped).returns(::User) }
        def build_author(*args, &blk); end
    
        # ... other generated methods
      end
    end