ruby-on-railsrubysorbet

How to avoid using Sorbet's T.must when the code flow already knows that value is not nil?


I'm using Sorbet on a Rails project and I have a method that does a calculation on a nilable property.

def age
  return unless dob && dob.year > 1900

  now = Time.now.utc.to_date
  now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1)
end

On that code, dob is nilable. If I run a type check on it I get a bunch of complains asking to wrap dob inside T.must(dob):

app/models/person.rb:18: Method year does not exist on NilClass component of T.nilable(Date) https://srb.help/7003
    18 |    return unless dob && dob.year > 1900
                                     ^^^^
  Got T.nilable(Date) originating from:
    app/models/person.rb:18:
    18 |    return unless dob && dob.year > 1900
                                 ^^^
  Autocorrect: Use `-a` to autocorrect
    app/models/person.rb:18: Replace with T.must(dob)
    18 |    return unless dob && dob.year > 1900
                                 ^^^

app/models/person.rb:21: Method year does not exist on NilClass component of T.nilable(Date) https://srb.help/7003
    21 |    now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1)
                           ^^^^
  Got T.nilable(Date) originating from:
    app/models/person.rb:21:
    21 |    now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1)
                       ^^^
  Autocorrect: Use `-a` to autocorrect
    app/models/person.rb:21: Replace with T.must(dob)
    21 |    now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1)
                       ^^^

app/models/person.rb:21: Method month does not exist on NilClass component of T.nilable(Date) https://srb.help/7003
    21 |    now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1)
                                                    ^^^^^
  Got T.nilable(Date) originating from:
    app/models/person.rb:21:
    21 |    now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1)
                                                ^^^
  Autocorrect: Use `-a` to autocorrect
    app/models/person.rb:21: Replace with T.must(dob)
    21 |    now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1)
                                                ^^^

app/models/person.rb:21: Method month does not exist on NilClass component of T.nilable(Date) https://srb.help/7003
    21 |    now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1)
                                                                               ^^^^^
  Got T.nilable(Date) originating from:
    app/models/person.rb:21:
    21 |    now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1)
                                                                           ^^^
  Autocorrect: Use `-a` to autocorrect
    app/models/person.rb:21: Replace with T.must(dob)
    21 |    now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1)
                                                                           ^^^

app/models/person.rb:21: Method day does not exist on NilClass component of T.nilable(Date) https://srb.help/7003
    21 |    now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1)
                                                                                                       ^^^
  Got T.nilable(Date) originating from:
    app/models/person.rb:21:
    21 |    now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1)
                                                                                                   ^^^
  Autocorrect: Use `-a` to autocorrect
    app/models/person.rb:21: Replace with T.must(dob)
    21 |    now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1)

What is the best way to tell Sorbet that dob won't be nil since I'm already returning if it is.


Solution

  • This is Sorbet's most-commonly asked question, and it appears at the top of the Sorbet FAQ page.

    The short answer is that you need to assign dob to a variable (despite not having any parentheses, dob is in fact a call to a method in this context, because there has not been an assignment to a variable called dob in this scope).

    In your case, the easiest fix is to write dob = self.dob at the top of the method, which calls the dob method once and assigns it to a variable:

    def age
      dob = self.dob
      return unless dob && dob.year > 1900
    
      now = Time.now.utc.to_date
      now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1)
    end
    

    ā†’ View full example on sorbet.run

    All references of dob after the first assignment are now reads from the local variable, instead of calls to the method, which allows Sorbet to remember any flow-sensitive type analyses it has performed.