Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sorbet not finding `has_many` association methods

I have a Rails model which has_many items:

class Plan < ApplicationRecord
  extend T::Sig

  has_many :items, dependent: :destroy

  before_save do
    # hyper simple test method to illustrat problem
    puts items
  end
end

However Sorbet can't seem to relate to the has_many :items. When I run Sorbet typechecking I get the following error:

$ srb tc
app/models/plan.rb:11: Method items does not exist on T.class_of(Plan) https://srb.help/7003
    11 |    items
            ^^^^^
  Did you mean:
    sorbet/rails-rbi/models/plan.rbi:86: Plan::GeneratedAssociationMethods#items
    86 |  def items; end

The answer to Sorbet's question is yes - I do mean that method. Where is the confusion coming from? Why doesn't the definition of .items in the RBI file satisfy Sorbet's need to know where this method is defined?

like image 985
Peter Nixey Avatar asked Jan 22 '26 19:01

Peter Nixey


1 Answers

Ok so this turned out to be a misunderstanding of Rails (Ruby?) rather than Sorbet. Skipping ahead this is actually a point to sorbet because it helped spot and solve this problem.

The problem is that when you pass a block to before_save, the block gets called on the class (Plan), not the instance (plan). Instead, an instance is passed into it.

So taking the original code:

class Plan < ApplicationRecord
  extend T::Sig

  has_many :items, dependent: :destroy

  before_save do
    # hyper simple test method to illustrate problem
    puts items
  end
end

This would result in the execution of Plan.before_save(plan). Where plan is the instance of Plan. So in the example above, items is being pulled out of thin air and won't work.

Two syntaxes which do work

class Plan < ApplicationRecord
  extend T::Sig

  has_many :items, dependent: :destroy

  before_save do |plan| # <= notice the argument
    puts plan.items
  end
end

will work. And so will:

class Plan < ApplicationRecord
  extend T::Sig

  has_many :items, dependent: :destroy
  before_save :put_items

  def put_items
    puts items
  end
end

I'm not quite sure what makes the second one work, whether it's Ruby magic or Rails magic but sometimes there's just too much magic for my liking.

like image 70
Peter Nixey Avatar answered Jan 25 '26 10:01

Peter Nixey



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!