Skip to content

Commit

Permalink
Allow shape types in T::Struct props (#4365)
Browse files Browse the repository at this point in the history
* Allow shape types in T::Struct props

Similar in spirit to #4361.

Fixes #3485

* Fix failing tests

* Fix test assertion
  • Loading branch information
jez committed Jul 15, 2021
1 parent b4e0f4e commit b8f4ccb
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 5 deletions.
33 changes: 33 additions & 0 deletions gems/sorbet-runtime/test/types/props/serializable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,39 @@ class StructWithTuples < T::Struct
end
end

class StructWithShapes < T::Struct
prop :empty_shape, {}
prop :symbol_key_shape, {foo: Integer}
prop :string_key_shape, {'foo' => Integer}
end

describe 'shapes' do
it 'typechecks' do
exn = assert_raises(TypeError) do
StructWithShapes.new(
empty_shape: {},
symbol_key_shape: {foo: 0},
string_key_shape: {:not_a_string => 0}
)
end
assert_includes(exn.message, '.string_key_shape to {:not_a_string=>0} (instance of Hash) - need a {"foo" => Integer}')
end

it 'roundtrips' do
expected = StructWithShapes.new(
empty_shape: {},
symbol_key_shape: {foo: 0},
string_key_shape: {'foo' => 0}
)

actual = StructWithShapes.from_hash(expected.serialize)

assert_equal(expected.empty_shape, actual.empty_shape)
assert_equal(expected.symbol_key_shape, actual.symbol_key_shape)
assert_equal(expected.string_key_shape, actual.string_key_shape)
end
end

class CustomType
extend T::Props::CustomType

Expand Down
26 changes: 26 additions & 0 deletions rewriter/Util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,32 @@ ast::ExpressionPtr ASTUtil::dupType(const ast::ExpressionPtr &orig) {
return ast::MK::Array(arrayLit->loc, std::move(elems));
}

auto *hashLit = ast::cast_tree<ast::Hash>(orig);
if (hashLit != nullptr) {
auto keys = ast::Hash::ENTRY_store{};
auto values = ast::Hash::ENTRY_store{};
ENFORCE(hashLit->keys.size() == hashLit->values.size());
for (size_t i = 0; i < hashLit->keys.size(); i++) {
const auto &key = hashLit->keys[i];

auto *keyLit = ast::cast_tree<ast::Literal>(key);
if (keyLit == nullptr) {
return nullptr;
}

const auto &value = hashLit->values[i];
auto duppedValue = dupType(value);
if (duppedValue == nullptr) {
return nullptr;
}

keys.emplace_back(key.deepCopy());
values.emplace_back(std::move(duppedValue));
}

return ast::MK::Hash(hashLit->loc, std::move(keys), std::move(values));
}

auto *cons = ast::cast_tree<ast::UnresolvedConstantLit>(orig);
if (!cons) {
return nullptr;
Expand Down
2 changes: 1 addition & 1 deletion test/testdata/rewriter/coerce_in_prop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ class Inputs
include T::Props

prop :supported_payment_methods1, T.coerce(PAYMENT_METHODS_HASH) # error-with-dupes: Unsupported method `T.coerce`
prop :supported_payment_methods2, T.coerce({a: Integer})
prop :supported_payment_methods2, T.coerce({a: Integer}) # error-with-dupes: Unsupported method `T.coerce`
end
4 changes: 3 additions & 1 deletion test/testdata/rewriter/prop_prohibit_shapes_and_tuples.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
class A
# We don't support shape or tuple types for props, and the way that manifests
# is that the prop rewrite pass just skips these two declarations.
prop :shape, {a: Integer, b: String} # error: Method `prop` does not exist
prop :shape, {a: Integer, b: String}
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Method `prop` does not exist
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Method `decorator` does not exist
prop :tuple, [Integer, String]
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Method `prop` does not exist
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Method `decorator` does not exist
Expand Down
34 changes: 34 additions & 0 deletions test/testdata/rewriter/prop_shape.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# typed: true
extend T::Sig

class StructWithShapes < T::Struct
prop :empty_shape, {}
prop :symbol_key_shape, {foo: Integer}
prop :string_key_shape, {'foo' => Integer}
prop :multiple_keys, {foo: Integer, bar: String, baz: Symbol}
prop :array_of_shape, T::Array[{foo: Integer}]
prop :shape_with_combinators, {foo: T.nilable(Integer)}
prop :combinator_with_shape, T.nilable({foo: Integer})
end

class StructWithBadShapes < T::Struct
prop :bad_literal, {foo: 42}
end

x = StructWithShapes.new(
empty_shape: {},
symbol_key_shape: {foo: 0},
string_key_shape: {'foo' => 0},
multiple_keys: {foo: 0, bar: '', baz: :a_symbol},
array_of_shape: [{foo: 0}],
shape_with_combinators: {foo: nil}
)

T.reveal_type(x.empty_shape) # error: `{} (shape of T::Hash[T.untyped, T.untyped])`
T.reveal_type(x.symbol_key_shape) # error: `{foo: Integer} (shape of T::Hash[T.untyped, T.untyped])`
T.reveal_type(x.string_key_shape) # error: `{String("foo") => Integer} (shape of T::Hash[T.untyped, T.untyped])`
T.reveal_type(x.multiple_keys) # error: `{foo: Integer, bar: String, baz: Symbol} (shape of T::Hash[T.untyped, T.untyped])`

T.reveal_type(x.array_of_shape) # error: `T::Array[{foo: Integer}]`
T.reveal_type(x.shape_with_combinators) # error: `{foo: T.nilable(Integer)} (shape of T::Hash[T.untyped, T.untyped])`
T.reveal_type(x.combinator_with_shape) # error: `T.nilable({foo: Integer})`
4 changes: 1 addition & 3 deletions test/testdata/rewriter/prop_skipped.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# typed: true

class A < T::Struct
# This prop won't be processed by the Prop dsl, as it contains a shape type
const :foo, T::Array[{foo: Integer}]
end

A.new(foo: [])
# ^^^^^^^^^^^^^^ error: Too many arguments
A.new(foo: [])

0 comments on commit b8f4ccb

Please sign in to comment.