Code Monkey home page Code Monkey logo

sorbet-rails's Introduction

Notice

This repository is no longer maintained and will be sunset. The Sorbet community has endorsed the Tapioca gem for generating RBI's, including those for Rails. Please consider migrating over!

sorbet-rails

Gem Version Build Status codecov

A set of tools to make the Sorbet typechecker work with Ruby on Rails seamlessly.

This gem adds a few Rake tasks to generate Ruby Interface (RBI) files for dynamic methods generated by Rails. It also includes signatures for related Rails classes. The RBI files are added to a sorbet/rails-rbi/ folder.

sorbet-rails supports Rails 5+ or later.

Notice: we no longer support Rails 4.2. Version 0.5.6 is the last version supporting Rails 4.2.

Initial Setup

  1. Follow the steps here to set up the latest version of Sorbet, up to being able to run srb tc.

  2. Add sorbet-rails to your Gemfile and install them with Bundler.

# -- Gemfile --
gem 'sorbet-rails'

Add sorbet-rails to the :default group because of "Features Provided at Runtime" below.

❯ bundle install
  1. Generate RBI files for your routes, models, etc
❯ bundle exec rake rails_rbi:routes
❯ bundle exec rake rails_rbi:models
❯ bundle exec rake rails_rbi:helpers
❯ bundle exec rake rails_rbi:mailers
❯ bundle exec rake rails_rbi:jobs
❯ bundle exec rake rails_rbi:custom

# or run them all at once
❯ bundle exec rake rails_rbi:all
  1. Update hidden-definition files and automatically upgrade each file's typecheck level:
❯ bundle exec srb rbi hidden-definitions
❯ bundle exec srb rbi suggest-typed

Because we've generated RBI files for routes, models, and helpers, a lot more files should be typecheckable now. Many methods in hidden.rbi may be removed because they are now typed.

Static RBI Generation

Models

This Rake task generates RBI files for all models in the Rails application (all descendants of ActiveRecord::Base):

❯ bundle exec rake rails_rbi:models

You can also regenerate RBI files for specific models. To accommodate for STI, this will generate rbi for all the subclasses of the models included.

❯ bundle exec rake rails_rbi:models[ModelName,AnotherOne,...]

The generation task currently creates the following signatures:

  • Column getters & setters
  • Associations getters & setters
  • Enum values, checkers & scopes
  • Named scopes
  • Model Relation class

It is possible to add custom RBI generation logic for your custom module or gems via the plugin system. Check out the plugins section below if you are interested.

We also add following methods to make type-checking easier:

pluck_to_tstruct instead of pluck

The pluck method in Rails is a performant way to query value without instantiating ActiveRecord objects. However, it doesn't have any type information: it doesn't have type information (or name) of the attribute plucked. Sorbet-rails provides pluck_to_tstruct method as a replacement that returns an array of T::Struct instead. The attributes plucked is based on props defined in the T::Struct

# -- API
Arel.pluck_to_tstruct(TA[ <TStructSubClass> ].new)

# -- example
class WizardStruct < T::Struct
  const :name, String
  const :house, T.nilable(String)
end

Wizard.pluck_to_tstruct(TA[WizardStruct].new)  # T::Array[WizardStruct]
Wizard.all.pluck_to_tstruct(TA[WizardStruct].new)  # T::Array[WizardStruct]

You can also pass a keyword argument called associations that represents a mapping of a T::Struct's keys to an associated table's columns.

# -- API
Arel.pluck_to_tstruct(TA[ <TStructSubclass> ].new, associations: < Hash<Symbol, String> >)

# -- example
class WizardWithWandStruct < T::Struct
  const :name, String
  const :house, T.nilable(String)
  const :wand_wood_type, String
end

Wizard.joins(:wand).pluck_to_tstruct(
  TA[WizardWithWandStruct].new,
  associations: { wand_wood_type: "wands.wood_type" }
)
Wizard.all.joins(:wand).pluck_to_tstruct(
  TA[WizardWithWandStruct].new,
  associations: { wand_wood_type: "wands.wood_type" }
)

This method is based on pluck_to_hash gem.

typed_enum instead of enum

If you use Rails enum, sorbet-rails will generate a corresponding T::Enum. It will also include getters, setters, and scope methods in the rbi file it generates.

ie.

  enum house: {
    Gryffindor: 0,
    Hufflepuff: 1,
    Ravenclaw: 2,
    Slytherin: 3,
  }

Will generate this enum:

class Wizard
  class House < T::Enum
    enums do
      Gryffindor = new('Gryffindor')
      Hufflepuff = new('Hufflepuff')
      Ravenclaw = new('Ravenclaw')
      Slytherin = new('Slytherin')
    end
  end
end

And these methods:

  sig { returns(T.nilable(String)) }
  def house; end

  sig { params(value: T.nilable(T.any(Integer, String, Symbol))).void }
  def house=(value); end

  sig { returns(T.nilable(Wizard::House)) }
  def typed_house; end

  sig { params(value: T.nilable(Wizard::House)).void }
  def typed_house=(value); end

Alternatively, you can replace your call to enum with typed_enum. This will hide the Rails methods (house) from Sorbet static-check (they are still usable in un-checked files).

  typed_enum house: {
    Gryffindor: 0,
    Hufflepuff: 1,
    Ravenclaw: 2,
    Slytherin: 3,
  }

Generates only typed enum setter & getter:

  sig { returns(T.nilable(Wizard::House)) }
  def typed_house; end

  sig { params(value: T.nilable(Wizard::House)).void }
  def typed_house=(value); end

RelationType alias

There are several kinds of relations of a model: User::ActiveRecord_Relation, User::ActiveRecord_AssociationRelation and User::ActiveRecord_Associations_CollectionProxy. Usually the code may need just any relation type. We add a Model::RelationType type alias for every model to use it.

class User
  RelationType = T.type_alias do
    T.any(
      User::ActiveRecord_Relation,
      User::ActiveRecord_AssociationRelation,
      User::ActiveRecord_Associations_CollectionProxy
    )
  end
end

Controllers

❯ bundle exec  rake rails_rbi:custom

sorbet-rails adds TypedParams to extract typed controller parameters.

TypedParams takes a parameter definition, which is a subclass of T::Struct and coerces the params object into an instance of that subclass using sorbet-coerce:

class MyCoolController < ApplicationController
  class MyActionParams < T::Struct
    const :id, T.nilable(Integer)
    const :show, T.nilable(T::Boolean)
    const :wands, T::Array[Integer]
  end
  sig { void }
  def my_action
    typed_params = TypedParams[MyActionParams].new.extract!(params)
    # T.reveal_type(typed_params) => MyActionParams
    # T.reveal_type(typed_params.show) => T.nilable(T::Boolean)
  end
end

If it fails to coerce the params into the right type, an ActionController::BadRequest exception will be raised with the coercion context for debugging.

Note: The API TypedParams[...].new.extract! may seem verbose, but necessary to support this feature. Ideally, the API can be simply TypedParams.extract!(...). However, sorbet doesn't support defining a method that accept a type and return an instance of the type. If this feature is supported by sorbet in the future, it will be easy to codemod to be TypedParams.extract(...)! part from your code. Note: require_typed and fetch_typed are deprecated in favor of TypedParams. They will be removed in v0.7.

Routes

This Rake task generates an RBI file defining _path and _url methods for all named routes in routes.rb:

❯ bundle exec rake rails_rbi:routes

Helpers

This Rake task generates a helpers.rbi file that includes a basic module definition which includes the Kernel module and ActionView::Helpers, to allow for some basic Ruby methods to be used in helpers without Sorbet complaining.

❯ bundle exec rake rails_rbi:helpers

If you have additional modules that are included in all your helpers and you want helpers.rbi to reflect this, you can configure it:

# -- config/initializers/sorbet_rails.rb
SorbetRails.configure do |config|
  config.extra_helper_includes = ['ApplicationHelper', 'Devise::Controllers::Helpers']
end

Mailers

This Rake task generates RBI files for all mailer classes in the Rails application (all descendants of ActionMailer::Base)

❯ bundle exec rake rails_rbi:mailers

Since mailing action methods are based on instance methods defined in a mailer class, the signature of a mailing action method will be dependent on the signature the instance method has

  • If there is a (sorbet) sig written for the instance method, it generates a matching sig for the mailing action method
  • If not, all the params in the mailing action method will be T.untyped.
  • For return type though, the mailing action method will return ActionMailer::MessageDelivery instead of the return type of the instance method.

Jobs

This Rake task generates RBI files for all jobs classes in the Rails application (all descendants of ActiveJob::Base).

❯ bundle exec rake rails_rbi:jobs

It will generate perform_later and perform_now methods for the job classes matching the signature of the perform method of the job. If there is a (sorbet) sig written for perform, it will use the same sig for perform_* methods.

Runtime Features

In addition to features provided by the static generator, sorbet-rails can provide additional features when required. This is why the installation instructions specify that sorbet-rails should be placed in the :default group of the Gemfile, not a specific environment group (eg. development only).

  • Model: The gem provides some helper method to a model to make type-checking easier:

    • find_n, first_n, last_n
    • where_missing
    • pluck_to_tstruct
    • typed_enum
  • Model Relation:

    • Make relation classes public. By default, relation classes like User::ActiveRecord_Relation, User::ActiveRecord_AssociationRelation are private
    • Add type alias, eg Model::RelationType, to represents any type of relation of a model.
  • Controller: use TypedParams to convert controller parameters to a typed structure

In addition to requireing sorbet-rails, you must also run bundle exec rake rails_rbi:custom, which will produce the RBI for these runtime features.

Discussion: #211, #214

Tips & Tricks

Look for # typed: ignore files

Because Sorbet's initial setup tries to flag files at whichever typecheck level generates 0 errors, there may be files in your repository that are # typed: ignore. This is because sometimes Rails allows very dynamic code that Sorbet does not believe it can typecheck.

It is worth going through the list of files that is ignored and resolve them (and auto upgrade the types of other files; see initial setup above). Usually this will make many more files able to be typechecked.

Overriding generated signatures if needed

sorbet-rails relies on Rails reflection to generate signatures. There are features this gem doesn't support yet such as attribute custom types. The gem also doesn't know the signature of any methods you have overridden. However, it is possible to override the signatures that sorbet-rails generates.

For example, here is how to override the signature for a method in a model:

# -- app/models/model_name.rbi --

# typed: strong
class ModelName
  sig { returns(T::Hash[...]) }
  def field_name; end

  sig { params(obj: T::Hash[....]).void }
  def field_name=(obj); end
end

Replace Rails.application.routes.url_helpers

When using url helpers like _url or _path methods outside of a controller, we usually have to add include Rails.application.routes.url_helpers in the class. However, Sorbet does not allow code that includes dynamic module. Sorbet Rails provides a drop-in replacement module for the dynamic url_helpers module, called GeneratedUrlHelpers. Following code change should resolve the sorbet type-check error:

class MyClass
-  include Rails.application.routes.url_helpers
+  include GeneratedUrlHelpers
end

find, first and last

These 3 methods can either return a single nilable record or an array of records. Sorbet does not allow us to define multiple signatures for a function (except stdlib). It doesn't support defining one function signature that has varying returning value depending on the input parameter type. We opt to define the most commonly used signature for these methods, and monkey-patch new functions for the secondary use case.

In short:

  • Use find, first and last to fetch a single record.
  • Use find_n, first_n, last_n to fetch an array of records.

find_by_<attributes>, <attribute>_changed?, etc.

Rails supports dynamic methods based on attribute names, such as find_by_<attribute>, <attribute>_changed?, etc. They all have static counterparts. Instead of generating all possible dynamic methods that Rails support, we recommend to use of the static version of these methods instead (also recommended by RuboCop).

Following are the list of attribute dynamic methods and their static counterparts. The static methods have proper signatures:

  • find_by_<attributes> -> find_by(<attributes>)
  • find_by_<attributes>! -> find_by!(<attributes>)
  • <attribute>_changed? -> attribute_changed?(<attribute>)
  • <attribute>_was -> attribute_was(<attribute>)
  • saved_change_to_<attribute>? -> saved_change_to_attribute?(<attribute>)
  • <attribute>_before_type_cast -> read_attribute_before_type_cast(<attribute>)
  • will_save_change_to_<attribute>? -> will_save_change_to_attribute?(<attribute>)

after_commit and other callbacks

Consider converting after_commit callbacks to use instance method functions. Sorbet doesn't support binding an optional block with a different context. Because of this, when using a callback with a custom block, the block is evaluated in the wrong context (Class-level context). Refer to this page for a full list of callbacks available in Rails.

Before:

after_commit do ... end

After:

after_commit :after_commit
def after_commit
  ...
end

If you wanted to make these changes using Codemod, try these commands:

# from methods like after_commit do <...> end
❯ codemod -d app/models/ --extensions rb \
  '(\s*)(before|after)_(validation|save|create|commit|find|initialize|destroy) do' \
  '\1\2_\3 :\2_\3\n\1def \2_\3'

# from methods like after_commit { <...> }
❯ codemod -d app/models/ --extensions rb \
  '(\s*)(before|after)_(validation|save|create|commit|find|initialize|destroy) \{ (.*) \}' \
  '\1\2_\3 :\2_\3\n\1def \2_\3\n\1\1\4\n\1end'

Note that Codemod's preview may show that the indentation is off, but it works.

unscoped with a block

The unscoped method returns a Relation when no block is provided. When a block is provided, unscoped calls the block and returns its result, which could be any type.

sorbet-rails chooses to define unscoped as returning a Relation because it's more common and more useful. If you want to use a block, either override the unscoped definition, or replace:

Model.unscoped do  end

with:

Model.unscoped.scoping do  end

select with a block

The select method in Rails has two modes: it can be given a list of symbols, in which case rails will only return the given columns from the database, or it can be given a block, in which case it acts like Enumerable.select and returns an array.

We have chosen to support select with a block as the primary use case, since it is a more prevalent use of this method. We provide select_columns as an alternative if you want to select a list of columns.

Model.select { |record| record.id > 1} # valid, returns an array
Model.select_columns(:id, :name) # valid, returns an association

Model.select(:id, :name) # sorbet error, use `select_column` instead

flatten an array of relation

When you call flatten on an array of ActiveRecord::Relation, sorbet doesn't recognize that it will flatten the relation and return an array of model. The work around is to call to_a on the relation first.

# doesn't work
arr = [Model.recent, Model.old].flatten # T::Array[Model::ActiveRecord_Relation]

# work around
arr = [Model.recent, Model.old].map(&:to_a).flatten # T::Array[Model]

flat_map has a similar issue.

foo.bars.flat_map { |b| b.scope } # T::Array[T.untyped]

foo.bars.flat_map { |b| b.scope.to_a } # T::Array[Scope]

Avoid and_call_original in rspecs

If you run into the following issue when running rspec, it's likely because you're using expect(:method_name).and_call_original to mock a method in RSpec. We've found the double mock doesn't interact well with Sorbet's sig wrapper and caused flaky spec. The spec should be rewritten to expect the outcome of the method instead. (It still works with expect(:method_name) and expect(:method_name).and_return(...)

     RuntimeError:
       `sig` not present for method `:method_name` but you're trying to run it anyways. This should
       only be executed if you used `alias_method` to grab a handle to a method after `sig`ing it, but
       that clearly isn't what you are doing. Maybe look to see if an exception was thrown in your `sig`
       lambda or somehow else your `sig` wasn't actually applied to the method. Contact #dev-productivity
       if you're really stuck.

where.missing does not exist on ActiveRecord::Relation

The where method in Rails has two modes: it can be passed no arguments where it will return an ActiveRecord::QueryMethods::WhereChain object, or when given arguments, returns an ActiveRecord::Relation object. By default we've opted to support where with arguments and have provided a where_missing method to avoid conflicts during static typing.

Wizard.where.missing(:wand) # sorbet error, use `where_missing` instead

Wizard.where_missing(:wand) # valid, returns a relation

Note: where.missing / where_missing are only available in Rails 6.1 or above

Extending RBI Generation logic

Extending Model Generation Task with Custom Plugins

sorbet-rails support a customizable plugin system that you can use to generate additional RBI for each model. This will be useful to generate RBI for methods dynamically added by gems or private concerns. If you write plugins for public gems, please feel free to contribute it to this repo.

Defining a Custom ModelPlugin

A custom plugin should be a subclass of SorbetRails::ModelPlugins::Base. Each plugin would implement a generate(root) method that generate additional rbi for the model.

At a high level, here is the structure of a plugin:

# -- lib/my_custom_plugin.rb
class MyCustomPlugin < SorbetRails::ModelPlugins::Base
  sig { override.params(root: Parlour::RbiGenerator::Namespace).void }
  def generate(root)
    # TODO: implement the generation logic
    # You can use @model_class and @available_classes here
  end
end

During the generation phase, the system will create a new instance of the plugin, with the model_class to be generated and a set of all available_classes (available models) detected in the Rails App.

We use Parlour gem to generate the RBI for a model. Please check out Parlour wiki for guide to add RBI, eg create a new module, class, or method in the generated file.

At a high level, you'd usually want to create a model-scoped module for your methods, and include or extend it in the base model class. The generation logic usually looks like this:

  def generate(root)
    # Make sure this is only run for relevant class
    return unless @model_class.include?(CustomModule)

    custom_module_name = self.model_module_name("CustomModule")
    custom_module_rbi = root.create_module(custom_module_name)

    # here we re-create the model class!
    model_class_rbi = root.create_class(self.model_class_name)
    model_class_rbi.create_extend(custom_module_name)

    # then create custom methods, constants, etc. for this module.
    custom_module_rbi.create_method(...)

    # this is allowed but not recommended, because it limit the ability to override the method.
    model_class_rbi.create_method(...)
  end

Notice that we re-create model_class_rbi here. Parlour's ConflictResolver will merge the classes or modules with the same name together to generate 1 beautiful RBI file. It'll also flag and skip if any method is created multiple times with conflict signatures. Check-out useful predefined module names & helper methods in model_utils.

It is also allowed to put methods into a model class directly. However, it is not recommended because it'll be harder to override the method. sorbet will enforce that the overriding method match the signature generated. It also makes the generated RBI file less modularized.

However, sometimes this is required to make sorbet recognize the signature. This is the case for class methods added by ActiveRecord::Concerns. Because ActiveSupport::Concern class methods will be inserted to the class directly, you need to also put the sig in the model class rbi directly.

It is also recommended to check if the generated methods are detected by sorbet as a gem method (in sorbet/rbi/gem/gem_name.rbi) or hidden methods (in sorbet/rbi/hidden-definitions/hidden.rbi). If so, you may need to re-run srb rbi hidden-definition or put method in the model class directly.

Check out the plugins written for sorbet-rails's own model RBI generation logic for examples.

Registering new plugins

You can register your plugins in an initializer:

# -- config/initializers/sorbet_rails.rb
SorbetRails::ModelRbiFormatter.register_plugin(MyCustomPlugin)

Enabling built-in plugins

sorbet-rails comes with a handful of gem plugins that can be enabled in an initializer. You can pass enabled gem plugins to config.enabled_gem_plugins, like so:

# -- config/initializers/sorbet_rails.rb
SorbetRails.configure do |config|
  config.enabled_gem_plugins = [
    :kaminari
  ]
end

These are the currently-supported gems and their symbolized names:

Gem Symbol
ElasticSearch :elastic_search
FriendlyId :friendly_id
Kaminari :kaminari
PgSearch :pg_search
Shrine :shrine
active_flag :active_flag
Paperclip :paperclip
AttrJson :attr_json
FlagShihTzu :flag_shih_tzu
AASM :aasm
Money-Rails :money_rails

You can also configure the core model plugins if needed. The default plugins are defined in the config. For the full list of plugin symbols, check out here.

Customize Generation Class

For mailer and job rbi generation, you can customize the logic by setting the generation class in the config:

SorbetRails.configure do |config|
  config.job_generator_class = CustomJobRbiGenerator
  config.mailer_generator_class = CustomMailerRbiGenerator
end

The custom generator can subclass the provided generators and override the populate_rbi method. For example:

class CustomJobRbiGenerator < SorbetRails::JobRbiFormatter
  def populate_rbi
    rbi_generator.root.add_comment("== Custom Generator ==")
    super
  end
end

Contributing

Contributions and ideas are welcome! Please see our contributing guide and don't hesitate to open an issue or send a pull request to improve the functionality of this gem.

This project adheres to the Contributor Covenant code of conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior to [email protected].

License

MIT

sorbet-rails's People

Contributors

alexdunae avatar ashkulz avatar caiofilipemr avatar connorshea avatar danielgilchrist avatar dependabot[bot] avatar donaldong avatar frewsxcv avatar gasparila avatar ghiculescu avatar hdoan741 avatar jaredbeck avatar jasnow avatar jeffcarbs avatar masylum avatar mgriisser avatar nacin avatar paracycle avatar pje avatar qmoya avatar reidab avatar reneklacan avatar rhc2104 avatar riley-klingler avatar rnvarma avatar rowanmcdonald avatar saiqulhaq avatar sbeckeriv avatar stevenwchien avatar toddsiegel avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sorbet-rails's Issues

Define 'new' method on model classes

Describe the bug:
ActiveRecord overrides the new method for all Model classes, e.g. User.new.

It's defined like this in activerecord.rbi:

module ActiveRecord::Inheritance::ClassMethods
  def new(attributes = nil, &block); end
end

And then User, or any other model, inherits that method.

Expected behavior:

I could have an app/controllers/users_controller.rb file like this:

class UsersController < ApplicationController
  def create
    @user = User.new({ username: 'connor' }) # @user is T.untyped because `new` is overridden.
    respond_to do |format|
      if @user.save
        format.html { redirect_to @user, success: "#{@user.username} was successfully created." }
    end
  end
end

The method should be generated as part of Sorbet Rails so its return type can be dynamically set:

class User
  sig { params(args: T.untyped, block: T.untyped).returns(User) }
  def self.new(*args, &block); end
end

Unfortunately it can't just be self_type because that doesn't return an instance of the caller :/

Gem Plugin Loading

Describe the bug:
It doesn't seem like the Kaminari plugin is being loaded (I only noticed now because I was just using my custom version until now). The page method isn't being generated by rake rails_rbi:models.

Steps to reproduce:

  1. Create a Rails app with Kaminari installed/configured
  2. Add sorbet and sorbet-rails
  3. Generate your model RBIs
  4. Note that uses of page still cause type errors because the method doesn't exist on any relevant model classes.

Expected behavior:
If I have Kaminari set up, the page method should be included in my RBIs.

Or, preferably, I could configure which plugins to load from the sorbet_rails.rb initializer.

Versions:

  • Ruby: 2.6.3
  • Rails: 6.0.0
  • Sorbet: 0.4.4667
  • Sorbet-Rails: 0.5.2

Readme mentions tasks that no longer exist?

Hi
Just giving this a spin and the README mentions

❯ rake rails_rbi:helpers
❯ rake rails_rbi:mailers
❯ rake rails_rbi:custom
❯ rake rails_rbi:all

But I dont seem to have any of these.

Documentation out of date?

Generate sigs for scope, has_many, etc.

It is not possible to generate the sigs right now because T.proc support is limited right now.
For example:
It doesn't work with generic sorbet/sorbet#1139
It doesn't work with variable inputs sorbet/sorbet#1142
T.proc.bind doesn't work with T.nilable sorbet/sorbet#498
T.proc.bind doesn't work with lambda sorbet/sorbet#1143

Once support is improved, we can generate scope it like this

  sig { 
    params(
      name: Symbol, 
      body: T.proc.bind(District::Relation).returns(T.untyped), 
      block: T.nilable(T.proc.void),
    ).void
  }
  def scope(name, body, &block); end

Please add instance methods for 4 items

Please add instance methods for:

  • ActiveStorage::Attachment
  • ActiveStorage::Blob
  • ActionMailbox::InboundEmail
  • ActionText::RichText

Versions:

  • Ruby: 2.4.6, 2.6.3
  • Rails: 5.2, 6.0, 6.1
  • Sorbet: Sorbet typechecker 0.4.4435
  • Sorbet-Rails: 0.4.0

Thanks

New model plugin: Paperclip

This plugin is working locally for me:

  # https://github.com/thoughtbot/paperclip
  class HasAttachedFileSorbetPlugin < SorbetRails::ModelPlugins::Base
    sig { params(root: Parlour::RbiGenerator::Namespace).void.override }
    def generate(root)
      # method added here: https://github.com/thoughtbot/paperclip/blob/v5.2.1/lib/paperclip/has_attached_file.rb#L110

      return unless model_class.respond_to?(:attachment_definitions)

      module_name = self.model_module_name("GeneratedPaperclipMethods")
      module_rbi = root.create_module(module_name)
      module_rbi.create_extend("T::Sig")

      model_class_rbi = root.create_class(self.model_class_name)
      model_class_rbi.create_include(module_name)

      Paperclip::AttachmentRegistry.names_for(model_class).each do |attachment|
        # https://github.com/thoughtbot/paperclip/blob/v5.2.1/lib/paperclip/has_attached_file.rb#L42
        module_rbi.create_method(
          attachment.to_s,
          return_type: "::Paperclip::Attachment",
        )

        # https://github.com/thoughtbot/paperclip/blob/v5.2.1/lib/paperclip/attachment.rb#L100
        module_rbi.create_method(
          "#{attachment}=",
          parameters: [
            Parameter.new("uploaded_file", type: "T.untyped") # could be a variety of things
          ],
          return_type: nil,
        )
      end
    end
  end

Given paperclip is deprecated, would you still accept a PR that adds it to this project? Also: keen for any feedback on the plugin structure.

Getting NoMethodError when your run "rake rails_rbi:helpers" command.

Describe the bug:

Getting NoMethodError: undefined method `length' for nil:NilClass
when your run "rake rails_rbi:helpers" command.

Steps to reproduce:

  1. Clone my https://github.com/jasnow/rails_app5_2 repo.
  2. Run "rake rails_rbi:helpers" and you get:
rake aborted!
NoMethodError: undefined method `length' for nil:NilClass
/Users/USER/.rvm/gems/[email protected]/gems/sorbet-rails-0.5.0/lib/sorbet-rails/tasks/rails_rbi.rake:53:in `block (2 levels) in <top (required)>'
/Users/USER/.rvm/gems/[email protected]/gems/rake-12.3.3/exe/rake:27:in `<top (required)>'
/Users/USER/.rvm/gems/[email protected]/bin/ruby_executable_hooks:24:in `eval'
/Users/USER/.rvm/gems/[email protected]/bin/ruby_executable_hooks:24:in `<main>'
Tasks: TOP => rails_rbi:helpers
(See full trace by running task with --trace)

Expected behavior:

-- Generate sigs for helpers --

Versions:

  • Ruby: ruby 2.4.6p354 (2019-04-01 revision 67394) [x86_64-darwin16]
  • Rails: 5.2.3
  • Sorbet: 0.4.4602
  • Sorbet-Rails: 0.5.0

Generate a helpers.rbi

Rails helpers are generated with no parent, so Sorbet thinks they don't include Kernel, which means something like this will cause the typechecker to fail, despite being perfectly valid:

# typed: true
module ApplicationHelper
  def example
    puts "test"
  end
end

If a Rails app has helper modules, a helpers.rbi should be generated like so:

# typed: strong
module ApplicationHelper
  include Kernel
end

module GamesHelper
  include Kernel
end

module SettingsHelper
  include Kernel
end

I'm not sure if this is quite the correct fix, but I think it makes sense. It'd also be a base to build from for better helper support in the gem.

Problem with spec/support/5.2.3/db/migrate symbolic link

FYI:

When I "git pull upstream" this morning to resync your code with my fork, I got:

Broken symlinks detected:
/Users/USER/Projects/sorbet-rails/spec/support/5.2.3/db/migrate

✗ One or more pre-commit hooks failed

Tried all kinds of things but the only thing that worked was to delete it from my copy.
Then "git commit" and "git push" worked.
Then I tried to add it back and got the same error as above.

Bugs: generated sig for mailer method doesn't include default value

Describe the bug:

  def notify(parent_id:, login_token: nil, should_update_sent_flag: true)

Generate

  sig { params(parent_id: T.untyped, login_token: T.untyped, should_update_sent_flag: T.untyped).returns(ActionMailer::MessageDelivery) }
  def self.notify(parent_id:, login_token:, should_update_sent_flag:); end

Steps to reproduce:

Expected behavior:

Versions:

  • Ruby:
  • Rails:
  • Sorbet:
  • Sorbet-Rails:

Ability to call a model's class methods on the model's CollectionProxy

Describe the bug:

Rails has the ability to call class methods on a model's CollectionProxy. See this SO for an example:

https://stackoverflow.com/questions/22304809/rails-ability-to-call-my-class-method-on-an-array

This behavior is not currently supported in sorbet-rails

Steps to reproduce:

N/A

Expected behavior:

It should typecheck

Versions:

  • Ruby: ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-darwin18]
  • Rails: Rails 5.2.3
  • Sorbet: Sorbet typechecker 0.4.4507 git d12b7144d2222398e0f28a772b7759fe26b2b9ba built on 2019-07-25 20:18:14 GMT with debug symbols
  • Sorbet-Rails: 0.4.0

Type of Rails.application should be YourApp::Application

Moved from: sorbet/sorbet-typed#83

Describe the bug:
In a Rails application the type of Rails.application is actually a subclass of Rails::Application that is generated when you type rails new. For example, rails new blog, Rails.application will create a class called Blog::Application defined in config/application.rb.

I am willing to make an attempt at fixing this if you think it's a valid change - let me know then I'll send a PR.

Steps to reproduce:

The impact of this is that any custom methods you define in application.rb on Blog::Application will not be visible to the type checker. If I have config/application.rb that looks like this:

# typed: true
require_relative 'boot'
require 'rails'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module Blog
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.2

    def name
      "Brad's awesome blog"
    end
  end
end

then I can't call Rails.application.name in a type-safe way.

Versions:

$ ruby --version
ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-darwin18]

$ rails --version
Rails 5.2.1

$ srb --version
Sorbet typechecker 0.4.4429 git 51504253c985d0a967d3df6a39ac44b25db2c481 built on 2019-07-12 00:02:11 GMT with debug symbols

Generate perform_now and perform_later

Describe the bug feature:

In #238 Harry suggested that we should generate RBI for perform_now and perform_later.

For sorbet-rails, I think we can generate perform_later and perform_now class methods based on the manually written signature, much like how we generate delivery method for mailers. If you're interested in doing that, I can give you more direction on how to do it. It should be simple because sorbet-rails already has a library method that extracts sorbet signature from a method

Steps to reproduce:

  1. rails g job myjob
  2. set job sigil to false or higher
  3. rake rails_rbi:all
  4. ack perform_now sorbet/rails-rbi no occurrences

Expected behavior:

rake rails_rbi:all should generate RBI for perform_now and perform_later.

Versions:

  • Ruby: all supported
  • Rails: all supported
  • Sorbet: all supported
  • Sorbet-Rails: future 😁

New "Can't find gem for", "srb tc" errors, and todo.rbi line

Describe the bug:

7 new "srb tc" errors with latest sorbet-rails version

Steps to reproduce:

Try this after upgrading to edge (master version of) sorbet-rails gem.

  1. Run sorbet "bundle install" with sorbet-rails pointing to a
    local updated copy of sorbet-rails
  2. Run "srb init" then "rake rails_rbi:routes" then "rake rbi:models" then "srb tc".
  3. Results:
  • Lots of new "Can't find gem for" messages.
  • This is new in sorbet/rbi/todo.rbi file: module T::InterfaceWrapper::Helpers; end
  • I got 8 "srb tc" errors - all Wizard or Wand related. Previously it was 0 errors. One example:
sorbet/rbi/hidden-definitions/hidden.rbi:19042: Method `Wizard.Slytherin` redefined without matching argument count. Expected: `0`, got: `1` https://srb.help/4010
       19042 |  def self.Slytherin(*args); end
                ^^^^^^^^^^^^^^^^^^^^^^^^^
    sorbet/rails-rbi/models/wizard.rbi:161: Previous definition
     161 |  def self.Slytherin; end
            ^^^^^^^^^^^^^^^^^^

Expected behavior:

Expect "srb tc" to produce 0 errors.
No "Can't find gem for" messages.
No new todo.rbi line.

Versions:

  • Ruby: ruby 2.4.6p354 (2019-04-01 revision 67394) [x86_64-darwin16]
  • Rails: Rails 5.2.3
  • Sorbet: 0.4.4602
  • Sorbet-Rails: 0.4.0 using "path: '../sorbet-rails' to use edge version.

Improve Strictness of Attribute methods

Describe the bug:

Currently the attribute methods are applied to the base class via a module. ::GeneratedAttributeMethods This means however that they are not enforced.

Steps to reproduce:

# typed: true
# == Schema Information
#
# Table name: payments
#
#  id                                   :integer          not null, primary key
#  amount_in_cents                      :integer          not null
class Payment < ActiveRecord
  def amount
    "a bad string" # should fail type check
  end
end

For more context: https://sorbet-ruby.slack.com/archives/CHN2L03NH/p1570153844191700

Solution:

The solution is in the generated RBI to apply these directly to the class. I'm pretty sure this would be a breaking change as existing code may break.

ActiveRecord_Relation used in types is private

Describe the bug:

Method signatures for models in many cases use Model::ActiveRecord_Relation.
However, that is a private constant (rails/rails#30943)

Steps to reproduce:

Generate any model types

  1. rake "rails_rbi:models[Document]
  2. This creates rbi file with multiple method signatures that look like
  sig { params(args: T.untyped).returns(Document::ActiveRecord_Relation) }
  def self.sorted_by(*args); end

Expected behavior:

Not entirely sure.

What's interesting is that when I try to access the constant in development but not in non-dev environments.

irb(main):002:0> Rails.env
=> "development"
irb(main):003:0> Document::ActiveRecord_Relation
=> Document::ActiveRecord_Relation
irb(main):024:0> Rails.env
=> "qa"
irb(main):025:0> Document::ActiveRecord_Relation
Traceback (most recent call last):
        1: from (irb):25
NameError (private constant #<Class:0x0000559833e401d0>::ActiveRecord_Relation referenced)

Versions:

  • Ruby: 2.6.0p0
  • Rails: 5.2.3
  • Sorbet: 0.4.4782
  • Sorbet-Rails: 0.5.8.1

Getting "expected \<some-error> but nothing was raised" during "rake" run

Steps to Reproduce:

Describe the bug:

  1. Yesterday and today, when I run "rake", I get these 6 failed tests:
rspec ./spec/custom_params_methods_spec.rb:58
rspec ./spec/custom_params_methods_spec.rb:64
rspec ./spec/custom_params_methods_spec.rb:98
rspec ./spec/custom_params_methods_spec.rb:131
rspec ./spec/boolean_string_spec.rb:52
rspec ./spec/integer_string_spec.rb:39
  1. All 6 error messages are of this format:
     Failure/Error:
       expect {
         ta.assert('<string>')
       }.to raise_error(some-error)

       expected <some-error> but nothing was raised
  1. Here is the "rake" log gist: https://gist.github.com/jasnow/9fd8dcd8ba7b31eb37827c01921d059a

  2. Yesterday when I was trying to debug this, I ran all the
    "rake update_spec_v#_#" commands and the
    spec/bin/update_test_data.sh script. Looking at my current
    fork on GitHub, I see updated gem and ruby versions in Gemfile
    and Gemfile.lock files and several "typed" changes.

  3. My local code is sync'ed with my fork on GitHub.

Expected behavior:

I expect all the test to pass as they do on TravisCI.

6. Versions:
-- Ruby: 2.5.6p201 (2019-08-28 revision 67796) [x86_64-darwin16]
-- Rails: 5.2.3
-- Sorbet: 0.4.4761
-- Sorbet-rails: 0.5.5.1
-- OS: 10.12.6

Support for mailers

Describe the bug:

Rails mailer actions are defined as instance methods but called as class methods. Sorbet is aware of the former, but not the later.

Is it silly that rails works this way?

Probably.

Steps to reproduce:

# typed: true
class UserMailer < ApplicationMailer
  def new_user_password(user)
    # ...
  end
end

# typed: true
class BatchPasswordGenerator
  # ..
  sig { params(user: ::User).void }
  def enqueue_new_user_password_email(user)
    ::UserMailer.
      new_user_password(user).
      deliver_later
  end
  # ..
end

Produces:

bin/srb t
redacted/batch_password_generator.rb:38: Method new_user_password does not exist on T.class_of(UserMailer) https://srb.help/7003
    38 |      ::UserMailer.
    39 |        new_user_password(redacted).
    app/mailers/user_mailer.rb:24: Did you mean: UserMailer#new_user_password?
    24 |  def new_user_password(redacted)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Errors: 1

Expected behavior:

srb t should not produce error.

Versions:

bin/rails -v
Rails 6.0.0
ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-darwin18]
bundle | grep sorb
Using sorbet-runtime 0.4.4686
Using sorbet-static 0.4.4686 (universal-darwin-18)
Using sorbet 0.4.4686
Using sorbet-progress 0.2.3
Using sorbet-rails 0.5.2

Support configurable generator for Model

If we refactor the ModelRbiFormatter to be more modular, we will be able to support custom generation logic:

  1. Customize the header.
    Great for us to be able to tag rbi file as autogenerated in code review tools (eg Phabricator)

  2. Add custom methods generator.
    People can add custom methods from a library or from an internal module.
    Would be nice to extract Rails methods to its own library too.
    We also has some need for this for Shrine & internal modules.

  3. (Extended) Ability to override ModelRbiFormatter with custom subclass in the rake task.
    In a rails initializer, people can set a subclass of ModelRbiFormatter so that it uses a custom one

Mark expected rbi files as generated

Describe the bug:
Currently, whenever some code changes the expected files, all changes are expanded. It shows repetitive changes and makes it harder to review the PR. It'll be useful to mark those files as "generated"

See instructions:
https://sorbet-ruby.slack.com/archives/CHN2L03NH/p1577557587024400
https://github.com/github/linguist#vendored-code

Steps to reproduce:
Create a PR with changes in expected rbi files

Expected behavior:

Changes in "expected_*.rbi" are not expanded in the PR view.

Versions:

  • Ruby:
  • Rails:
  • Sorbet:
  • Sorbet-Rails:

Running tests raises a NameError for sigs receiving or returning an ActiveRecord_AssociationRelation

Describe the bug:

When you declare a method which receives or returns an ActiveRecord_AssociationRelation of some sort, tests fail with:

     NameError:
       private constant #<Class:0x00007fa75c4e8d28>::ActiveRecord_AssociationRelation referenced

Steps to reproduce:

create a method which returns an association relation on a model

class Post < ApplicationRecord
  belongs_to :user

  scope :published ->() { where(published: true) }
end

class User < ApplicationRecord
  extend T::Sig

  has_many :posts

  sig { returns(Post::ActiveRecord_AssociationRelation) }
  def published_posts
    posts.published
  end
end

RSpec.describe User do
  describe '#published_posts' do
    it 'returns only the published posts' do
      user = User.create!
      published = user.posts.create(published: true)
      user.posts.create(published: false)

      expect(user.published_posts).to eq([published])
    end
  end
end

This would raise a NameError claming that Post::ActiveRecord_AssociationRelation is a private class.

Note as well that in my case the models are on a rails engine.

Expected behavior:

The test passes

Versions:

  • Ruby: 2.4
  • Rails: 5.2.3
  • Sorbet: Sorbet typechecker 0.4.4891 git 4c9a4534439483b46e869c7212007ad103923203 built on 2019-10-17 22:56:24 GMT debug_symbols=true
  • Sorbet-Rails: 0.5.8

Add cursed methods to Rails models

These are all a mess and I'd prefer not to implement them, but I wanted to at least document them here, since they're taking up a lot of my hidden.rbi.

Is there a way to remove these from hidden.rbi without adding a bunch of useless methods to these classes?

Association Callback Methods

For every attribute on a model that has a relation with any other model, ActiveRecord/ActiveModel will have 12 methods. For example, let's say you add a relation on a Post that assigns a User as an author. Rails would create these methods:

# before add
def before_add_for_author; end
def before_add_for_author?; end
def before_add_for_author=(val); end

# before remove
def before_remove_for_author; end
def before_remove_for_author?; end
def before_remove_for_author=(val); end

# after add
def after_add_for_author; end
def after_add_for_author?; end
def after_add_for_author=(val); end

# after remove
def after_remove_for_author; end
def after_remove_for_author?; end
def after_remove_for_author=(val); end

Defined here: https://github.com/rails/rails/blob/5-2-stable/activerecord/lib/active_record/associations/builder/collection_association.rb#L32

These have barely any usage on GitHub, so I'm honestly not sure why they haven't been deprecated, but they do technically exist.

Dirty Methods

These only found their way into my hidden.rbi with Rails 6, but it seems like they've been there since at least 5.2? I'm not really sure why Sorbet couldn't find them before.

In 6.0 they're defined here: https://github.com/rails/rails/blob/v6.0.0.rc2/activemodel/lib/active_model/dirty.rb#L124-L126

If you declare an attribute name, you get these:

def name_change(*args); end
def name_changed?(*args); end
def name_previous_change(*args); end
def name_previously_changed?(*args); end
def name_was(*args); end
def name_will_change!(*args); end
def restore_name!(*args); end

There are also a few defined here: https://github.com/rails/rails/blob/v6.0.0.rc2/activerecord/lib/active_record/attribute_methods/dirty.rb#L19-L26

def saved_change_to_name?(*args); end
def saved_change_to_name(*args); end
def name_before_last_save(*args); end
def will_save_change_to_name?(*args); end
def name_change_to_be_saved(*args); end
def name_in_database(*args); end

And two here: https://github.com/rails/rails/blob/v6.0.0.rc2/activerecord/lib/active_record/attribute_methods/before_type_cast.rb#L32-L33

def name_before_type_cast(*args); end
def name_came_from_user?(*args); end

So that's 15 methods added per attribute (and this isn't counting the setter, getter, or asker), fun!

Add rake rails_rbi:all

I'd like to regenerate the RBIs for models, routes, and helpers with one task. rake rails_rbi:all (or maybe just rake rails_rbi?) should invoke each of the three RBI tasks.

5 failed specs after upgrading sorbet-rails

Describe the bug:

Failed specs after upgrading sorbet-rails

Steps to reproduce:

After updating my fork of sorbet-rails this morning and running "bundle update" and "rake", I get 5 failed specs.

Failed examples:

spec ./spec/rake_rails_rbi_models_spec.rb:10 # rake rails_rbi:models generates model files correctly
rspec ./spec/rake_rails_rbi_models_spec.rb:51 # rake rails_rbi:models generates more than 1 selected models correctly
rspec ./spec/sorbet_spec.rb:54 # sorbet returns expected sorbet tc result
rspec ./spec/sorbet_spec.rb:64 # sorbet passes sorbet dynamic checks
rspec ./spec/sorbet_spec.rb:71 # sorbet runs with srb tc --lsp

Expected behavior:

No failed specs.

Versions:

  • Ruby: 2.5.5p157
  • Rails: 5.2.3
  • Sorbet: 0.4.4673
  • Sorbet-Rails: 0.5.2

Select has incorrect return type when passed a block

Describe the bug:
ApplicationRecord.select has two different modes: one that takes a block and returns an Array, and one that takes any number of fields and returns an ActiveRecord::Relation. The sig that sorbet-rails generates claims it can only return a Relation.

Steps to reproduce:

class User < ApplicationRecord
end

Generated rbi file:

  sig { params(args: T.untyped, block: T.nilable(T.proc.void)).returns(User::ActiveRecord_Relation) }
  def select(*args, &block); end

Expected behavior:
To be honest, I'm not quite sure. Making the return type of select be T.any(User::ActiveRecord_Relation, T::Array[User]) is not great, because callers will always have to check which type is being returned, even though it can always be inferred from the argument type. But it might be the best we can do, since ruby doesn't support overloading.

Versions:

  • Ruby: 2.6.2
  • Rails: Rails 5.2.3
  • Sorbet: 0.4.4651
  • Sorbet-Rails: 0.5.0

Fix warning "class variable access from toplevel"

Describe the bug:

We would like to get rid of this warning
lib/sorbet-rails/tasks/rails_rbi.rake:7: warning: class variable access from toplevel

Steps to reproduce:

It's because I added the following code in rails_rbi.rake. If we find another way to copy file, it'll be fine

# this is ugly but it's a way to get the current directory of this script
# maybe someone coming along will know a better way
@@sorbet_rails_rake_dir = File.dirname(__FILE__)

Expected behavior:
No warning

Versions:

  • Ruby:
  • Rails:
  • Sorbet:
  • Sorbet-Rails: 0.4.0

Support Rails version 5.1

Some preliminary discussion around this problem can be found in this slack thread

when i run rake rails_rbi:models it skips all of the instance methods for attributes defined in the schema

e.g.

-- Generate sigs for ServiceProviderUsersServiceClientUser --
Skip method 'created_at' because it is not autogenerated by Rails.
Skip method 'created_at=' because it is not autogenerated by Rails.
Skip method 'hourly_rate_in_cents' because it is not autogenerated by Rails.
Skip method 'hourly_rate_in_cents=' because it is not autogenerated by Rails.

but in a console:

irb(main):005:0> ServiceProviderUsersServiceClientUser.instance_method(:hourly_rate_in_cents).source_location
=> ["/bundle/gems/activerecord-5.1.7/lib/active_record/attribute_methods/read.rb", 34]

irb(main):004:0> ServiceProviderUsersServiceClientUser.instance_method(:hourly_rate_in_cents).owner   
=> #<#<Class:0x0000563fdf89fd58>:0x0000563fdf89fda8>

Deps

  • ruby 2.6.2
  • rails-5.1.7
  • activerecord-5.1.7
  • sorbet-0.4.4253
  • sorbet-runtime-0.4.4253
  • sorbet-rails-0.1.3

Support for serialize?

Just trying out Sorbet and this gem for the first time, so maybe there's a way to handle this already?

I have an ActiveRecord class that uses serialize field_name, Hash and the rake rails_rbi:models command has generated an rbi file where both the getter and setter methods for field_name are down as using the datatype T.nilable(String), which is the datatype of the underlying database field, rather than referencing Hash which is what those methods use thanks to serialize.

I've edited the generated rbi file for now, but wondered if there's a sig or something I should be writing on the class to make this work already? Or maybe this is a feature request for the gem?

Negative enum scope support on Rails 6.0

Negative enum scopes are a feature in Rails 6, it'd be nice if they were generated as part of sorbet-rails :)

Expected behavior:
sorbet-rails would generate negative enum scopes, e.g. if you had an enum for User, like so:

enum role: {
  member: 0,
  moderator: 1,
  admin: 2
}

Then you could write code like this:

user = User.new
return PermissionsError if user.not_admin?

Currently, it doesn't seem that sorbet-rails generates these.

Unexpected modified files after running "run_all_specs.sh" today

Describe the bug:

Unexpected modified files after running "run_all_specs.sh" today

Steps to reproduce:

After upgrading sorbet-rails repos (488 files) this morning, I ran "./spec/bin/run_all_specs.sh" and it resulted in 35 modified files (new "typed:" lins I think) in sorbet-rails/spec/support/v5.2-no-sorbet. Confirmed connection with PR#117 and #126 .

Expected behavior:

No modified files after upgrading to latest sorbet-rails repo.

Versions:

  • Ruby: ruby 2.4.6p354 (2019-04-01 revision 67394) [x86_64-darwin16]
  • Rails: 5.2.3
  • Sorbet: 0.4.4602
  • Sorbet-Rails: 0.5.0

Add support for helpers method in controllers

Rails 5.0+ has a method called helpers which is available in controllers. https://blog.bigbinary.com/2016/06/26/rails-add-helpers-method-to-ease-usage-of-helper-modules-in-controllers.html

So, for example if you had this application_helper.rb:

module ApplicationHelper
  extend T::Sig

  sig { returns(Integer) }
  def forty_two
    return 42
  end
end

Then, in users_controller.rb you could have code like helpers.forty_two. helpers will include methods from any of the helper classes you've created.

class UsersController < ApplicationController
  def index
    @number = helpers.forty_two
  end
end

It's not possible to create a type signature for this method statically, it has to be done dynamically.

e.g. like this:

module ActionController::Helpers
  sig { returns(T.all(ApplicationHelper, GamesHelper, UsersHelper)) }
  def helpers; end
end

Generate methods for ActiveStorage attachments

Describe the bug:

SorbetRails doesn't currently generate methods for ActiveStorage attachments. For example, in my Rails app users have avatars and games have covers, but neither of these are typed.

Steps to reproduce:

class User < ApplicationRecord
  has_one_attached :avatar
end
class Game < ApplicationRecord
  has_one_attached :cover
end

In hidden.rbi we have some methods like this:

module User::GeneratedAssociationMethods
  def avatar(); end

  def avatar=(attachable); end
end

Expected behavior:

It should return either ActiveStorage::Attached::One or ActiveStorage::Attached::Many depending on the definition in the model.

This is what I'd expect to see in game.rbi:

module Game::GeneratedAttributeMethods
  extend T::Sig
  
  sig { returns(T.nilable(ActiveStorage::Attached::One)) }
  def cover; end

  sig { params(attachable: T.untyped).returns(T.untyped) }
  def cover=(attachable); end

  # or, if we had something defined like `has_many_attached :covers`
  sig { returns(T.nilable(ActiveStorage::Attached::Many)) }
  def covers; end

  sig { params(attachable: T.untyped).returns(T.untyped) }
  def covers=(*attachables); end
end

I'm not entirely sure what the setters take or return, unfortunately.

Versions:

  • Ruby: 2.6.2
  • Rails: 6.0.0
  • Sorbet:
  • Sorbet-Rails: 0.5.3 (master)

Add config line to v5.0's config/application.rb file

Added the line below to the bottom of sorbet-rails/spec/support/v5.0/config/application.rb file
to get rid of "DEPRECATION WARNING: Time columns will become time zone aware in Rails 5.1." messages.

config.active_record.time_zone_aware_types = [:datetime]

Only v5.0 needs this line.

Improve gem tests by running Sorbet on a sample input

Gem's tests compare expected output from templates with the output from the code.
It is very useful as it proves that the rbi files are generated.

However there's no test that proves that generated rbi files are valid.

Task:
test generated rbi files against a sample code using sorbet

Add pluck_to_tstruct to Model & Relation

Describe the bug:
This will be a convenient thing for getting structured objects out of a relation.

Expected behavior:
Wizard.all.pluck_to_struct(TA[StructName].new, :attribute1, :attribute2) -> T::Array[StructName]

Versions:

  • Ruby:
  • Rails:
  • Sorbet:
  • Sorbet-Rails:

Use HashWithIndifferentAccess for enum values

Currently sorbet-rails uses T::Hash[T.any(String, Symbol), Integer] for enum values of a model. This works but the keys method would return an array of Symbol or String. Rails actually use HashWithIndifferentAccess, which guarantee (softly) that .keys would return an array of String.

It's blocking on the typedef of HashWithIndifferentAccess itself (which should/could be added to sorbet-typed)

Segmentation fault on 0.4.4295

cat Gemfile
...
group :development do
  gem 'sorbet'
  gem 'sorbet-runtime'
  gem 'sorbet-rails'
end
...
bundle install
bundle exec srb rbi sorbet-typed
rake rails_rbi:routes
rake rails_rbi:models

but when I tried

bundle exec srb tc --suggest-typed --typed=strict --error-white-list=7022 --autocorrect

I got following error

vendor/bundler/ruby/2.6.0/gems/sorbet-0.4.4295/bin/srb: line 43: 83774 Segmentation fault: 11  "${sorbet[0]}" "${args[@]}"

Can you help me to figure out it?

OS: macOS Mojave 10.14.5 (18F132)

Should we support jobs with specific arguments?

Describe the bug:

When hidden.rbi contains something like this,

class DeleteBooksJob
  def perform(*args, &blk); end
end

(I'll refer to these as "non-specific" arguments.)

And jobs/delete_books_job.rb looks like this,

  sig {
    params(
      acting_user_id: Integer,
      book_ids: T::Array[Integer]
    )
  }
  def perform(acting_user_id, book_ids)

(I'll refer to these as "specific" arguments.)

then srb produces an error

be srb t
sorbet/rbi/hidden-definitions/hidden.rbi:8786: Method DeleteBooksJob#perform redefined without matching argument count. Expected: 2, got: 1 https://srb.help/4010
    8786 |  def perform(*args, &blk); end
            ^^^^^^^^^^^^^^^^^^^^^^^^
    app/jobs/delete_books_job.rb:13: Previous definition
    13 |  def perform(acting_user_id, book_ids)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Errors: 1

My question is:

Should we support jobs with specific arguments, according to my definition of "specific" given above?

Steps to reproduce:

rails new sorbet_rails_issue
bin/rails g model books title:string
bin/rails db:migrate
bin/rails g job delete_books
# install sorbet, then sorbet-rails according to standard instructions

Edit jobs/delete_books_job.rb as follows:

# typed: false

class DeleteBooksJob < ApplicationJob
  extend T::Sig
  queue_as :default

  sig {
    params(
      acting_user_id: Integer,
      book_ids: T::Array[Integer]
    ).void
  }
  def perform(acting_user_id, book_ids)
    # Do something later
  end
end

Notice the typed: false.

Run srb, see error:

sorbet/rbi/hidden-definitions/hidden.rbi:8786: Method DeleteBooksJob#perform redefined without matching argument count. Expected: 2, got: 1 https://srb.help/4010
    8786 |  def perform(*args, &blk); end
            ^^^^^^^^^^^^^^^^^^^^^^^^
    app/jobs/delete_books_job.rb:13: Previous definition
    13 |  def perform(acting_user_id, book_ids)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expected behavior:

srb should not produce error.

Versions:

  • Ruby: 2.6.5
  • Rails: 6.0.1
  • Sorbet: 0.4.5133
  • Sorbet-Rails: 0.5.8.1

A lot more "typed: ignore" files are been generated with srb commands

Describe the bug:

Yesterday, I saw a lot more "typed: ignore" files for my repos after running srb commands.
I saw several of them went from 0 "typed: ignore"s to 30+.
Sample repo: https://github.com/jasnow/rails_5.2_new .
Gist of "typed: ignore" files: https://gist.github.com/jasnow/b26be8a79da4ff230c68e31769ae5f69

Steps to reproduce:

To see "srb tc" errors, just change "ignore" to "false" and run "srb tc". Thanks.

Expected behavior:

Very few or zero "typed: ignore" files

Versions:

For the above sample repo:

  • Ruby: 2.4.7
  • Rails: 5.2.3
  • Sorbet: 0.4.4709
  • Sorbet-Rails: 0.5.3

Note that this also happened for Ruby 2.6 and Rails 6.0 repos.

5 bundled_rbi related "srb tc" errors in rails_app5_2 repo for gems/sorbet-rails.rbi file

Describe the bug:

5 bundled_rbi related "srb tc" errors in https://github.com/jasnow/rails_app5_2 repo for gems/sorbet-rails.rbi file

Steps to reproduce:

On October 9, when I changed typed from false to true for
sorbet/rbi/gems/sorbet-rails.rbi and ran "srb tc", I got 5 errors.
See gist below for details.
https://gist.github.com/jasnow/b99bc9f4e4ccc5c50b7f219b0767cc78

Today when I repeat it, I get 9 errors.
Ran "srb rbi hidden-definitions" and got it down to 6 errors.
Ran "srb init"/etc again and got it down to the above 5 errors.

All 5 errors appear to be related to the sorbet-rails/bundled_rbi files.
If I comment out the contents of these files, "srb tc" is clean.

Expected behavior:

No errors

Versions:

  • Ruby: 2.4.9p362 (2019-10-02 revision 67824) [x86_64-darwin16]
  • Rails: 5.2.3
  • Sorbet: 0.4.4865
  • Sorbet-Rails: 0.5.7

undefined method `through_reflection?' for #<ActiveRecord::Reflection::HasManyReflection:0x00007f9e72ce1b08>

When running rake rails_rbi:models I got this error for nearly every model.

-- Generate sigs for Invoice --
---
Error when handling model Invoice: undefined method `through_reflection?' for #<ActiveRecord::Reflection::HasManyReflection:0x00007f9e8ddc1418>
Did you mean?  through_reflection

I'm on Rails 4.2.10 and Ruby 2.4.2. Is this a version thing? I don't see anywhere that says that Rails 4 is covered, but I also don't see that it isn't covered.

Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.