Proc parameter should get type instead of nil
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? https://github.com/soutaro/steep/issues/778
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.
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
- 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? }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Fix the
ProcArgumentTypeMismatcherror 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? }
~~~~~~~~
- 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.
Sorry for the confusion. My last comment was incorrect.
There are two problems:
- The type of
Kernel#procwas 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 https://github.com/ruby/rbs/pull/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.