Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proc parameter should get type instead of nil #1234

Open
Coridyn opened this issue Sep 26, 2024 · 3 comments
Open

Proc parameter should get type instead of nil #1234

Coridyn opened this issue Sep 26, 2024 · 3 comments

Comments

@Coridyn
Copy link

Coridyn commented Sep 26, 2024

Here is a reproduction repo: https://github.com/Coridyn/steep-proc-type-error

With this implementation and rbs file:

Actual behaviour

The parameter gets a type of nil and fails type checking on property accesses.

Expected behaviour

I expect this to pass type checking.

Either the proc's item parameter should infer the type of Model from the scope in which it was defined.
Or, if the type cannot be inferred, fall back to a type of untyped.

model.rb

class Model
  has_paper_trail if: proc { |item| item.is_live? }
  
  def self.has_paper_trail(options = {})
    # ...
  end
  
  def is_live?
    true
  end
end

model.rbs

class Model
  def self.has_paper_trail: (?untyped) -> void
  def is_live?: () -> true
end

Running steep check outputs this type error:

lib/model.rb:2:41: [error] Type `nil` does not have method `is_live?`
│ Diagnostic ID: Ruby::NoMethod
│
└   has_paper_trail if: proc { |item| item.is_live? }
                                           ~~~~~~~~

Detected 1 problem from 1 file

Superficially, it seems similar to this earlier issue relating to blocks? #778

@Coridyn Coridyn changed the title Proc parameter should get untyped parameter instead of nil Proc parameter should get type instead of nil Sep 26, 2024
@tk0miya
Copy link
Contributor

tk0miya commented Oct 3, 2024

How about this?

# rbs
class Model
  def self.has_paper_trail: (?untyped, ?if: ^(instance) -> boolish) -> void
  def is_live?: () -> true
end

It tells us that Model.has_paper_trail takes a keyword argument named if, whose value is typed as ^(instance) -> boolish.

@Coridyn
Copy link
Author

Coridyn commented Oct 4, 2024

Thank you for the reply.

Unfortunately I get the same error with the keyword argument approach you have suggested.

I have tried a few different representations of the parameters but keep running into the same Type `nil` does not have method `is_live?` error.

I think issue is in the .rb type inference side, rather than the .rbs type definition side.

These are the different approaches I have tried based on your suggestion.
They are all failing with the same error trying to access the method on the proc argument.

NOTE: I have updated my repoduction repository with these new failing examples: https://github.com/Coridyn/steep-proc-type-error


keyword arguments

  1. Try keyword argument as suggested by @tk0miya

❌ Fails with same NoMethod error in .rb file
❌ Fails with an additional ArgumentTypeMismatch error

keyword_argument_1.rb

# keyword_argument_1.rb

class KeywordArgument1
  has_paper_trail if: proc { |item| item.is_live? } # ❌
  
  def self.has_paper_trail(if:)
    # ...
  end
  
  def is_live?
    true
  end
end

keyword_argument_1.rbs

# keyword_argument_1.rbs

class KeywordArgument1
  def self.has_paper_trail: (if: ^(instance) -> bool) -> void
  
  def is_live?: () -> true
end

Result

# same error as before
lib/keyword_argument_1.rb:2:41: [error] Type `nil` does not have method `is_live?`
│ Diagnostic ID: Ruby::NoMethod
│
└   has_paper_trail if: proc { |item| item.is_live? }
                                           ~~~~~~~~

# extra type error (this one is kind of expected, I think)
lib/keyword_argument_1.rb:2:22: [error] Cannot pass a value of type `::Proc` as an argument of type `^(::KeywordArgument1) -> bool`
│   ::Proc <: ^(::KeywordArgument1) -> bool
│
│ Diagnostic ID: Ruby::ArgumentTypeMismatch
│
└   has_paper_trail if: proc { |item| item.is_live? }
                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

  1. Fix the Proc ArgumentTypeMismatch error from above

❌ Fails with same NoMethod error in .rb file

keyword_argument_2.rb

class KeywordArgument2
  def self.has_paper_trail: (if: Proc) -> void # ❌
  
  def is_live?: () -> true
end

keyword_argument_2.rbs

class KeywordArgument2
  has_paper_trail if: proc { |item| item.is_live? }
  
  def self.has_paper_trail(if:)
    # ...
  end
  
  def is_live?
    true
  end
end

Result

lib/keyword_argument_2.rb:2:41: [error] Type `nil` does not have method `is_live?`
│ Diagnostic ID: Ruby::NoMethod
│
└   has_paper_trail if: proc { |item| item.is_live? }
                                           ~~~~~~~~

  1. Try using an object interface that defines the types for the allowed options hash

❌ Fails with same NoMethod error in .rb file

options_interface.rb

class OptionsInterface
  has_paper_trail  # ✅
  has_paper_trail on: [:destroy] # ✅
  has_paper_trail on: [:destroy], if: proc { |item| item.is_live? } # ❌
  
  def self.has_paper_trail(options = {})
    # ...
  end
  
  def is_live?
    true
  end
end

options_interface.rbs

class OptionsInterface
  type options[T] = {
      ?on: Array[Symbol],
      ?if: ^(T) -> bool,
  }
  
  def self.has_paper_trail: (?options[instance]) -> void
  
  def is_live?: () -> true
end

Result

lib/options_interface.rb:4:57: [error] Type `nil` does not have method `is_live?`
│ Diagnostic ID: Ruby::NoMethod
│
└   has_paper_trail on: [:destroy], if: proc { |item| item.is_live? }
                                                           ~~~~~~~~

These approaches are all failing with the same Type `nil` does not have method `is_live?` error in the .rb file.

That is what makes me think this is an issue with the type inference on the proc arguments in the .rb file, rather than a problem with the type definition in the .rbs file.

@tk0miya
Copy link
Contributor

tk0miya commented Oct 6, 2024

Sorry for the confusion. My last comment was incorrect.

There are two problems:

  • The type of Kernel#proc was incorrect
  • For now, no way to give types for Proc object in Steep

The former one is a bug of the type. The type of Kernel#proc is () { () -> untyped } -> Proc. It means the block of the proc call takes no argument at all. This is why the arguments are typed as nil.

This bug has been fixed by ruby/rbs#2036 in last week. After the fix, the block arguments will be typed as untyped.

The latter one is a limitation of Steep. If my understanding is correct, there are two kinds of "proc" types. One is to allow function (ex. -> (arg) { ... }, and another is for an instance of the Proc class. And the type I mentioned in the last comment is a type for the allow function. It does not match to the Proc object.

In your case, it will get better if you rewrite your code like this:

class KeywordArgument1
  has_paper_trail if: ->(item) { item.is_live? }
end

Because the type of self.has_paper_trail is (if: ^(instance) -> bool) -> void. It means the method takes an allow function styled proc which takes an argument typed as "instance", via the if keyword.

On the other hand, as far as I know, there is no way to give argument types for the Proc objects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants