-
Notifications
You must be signed in to change notification settings - Fork 506
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
[sorbet-runtime] Add T.must_because type assertion #6395
Changes from 10 commits
dfbff54
7ceb165
76a4b31
520588a
e06f296
d888320
9c8ed58
e69829d
316a9af
3bc956a
f6143c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -221,6 +221,34 @@ def self.must(arg) | |
end | ||
end | ||
|
||
# A convenience method to `raise` with a provided error reason when the argument | ||
# is `nil` and return it otherwise. | ||
# | ||
# Intended to be used as: | ||
# | ||
# needs_foo(T.must_because(maybe_gives_foo) {"reason_foo_should_not_be_nil"}) | ||
# | ||
# Equivalent to: | ||
# | ||
# foo = maybe_gives_foo | ||
# raise "reason_foo_should_not_be_nil" if foo.nil? | ||
# needs_foo(foo) | ||
# | ||
# Intended to be used to promise sorbet that a given nilable value happens | ||
# to contain a non-nil value at this point. | ||
# | ||
# `sig {params(arg: T.nilable(A), reason_blk: T.proc.returns(String)).returns(A)}` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you move this comment to the RBI, where it will be shown on hover? (The only people that will see it here are people who are changing sorbet-runtime.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current added comment in the RBI matches the comment structure of |
||
def self.must_because(arg, &reason_blk) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we remove the |
||
return arg if arg | ||
return arg if arg == false | ||
|
||
begin | ||
raise TypeError.new("Unexpected `nil` because #{yield}") | ||
rescue TypeError => e # raise into rescue to ensure e.backtrace is populated | ||
T::Configuration.inline_type_error_handler(e, {kind: 'T.must_because', value: arg, type: nil}) | ||
end | ||
end | ||
|
||
# A way to ask Sorbet to show what type it thinks an expression has. | ||
# This can be useful for debugging and checking assumptions. | ||
# In the runtime, merely returns the value passed in. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# frozen_string_literal: true | ||
require_relative '../test_helper' | ||
|
||
module Opus::Types::Test | ||
class MustBecauseTest < Critic::Unit::UnitTest | ||
EXAMPLE_REASON = 'some_must_because_reason' | ||
|
||
it 'allows non-nil' do | ||
assert_equal(:a, T.must_because(:a) {EXAMPLE_REASON}) | ||
assert_equal(0, T.must_because(0) {EXAMPLE_REASON}) | ||
assert_equal("", T.must_because("") {EXAMPLE_REASON}) | ||
assert_equal(false, T.must_because(false) {EXAMPLE_REASON}) | ||
end | ||
|
||
it 'disallows nil' do | ||
e = assert_raises(TypeError) do | ||
T.must_because(nil) {EXAMPLE_REASON} | ||
end | ||
|
||
assert_equal("Unexpected `nil` because #{EXAMPLE_REASON}", e.message) | ||
end | ||
|
||
it 'does not calculate the reason unless nil is passed' do | ||
T.must_because(:a) do | ||
raise('reason block should not have been called') | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -189,6 +189,16 @@ module T | |
sig {params(arg: T.untyped).returns(T.untyped)} | ||
def self.must(arg); end | ||
|
||
# Statically, declares to Sorbet that the argument is never `nil`, despite | ||
# what the type system would otherwise infer for the type. | ||
# | ||
# At runtime, raises an exception contining the provided reason if the | ||
# argument is ever `nil`. | ||
# | ||
# For more, see https://sorbet.org/docs/type-assertions#tmust_because | ||
sig {params(arg: T.untyped, reason_blk: T.proc.returns(String)).returns(T.untyped)} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jez Now that we can implement T.must-likes with generics, should I be typing this signature more precisely: https://github.com/sorbet/sorbet/compare/andrejewski/t-must-because...andrejewski/t-must-because-stronger-rbi?expand=1 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it's already computed by the intrinsic in calls.cc, so the signature is redundant. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically there are bugs in the signature-based approach, which are almost impossible to reproduce but for something like |
||
def self.must_because(arg, &reason_blk); end | ||
|
||
# A way to assert that a given branch of control flow is unreachable. | ||
# | ||
# Most commonly used to assert that a `case` or `if` expression exhaustively | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# typed: strict | ||
|
||
def test_must_because # error: does not have a `sig` | ||
x = T.cast(nil, T.nilable(String)) # error: `T.cast` is useless | ||
T.assert_type!(T.must_because(x) {'reason'}, String) | ||
|
||
T.must_because(x) {'reason'} | ||
T.must_because() | ||
# ^^ error: Not enough arguments | ||
# ^ error: requires a block parameter | ||
|
||
T.must_because(x) # error: requires a block parameter | ||
T.must_because(x, 0) | ||
# ^ error: Expected: `1`, got: `2` | ||
# ^ error: requires a block parameter | ||
T.must_because(x) {0} # error: Expected `String` | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorbet won’t automatically figure out that this method defined in sorbet-runtime exists. you’ll need to duplicate the signature to the file in
rbi/
whereT.must
is defined. Also it’d be great if you added some tests intest/testdata/infer/must.rb
to confirm that the RBI you’ve added works as expectedThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you likely also want to add a test in
test/testdata/infer/must_untyped.rb
to document thatT.must
andT.must_because
have different behavior when called on someT.untyped
valueIf you want to go even further, the code which implements that error is in
core/types/calls.cc
. You could cargo cult some of that code to make that error apply toT.must_because
as well if you like.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I bias towards have the same affordances so there's fewer gotchas and more reason to perhaps always use
must_because
overmust