From c915e068b9b617e321e87848ac9e27eefdda380f Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Sun, 14 Aug 2022 01:05:58 +0200 Subject: [PATCH 01/53] Set up a compatibility system, implement checks for some types --- src/ClassLikeType.php | 8 +- src/Compatibility.php | 162 ++++++++++++++++++ src/UnionType.php | 11 ++ tests/functional/CompatibilityTest.php | 110 ++++++++++++ tests/functional/compatible-types.md | 0 tests/functional/compatible-types/bool.md | 10 ++ .../compatible-types/int-literal.md | 55 ++++++ tests/functional/compatible-types/int.md | 88 ++++++++++ tests/functional/compatible-types/misc.md | 2 + tests/functional/compatible-types/string.md | 44 +++++ tests/functional/compatible-types/union.md | 8 + tests/functional/types/bool.txt | 3 + tests/functional/types/int-literal.txt | 5 + tests/functional/types/int.txt | 10 ++ tests/functional/types/misc.txt | 1 + tests/functional/types/string.txt | 6 + tests/functional/types/union.txt | 3 + 17 files changed, 524 insertions(+), 2 deletions(-) create mode 100644 src/Compatibility.php create mode 100644 tests/functional/CompatibilityTest.php create mode 100644 tests/functional/compatible-types.md create mode 100644 tests/functional/compatible-types/bool.md create mode 100644 tests/functional/compatible-types/int-literal.md create mode 100644 tests/functional/compatible-types/int.md create mode 100644 tests/functional/compatible-types/misc.md create mode 100644 tests/functional/compatible-types/string.md create mode 100644 tests/functional/compatible-types/union.md create mode 100644 tests/functional/types/bool.txt create mode 100644 tests/functional/types/int-literal.txt create mode 100644 tests/functional/types/int.txt create mode 100644 tests/functional/types/misc.txt create mode 100644 tests/functional/types/string.txt create mode 100644 tests/functional/types/union.txt diff --git a/src/ClassLikeType.php b/src/ClassLikeType.php index 8a066ac..4385edb 100644 --- a/src/ClassLikeType.php +++ b/src/ClassLikeType.php @@ -12,9 +12,13 @@ final class ClassLikeType extends AbstractType /** * @param non-empty-string $name * @param list $typeParameters + * @param list $parents */ - public function __construct(public readonly string $name, public readonly array $typeParameters = []) - { + public function __construct( + public readonly string $name, + public readonly array $typeParameters = [], + public readonly array $parents = [], + ) { } public function toNode(): NodeInterface diff --git a/src/Compatibility.php b/src/Compatibility.php new file mode 100644 index 0000000..e555105 --- /dev/null +++ b/src/Compatibility.php @@ -0,0 +1,162 @@ + self::checkBool($super, $sub), + ClassLikeType::class => self::checkClassLike($super, $sub), + ClassStringType::class => self::checkClassString($super, $sub), + FloatType::class => self::checkFloat($sub), + IntLiteralType::class => self::checkIntLiteral($super, $sub), + IntType::class => self::checkInt($super, $sub), + MixedType::class => true, + NeverType::class => false, + StringType::class => self::checkString($super, $sub), + UnionType::class => self::checkUnion($super, $sub), + default => throw new LogicException(sprintf('Unsupported type "%s"', $superClass)), + }; + } + + private static function checkClassString(ClassStringType $super, AbstractType $sub): bool + { + if (!$sub instanceof ClassStringType) { + return false; + } + if ($super->class === null) { + return true; + } + if ($sub->class === null) { + return false; + } + return self::check($super->class, $sub->class); + } + + private static function checkClassLike(ClassLikeType $super, AbstractType $sub): bool + { + if (!$sub instanceof ClassLikeType) { + return false; + } + if ($super->name === $sub->name) { + return true; + } + foreach ($sub->parents as $parent) { + if (self::check($super, $parent)) { + return true; + } + } + return false; + } + + private static function checkString(StringType $super, AbstractType $sub): bool + { + if ($sub instanceof ClassStringType) { + if ($super->numeric) { + return false; + } + return true; + } + if (!$sub instanceof StringType) { + return false; + } + if ($super->numeric) { + return $sub->numeric; + } + if ($super->nonEmpty) { + return $sub->nonEmpty; + } + return true; + } + + private static function checkBool(BoolType $super, AbstractType $sub): bool + { + if (!$sub instanceof BoolType) { + return false; + } + if ($super->value === null) { + return true; + } + return $super->value === $sub->value; + } + + private static function checkInt(IntType $super, AbstractType $sub): bool + { + if ($sub instanceof IntLiteralType) { + return ($super->min ?? PHP_INT_MIN) <= $sub->value && $sub->value <= ($super->max ?? PHP_INT_MAX); + } + if (!$sub instanceof IntType) { + return false; + } + [$superMin, $superMax, $subMin, $subMax] = [ + $super->min ?? PHP_INT_MIN, + $super->max ?? PHP_INT_MAX, + $sub->min ?? PHP_INT_MIN, + $sub->max ?? PHP_INT_MAX, + ]; + return $superMin <= $subMin && $superMax >= $subMax; + } + + private static function checkIntLiteral(IntLiteralType $super, AbstractType $sub): bool + { + if ($sub instanceof IntLiteralType) { + return $super->value === $sub->value; + } + if ($sub instanceof IntType) { + return $sub->min === $super->value && $sub->max === $super->value; + } + return false; + } + + private static function checkFloat(AbstractType $sub): bool + { + return $sub instanceof FloatType + || $sub instanceof IntLiteralType + || $sub instanceof IntType; + } + + private static function checkUnion(UnionType $super, AbstractType $sub): bool + { + if ($sub instanceof UnionType) { + $superTypes = $super->flatten(); + foreach ($sub->flatten() as $type) { + if (self::isSubtypeOfAny($type, $superTypes)) { + continue; + } + return false; + } + return true; + } + return self::check($super->left, $sub) || self::check($super->right, $sub); + } + + /** + * @param list $haystack + */ + private static function isSubtypeOfAny(AbstractType $type, array $haystack): bool + { + foreach ($haystack as $item) { + if (!self::check($item, $type)) { + continue; + } + return true; + } + return false; + } +} diff --git a/src/UnionType.php b/src/UnionType.php index 9077066..0805c4f 100644 --- a/src/UnionType.php +++ b/src/UnionType.php @@ -17,4 +17,15 @@ public function toNode(): NodeInterface { return new UnionNode($this->left->toNode(), $this->right->toNode()); } + + /** + * @return list + */ + public function flatten(): array + { + return array_merge( + $this->left instanceof UnionType ? $this->left->flatten() : [$this->left], + $this->right instanceof UnionType ? $this->right->flatten() : [$this->right] + ); + } } diff --git a/tests/functional/CompatibilityTest.php b/tests/functional/CompatibilityTest.php new file mode 100644 index 0000000..3fc8c96 --- /dev/null +++ b/tests/functional/CompatibilityTest.php @@ -0,0 +1,110 @@ + + */ + private static function compatibleTypes(): array + { + $types = []; + foreach (self::filesInDirectory(__DIR__ . '/compatible-types/') as $file) { + foreach (explode("\n", file_get_contents($file)) as $line) { + $isMatch = \Safe\preg_match('/^- `(?.+)` is a subtype of `(?.+)`/', $line, $matches); + if (!$isMatch) { + continue; + } + $types[] = [$matches['super'], $matches['sub']]; + } + } + return $types; + } + + /** + * @return iterable + */ + private static function filesInDirectory(string $directory): iterable + { + foreach (new DirectoryIterator($directory) as $file) { + if ($file->isDot()) { + continue; + } + + yield $file->getPathname(); + } + } + + /** + * @dataProvider cases + */ + public function testCompatibility(string $super, string $sub, bool $expected): void + { + $scope = Scope::global(); + $fooInterface = new ClassLikeType('FooInterface'); + $scope->register('FooInterface', $fooInterface); + $scope->register('Foo', new ClassLikeType('Foo', parents: [$fooInterface])); + $superType = Type::fromString($super, $scope); + $subType = Type::fromString($sub, $scope); + + $message = $expected + ? sprintf('Expected "%s" to be a subtype of "%s", but it is not', $sub, $super) + : sprintf('Expected "%s" not to be a subtype of "%s", but it is', $sub, $super); + self::assertSame($expected, Compatibility::check($superType, $subType), $message); + } + + /** + * @return iterable + */ + public function cases(): iterable + { + $compatibleTypes = self::compatibleTypes(); + foreach ($this->types() as $super) { + foreach ($this->types() as $sub) { + $expected = in_array([$super, $sub], $compatibleTypes, true); + $name = $expected + ? sprintf('%s is a subtype of %s', $sub, $super) + : sprintf('%s is not a subtype of %s', $sub, $super); + yield $name => [$super, $sub, $expected]; + } + } + } + + /** + * @return iterable + */ + private function types(): iterable + { + foreach ($this->typeFiles() as $file) { + foreach (explode("\n", file_get_contents($file)) as $line) { + if ($line === '') { + continue; + } + yield $line; + } + } + } + + /** + * @return iterable + */ + private function typeFiles(): iterable + { + return self::filesInDirectory(__DIR__ . '/types/'); + } +} diff --git a/tests/functional/compatible-types.md b/tests/functional/compatible-types.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/functional/compatible-types/bool.md b/tests/functional/compatible-types/bool.md new file mode 100644 index 0000000..c2b5fcb --- /dev/null +++ b/tests/functional/compatible-types/bool.md @@ -0,0 +1,10 @@ +- `bool` is a subtype of `bool` +- `bool` is a subtype of `string | int | bool` + +- `false` is a subtype of `bool` +- `false` is a subtype of `false` +- `false` is a subtype of `string | int | bool` + +- `true` is a subtype of `bool` +- `true` is a subtype of `true` +- `true` is a subtype of `string | int | bool` diff --git a/tests/functional/compatible-types/int-literal.md b/tests/functional/compatible-types/int-literal.md new file mode 100644 index 0000000..b069157 --- /dev/null +++ b/tests/functional/compatible-types/int-literal.md @@ -0,0 +1,55 @@ +- `-42` is a subtype of `-42` +- `-42` is a subtype of `float` +- `-42` is a subtype of `int` +- `-42` is a subtype of `int | string` +- `-42` is a subtype of `int<-123, 321>` +- `-42` is a subtype of `int` +- `-42` is a subtype of `int` +- `-42` is a subtype of `negative-int` +- `-42` is a subtype of `string | int` +- `-42` is a subtype of `string | int | bool` + +- `-1` is a subtype of `-1` +- `-1` is a subtype of `float` +- `-1` is a subtype of `int` +- `-1` is a subtype of `int | string` +- `-1` is a subtype of `int<-123, 321>` +- `-1` is a subtype of `int` +- `-1` is a subtype of `int` +- `-1` is a subtype of `negative-int` +- `-1` is a subtype of `string | int` +- `-1` is a subtype of `string | int | bool` + +- `0` is a subtype of `0` +- `0` is a subtype of `float` +- `0` is a subtype of `int` +- `0` is a subtype of `int | string` +- `0` is a subtype of `int<-123, 321>` +- `0` is a subtype of `int` +- `0` is a subtype of `string | int` +- `0` is a subtype of `string | int | bool` + +- `1` is a subtype of `1` +- `1` is a subtype of `float` +- `1` is a subtype of `int` +- `1` is a subtype of `int | string` +- `1` is a subtype of `int<-123, 321>` +- `1` is a subtype of `int<1, max>` +- `1` is a subtype of `int` +- `1` is a subtype of `positive-int` +- `1` is a subtype of `string | int` +- `1` is a subtype of `string | int | bool` + +- `23` is a subtype of `23` +- `23` is a subtype of `float` +- `23` is a subtype of `int` +- `23` is a subtype of `int | string` +- `23` is a subtype of `int<-123, 321>` +- `23` is a subtype of `int<1, max>` +- `23` is a subtype of `int<23, 23>` +- `23` is a subtype of `int<23, max>` +- `23` is a subtype of `int<23, 42>` +- `23` is a subtype of `int` +- `23` is a subtype of `positive-int` +- `23` is a subtype of `string | int` +- `23` is a subtype of `string | int | bool` diff --git a/tests/functional/compatible-types/int.md b/tests/functional/compatible-types/int.md new file mode 100644 index 0000000..542fcc7 --- /dev/null +++ b/tests/functional/compatible-types/int.md @@ -0,0 +1,88 @@ +- `int` is a subtype of `float` +- `int` is a subtype of `int` +- `int` is a subtype of `int | string` +- `int` is a subtype of `string | int` +- `int` is a subtype of `string | int | bool` + +- `int<-123, 321>` is a subtype of `float` +- `int<-123, 321>` is a subtype of `int` +- `int<-123, 321>` is a subtype of `int | string` +- `int<-123, 321>` is a subtype of `int<-123, 321>` +- `int<-123, 321>` is a subtype of `string | int` +- `int<-123, 321>` is a subtype of `string | int | bool` + +- `int<1, max>` is a subtype of `float` +- `int<1, max>` is a subtype of `int` +- `int<1, max>` is a subtype of `int | string` +- `int<1, max>` is a subtype of `int<1, max>` +- `int<1, max>` is a subtype of `positive-int` +- `int<1, max>` is a subtype of `string | int` +- `int<1, max>` is a subtype of `string | int | bool` + +- `int<23, 23>` is a subtype of `23` +- `int<23, 23>` is a subtype of `float` +- `int<23, 23>` is a subtype of `int` +- `int<23, 23>` is a subtype of `int | string` +- `int<23, 23>` is a subtype of `int<-123, 321>` +- `int<23, 23>` is a subtype of `int<1, max>` +- `int<23, 23>` is a subtype of `int<23, 23>` +- `int<23, 23>` is a subtype of `int<23, 42>` +- `int<23, 23>` is a subtype of `int<23, max>` +- `int<23, 23>` is a subtype of `int` +- `int<23, 23>` is a subtype of `positive-int` +- `int<23, 23>` is a subtype of `string | int` +- `int<23, 23>` is a subtype of `string | int | bool` + +- `int<23, 42>` is a subtype of `float` +- `int<23, 42>` is a subtype of `int` +- `int<23, 42>` is a subtype of `int | string` +- `int<23, 42>` is a subtype of `int<-123, 321>` +- `int<23, 42>` is a subtype of `int<1, max>` +- `int<23, 42>` is a subtype of `int<23, 42>` +- `int<23, 42>` is a subtype of `int<23, max>` +- `int<23, 42>` is a subtype of `int` +- `int<23, 42>` is a subtype of `positive-int` +- `int<23, 42>` is a subtype of `string | int` +- `int<23, 42>` is a subtype of `string | int | bool` + +- `int<23, max>` is a subtype of `float` +- `int<23, max>` is a subtype of `int` +- `int<23, max>` is a subtype of `int | string` +- `int<23, max>` is a subtype of `int<1, max>` +- `int<23, max>` is a subtype of `int<23, max>` +- `int<23, max>` is a subtype of `positive-int` +- `int<23, max>` is a subtype of `string | int` +- `int<23, max>` is a subtype of `string | int | bool` + +- `int` is a subtype of `float` +- `int` is a subtype of `int` +- `int` is a subtype of `int | string` +- `int` is a subtype of `int` +- `int` is a subtype of `int` +- `int` is a subtype of `negative-int` +- `int` is a subtype of `string | int` +- `int` is a subtype of `string | int | bool` + +- `int` is a subtype of `float` +- `int` is a subtype of `int` +- `int` is a subtype of `int | string` +- `int` is a subtype of `int` +- `int` is a subtype of `string | int` +- `int` is a subtype of `string | int | bool` + +- `negative-int` is a subtype of `float` +- `negative-int` is a subtype of `int` +- `negative-int` is a subtype of `int | string` +- `negative-int` is a subtype of `int` +- `negative-int` is a subtype of `int` +- `negative-int` is a subtype of `negative-int` +- `negative-int` is a subtype of `string | int` +- `negative-int` is a subtype of `string | int | bool` + +- `positive-int` is a subtype of `float` +- `positive-int` is a subtype of `int` +- `positive-int` is a subtype of `int | string` +- `positive-int` is a subtype of `int<1, max>` +- `positive-int` is a subtype of `positive-int` +- `positive-int` is a subtype of `string | int` +- `positive-int` is a subtype of `string | int | bool` diff --git a/tests/functional/compatible-types/misc.md b/tests/functional/compatible-types/misc.md new file mode 100644 index 0000000..761079a --- /dev/null +++ b/tests/functional/compatible-types/misc.md @@ -0,0 +1,2 @@ +- `float` is a subtype of `float` +- `mixed` is a subtype of `mixed` diff --git a/tests/functional/compatible-types/string.md b/tests/functional/compatible-types/string.md new file mode 100644 index 0000000..8fa3df6 --- /dev/null +++ b/tests/functional/compatible-types/string.md @@ -0,0 +1,44 @@ +- `class-string` is a subtype of `class-string` +- `class-string` is a subtype of `int | string` +- `class-string` is a subtype of `mixed` +- `class-string` is a subtype of `non-empty-string` +- `class-string` is a subtype of `string` +- `class-string` is a subtype of `string | int` +- `class-string` is a subtype of `string | int | bool` + +- `class-string` is a subtype of `class-string` +- `class-string` is a subtype of `class-string` +- `class-string` is a subtype of `class-string` +- `class-string` is a subtype of `int | string` +- `class-string` is a subtype of `mixed` +- `class-string` is a subtype of `non-empty-string` +- `class-string` is a subtype of `string` +- `class-string` is a subtype of `string | int` +- `class-string` is a subtype of `string | int | bool` + +- `class-string` is a subtype of `class-string` +- `class-string` is a subtype of `class-string` +- `class-string` is a subtype of `int | string` +- `class-string` is a subtype of `mixed` +- `class-string` is a subtype of `non-empty-string` +- `class-string` is a subtype of `string` +- `class-string` is a subtype of `string | int` +- `class-string` is a subtype of `string | int | bool` + +- `non-empty-string` is a subtype of `int | string` +- `non-empty-string` is a subtype of `non-empty-string` +- `non-empty-string` is a subtype of `string` +- `non-empty-string` is a subtype of `string | int` +- `non-empty-string` is a subtype of `string | int | bool` + +- `numeric-string` is a subtype of `int | string` +- `numeric-string` is a subtype of `non-empty-string` +- `numeric-string` is a subtype of `numeric-string` +- `numeric-string` is a subtype of `string` +- `numeric-string` is a subtype of `string | int` +- `numeric-string` is a subtype of `string | int | bool` + +- `string` is a subtype of `int | string` +- `string` is a subtype of `string` +- `string` is a subtype of `string | int` +- `string` is a subtype of `string | int | bool` diff --git a/tests/functional/compatible-types/union.md b/tests/functional/compatible-types/union.md new file mode 100644 index 0000000..b94e617 --- /dev/null +++ b/tests/functional/compatible-types/union.md @@ -0,0 +1,8 @@ +- `int | string` is a subtype of `int | string` +- `int | string` is a subtype of `string | int` +- `int | string` is a subtype of `string | int | bool` +- `string | int` is a subtype of `int | string` +- `string | int` is a subtype of `string | int` +- `string | int` is a subtype of `string | int | bool` +- `string | int | bool` is a subtype of `int | string | bool` +- `string | int | bool` is a subtype of `string | int | bool` diff --git a/tests/functional/types/bool.txt b/tests/functional/types/bool.txt new file mode 100644 index 0000000..f5399db --- /dev/null +++ b/tests/functional/types/bool.txt @@ -0,0 +1,3 @@ +bool +false +true diff --git a/tests/functional/types/int-literal.txt b/tests/functional/types/int-literal.txt new file mode 100644 index 0000000..4e45a34 --- /dev/null +++ b/tests/functional/types/int-literal.txt @@ -0,0 +1,5 @@ +-42 +-1 +0 +1 +23 diff --git a/tests/functional/types/int.txt b/tests/functional/types/int.txt new file mode 100644 index 0000000..a08a415 --- /dev/null +++ b/tests/functional/types/int.txt @@ -0,0 +1,10 @@ +int +int<-123, 321> +int<1, max> +int<23, 23> +int<23, 42> +int<23, max> +int +int +negative-int +positive-int diff --git a/tests/functional/types/misc.txt b/tests/functional/types/misc.txt new file mode 100644 index 0000000..14cadcb --- /dev/null +++ b/tests/functional/types/misc.txt @@ -0,0 +1 @@ +float diff --git a/tests/functional/types/string.txt b/tests/functional/types/string.txt new file mode 100644 index 0000000..0a896e8 --- /dev/null +++ b/tests/functional/types/string.txt @@ -0,0 +1,6 @@ +class-string +class-string +class-string +non-empty-string +numeric-string +string diff --git a/tests/functional/types/union.txt b/tests/functional/types/union.txt new file mode 100644 index 0000000..9990662 --- /dev/null +++ b/tests/functional/types/union.txt @@ -0,0 +1,3 @@ +int | string +string | int +string | int | bool From 3cbafd320af4b85ef4dac0b0c4900d6084c76f35 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Sun, 14 Aug 2022 01:15:12 +0200 Subject: [PATCH 02/53] Implement compatibility checks for scalars --- src/Compatibility.php | 13 +++++++++++++ tests/functional/compatible-types/bool.md | 3 +++ tests/functional/compatible-types/int-literal.md | 5 +++++ tests/functional/compatible-types/int.md | 10 ++++++++++ tests/functional/compatible-types/misc.md | 4 ++++ tests/functional/compatible-types/string.md | 6 ++++++ tests/functional/compatible-types/union.md | 5 +++++ tests/functional/types/misc.txt | 1 + 8 files changed, 47 insertions(+) diff --git a/src/Compatibility.php b/src/Compatibility.php index e555105..db3ffbd 100644 --- a/src/Compatibility.php +++ b/src/Compatibility.php @@ -29,6 +29,7 @@ public static function check(AbstractType $super, AbstractType $sub): bool IntType::class => self::checkInt($super, $sub), MixedType::class => true, NeverType::class => false, + ScalarType::class => self::checkScalar($sub), StringType::class => self::checkString($super, $sub), UnionType::class => self::checkUnion($super, $sub), default => throw new LogicException(sprintf('Unsupported type "%s"', $superClass)), @@ -159,4 +160,16 @@ private static function isSubtypeOfAny(AbstractType $type, array $haystack): boo } return false; } + + private static function checkScalar(AbstractType $sub): bool + { + return $sub instanceof ScalarType + || $sub instanceof IntLiteralType + || $sub instanceof IntType + || $sub instanceof FloatType + || $sub instanceof BoolType + || $sub instanceof StringType + || $sub instanceof ClassStringType + || ($sub instanceof UnionType && self::checkScalar($sub->left) && self::checkScalar($sub->right)); + } } diff --git a/tests/functional/compatible-types/bool.md b/tests/functional/compatible-types/bool.md index c2b5fcb..02ca3c6 100644 --- a/tests/functional/compatible-types/bool.md +++ b/tests/functional/compatible-types/bool.md @@ -1,10 +1,13 @@ - `bool` is a subtype of `bool` +- `bool` is a subtype of `scalar` - `bool` is a subtype of `string | int | bool` - `false` is a subtype of `bool` - `false` is a subtype of `false` +- `false` is a subtype of `scalar` - `false` is a subtype of `string | int | bool` - `true` is a subtype of `bool` - `true` is a subtype of `true` +- `true` is a subtype of `scalar` - `true` is a subtype of `string | int | bool` diff --git a/tests/functional/compatible-types/int-literal.md b/tests/functional/compatible-types/int-literal.md index b069157..ae6ed4f 100644 --- a/tests/functional/compatible-types/int-literal.md +++ b/tests/functional/compatible-types/int-literal.md @@ -6,6 +6,7 @@ - `-42` is a subtype of `int` - `-42` is a subtype of `int` - `-42` is a subtype of `negative-int` +- `-42` is a subtype of `scalar` - `-42` is a subtype of `string | int` - `-42` is a subtype of `string | int | bool` @@ -17,6 +18,7 @@ - `-1` is a subtype of `int` - `-1` is a subtype of `int` - `-1` is a subtype of `negative-int` +- `-1` is a subtype of `scalar` - `-1` is a subtype of `string | int` - `-1` is a subtype of `string | int | bool` @@ -26,6 +28,7 @@ - `0` is a subtype of `int | string` - `0` is a subtype of `int<-123, 321>` - `0` is a subtype of `int` +- `0` is a subtype of `scalar` - `0` is a subtype of `string | int` - `0` is a subtype of `string | int | bool` @@ -37,6 +40,7 @@ - `1` is a subtype of `int<1, max>` - `1` is a subtype of `int` - `1` is a subtype of `positive-int` +- `1` is a subtype of `scalar` - `1` is a subtype of `string | int` - `1` is a subtype of `string | int | bool` @@ -51,5 +55,6 @@ - `23` is a subtype of `int<23, 42>` - `23` is a subtype of `int` - `23` is a subtype of `positive-int` +- `23` is a subtype of `scalar` - `23` is a subtype of `string | int` - `23` is a subtype of `string | int | bool` diff --git a/tests/functional/compatible-types/int.md b/tests/functional/compatible-types/int.md index 542fcc7..1e0613a 100644 --- a/tests/functional/compatible-types/int.md +++ b/tests/functional/compatible-types/int.md @@ -1,6 +1,7 @@ - `int` is a subtype of `float` - `int` is a subtype of `int` - `int` is a subtype of `int | string` +- `int` is a subtype of `scalar` - `int` is a subtype of `string | int` - `int` is a subtype of `string | int | bool` @@ -8,6 +9,7 @@ - `int<-123, 321>` is a subtype of `int` - `int<-123, 321>` is a subtype of `int | string` - `int<-123, 321>` is a subtype of `int<-123, 321>` +- `int<-123, 321>` is a subtype of `scalar` - `int<-123, 321>` is a subtype of `string | int` - `int<-123, 321>` is a subtype of `string | int | bool` @@ -16,6 +18,7 @@ - `int<1, max>` is a subtype of `int | string` - `int<1, max>` is a subtype of `int<1, max>` - `int<1, max>` is a subtype of `positive-int` +- `int<1, max>` is a subtype of `scalar` - `int<1, max>` is a subtype of `string | int` - `int<1, max>` is a subtype of `string | int | bool` @@ -30,6 +33,7 @@ - `int<23, 23>` is a subtype of `int<23, max>` - `int<23, 23>` is a subtype of `int` - `int<23, 23>` is a subtype of `positive-int` +- `int<23, 23>` is a subtype of `scalar` - `int<23, 23>` is a subtype of `string | int` - `int<23, 23>` is a subtype of `string | int | bool` @@ -42,6 +46,7 @@ - `int<23, 42>` is a subtype of `int<23, max>` - `int<23, 42>` is a subtype of `int` - `int<23, 42>` is a subtype of `positive-int` +- `int<23, 42>` is a subtype of `scalar` - `int<23, 42>` is a subtype of `string | int` - `int<23, 42>` is a subtype of `string | int | bool` @@ -51,6 +56,7 @@ - `int<23, max>` is a subtype of `int<1, max>` - `int<23, max>` is a subtype of `int<23, max>` - `int<23, max>` is a subtype of `positive-int` +- `int<23, max>` is a subtype of `scalar` - `int<23, max>` is a subtype of `string | int` - `int<23, max>` is a subtype of `string | int | bool` @@ -60,6 +66,7 @@ - `int` is a subtype of `int` - `int` is a subtype of `int` - `int` is a subtype of `negative-int` +- `int` is a subtype of `scalar` - `int` is a subtype of `string | int` - `int` is a subtype of `string | int | bool` @@ -67,6 +74,7 @@ - `int` is a subtype of `int` - `int` is a subtype of `int | string` - `int` is a subtype of `int` +- `int` is a subtype of `scalar` - `int` is a subtype of `string | int` - `int` is a subtype of `string | int | bool` @@ -76,6 +84,7 @@ - `negative-int` is a subtype of `int` - `negative-int` is a subtype of `int` - `negative-int` is a subtype of `negative-int` +- `negative-int` is a subtype of `scalar` - `negative-int` is a subtype of `string | int` - `negative-int` is a subtype of `string | int | bool` @@ -84,5 +93,6 @@ - `positive-int` is a subtype of `int | string` - `positive-int` is a subtype of `int<1, max>` - `positive-int` is a subtype of `positive-int` +- `positive-int` is a subtype of `scalar` - `positive-int` is a subtype of `string | int` - `positive-int` is a subtype of `string | int | bool` diff --git a/tests/functional/compatible-types/misc.md b/tests/functional/compatible-types/misc.md index 761079a..a7329b4 100644 --- a/tests/functional/compatible-types/misc.md +++ b/tests/functional/compatible-types/misc.md @@ -1,2 +1,6 @@ - `float` is a subtype of `float` +- `float` is a subtype of `scalar` + - `mixed` is a subtype of `mixed` + +- `scalar` is a subtype of `scalar` diff --git a/tests/functional/compatible-types/string.md b/tests/functional/compatible-types/string.md index 8fa3df6..7c8c64a 100644 --- a/tests/functional/compatible-types/string.md +++ b/tests/functional/compatible-types/string.md @@ -2,6 +2,7 @@ - `class-string` is a subtype of `int | string` - `class-string` is a subtype of `mixed` - `class-string` is a subtype of `non-empty-string` +- `class-string` is a subtype of `scalar` - `class-string` is a subtype of `string` - `class-string` is a subtype of `string | int` - `class-string` is a subtype of `string | int | bool` @@ -12,6 +13,7 @@ - `class-string` is a subtype of `int | string` - `class-string` is a subtype of `mixed` - `class-string` is a subtype of `non-empty-string` +- `class-string` is a subtype of `scalar` - `class-string` is a subtype of `string` - `class-string` is a subtype of `string | int` - `class-string` is a subtype of `string | int | bool` @@ -21,12 +23,14 @@ - `class-string` is a subtype of `int | string` - `class-string` is a subtype of `mixed` - `class-string` is a subtype of `non-empty-string` +- `class-string` is a subtype of `scalar` - `class-string` is a subtype of `string` - `class-string` is a subtype of `string | int` - `class-string` is a subtype of `string | int | bool` - `non-empty-string` is a subtype of `int | string` - `non-empty-string` is a subtype of `non-empty-string` +- `non-empty-string` is a subtype of `scalar` - `non-empty-string` is a subtype of `string` - `non-empty-string` is a subtype of `string | int` - `non-empty-string` is a subtype of `string | int | bool` @@ -34,11 +38,13 @@ - `numeric-string` is a subtype of `int | string` - `numeric-string` is a subtype of `non-empty-string` - `numeric-string` is a subtype of `numeric-string` +- `numeric-string` is a subtype of `scalar` - `numeric-string` is a subtype of `string` - `numeric-string` is a subtype of `string | int` - `numeric-string` is a subtype of `string | int | bool` - `string` is a subtype of `int | string` +- `string` is a subtype of `scalar` - `string` is a subtype of `string` - `string` is a subtype of `string | int` - `string` is a subtype of `string | int | bool` diff --git a/tests/functional/compatible-types/union.md b/tests/functional/compatible-types/union.md index b94e617..abafc08 100644 --- a/tests/functional/compatible-types/union.md +++ b/tests/functional/compatible-types/union.md @@ -1,8 +1,13 @@ - `int | string` is a subtype of `int | string` +- `int | string` is a subtype of `scalar` - `int | string` is a subtype of `string | int` - `int | string` is a subtype of `string | int | bool` + - `string | int` is a subtype of `int | string` +- `string | int` is a subtype of `scalar` - `string | int` is a subtype of `string | int` - `string | int` is a subtype of `string | int | bool` + - `string | int | bool` is a subtype of `int | string | bool` +- `string | int | bool` is a subtype of `scalar` - `string | int | bool` is a subtype of `string | int | bool` diff --git a/tests/functional/types/misc.txt b/tests/functional/types/misc.txt index 14cadcb..9509acf 100644 --- a/tests/functional/types/misc.txt +++ b/tests/functional/types/misc.txt @@ -1 +1,2 @@ float +scalar From 1f2b2d7ff2dde18ecafa5faaa4b34e82ba8f0c41 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Sun, 14 Aug 2022 01:17:18 +0200 Subject: [PATCH 03/53] Implement compatibility checks for null --- src/Compatibility.php | 9 +++++---- tests/functional/compatible-types/misc.md | 2 ++ tests/functional/types/misc.txt | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Compatibility.php b/src/Compatibility.php index db3ffbd..c43c48e 100644 --- a/src/Compatibility.php +++ b/src/Compatibility.php @@ -29,6 +29,7 @@ public static function check(AbstractType $super, AbstractType $sub): bool IntType::class => self::checkInt($super, $sub), MixedType::class => true, NeverType::class => false, + NullType::class => $sub instanceof NullType, ScalarType::class => self::checkScalar($sub), StringType::class => self::checkString($super, $sub), UnionType::class => self::checkUnion($super, $sub), @@ -106,10 +107,10 @@ private static function checkInt(IntType $super, AbstractType $sub): bool return false; } [$superMin, $superMax, $subMin, $subMax] = [ - $super->min ?? PHP_INT_MIN, - $super->max ?? PHP_INT_MAX, - $sub->min ?? PHP_INT_MIN, - $sub->max ?? PHP_INT_MAX, + $super->min ?? PHP_INT_MIN, + $super->max ?? PHP_INT_MAX, + $sub->min ?? PHP_INT_MIN, + $sub->max ?? PHP_INT_MAX, ]; return $superMin <= $subMin && $superMax >= $subMax; } diff --git a/tests/functional/compatible-types/misc.md b/tests/functional/compatible-types/misc.md index a7329b4..3f7ba9b 100644 --- a/tests/functional/compatible-types/misc.md +++ b/tests/functional/compatible-types/misc.md @@ -3,4 +3,6 @@ - `mixed` is a subtype of `mixed` +- `null` is a subtype of `null` + - `scalar` is a subtype of `scalar` diff --git a/tests/functional/types/misc.txt b/tests/functional/types/misc.txt index 9509acf..a840023 100644 --- a/tests/functional/types/misc.txt +++ b/tests/functional/types/misc.txt @@ -1,2 +1,3 @@ float +null scalar From 8559e120a4e7fbbe37e1912ea5508f483767855e Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Sun, 14 Aug 2022 13:39:58 +0200 Subject: [PATCH 04/53] Test if all types are compatible with mixed --- tests/functional/CompatibilityTest.php | 72 ++++++++++++++++++-------- 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/tests/functional/CompatibilityTest.php b/tests/functional/CompatibilityTest.php index 3fc8c96..0337ea7 100644 --- a/tests/functional/CompatibilityTest.php +++ b/tests/functional/CompatibilityTest.php @@ -7,6 +7,7 @@ use DirectoryIterator; use PhpTypes\Types\ClassLikeType; use PhpTypes\Types\Compatibility; +use PhpTypes\Types\MixedType; use PhpTypes\Types\Scope; use PhpTypes\Types\Type; use PHPUnit\Framework\TestCase; @@ -18,6 +19,8 @@ final class CompatibilityTest extends TestCase { + private Scope $scope; + /** * @return list */ @@ -50,17 +53,36 @@ private static function filesInDirectory(string $directory): iterable } } + /** + * @return iterable + */ + private static function types(): iterable + { + foreach (self::typeFiles() as $file) { + foreach (explode("\n", file_get_contents($file)) as $line) { + if ($line === '') { + continue; + } + yield $line; + } + } + } + + /** + * @return iterable + */ + private static function typeFiles(): iterable + { + return self::filesInDirectory(__DIR__ . '/types/'); + } + /** * @dataProvider cases */ public function testCompatibility(string $super, string $sub, bool $expected): void { - $scope = Scope::global(); - $fooInterface = new ClassLikeType('FooInterface'); - $scope->register('FooInterface', $fooInterface); - $scope->register('Foo', new ClassLikeType('Foo', parents: [$fooInterface])); - $superType = Type::fromString($super, $scope); - $subType = Type::fromString($sub, $scope); + $superType = Type::fromString($super, $this->scope); + $subType = Type::fromString($sub, $this->scope); $message = $expected ? sprintf('Expected "%s" to be a subtype of "%s", but it is not', $sub, $super) @@ -74,8 +96,8 @@ public function testCompatibility(string $super, string $sub, bool $expected): v public function cases(): iterable { $compatibleTypes = self::compatibleTypes(); - foreach ($this->types() as $super) { - foreach ($this->types() as $sub) { + foreach (self::types() as $super) { + foreach (self::types() as $sub) { $expected = in_array([$super, $sub], $compatibleTypes, true); $name = $expected ? sprintf('%s is a subtype of %s', $sub, $super) @@ -86,25 +108,33 @@ public function cases(): iterable } /** - * @return iterable + * @dataProvider allTypes */ - private function types(): iterable + public function testEveryTypeIsCompatibleWithMixed(string $type): void { - foreach ($this->typeFiles() as $file) { - foreach (explode("\n", file_get_contents($file)) as $line) { - if ($line === '') { - continue; - } - yield $line; - } - } + self::assertTrue( + Compatibility::check(new MixedType(), Type::fromString($type, $this->scope)), + sprintf('Expected "%s" to be a subtype of "mixed", but it is not', $type), + ); } /** - * @return iterable + * @return iterable */ - private function typeFiles(): iterable + public function allTypes(): iterable { - return self::filesInDirectory(__DIR__ . '/types/'); + foreach (self::types() as $type) { + yield $type => [$type]; + } + } + + protected function setUp(): void + { + parent::setUp(); + + $this->scope = Scope::global(); + $fooInterface = new ClassLikeType('FooInterface'); + $this->scope->register('FooInterface', $fooInterface); + $this->scope->register('Foo', new ClassLikeType('Foo', parents: [$fooInterface])); } } From c1109f260d0b86d06297652cc46cd93c4b77eac1 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Sun, 14 Aug 2022 13:48:11 +0200 Subject: [PATCH 05/53] Test compatibility of lists --- src/Compatibility.php | 20 ++++++++++++++++---- tests/functional/compatible-types/list.md | 9 +++++++++ tests/functional/types/list.txt | 4 ++++ 3 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 tests/functional/compatible-types/list.md create mode 100644 tests/functional/types/list.txt diff --git a/src/Compatibility.php b/src/Compatibility.php index c43c48e..c364f2e 100644 --- a/src/Compatibility.php +++ b/src/Compatibility.php @@ -27,6 +27,7 @@ public static function check(AbstractType $super, AbstractType $sub): bool FloatType::class => self::checkFloat($sub), IntLiteralType::class => self::checkIntLiteral($super, $sub), IntType::class => self::checkInt($super, $sub), + ListType::class => self::checkList($super, $sub), MixedType::class => true, NeverType::class => false, NullType::class => $sub instanceof NullType, @@ -107,10 +108,10 @@ private static function checkInt(IntType $super, AbstractType $sub): bool return false; } [$superMin, $superMax, $subMin, $subMax] = [ - $super->min ?? PHP_INT_MIN, - $super->max ?? PHP_INT_MAX, - $sub->min ?? PHP_INT_MIN, - $sub->max ?? PHP_INT_MAX, + $super->min ?? PHP_INT_MIN, + $super->max ?? PHP_INT_MAX, + $sub->min ?? PHP_INT_MIN, + $sub->max ?? PHP_INT_MAX, ]; return $superMin <= $subMin && $superMax >= $subMax; } @@ -173,4 +174,15 @@ private static function checkScalar(AbstractType $sub): bool || $sub instanceof ClassStringType || ($sub instanceof UnionType && self::checkScalar($sub->left) && self::checkScalar($sub->right)); } + + private static function checkList(ListType $super, AbstractType $sub): bool + { + if (!$sub instanceof ListType) { + return false; + } + if ($super->nonEmpty && !$sub->nonEmpty) { + return false; + } + return self::check($super->type, $sub->type); + } } diff --git a/tests/functional/compatible-types/list.md b/tests/functional/compatible-types/list.md new file mode 100644 index 0000000..e4e0664 --- /dev/null +++ b/tests/functional/compatible-types/list.md @@ -0,0 +1,9 @@ +- `list` is a subtype of `list` +- `list` is a subtype of `list` + +- `list` is a subtype of `list` + +- `list` is a subtype of `list` + +- `non-empty-list` is a subtype of `list` +- `non-empty-list` is a subtype of `non-empty-list` diff --git a/tests/functional/types/list.txt b/tests/functional/types/list.txt new file mode 100644 index 0000000..22e97e5 --- /dev/null +++ b/tests/functional/types/list.txt @@ -0,0 +1,4 @@ +list +list +list +non-empty-list From 2628e870e14918f9b0d8a2b561ca3f677c365801 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Sun, 14 Aug 2022 14:26:00 +0200 Subject: [PATCH 06/53] Test compatibility of maps --- src/Compatibility.php | 15 ++++++++ src/ListType.php | 5 +++ tests/functional/CompatibilityTest.php | 41 +++++++++++++++------ tests/functional/compatible-types/list.md | 17 +++++++++ tests/functional/compatible-types/map.md | 39 ++++++++++++++++++++ tests/functional/compatible-types/misc.md | 2 - tests/functional/compatible-types/string.md | 3 -- tests/functional/compatible-types/union.md | 1 - tests/functional/types/map.txt | 8 ++++ 9 files changed, 114 insertions(+), 17 deletions(-) create mode 100644 tests/functional/compatible-types/map.md create mode 100644 tests/functional/types/map.txt diff --git a/src/Compatibility.php b/src/Compatibility.php index c364f2e..ff8e476 100644 --- a/src/Compatibility.php +++ b/src/Compatibility.php @@ -28,6 +28,7 @@ public static function check(AbstractType $super, AbstractType $sub): bool IntLiteralType::class => self::checkIntLiteral($super, $sub), IntType::class => self::checkInt($super, $sub), ListType::class => self::checkList($super, $sub), + MapType::class => self::checkMap($super, $sub), MixedType::class => true, NeverType::class => false, NullType::class => $sub instanceof NullType, @@ -185,4 +186,18 @@ private static function checkList(ListType $super, AbstractType $sub): bool } return self::check($super->type, $sub->type); } + + private static function checkMap(MapType $super, AbstractType $sub): bool + { + if ($sub instanceof ListType) { + $sub = $sub->toMap(); + } + if (!$sub instanceof MapType) { + return false; + } + if ($super->nonEmpty && !$sub->nonEmpty) { + return false; + } + return self::check($super->keyType, $sub->keyType) && self::check($super->valueType, $sub->valueType); + } } diff --git a/src/ListType.php b/src/ListType.php index e590048..af51463 100644 --- a/src/ListType.php +++ b/src/ListType.php @@ -22,4 +22,9 @@ public function toNode(): NodeInterface { return new IdentifierNode($this->nonEmpty ? 'non-empty-list' : 'list', [$this->type->toNode()]); } + + public function toMap(): MapType + { + return new MapType(new IntType(), $this->type, $this->nonEmpty); + } } diff --git a/tests/functional/CompatibilityTest.php b/tests/functional/CompatibilityTest.php index 0337ea7..3d4d8e9 100644 --- a/tests/functional/CompatibilityTest.php +++ b/tests/functional/CompatibilityTest.php @@ -5,6 +5,7 @@ namespace PhpTypes\Types\Tests\Functional; use DirectoryIterator; +use LogicException; use PhpTypes\Types\ClassLikeType; use PhpTypes\Types\Compatibility; use PhpTypes\Types\MixedType; @@ -12,14 +13,15 @@ use PhpTypes\Types\Type; use PHPUnit\Framework\TestCase; +use function array_map; +use function array_search; use function explode; use function file_get_contents; -use function in_array; use function sprintf; final class CompatibilityTest extends TestCase { - private Scope $scope; + private static Scope $scope; /** * @return list @@ -81,8 +83,8 @@ private static function typeFiles(): iterable */ public function testCompatibility(string $super, string $sub, bool $expected): void { - $superType = Type::fromString($super, $this->scope); - $subType = Type::fromString($sub, $this->scope); + $superType = Type::fromString($super, self::$scope); + $subType = Type::fromString($sub, self::$scope); $message = $expected ? sprintf('Expected "%s" to be a subtype of "%s", but it is not', $sub, $super) @@ -98,13 +100,30 @@ public function cases(): iterable $compatibleTypes = self::compatibleTypes(); foreach (self::types() as $super) { foreach (self::types() as $sub) { - $expected = in_array([$super, $sub], $compatibleTypes, true); + $compatibleTypesKey = array_search([$super, $sub], $compatibleTypes, true); + $expected = $compatibleTypesKey !== false; $name = $expected ? sprintf('%s is a subtype of %s', $sub, $super) : sprintf('%s is not a subtype of %s', $sub, $super); yield $name => [$super, $sub, $expected]; + unset($compatibleTypes[$compatibleTypesKey]); } } + if ($compatibleTypes === []) { + return; + } + throw new LogicException( + sprintf( + "There are %s unchecked compatibility declarations:\n%s", + count($compatibleTypes), + implode( + "\n", + array_map(static function (array $compatibleType): string { + return sprintf('- `%s` is a subtype of `%s`', $compatibleType[1], $compatibleType[0]); + }, $compatibleTypes), + ), + ) + ); } /** @@ -113,7 +132,7 @@ public function cases(): iterable public function testEveryTypeIsCompatibleWithMixed(string $type): void { self::assertTrue( - Compatibility::check(new MixedType(), Type::fromString($type, $this->scope)), + Compatibility::check(new MixedType(), Type::fromString($type, self::$scope)), sprintf('Expected "%s" to be a subtype of "mixed", but it is not', $type), ); } @@ -128,13 +147,13 @@ public function allTypes(): iterable } } - protected function setUp(): void + public static function setUpBeforeClass(): void { - parent::setUp(); + parent::setUpBeforeClass(); - $this->scope = Scope::global(); + self::$scope = Scope::global(); $fooInterface = new ClassLikeType('FooInterface'); - $this->scope->register('FooInterface', $fooInterface); - $this->scope->register('Foo', new ClassLikeType('Foo', parents: [$fooInterface])); + self::$scope->register('FooInterface', $fooInterface); + self::$scope->register('Foo', new ClassLikeType('Foo', parents: [$fooInterface])); } } diff --git a/tests/functional/compatible-types/list.md b/tests/functional/compatible-types/list.md index e4e0664..e2383b4 100644 --- a/tests/functional/compatible-types/list.md +++ b/tests/functional/compatible-types/list.md @@ -1,9 +1,26 @@ +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `array` - `list` is a subtype of `list` - `list` is a subtype of `list` +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `array` - `list` is a subtype of `list` +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `array` - `list` is a subtype of `list` +- `list` is a subtype of `array` +- `non-empty-list` is a subtype of `array` +- `non-empty-list` is a subtype of `array` +- `non-empty-list` is a subtype of `array` +- `non-empty-list` is a subtype of `array` - `non-empty-list` is a subtype of `list` +- `non-empty-list` is a subtype of `non-empty-array` - `non-empty-list` is a subtype of `non-empty-list` diff --git a/tests/functional/compatible-types/map.md b/tests/functional/compatible-types/map.md new file mode 100644 index 0000000..4997d4a --- /dev/null +++ b/tests/functional/compatible-types/map.md @@ -0,0 +1,39 @@ +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` + +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` + +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` + +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` + +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` + +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` + +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` + +- `non-empty-array` is a subtype of `array` +- `non-empty-array` is a subtype of `array` +- `non-empty-array` is a subtype of `array` +- `non-empty-array` is a subtype of `array` +- `non-empty-array` is a subtype of `non-empty-array` diff --git a/tests/functional/compatible-types/misc.md b/tests/functional/compatible-types/misc.md index 3f7ba9b..f8b4d91 100644 --- a/tests/functional/compatible-types/misc.md +++ b/tests/functional/compatible-types/misc.md @@ -1,8 +1,6 @@ - `float` is a subtype of `float` - `float` is a subtype of `scalar` -- `mixed` is a subtype of `mixed` - - `null` is a subtype of `null` - `scalar` is a subtype of `scalar` diff --git a/tests/functional/compatible-types/string.md b/tests/functional/compatible-types/string.md index 7c8c64a..e3510c5 100644 --- a/tests/functional/compatible-types/string.md +++ b/tests/functional/compatible-types/string.md @@ -1,6 +1,5 @@ - `class-string` is a subtype of `class-string` - `class-string` is a subtype of `int | string` -- `class-string` is a subtype of `mixed` - `class-string` is a subtype of `non-empty-string` - `class-string` is a subtype of `scalar` - `class-string` is a subtype of `string` @@ -11,7 +10,6 @@ - `class-string` is a subtype of `class-string` - `class-string` is a subtype of `class-string` - `class-string` is a subtype of `int | string` -- `class-string` is a subtype of `mixed` - `class-string` is a subtype of `non-empty-string` - `class-string` is a subtype of `scalar` - `class-string` is a subtype of `string` @@ -21,7 +19,6 @@ - `class-string` is a subtype of `class-string` - `class-string` is a subtype of `class-string` - `class-string` is a subtype of `int | string` -- `class-string` is a subtype of `mixed` - `class-string` is a subtype of `non-empty-string` - `class-string` is a subtype of `scalar` - `class-string` is a subtype of `string` diff --git a/tests/functional/compatible-types/union.md b/tests/functional/compatible-types/union.md index abafc08..11a790f 100644 --- a/tests/functional/compatible-types/union.md +++ b/tests/functional/compatible-types/union.md @@ -8,6 +8,5 @@ - `string | int` is a subtype of `string | int` - `string | int` is a subtype of `string | int | bool` -- `string | int | bool` is a subtype of `int | string | bool` - `string | int | bool` is a subtype of `scalar` - `string | int | bool` is a subtype of `string | int | bool` diff --git a/tests/functional/types/map.txt b/tests/functional/types/map.txt new file mode 100644 index 0000000..2a2987c --- /dev/null +++ b/tests/functional/types/map.txt @@ -0,0 +1,8 @@ +array +array +array +array +array +array +array +non-empty-array From 0de5d7359733bf3deea1ff88caed114ad562a618 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Sun, 14 Aug 2022 16:32:15 +0200 Subject: [PATCH 07/53] Test compatibility of tuples --- src/Compatibility.php | 30 +++++++++++++++++++++- src/TupleType.php | 16 ++++++++++++ tests/functional/compatible-types/list.md | 8 ++++++ tests/functional/compatible-types/tuple.md | 27 +++++++++++++++++++ tests/functional/types/list.txt | 1 + tests/functional/types/tuple.txt | 4 +++ 6 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 tests/functional/compatible-types/tuple.md create mode 100644 tests/functional/types/tuple.txt diff --git a/src/Compatibility.php b/src/Compatibility.php index ff8e476..834537e 100644 --- a/src/Compatibility.php +++ b/src/Compatibility.php @@ -6,6 +6,7 @@ use LogicException; +use function count; use function get_class; use const PHP_INT_MAX; @@ -34,6 +35,7 @@ public static function check(AbstractType $super, AbstractType $sub): bool NullType::class => $sub instanceof NullType, ScalarType::class => self::checkScalar($sub), StringType::class => self::checkString($super, $sub), + TupleType::class => self::checkTuple($super, $sub), UnionType::class => self::checkUnion($super, $sub), default => throw new LogicException(sprintf('Unsupported type "%s"', $superClass)), }; @@ -178,6 +180,15 @@ private static function checkScalar(AbstractType $sub): bool private static function checkList(ListType $super, AbstractType $sub): bool { + if ($sub instanceof TupleType) { + foreach ($sub->elements as $element) { + if (self::check($super->type, $element)) { + continue; + } + return false; + } + return true; + } if (!$sub instanceof ListType) { return false; } @@ -189,7 +200,7 @@ private static function checkList(ListType $super, AbstractType $sub): bool private static function checkMap(MapType $super, AbstractType $sub): bool { - if ($sub instanceof ListType) { + if ($sub instanceof ListType || $sub instanceof TupleType) { $sub = $sub->toMap(); } if (!$sub instanceof MapType) { @@ -200,4 +211,21 @@ private static function checkMap(MapType $super, AbstractType $sub): bool } return self::check($super->keyType, $sub->keyType) && self::check($super->valueType, $sub->valueType); } + + private static function checkTuple(TupleType $super, AbstractType $sub): bool + { + if (!$sub instanceof TupleType) { + return false; + } + if (count($super->elements) > count($sub->elements)) { + return false; + } + foreach ($super->elements as $i => $element) { + if (self::check($element, $sub->elements[$i])) { + continue; + } + return false; + } + return true; + } } diff --git a/src/TupleType.php b/src/TupleType.php index cecca99..3687473 100644 --- a/src/TupleType.php +++ b/src/TupleType.php @@ -24,4 +24,20 @@ public function toNode(): NodeInterface } return new TupleNode($elementNodes); } + + public function toMap(): MapType + { + $valueType = null; + foreach ($this->elements as $element) { + if ($valueType === null) { + $valueType = $element; + continue; + } + if (Compatibility::check($valueType, $element)) { + continue; + } + $valueType = new UnionType($valueType, $element); + } + return new MapType(new IntType(), $valueType); + } } diff --git a/tests/functional/compatible-types/list.md b/tests/functional/compatible-types/list.md index e2383b4..584df4d 100644 --- a/tests/functional/compatible-types/list.md +++ b/tests/functional/compatible-types/list.md @@ -5,15 +5,22 @@ - `list` is a subtype of `array` - `list` is a subtype of `list` - `list` is a subtype of `list` +- `list` is a subtype of `list` - `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `list` +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `array` +- `list` is a subtype of `list` + - `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` +- `list` is a subtype of `list` - `list` is a subtype of `list` - `list` is a subtype of `array` @@ -21,6 +28,7 @@ - `non-empty-list` is a subtype of `array` - `non-empty-list` is a subtype of `array` - `non-empty-list` is a subtype of `array` +- `non-empty-list` is a subtype of `list` - `non-empty-list` is a subtype of `list` - `non-empty-list` is a subtype of `non-empty-array` - `non-empty-list` is a subtype of `non-empty-list` diff --git a/tests/functional/compatible-types/tuple.md b/tests/functional/compatible-types/tuple.md new file mode 100644 index 0000000..8e22726 --- /dev/null +++ b/tests/functional/compatible-types/tuple.md @@ -0,0 +1,27 @@ +- `array{int, string}` is a subtype of `array` +- `array{int, string}` is a subtype of `array` +- `array{int, string}` is a subtype of `array` +- `array{int, string}` is a subtype of `array{int, string}` +- `array{int, string}` is a subtype of `list` + +- `array{string, int}` is a subtype of `array` +- `array{string, int}` is a subtype of `array` +- `array{string, int}` is a subtype of `array` +- `array{string, int}` is a subtype of `array{string, int}` +- `array{string, int}` is a subtype of `list` + +- `array{string, int, string}` is a subtype of `array` +- `array{string, int, string}` is a subtype of `array` +- `array{string, int, string}` is a subtype of `array` +- `array{string, int, string}` is a subtype of `array{string, int}` +- `array{string, int, string}` is a subtype of `array{string, int, string}` +- `array{string, int, string}` is a subtype of `list` + +- `array{string, string}` is a subtype of `array` +- `array{string, string}` is a subtype of `array` +- `array{string, string}` is a subtype of `array` +- `array{string, string}` is a subtype of `array` +- `array{string, string}` is a subtype of `array{string, string}` +- `array{string, string}` is a subtype of `list` +- `array{string, string}` is a subtype of `list` +- `array{string, string}` is a subtype of `non-empty-list` diff --git a/tests/functional/types/list.txt b/tests/functional/types/list.txt index 22e97e5..ec1cdd2 100644 --- a/tests/functional/types/list.txt +++ b/tests/functional/types/list.txt @@ -1,4 +1,5 @@ list list list +list non-empty-list diff --git a/tests/functional/types/tuple.txt b/tests/functional/types/tuple.txt new file mode 100644 index 0000000..5d6d4ee --- /dev/null +++ b/tests/functional/types/tuple.txt @@ -0,0 +1,4 @@ +array{int, string} +array{string, int} +array{string, int, string} +array{string, string} From 451cafad3279bdea86cd61bda83fd718cd1bbff5 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Mon, 15 Aug 2022 14:10:19 +0200 Subject: [PATCH 08/53] Test compatibility of structs and string literals --- src/Compatibility.php | 40 +++++++++++++++++-- src/StructType.php | 15 +++++++ .../compatible-types/string-literal.md | 27 +++++++++++++ tests/functional/compatible-types/struct.md | 17 ++++++++ tests/functional/types/string-literal.txt | 4 ++ tests/functional/types/struct.txt | 3 ++ 6 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 tests/functional/compatible-types/string-literal.md create mode 100644 tests/functional/compatible-types/struct.md create mode 100644 tests/functional/types/string-literal.txt create mode 100644 tests/functional/types/struct.txt diff --git a/src/Compatibility.php b/src/Compatibility.php index 834537e..1977504 100644 --- a/src/Compatibility.php +++ b/src/Compatibility.php @@ -8,6 +8,7 @@ use function count; use function get_class; +use function is_numeric; use const PHP_INT_MAX; use const PHP_INT_MIN; @@ -34,7 +35,9 @@ public static function check(AbstractType $super, AbstractType $sub): bool NeverType::class => false, NullType::class => $sub instanceof NullType, ScalarType::class => self::checkScalar($sub), + StringLiteralType::class => self::checkStringLiteral($super, $sub), StringType::class => self::checkString($super, $sub), + StructType::class => self::checkStruct($super, $sub), TupleType::class => self::checkTuple($super, $sub), UnionType::class => self::checkUnion($super, $sub), default => throw new LogicException(sprintf('Unsupported type "%s"', $superClass)), @@ -73,12 +76,18 @@ private static function checkClassLike(ClassLikeType $super, AbstractType $sub): private static function checkString(StringType $super, AbstractType $sub): bool { - if ($sub instanceof ClassStringType) { + if ($sub instanceof StringLiteralType) { if ($super->numeric) { - return false; + return is_numeric($sub->value); + } + if ($super->nonEmpty) { + return $sub->value !== ''; } return true; } + if ($sub instanceof ClassStringType) { + return !$super->numeric; + } if (!$sub instanceof StringType) { return false; } @@ -200,7 +209,7 @@ private static function checkList(ListType $super, AbstractType $sub): bool private static function checkMap(MapType $super, AbstractType $sub): bool { - if ($sub instanceof ListType || $sub instanceof TupleType) { + if ($sub instanceof ListType || $sub instanceof TupleType || $sub instanceof StructType) { $sub = $sub->toMap(); } if (!$sub instanceof MapType) { @@ -228,4 +237,29 @@ private static function checkTuple(TupleType $super, AbstractType $sub): bool } return true; } + + private static function checkStruct(StructType $super, AbstractType $sub): bool + { + if (!$sub instanceof StructType) { + return false; + } + foreach ($super->members as $name => $member) { + $subMember = $sub->members[$name] ?? null; + if ($subMember === null) { + return false; + } + if (!self::check($member->type, $subMember->type)) { + return false; + } + if ($subMember->optional && !$member->optional) { + return false; + } + } + return true; + } + + private static function checkStringLiteral(StringLiteralType $super, AbstractType $sub): bool + { + return $sub instanceof StringLiteralType && $super->value === $sub->value; + } } diff --git a/src/StructType.php b/src/StructType.php index e2f9471..e28a962 100644 --- a/src/StructType.php +++ b/src/StructType.php @@ -25,4 +25,19 @@ public function toNode(): NodeInterface } return new StructNode($members); } + + public function toMap(): MapType + { + /** @var array{AbstractType, AbstractType}|null $types */ + $types = null; + foreach ($this->members as $name => $member) { + if ($types === null) { + $types = [new StringLiteralType($name), $member->type]; + continue; + } + $types[0] = new UnionType($types[0], new StringLiteralType($name)); + $types[1] = new UnionType($types[1], $member->type); + } + return new MapType($types[0], $types[1], true); + } } diff --git a/tests/functional/compatible-types/string-literal.md b/tests/functional/compatible-types/string-literal.md new file mode 100644 index 0000000..d58ccfc --- /dev/null +++ b/tests/functional/compatible-types/string-literal.md @@ -0,0 +1,27 @@ +- `''` is a subtype of `''` +- `''` is a subtype of `int | string` +- `''` is a subtype of `string` +- `''` is a subtype of `string | int` +- `''` is a subtype of `string | int | bool` + +- `'42'` is a subtype of `'42'` +- `'42'` is a subtype of `int | string` +- `'42'` is a subtype of `non-empty-string` +- `'42'` is a subtype of `numeric-string` +- `'42'` is a subtype of `string` +- `'42'` is a subtype of `string | int` +- `'42'` is a subtype of `string | int | bool` + +- `'foo'` is a subtype of `'foo'` +- `'foo'` is a subtype of `int | string` +- `'foo'` is a subtype of `non-empty-string` +- `'foo'` is a subtype of `string` +- `'foo'` is a subtype of `string | int` +- `'foo'` is a subtype of `string | int | bool` + +- `'bar'` is a subtype of `'bar'` +- `'bar'` is a subtype of `int | string` +- `'bar'` is a subtype of `non-empty-string` +- `'bar'` is a subtype of `string` +- `'bar'` is a subtype of `string | int` +- `'bar'` is a subtype of `string | int | bool` diff --git a/tests/functional/compatible-types/struct.md b/tests/functional/compatible-types/struct.md new file mode 100644 index 0000000..a0c66a4 --- /dev/null +++ b/tests/functional/compatible-types/struct.md @@ -0,0 +1,17 @@ +- `array{name: string}` is a subtype of `array` +- `array{name: string}` is a subtype of `array` +- `array{name: string}` is a subtype of `array` +- `array{name: string}` is a subtype of `array{name: string}` + +- `array{name: string, age: int}` is a subtype of `array` +- `array{name: string, age: int}` is a subtype of `array` +- `array{name: string, age: int}` is a subtype of `array` +- `array{name: string, age: int}` is a subtype of `array{name: string}` +- `array{name: string, age: int}` is a subtype of `array{name: string, age: int}` +- `array{name: string, age: int}` is a subtype of `array{name: string, age?: int}` + +- `array{name: string, age?: int}` is a subtype of `array` +- `array{name: string, age?: int}` is a subtype of `array` +- `array{name: string, age?: int}` is a subtype of `array` +- `array{name: string, age?: int}` is a subtype of `array{name: string}` +- `array{name: string, age?: int}` is a subtype of `array{name: string, age?: int}` diff --git a/tests/functional/types/string-literal.txt b/tests/functional/types/string-literal.txt new file mode 100644 index 0000000..85c8ff7 --- /dev/null +++ b/tests/functional/types/string-literal.txt @@ -0,0 +1,4 @@ +'' +'42' +'foo' +'bar' diff --git a/tests/functional/types/struct.txt b/tests/functional/types/struct.txt new file mode 100644 index 0000000..71c29fb --- /dev/null +++ b/tests/functional/types/struct.txt @@ -0,0 +1,3 @@ +array{name: string} +array{name: string, age: int} +array{name: string, age?: int} From 3b5e24a26dff03cfc81611ac66971ad78a37603b Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Mon, 15 Aug 2022 14:16:03 +0200 Subject: [PATCH 09/53] Fix a bug where string literals weren't considered scalars --- src/Compatibility.php | 1 + tests/functional/compatible-types/string-literal.md | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/Compatibility.php b/src/Compatibility.php index 1977504..a1e168f 100644 --- a/src/Compatibility.php +++ b/src/Compatibility.php @@ -183,6 +183,7 @@ private static function checkScalar(AbstractType $sub): bool || $sub instanceof FloatType || $sub instanceof BoolType || $sub instanceof StringType + || $sub instanceof StringLiteralType || $sub instanceof ClassStringType || ($sub instanceof UnionType && self::checkScalar($sub->left) && self::checkScalar($sub->right)); } diff --git a/tests/functional/compatible-types/string-literal.md b/tests/functional/compatible-types/string-literal.md index d58ccfc..3e36107 100644 --- a/tests/functional/compatible-types/string-literal.md +++ b/tests/functional/compatible-types/string-literal.md @@ -1,5 +1,6 @@ - `''` is a subtype of `''` - `''` is a subtype of `int | string` +- `''` is a subtype of `scalar` - `''` is a subtype of `string` - `''` is a subtype of `string | int` - `''` is a subtype of `string | int | bool` @@ -8,6 +9,7 @@ - `'42'` is a subtype of `int | string` - `'42'` is a subtype of `non-empty-string` - `'42'` is a subtype of `numeric-string` +- `'42'` is a subtype of `scalar` - `'42'` is a subtype of `string` - `'42'` is a subtype of `string | int` - `'42'` is a subtype of `string | int | bool` @@ -15,6 +17,7 @@ - `'foo'` is a subtype of `'foo'` - `'foo'` is a subtype of `int | string` - `'foo'` is a subtype of `non-empty-string` +- `'foo'` is a subtype of `scalar` - `'foo'` is a subtype of `string` - `'foo'` is a subtype of `string | int` - `'foo'` is a subtype of `string | int | bool` @@ -22,6 +25,7 @@ - `'bar'` is a subtype of `'bar'` - `'bar'` is a subtype of `int | string` - `'bar'` is a subtype of `non-empty-string` +- `'bar'` is a subtype of `scalar` - `'bar'` is a subtype of `string` - `'bar'` is a subtype of `string | int` - `'bar'` is a subtype of `string | int | bool` From 636697f7aa83e22d5dc09647d14cd7ec937f4bf6 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Mon, 15 Aug 2022 14:45:55 +0200 Subject: [PATCH 10/53] Test compatibility of callables --- src/Compatibility.php | 25 +++++++++++++++++++ tests/functional/compatible-types/callable.md | 22 ++++++++++++++++ tests/functional/types/callable.txt | 7 ++++++ 3 files changed, 54 insertions(+) create mode 100644 tests/functional/compatible-types/callable.md create mode 100644 tests/functional/types/callable.txt diff --git a/src/Compatibility.php b/src/Compatibility.php index a1e168f..03c80bd 100644 --- a/src/Compatibility.php +++ b/src/Compatibility.php @@ -24,6 +24,7 @@ public static function check(AbstractType $super, AbstractType $sub): bool $superClass = get_class($super); return match ($superClass) { BoolType::class => self::checkBool($super, $sub), + CallableType::class => self::checkCallable($super, $sub), ClassLikeType::class => self::checkClassLike($super, $sub), ClassStringType::class => self::checkClassString($super, $sub), FloatType::class => self::checkFloat($sub), @@ -263,4 +264,28 @@ private static function checkStringLiteral(StringLiteralType $super, AbstractTyp { return $sub instanceof StringLiteralType && $super->value === $sub->value; } + + private static function checkCallable(CallableType $super, AbstractType $sub): bool + { + if (!$sub instanceof CallableType) { + return false; + } + if (!$super->returnType instanceof VoidType && !self::check($super->returnType, $sub->returnType)) { + return false; + } + if (count($sub->parameters) > count($super->parameters)) { + return false; + } + foreach ($sub->parameters as $i => $parameter) { + $superParameter = $super->parameters[$i]; + if (!$parameter->optional && $superParameter->optional) { + return false; + } + if (self::check($parameter->type, $superParameter->type)) { + continue; + } + return false; + } + return true; + } } diff --git a/tests/functional/compatible-types/callable.md b/tests/functional/compatible-types/callable.md new file mode 100644 index 0000000..6556d94 --- /dev/null +++ b/tests/functional/compatible-types/callable.md @@ -0,0 +1,22 @@ +- `callable(): string` is a subtype of `callable(): string` +- `callable(): string` is a subtype of `callable(): void` + +- `callable(): void` is a subtype of `callable(): void` + +- `callable(string): float` is a subtype of `callable(string): float` + +- `callable(string): int` is a subtype of `callable(string): float` +- `callable(string): int` is a subtype of `callable(string): int` +- `callable(string): int` is a subtype of `callable(string, int): int` + +- `callable(string=): int` is a subtype of `callable(string): float` +- `callable(string=): int` is a subtype of `callable(string): int` +- `callable(string=): int` is a subtype of `callable(string=): int` +- `callable(string=): int` is a subtype of `callable(string, int): int` + +- `callable(string, int): int` is a subtype of `callable(string, int): int` + +- `callable(string | int): int` is a subtype of `callable(string): float` +- `callable(string | int): int` is a subtype of `callable(string): int` +- `callable(string | int): int` is a subtype of `callable(string, int): int` +- `callable(string | int): int` is a subtype of `callable(string | int): int` diff --git a/tests/functional/types/callable.txt b/tests/functional/types/callable.txt new file mode 100644 index 0000000..6b5c983 --- /dev/null +++ b/tests/functional/types/callable.txt @@ -0,0 +1,7 @@ +callable(): void +callable(): string +callable(string): float +callable(string): int +callable(string=): int +callable(string, int): int +callable(string | int): int From 3562f1f416df034c446d07e6945b0b6b6772e500 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Mon, 15 Aug 2022 16:10:04 +0200 Subject: [PATCH 11/53] Test compatibility of iterables --- src/Compatibility.php | 20 ++++++- src/Conversion/ToIterableInterface.php | 13 +++++ src/Conversion/ToMapInterface.php | 13 +++++ src/ListType.php | 9 +++- src/MapType.php | 8 ++- src/StructType.php | 21 +++++++- src/TupleType.php | 16 +++++- src/UnionType.php | 54 ++++++++++++++++++- tests/functional/compatible-types/iterable.md | 47 ++++++++++++++++ tests/functional/compatible-types/list.md | 24 +++++++++ tests/functional/compatible-types/map.md | 47 ++++++++++++++++ tests/functional/compatible-types/struct.md | 16 ++++++ tests/functional/compatible-types/tuple.md | 18 +++++++ tests/functional/compatible-types/union.md | 10 ++++ tests/functional/types/iterable.txt | 9 ++++ tests/functional/types/map.txt | 2 + tests/functional/types/union.txt | 1 + 17 files changed, 319 insertions(+), 9 deletions(-) create mode 100644 src/Conversion/ToIterableInterface.php create mode 100644 src/Conversion/ToMapInterface.php create mode 100644 tests/functional/compatible-types/iterable.md create mode 100644 tests/functional/types/iterable.txt diff --git a/src/Compatibility.php b/src/Compatibility.php index 03c80bd..e7e7c45 100644 --- a/src/Compatibility.php +++ b/src/Compatibility.php @@ -5,6 +5,8 @@ namespace PhpTypes\Types; use LogicException; +use PhpTypes\Types\Conversion\ToIterableInterface; +use PhpTypes\Types\Conversion\ToMapInterface; use function count; use function get_class; @@ -30,6 +32,7 @@ public static function check(AbstractType $super, AbstractType $sub): bool FloatType::class => self::checkFloat($sub), IntLiteralType::class => self::checkIntLiteral($super, $sub), IntType::class => self::checkInt($super, $sub), + IterableType::class => self::checkIterable($super, $sub), ListType::class => self::checkList($super, $sub), MapType::class => self::checkMap($super, $sub), MixedType::class => true, @@ -191,6 +194,9 @@ private static function checkScalar(AbstractType $sub): bool private static function checkList(ListType $super, AbstractType $sub): bool { + if ($sub instanceof UnionType) { + $sub = $sub->toList(); + } if ($sub instanceof TupleType) { foreach ($sub->elements as $element) { if (self::check($super->type, $element)) { @@ -211,7 +217,7 @@ private static function checkList(ListType $super, AbstractType $sub): bool private static function checkMap(MapType $super, AbstractType $sub): bool { - if ($sub instanceof ListType || $sub instanceof TupleType || $sub instanceof StructType) { + if ($sub instanceof ToMapInterface) { $sub = $sub->toMap(); } if (!$sub instanceof MapType) { @@ -288,4 +294,16 @@ private static function checkCallable(CallableType $super, AbstractType $sub): b } return true; } + + private static function checkIterable(IterableType $super, AbstractType $sub): bool + { + if ($sub instanceof ToIterableInterface) { + $sub = $sub->toIterable(); + } + if (!$sub instanceof IterableType) { + return false; + } + return self::check($super->keyType, $sub->keyType) + && self::check($super->valueType, $sub->valueType); + } } diff --git a/src/Conversion/ToIterableInterface.php b/src/Conversion/ToIterableInterface.php new file mode 100644 index 0000000..039139e --- /dev/null +++ b/src/Conversion/ToIterableInterface.php @@ -0,0 +1,13 @@ +type, $this->nonEmpty); } + + public function toIterable(): IterableType + { + return new IterableType(new IntType(), $this->type); + } } diff --git a/src/MapType.php b/src/MapType.php index 81ac46b..877ec40 100644 --- a/src/MapType.php +++ b/src/MapType.php @@ -6,10 +6,11 @@ use PhpTypes\Ast\Node\IdentifierNode; use PhpTypes\Ast\Node\NodeInterface; +use PhpTypes\Types\Conversion\ToIterableInterface; use function in_array; -final class MapType extends AbstractType +final class MapType extends AbstractType implements ToIterableInterface { public function __construct( public readonly AbstractType $keyType, @@ -42,4 +43,9 @@ public function toNode(): NodeInterface [$keyNode, $this->valueType->toNode()] ); } + + public function toIterable(): IterableType + { + return new IterableType($this->keyType, $this->valueType); + } } diff --git a/src/StructType.php b/src/StructType.php index e28a962..a0d9e29 100644 --- a/src/StructType.php +++ b/src/StructType.php @@ -6,9 +6,11 @@ use PhpTypes\Ast\Node\NodeInterface; use PhpTypes\Ast\Node\StructNode; +use PhpTypes\Types\Conversion\ToIterableInterface; +use PhpTypes\Types\Conversion\ToMapInterface; use PhpTypes\Types\Dto\StructMember; -final class StructType extends AbstractType +final class StructType extends AbstractType implements ToIterableInterface, ToMapInterface { /** * @param array $members @@ -27,6 +29,21 @@ public function toNode(): NodeInterface } public function toMap(): MapType + { + $types = $this->keyAndValueType(); + return new MapType($types[0], $types[1], true); + } + + public function toIterable(): IterableType + { + $types = $this->keyAndValueType(); + return new IterableType($types[0], $types[1]); + } + + /** + * @return array{AbstractType, AbstractType} + */ + public function keyAndValueType(): array { /** @var array{AbstractType, AbstractType}|null $types */ $types = null; @@ -38,6 +55,6 @@ public function toMap(): MapType $types[0] = new UnionType($types[0], new StringLiteralType($name)); $types[1] = new UnionType($types[1], $member->type); } - return new MapType($types[0], $types[1], true); + return $types; } } diff --git a/src/TupleType.php b/src/TupleType.php index 3687473..192e14d 100644 --- a/src/TupleType.php +++ b/src/TupleType.php @@ -6,8 +6,10 @@ use PhpTypes\Ast\Node\NodeInterface; use PhpTypes\Ast\Node\TupleNode; +use PhpTypes\Types\Conversion\ToIterableInterface; +use PhpTypes\Types\Conversion\ToMapInterface; -final class TupleType extends AbstractType +final class TupleType extends AbstractType implements ToIterableInterface, ToMapInterface { /** * @param list $elements @@ -26,6 +28,16 @@ public function toNode(): NodeInterface } public function toMap(): MapType + { + return new MapType(new IntType(), $this->valueType()); + } + + public function toIterable(): IterableType + { + return new IterableType(new IntType(), $this->valueType()); + } + + public function valueType(): AbstractType { $valueType = null; foreach ($this->elements as $element) { @@ -38,6 +50,6 @@ public function toMap(): MapType } $valueType = new UnionType($valueType, $element); } - return new MapType(new IntType(), $valueType); + return $valueType; } } diff --git a/src/UnionType.php b/src/UnionType.php index 0805c4f..9b1d516 100644 --- a/src/UnionType.php +++ b/src/UnionType.php @@ -6,8 +6,10 @@ use PhpTypes\Ast\Node\NodeInterface; use PhpTypes\Ast\Node\UnionNode; +use PhpTypes\Types\Conversion\ToIterableInterface; +use PhpTypes\Types\Conversion\ToMapInterface; -final class UnionType extends AbstractType +final class UnionType extends AbstractType implements ToIterableInterface, ToMapInterface { public function __construct(public readonly AbstractType $left, public readonly AbstractType $right) { @@ -25,7 +27,55 @@ public function flatten(): array { return array_merge( $this->left instanceof UnionType ? $this->left->flatten() : [$this->left], - $this->right instanceof UnionType ? $this->right->flatten() : [$this->right] + $this->right instanceof UnionType ? $this->right->flatten() : [$this->right], + ); + } + + public function toIterable(): IterableType|NeverType + { + $left = $this->left instanceof ToIterableInterface ? $this->left->toIterable() : $this->left; + if (!$left instanceof IterableType) { + return new NeverType(); + } + $right = $this->right instanceof ToIterableInterface ? $this->right->toIterable() : $this->right; + if (!$right instanceof IterableType) { + return new NeverType(); + } + return new IterableType( + new UnionType($left->keyType, $right->keyType), + new UnionType($left->valueType, $right->valueType), + ); + } + + public function toMap(): MapType|NeverType + { + $left = $this->left instanceof ToMapInterface ? $this->left->toMap() : $this->left; + if (!$left instanceof MapType) { + return new NeverType(); + } + $right = $this->right instanceof ToMapInterface ? $this->right->toMap() : $this->right; + if (!$right instanceof MapType) { + return new NeverType(); + } + return new MapType( + new UnionType($left->keyType, $right->keyType), + new UnionType($left->valueType, $right->valueType), + ); + } + + public function toList(): ListType|NeverType + { + $left = $this->left instanceof self ? $this->left->toList() : $this->left; + if (!$left instanceof ListType) { + return new NeverType(); + } + $right = $this->right instanceof self ? $this->right->toList() : $this->right; + if (!$right instanceof ListType) { + return new NeverType(); + } + return new ListType( + new UnionType($left->type, $right->type), + $left->nonEmpty || $right->nonEmpty, ); } } diff --git a/tests/functional/compatible-types/iterable.md b/tests/functional/compatible-types/iterable.md new file mode 100644 index 0000000..894e91c --- /dev/null +++ b/tests/functional/compatible-types/iterable.md @@ -0,0 +1,47 @@ +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` + +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` + +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` + +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` + +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` + +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` + +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` + +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` + +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` diff --git a/tests/functional/compatible-types/list.md b/tests/functional/compatible-types/list.md index 584df4d..272ba3f 100644 --- a/tests/functional/compatible-types/list.md +++ b/tests/functional/compatible-types/list.md @@ -1,34 +1,58 @@ - `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` +- `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` +- `list` is a subtype of `iterable` +- `list` is a subtype of `iterable` +- `list` is a subtype of `iterable` - `list` is a subtype of `list` - `list` is a subtype of `list` - `list` is a subtype of `list` +- `list` is a subtype of `list | list` - `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` +- `list` is a subtype of `iterable` +- `list` is a subtype of `iterable` +- `list` is a subtype of `iterable` - `list` is a subtype of `list` - `list` is a subtype of `array` - `list` is a subtype of `array` +- `list` is a subtype of `array` - `list` is a subtype of `array` +- `list` is a subtype of `iterable` +- `list` is a subtype of `iterable` +- `list` is a subtype of `iterable` - `list` is a subtype of `list` - `list` is a subtype of `array` - `list` is a subtype of `array` +- `list` is a subtype of `array` - `list` is a subtype of `array` +- `list` is a subtype of `iterable` +- `list` is a subtype of `iterable` +- `list` is a subtype of `iterable` +- `list` is a subtype of `iterable` - `list` is a subtype of `list` - `list` is a subtype of `list` +- `list` is a subtype of `list | list` - `list` is a subtype of `array` - `non-empty-list` is a subtype of `array` - `non-empty-list` is a subtype of `array` +- `non-empty-list` is a subtype of `array` - `non-empty-list` is a subtype of `array` - `non-empty-list` is a subtype of `array` +- `non-empty-list` is a subtype of `iterable` +- `non-empty-list` is a subtype of `iterable` +- `non-empty-list` is a subtype of `iterable` +- `non-empty-list` is a subtype of `iterable` - `non-empty-list` is a subtype of `list` - `non-empty-list` is a subtype of `list` +- `non-empty-list` is a subtype of `list | list` - `non-empty-list` is a subtype of `non-empty-array` - `non-empty-list` is a subtype of `non-empty-list` diff --git a/tests/functional/compatible-types/map.md b/tests/functional/compatible-types/map.md index 4997d4a..13bf143 100644 --- a/tests/functional/compatible-types/map.md +++ b/tests/functional/compatible-types/map.md @@ -1,39 +1,86 @@ - `array` is a subtype of `array` - `array` is a subtype of `array` - `array` is a subtype of `array` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` + +- `array<'name' | 'age', string | int>` is a subtype of `array` +- `array<'name' | 'age', string | int>` is a subtype of `array<'name' | 'age', string | int>` +- `array<'name' | 'age', string | int>` is a subtype of `array` +- `array<'name' | 'age', string | int>` is a subtype of `array` +- `array<'name' | 'age', string | int>` is a subtype of `array` +- `array<'name' | 'age', string | int>` is a subtype of `iterable` +- `array<'name' | 'age', string | int>` is a subtype of `iterable` +- `array<'name' | 'age', string | int>` is a subtype of `iterable` - `array` is a subtype of `array` - `array` is a subtype of `array` - `array` is a subtype of `array` - `array` is a subtype of `array` - `array` is a subtype of `array` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` - `array` is a subtype of `array` - `array` is a subtype of `array` - `array` is a subtype of `array` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` + +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `array` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` - `array` is a subtype of `array` - `array` is a subtype of `array` - `array` is a subtype of `array` - `array` is a subtype of `array` - `array` is a subtype of `array` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` - `array` is a subtype of `array` - `array` is a subtype of `array` +- `array` is a subtype of `array` - `array` is a subtype of `array` - `array` is a subtype of `array` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` - `array` is a subtype of `array` - `array` is a subtype of `array` - `array` is a subtype of `array` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` - `array` is a subtype of `array` - `array` is a subtype of `array` - `array` is a subtype of `array` - `array` is a subtype of `array` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` +- `array` is a subtype of `iterable` - `non-empty-array` is a subtype of `array` - `non-empty-array` is a subtype of `array` +- `non-empty-array` is a subtype of `array` - `non-empty-array` is a subtype of `array` - `non-empty-array` is a subtype of `array` +- `non-empty-array` is a subtype of `iterable` +- `non-empty-array` is a subtype of `iterable` +- `non-empty-array` is a subtype of `iterable` +- `non-empty-array` is a subtype of `iterable` - `non-empty-array` is a subtype of `non-empty-array` diff --git a/tests/functional/compatible-types/struct.md b/tests/functional/compatible-types/struct.md index a0c66a4..af81a31 100644 --- a/tests/functional/compatible-types/struct.md +++ b/tests/functional/compatible-types/struct.md @@ -1,17 +1,33 @@ - `array{name: string}` is a subtype of `array` +- `array{name: string}` is a subtype of `array<'name' | 'age', string | int>` - `array{name: string}` is a subtype of `array` +- `array{name: string}` is a subtype of `array` - `array{name: string}` is a subtype of `array` - `array{name: string}` is a subtype of `array{name: string}` +- `array{name: string}` is a subtype of `iterable` +- `array{name: string}` is a subtype of `iterable` +- `array{name: string}` is a subtype of `iterable` +- `array{name: string}` is a subtype of `iterable` - `array{name: string, age: int}` is a subtype of `array` +- `array{name: string, age: int}` is a subtype of `array<'name' | 'age', string | int>` - `array{name: string, age: int}` is a subtype of `array` +- `array{name: string, age: int}` is a subtype of `array` - `array{name: string, age: int}` is a subtype of `array` - `array{name: string, age: int}` is a subtype of `array{name: string}` - `array{name: string, age: int}` is a subtype of `array{name: string, age: int}` - `array{name: string, age: int}` is a subtype of `array{name: string, age?: int}` +- `array{name: string, age: int}` is a subtype of `iterable` +- `array{name: string, age: int}` is a subtype of `iterable` +- `array{name: string, age: int}` is a subtype of `iterable` - `array{name: string, age?: int}` is a subtype of `array` +- `array{name: string, age?: int}` is a subtype of `array<'name' | 'age', string | int>` - `array{name: string, age?: int}` is a subtype of `array` +- `array{name: string, age?: int}` is a subtype of `array` - `array{name: string, age?: int}` is a subtype of `array` - `array{name: string, age?: int}` is a subtype of `array{name: string}` - `array{name: string, age?: int}` is a subtype of `array{name: string, age?: int}` +- `array{name: string, age?: int}` is a subtype of `iterable` +- `array{name: string, age?: int}` is a subtype of `iterable` +- `array{name: string, age?: int}` is a subtype of `iterable` diff --git a/tests/functional/compatible-types/tuple.md b/tests/functional/compatible-types/tuple.md index 8e22726..6cdfba6 100644 --- a/tests/functional/compatible-types/tuple.md +++ b/tests/functional/compatible-types/tuple.md @@ -1,27 +1,45 @@ - `array{int, string}` is a subtype of `array` - `array{int, string}` is a subtype of `array` +- `array{int, string}` is a subtype of `array` - `array{int, string}` is a subtype of `array` - `array{int, string}` is a subtype of `array{int, string}` +- `array{int, string}` is a subtype of `iterable` +- `array{int, string}` is a subtype of `iterable` +- `array{int, string}` is a subtype of `iterable` - `array{int, string}` is a subtype of `list` - `array{string, int}` is a subtype of `array` - `array{string, int}` is a subtype of `array` +- `array{string, int}` is a subtype of `array` - `array{string, int}` is a subtype of `array` - `array{string, int}` is a subtype of `array{string, int}` +- `array{string, int}` is a subtype of `iterable` +- `array{string, int}` is a subtype of `iterable` +- `array{string, int}` is a subtype of `iterable` - `array{string, int}` is a subtype of `list` - `array{string, int, string}` is a subtype of `array` - `array{string, int, string}` is a subtype of `array` +- `array{string, int, string}` is a subtype of `array` - `array{string, int, string}` is a subtype of `array` - `array{string, int, string}` is a subtype of `array{string, int}` - `array{string, int, string}` is a subtype of `array{string, int, string}` +- `array{string, int, string}` is a subtype of `iterable` +- `array{string, int, string}` is a subtype of `iterable` +- `array{string, int, string}` is a subtype of `iterable` - `array{string, int, string}` is a subtype of `list` - `array{string, string}` is a subtype of `array` - `array{string, string}` is a subtype of `array` +- `array{string, string}` is a subtype of `array` - `array{string, string}` is a subtype of `array` - `array{string, string}` is a subtype of `array` - `array{string, string}` is a subtype of `array{string, string}` +- `array{string, string}` is a subtype of `iterable` +- `array{string, string}` is a subtype of `iterable` +- `array{string, string}` is a subtype of `iterable` +- `array{string, string}` is a subtype of `iterable` - `array{string, string}` is a subtype of `list` - `array{string, string}` is a subtype of `list` +- `array{string, string}` is a subtype of `list | list` - `array{string, string}` is a subtype of `non-empty-list` diff --git a/tests/functional/compatible-types/union.md b/tests/functional/compatible-types/union.md index 11a790f..9502bd0 100644 --- a/tests/functional/compatible-types/union.md +++ b/tests/functional/compatible-types/union.md @@ -3,6 +3,16 @@ - `int | string` is a subtype of `string | int` - `int | string` is a subtype of `string | int | bool` +- `list | list` is a subtype of `array` +- `list | list` is a subtype of `array` +- `list | list` is a subtype of `array` +- `list | list` is a subtype of `array` +- `list | list` is a subtype of `list` +- `list | list` is a subtype of `list | list` +- `list | list` is a subtype of `iterable` +- `list | list` is a subtype of `iterable` +- `list | list` is a subtype of `iterable` + - `string | int` is a subtype of `int | string` - `string | int` is a subtype of `scalar` - `string | int` is a subtype of `string | int` diff --git a/tests/functional/types/iterable.txt b/tests/functional/types/iterable.txt new file mode 100644 index 0000000..71aa2e5 --- /dev/null +++ b/tests/functional/types/iterable.txt @@ -0,0 +1,9 @@ +iterable +iterable +iterable +iterable +iterable +iterable +iterable +iterable +iterable diff --git a/tests/functional/types/map.txt b/tests/functional/types/map.txt index 2a2987c..139d212 100644 --- a/tests/functional/types/map.txt +++ b/tests/functional/types/map.txt @@ -5,4 +5,6 @@ array array array array +array +array<'name' | 'age', string | int> non-empty-array diff --git a/tests/functional/types/union.txt b/tests/functional/types/union.txt index 9990662..545edc3 100644 --- a/tests/functional/types/union.txt +++ b/tests/functional/types/union.txt @@ -1,3 +1,4 @@ int | string string | int string | int | bool +list | list From 1bdbef3853477fc37691cea14bb93e6fc89d02c4 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Mon, 15 Aug 2022 17:18:29 +0200 Subject: [PATCH 12/53] Add alias tests --- src/Compatibility.php | 3 + src/Type.php | 21 ++++- tests/functional/CompatibilityTest.php | 81 ++++++++++++++++--- tests/functional/aliases.md | 20 +++++ tests/functional/compatible-types.md | 0 tests/functional/compatible-types/bool.md | 9 +++ .../compatible-types/string-literal.md | 6 ++ tests/functional/compatible-types/string.md | 6 ++ tests/functional/compatible-types/struct.md | 14 ++++ tests/functional/compatible-types/union.md | 34 ++++++++ tests/functional/types/struct.txt | 1 + tests/functional/types/union.txt | 5 ++ 12 files changed, 187 insertions(+), 13 deletions(-) create mode 100644 tests/functional/aliases.md delete mode 100644 tests/functional/compatible-types.md diff --git a/src/Compatibility.php b/src/Compatibility.php index e7e7c45..170a217 100644 --- a/src/Compatibility.php +++ b/src/Compatibility.php @@ -106,6 +106,9 @@ private static function checkString(StringType $super, AbstractType $sub): bool private static function checkBool(BoolType $super, AbstractType $sub): bool { + if ($sub instanceof UnionType) { + return self::check($super, $sub->left) && self::check($super, $sub->right); + } if (!$sub instanceof BoolType) { return false; } diff --git a/src/Type.php b/src/Type.php index 5d03eef..0f5f71d 100644 --- a/src/Type.php +++ b/src/Type.php @@ -61,10 +61,23 @@ private static function fromIdentifier(IdentifierNode $node, Scope $scope): Abst private static function fromUnion(UnionNode $node, Scope $scope): AbstractType { - return new UnionType( - self::fromNode($node->left, $scope), - self::fromNode($node->right, $scope), - ); + $left = self::fromNode($node->left, $scope); + $right = self::fromNode($node->right, $scope); + if (Compatibility::check($left, $right)) { + return $left; + } + if (Compatibility::check($right, $left)) { + return $right; + } + if ($left instanceof BoolType && $right instanceof BoolType) { + if ( + ($left->value === true && $right->value === false) + || ($left->value === false && $right->value === true) + ) { + return new BoolType(); + } + } + return new UnionType($left, $right); } private static function fromTuple(TupleNode $node, Scope $scope): TupleType diff --git a/tests/functional/CompatibilityTest.php b/tests/functional/CompatibilityTest.php index 3d4d8e9..ebddcb1 100644 --- a/tests/functional/CompatibilityTest.php +++ b/tests/functional/CompatibilityTest.php @@ -17,12 +17,24 @@ use function array_search; use function explode; use function file_get_contents; +use function in_array; +use function sort; use function sprintf; final class CompatibilityTest extends TestCase { private static Scope $scope; + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + + self::$scope = Scope::global(); + $fooInterface = new ClassLikeType('FooInterface'); + self::$scope->register('FooInterface', $fooInterface); + self::$scope->register('Foo', new ClassLikeType('Foo', parents: [$fooInterface])); + } + /** * @return list */ @@ -32,7 +44,7 @@ private static function compatibleTypes(): array foreach (self::filesInDirectory(__DIR__ . '/compatible-types/') as $file) { foreach (explode("\n", file_get_contents($file)) as $line) { $isMatch = \Safe\preg_match('/^- `(?.+)` is a subtype of `(?.+)`/', $line, $matches); - if (!$isMatch) { + if ($isMatch === 0) { continue; } $types[] = [$matches['super'], $matches['sub']]; @@ -79,7 +91,25 @@ private static function typeFiles(): iterable } /** - * @dataProvider cases + * @return list + */ + private static function aliases(): array + { + $aliases = []; + foreach (explode("\n", file_get_contents(__DIR__ . '/aliases.md')) as $line) { + $isMatch = \Safe\preg_match('/^- `(?.+)` is an alias of `(?.+)`/', $line, $matches); + if ($isMatch === 0) { + continue; + } + $tuple = [$matches['a'], $matches['b']]; + sort($tuple); + $aliases[] = $tuple; + } + return $aliases; + } + + /** + * @dataProvider compatibilityCases */ public function testCompatibility(string $super, string $sub, bool $expected): void { @@ -95,7 +125,7 @@ public function testCompatibility(string $super, string $sub, bool $expected): v /** * @return iterable */ - public function cases(): iterable + public function compatibilityCases(): iterable { $compatibleTypes = self::compatibleTypes(); foreach (self::types() as $super) { @@ -147,13 +177,46 @@ public function allTypes(): iterable } } - public static function setUpBeforeClass(): void + /** + * @dataProvider aliasCases + */ + public function testAliases(string $a, string $b, bool $expected): void { - parent::setUpBeforeClass(); + $aType = Type::fromString($a, self::$scope); + $bType = Type::fromString($b, self::$scope); - self::$scope = Scope::global(); - $fooInterface = new ClassLikeType('FooInterface'); - self::$scope->register('FooInterface', $fooInterface); - self::$scope->register('Foo', new ClassLikeType('Foo', parents: [$fooInterface])); + $isAlias = Compatibility::check($aType, $bType) && Compatibility::check($bType, $aType); + + $message = $expected + ? sprintf('Expected "%s" to be an alias of "%s", but it is not', $b, $a) + : sprintf('Expected "%s" not to be an alias of "%s", but it is', $b, $a); + self::assertSame($expected, $isAlias, $message); + } + + /** + * @return iterable + */ + public function aliasCases(): iterable + { + $aliases = self::aliases(); + $seen = []; + foreach (self::types() as $a) { + foreach (self::types() as $b) { + if ($a === $b) { + continue; + } + $tuple = [$a, $b]; + sort($tuple); + if (in_array($tuple, $seen, true)) { + continue; + } + $expected = array_search($tuple, $aliases, true) !== false; + $name = $expected + ? sprintf('%s is an alias of %s', $b, $a) + : sprintf('%s is not an alias of %s', $b, $a); + yield $name => [$a, $b, $expected]; + $seen[] = $tuple; + } + } } } diff --git a/tests/functional/aliases.md b/tests/functional/aliases.md new file mode 100644 index 0000000..7f7bbd3 --- /dev/null +++ b/tests/functional/aliases.md @@ -0,0 +1,20 @@ +- `array` is an alias of `array` +- `array` is an alias of `array` +- `array` is an alias of `array` +- `array` is an alias of `array` +- `array{name: string, age?: int}` is an alias of `array{age?: int, name: string}` +- `int<23, 23>` is an alias of `23` +- `int<1, max>` is an alias of `positive-int` +- `int` is an alias of `negative-int` +- `iterable` is an alias of `iterable` +- `iterable` is an alias of `iterable` +- `iterable` is an alias of `iterable` +- `iterable` is an alias of `iterable` +- `string | int` is an alias of `int | string` +- `true | false` is an alias of `bool` +- `false | true` is an alias of `bool` +- `false | true` is an alias of `true | false` +- `bool | true` is an alias of `bool` +- `bool | true` is an alias of `true | false` +- `bool | true` is an alias of `false | true` +- `string | 'foo'` is an alias of `string` diff --git a/tests/functional/compatible-types.md b/tests/functional/compatible-types.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/functional/compatible-types/bool.md b/tests/functional/compatible-types/bool.md index 02ca3c6..08872ca 100644 --- a/tests/functional/compatible-types/bool.md +++ b/tests/functional/compatible-types/bool.md @@ -1,13 +1,22 @@ - `bool` is a subtype of `bool` +- `bool` is a subtype of `bool | true` +- `bool` is a subtype of `false | true` - `bool` is a subtype of `scalar` - `bool` is a subtype of `string | int | bool` +- `bool` is a subtype of `true | false` - `false` is a subtype of `bool` +- `false` is a subtype of `bool | true` - `false` is a subtype of `false` +- `false` is a subtype of `false | true` - `false` is a subtype of `scalar` - `false` is a subtype of `string | int | bool` +- `false` is a subtype of `true | false` - `true` is a subtype of `bool` +- `true` is a subtype of `bool | true` +- `true` is a subtype of `false | true` - `true` is a subtype of `true` - `true` is a subtype of `scalar` - `true` is a subtype of `string | int | bool` +- `true` is a subtype of `true | false` diff --git a/tests/functional/compatible-types/string-literal.md b/tests/functional/compatible-types/string-literal.md index 3e36107..f533436 100644 --- a/tests/functional/compatible-types/string-literal.md +++ b/tests/functional/compatible-types/string-literal.md @@ -2,6 +2,7 @@ - `''` is a subtype of `int | string` - `''` is a subtype of `scalar` - `''` is a subtype of `string` +- `''` is a subtype of `string | 'foo'` - `''` is a subtype of `string | int` - `''` is a subtype of `string | int | bool` @@ -11,21 +12,26 @@ - `'42'` is a subtype of `numeric-string` - `'42'` is a subtype of `scalar` - `'42'` is a subtype of `string` +- `'42'` is a subtype of `string | 'foo'` - `'42'` is a subtype of `string | int` - `'42'` is a subtype of `string | int | bool` - `'foo'` is a subtype of `'foo'` +- `'foo'` is a subtype of `'foo' | 'bar'` - `'foo'` is a subtype of `int | string` - `'foo'` is a subtype of `non-empty-string` - `'foo'` is a subtype of `scalar` - `'foo'` is a subtype of `string` +- `'foo'` is a subtype of `string | 'foo'` - `'foo'` is a subtype of `string | int` - `'foo'` is a subtype of `string | int | bool` - `'bar'` is a subtype of `'bar'` +- `'bar'` is a subtype of `'foo' | 'bar'` - `'bar'` is a subtype of `int | string` - `'bar'` is a subtype of `non-empty-string` - `'bar'` is a subtype of `scalar` - `'bar'` is a subtype of `string` +- `'bar'` is a subtype of `string | 'foo'` - `'bar'` is a subtype of `string | int` - `'bar'` is a subtype of `string | int | bool` diff --git a/tests/functional/compatible-types/string.md b/tests/functional/compatible-types/string.md index e3510c5..41f5730 100644 --- a/tests/functional/compatible-types/string.md +++ b/tests/functional/compatible-types/string.md @@ -3,6 +3,7 @@ - `class-string` is a subtype of `non-empty-string` - `class-string` is a subtype of `scalar` - `class-string` is a subtype of `string` +- `class-string` is a subtype of `string | 'foo'` - `class-string` is a subtype of `string | int` - `class-string` is a subtype of `string | int | bool` @@ -13,6 +14,7 @@ - `class-string` is a subtype of `non-empty-string` - `class-string` is a subtype of `scalar` - `class-string` is a subtype of `string` +- `class-string` is a subtype of `string | 'foo'` - `class-string` is a subtype of `string | int` - `class-string` is a subtype of `string | int | bool` @@ -22,6 +24,7 @@ - `class-string` is a subtype of `non-empty-string` - `class-string` is a subtype of `scalar` - `class-string` is a subtype of `string` +- `class-string` is a subtype of `string | 'foo'` - `class-string` is a subtype of `string | int` - `class-string` is a subtype of `string | int | bool` @@ -29,6 +32,7 @@ - `non-empty-string` is a subtype of `non-empty-string` - `non-empty-string` is a subtype of `scalar` - `non-empty-string` is a subtype of `string` +- `non-empty-string` is a subtype of `string | 'foo'` - `non-empty-string` is a subtype of `string | int` - `non-empty-string` is a subtype of `string | int | bool` @@ -37,11 +41,13 @@ - `numeric-string` is a subtype of `numeric-string` - `numeric-string` is a subtype of `scalar` - `numeric-string` is a subtype of `string` +- `numeric-string` is a subtype of `string | 'foo'` - `numeric-string` is a subtype of `string | int` - `numeric-string` is a subtype of `string | int | bool` - `string` is a subtype of `int | string` - `string` is a subtype of `scalar` - `string` is a subtype of `string` +- `string` is a subtype of `string | 'foo'` - `string` is a subtype of `string | int` - `string` is a subtype of `string | int | bool` diff --git a/tests/functional/compatible-types/struct.md b/tests/functional/compatible-types/struct.md index af81a31..f7e1bd3 100644 --- a/tests/functional/compatible-types/struct.md +++ b/tests/functional/compatible-types/struct.md @@ -1,3 +1,15 @@ +- `array{age?: int, name: string}` is a subtype of `array` +- `array{age?: int, name: string}` is a subtype of `array<'name' | 'age', string | int>` +- `array{age?: int, name: string}` is a subtype of `array` +- `array{age?: int, name: string}` is a subtype of `array` +- `array{age?: int, name: string}` is a subtype of `array` +- `array{age?: int, name: string}` is a subtype of `array{age?: int, name: string}` +- `array{age?: int, name: string}` is a subtype of `array{name: string}` +- `array{age?: int, name: string}` is a subtype of `array{name: string, age?: int}` +- `array{age?: int, name: string}` is a subtype of `iterable` +- `array{age?: int, name: string}` is a subtype of `iterable` +- `array{age?: int, name: string}` is a subtype of `iterable` + - `array{name: string}` is a subtype of `array` - `array{name: string}` is a subtype of `array<'name' | 'age', string | int>` - `array{name: string}` is a subtype of `array` @@ -14,6 +26,7 @@ - `array{name: string, age: int}` is a subtype of `array` - `array{name: string, age: int}` is a subtype of `array` - `array{name: string, age: int}` is a subtype of `array` +- `array{name: string, age: int}` is a subtype of `array{age?: int, name: string}` - `array{name: string, age: int}` is a subtype of `array{name: string}` - `array{name: string, age: int}` is a subtype of `array{name: string, age: int}` - `array{name: string, age: int}` is a subtype of `array{name: string, age?: int}` @@ -26,6 +39,7 @@ - `array{name: string, age?: int}` is a subtype of `array` - `array{name: string, age?: int}` is a subtype of `array` - `array{name: string, age?: int}` is a subtype of `array` +- `array{name: string, age?: int}` is a subtype of `array{age?: int, name: string}` - `array{name: string, age?: int}` is a subtype of `array{name: string}` - `array{name: string, age?: int}` is a subtype of `array{name: string, age?: int}` - `array{name: string, age?: int}` is a subtype of `iterable` diff --git a/tests/functional/compatible-types/union.md b/tests/functional/compatible-types/union.md index 9502bd0..1364ef8 100644 --- a/tests/functional/compatible-types/union.md +++ b/tests/functional/compatible-types/union.md @@ -1,3 +1,23 @@ +- `'foo' | 'bar'` is a subtype of `'foo' | 'bar'` +- `'foo' | 'bar'` is a subtype of `int | string` +- `'foo' | 'bar'` is a subtype of `scalar` +- `'foo' | 'bar'` is a subtype of `string | int` +- `'foo' | 'bar'` is a subtype of `string | int | bool` + +- `bool | true` is a subtype of `bool` +- `bool | true` is a subtype of `bool | true` +- `bool | true` is a subtype of `false | true` +- `bool | true` is a subtype of `scalar` +- `bool | true` is a subtype of `string | int | bool` +- `bool | true` is a subtype of `true | false` + +- `false | true` is a subtype of `bool` +- `false | true` is a subtype of `bool | true` +- `false | true` is a subtype of `false | true` +- `false | true` is a subtype of `scalar` +- `false | true` is a subtype of `string | int | bool` +- `false | true` is a subtype of `true | false` + - `int | string` is a subtype of `int | string` - `int | string` is a subtype of `scalar` - `int | string` is a subtype of `string | int` @@ -13,6 +33,13 @@ - `list | list` is a subtype of `iterable` - `list | list` is a subtype of `iterable` +- `string | 'foo'` is a subtype of `int | string` +- `string | 'foo'` is a subtype of `scalar` +- `string | 'foo'` is a subtype of `string` +- `string | 'foo'` is a subtype of `string | 'foo'` +- `string | 'foo'` is a subtype of `string | int` +- `string | 'foo'` is a subtype of `string | int | bool` + - `string | int` is a subtype of `int | string` - `string | int` is a subtype of `scalar` - `string | int` is a subtype of `string | int` @@ -20,3 +47,10 @@ - `string | int | bool` is a subtype of `scalar` - `string | int | bool` is a subtype of `string | int | bool` + +- `true | false` is a subtype of `bool` +- `true | false` is a subtype of `bool | true` +- `true | false` is a subtype of `false | true` +- `true | false` is a subtype of `scalar` +- `true | false` is a subtype of `string | int | bool` +- `true | false` is a subtype of `true | false` diff --git a/tests/functional/types/struct.txt b/tests/functional/types/struct.txt index 71c29fb..a58b185 100644 --- a/tests/functional/types/struct.txt +++ b/tests/functional/types/struct.txt @@ -1,3 +1,4 @@ +array{age?: int, name: string} array{name: string} array{name: string, age: int} array{name: string, age?: int} diff --git a/tests/functional/types/union.txt b/tests/functional/types/union.txt index 545edc3..ab39885 100644 --- a/tests/functional/types/union.txt +++ b/tests/functional/types/union.txt @@ -2,3 +2,8 @@ int | string string | int string | int | bool list | list +true | false +false | true +'foo' | 'bar' +bool | true +string | 'foo' From f086069a77f562213d706a75f92b1b45c37b6ff6 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 16 Aug 2022 19:37:26 +0200 Subject: [PATCH 13/53] Improve compatibility checking with subtype unions --- src/Compatibility.php | 25 +++++++++++----------- src/ScalarType.php | 2 ++ tests/functional/compatible-types/union.md | 3 +++ 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/Compatibility.php b/src/Compatibility.php index 170a217..ad833c5 100644 --- a/src/Compatibility.php +++ b/src/Compatibility.php @@ -23,6 +23,9 @@ private function __construct() public static function check(AbstractType $super, AbstractType $sub): bool { + if (!$super instanceof UnionType && $sub instanceof UnionType) { + return self::checkSubUnion($super, $sub); + } $superClass = get_class($super); return match ($superClass) { BoolType::class => self::checkBool($super, $sub), @@ -106,9 +109,6 @@ private static function checkString(StringType $super, AbstractType $sub): bool private static function checkBool(BoolType $super, AbstractType $sub): bool { - if ($sub instanceof UnionType) { - return self::check($super, $sub->left) && self::check($super, $sub->right); - } if (!$sub instanceof BoolType) { return false; } @@ -127,10 +127,10 @@ private static function checkInt(IntType $super, AbstractType $sub): bool return false; } [$superMin, $superMax, $subMin, $subMax] = [ - $super->min ?? PHP_INT_MIN, - $super->max ?? PHP_INT_MAX, - $sub->min ?? PHP_INT_MIN, - $sub->max ?? PHP_INT_MAX, + $super->min ?? PHP_INT_MIN, + $super->max ?? PHP_INT_MAX, + $sub->min ?? PHP_INT_MIN, + $sub->max ?? PHP_INT_MAX, ]; return $superMin <= $subMin && $superMax >= $subMax; } @@ -191,15 +191,11 @@ private static function checkScalar(AbstractType $sub): bool || $sub instanceof BoolType || $sub instanceof StringType || $sub instanceof StringLiteralType - || $sub instanceof ClassStringType - || ($sub instanceof UnionType && self::checkScalar($sub->left) && self::checkScalar($sub->right)); + || $sub instanceof ClassStringType; } private static function checkList(ListType $super, AbstractType $sub): bool { - if ($sub instanceof UnionType) { - $sub = $sub->toList(); - } if ($sub instanceof TupleType) { foreach ($sub->elements as $element) { if (self::check($super->type, $element)) { @@ -309,4 +305,9 @@ private static function checkIterable(IterableType $super, AbstractType $sub): b return self::check($super->keyType, $sub->keyType) && self::check($super->valueType, $sub->valueType); } + + private static function checkSubUnion(AbstractType $super, UnionType $sub): bool + { + return self::check($super, $sub->left) && self::check($super, $sub->right); + } } diff --git a/src/ScalarType.php b/src/ScalarType.php index c5b1573..2c138a2 100644 --- a/src/ScalarType.php +++ b/src/ScalarType.php @@ -1,5 +1,7 @@ Date: Tue, 16 Aug 2022 19:40:01 +0200 Subject: [PATCH 14/53] Fix the PHPStan config --- phpstan.dist.neon | 7 ------- 1 file changed, 7 deletions(-) diff --git a/phpstan.dist.neon b/phpstan.dist.neon index b4851a3..1494f51 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -3,10 +3,3 @@ parameters: paths: - src - tests - scanFiles: - - generated/PhpTypes/Ast/Generated/PhpTypesBaseListener.php - - generated/PhpTypes/Ast/Generated/PhpTypesBaseVisitor.php - - generated/PhpTypes/Ast/Generated/PhpTypesLexer.php - - generated/PhpTypes/Ast/Generated/PhpTypesListener.php - - generated/PhpTypes/Ast/Generated/PhpTypesParser.php - - generated/PhpTypes/Ast/Generated/PhpTypesVisitor.php From 8727ad61de185c168717b16dae57f22fa2eef4d4 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 16 Aug 2022 20:07:35 +0200 Subject: [PATCH 15/53] Fix some issues uncovered by PHPStan --- src/BoolType.php | 8 ++++- src/Compatibility.php | 43 +++++++++++++------------- src/Scope.php | 15 --------- src/StructType.php | 3 ++ src/TupleType.php | 3 ++ src/Type.php | 5 ++- tests/functional/CompatibilityTest.php | 8 ++--- 7 files changed, 42 insertions(+), 43 deletions(-) diff --git a/src/BoolType.php b/src/BoolType.php index 4256dca..35b6e08 100644 --- a/src/BoolType.php +++ b/src/BoolType.php @@ -24,6 +24,12 @@ public function __toString(): string public function toNode(): NodeInterface { - return new IdentifierNode((string)$this); + return new IdentifierNode( + match ($this->value) { + null => 'bool', + true => 'true', + false => 'false', + } + ); } } diff --git a/src/Compatibility.php b/src/Compatibility.php index ad833c5..b0ca5bf 100644 --- a/src/Compatibility.php +++ b/src/Compatibility.php @@ -26,28 +26,27 @@ public static function check(AbstractType $super, AbstractType $sub): bool if (!$super instanceof UnionType && $sub instanceof UnionType) { return self::checkSubUnion($super, $sub); } - $superClass = get_class($super); - return match ($superClass) { - BoolType::class => self::checkBool($super, $sub), - CallableType::class => self::checkCallable($super, $sub), - ClassLikeType::class => self::checkClassLike($super, $sub), - ClassStringType::class => self::checkClassString($super, $sub), - FloatType::class => self::checkFloat($sub), - IntLiteralType::class => self::checkIntLiteral($super, $sub), - IntType::class => self::checkInt($super, $sub), - IterableType::class => self::checkIterable($super, $sub), - ListType::class => self::checkList($super, $sub), - MapType::class => self::checkMap($super, $sub), - MixedType::class => true, - NeverType::class => false, - NullType::class => $sub instanceof NullType, - ScalarType::class => self::checkScalar($sub), - StringLiteralType::class => self::checkStringLiteral($super, $sub), - StringType::class => self::checkString($super, $sub), - StructType::class => self::checkStruct($super, $sub), - TupleType::class => self::checkTuple($super, $sub), - UnionType::class => self::checkUnion($super, $sub), - default => throw new LogicException(sprintf('Unsupported type "%s"', $superClass)), + return match (true) { + $super instanceof BoolType => self::checkBool($super, $sub), + $super instanceof CallableType => self::checkCallable($super, $sub), + $super instanceof ClassLikeType => self::checkClassLike($super, $sub), + $super instanceof ClassStringType => self::checkClassString($super, $sub), + $super instanceof FloatType => self::checkFloat($sub), + $super instanceof IntLiteralType => self::checkIntLiteral($super, $sub), + $super instanceof IntType => self::checkInt($super, $sub), + $super instanceof IterableType => self::checkIterable($super, $sub), + $super instanceof ListType => self::checkList($super, $sub), + $super instanceof MapType => self::checkMap($super, $sub), + $super instanceof MixedType => true, + $super instanceof NeverType => false, + $super instanceof NullType => $sub instanceof NullType, + $super instanceof ScalarType => self::checkScalar($sub), + $super instanceof StringLiteralType => self::checkStringLiteral($super, $sub), + $super instanceof StringType => self::checkString($super, $sub), + $super instanceof StructType => self::checkStruct($super, $sub), + $super instanceof TupleType => self::checkTuple($super, $sub), + $super instanceof UnionType => self::checkUnion($super, $sub), + default => throw new LogicException(sprintf('Unsupported type "%s"', get_class($super))), }; } diff --git a/src/Scope.php b/src/Scope.php index a537e69..cbfb388 100644 --- a/src/Scope.php +++ b/src/Scope.php @@ -4,7 +4,6 @@ namespace PhpTypes\Types; -use PhpTypes\Ast\Node\IdentifierNode; use RuntimeException; use function count; @@ -63,20 +62,6 @@ private static function int(array $typeParameters): IntType $typeParameters[1]->value, ); } - if ( - $typeParameters[0] instanceof IntLiteralType - && $typeParameters[1] instanceof IdentifierNode - && $typeParameters[1]->name === 'max' - ) { - return IntType::min($typeParameters[0]->value); - } - if ( - $typeParameters[0] instanceof IdentifierNode - && $typeParameters[0]->name === 'min' - && $typeParameters[1] instanceof IntLiteralType - ) { - return IntType::max($typeParameters[1]->value); - } throw new RuntimeException( sprintf( 'Integer types must take one of the following forms: ' . diff --git a/src/StructType.php b/src/StructType.php index a0d9e29..a7f34d4 100644 --- a/src/StructType.php +++ b/src/StructType.php @@ -45,6 +45,9 @@ public function toIterable(): IterableType */ public function keyAndValueType(): array { + if ($this->members === []) { + return [StringType::nonEmpty(), new MixedType()]; + } /** @var array{AbstractType, AbstractType}|null $types */ $types = null; foreach ($this->members as $name => $member) { diff --git a/src/TupleType.php b/src/TupleType.php index 192e14d..4d4f153 100644 --- a/src/TupleType.php +++ b/src/TupleType.php @@ -39,6 +39,9 @@ public function toIterable(): IterableType public function valueType(): AbstractType { + if ($this->elements === []) { + return new MixedType(); + } $valueType = null; foreach ($this->elements as $element) { if ($valueType === null) { diff --git a/src/Type.php b/src/Type.php index 0f5f71d..0f1f7b5 100644 --- a/src/Type.php +++ b/src/Type.php @@ -44,6 +44,9 @@ private static function fromNode(NodeInterface $node, Scope $scope): AbstractTyp $node instanceof StructNode => self::fromStruct($node->members, $scope), $node instanceof TupleNode => self::fromTuple($node, $scope), $node instanceof UnionNode => self::fromUnion($node, $scope), + default => throw new RuntimeException( + sprintf('Unsupported node type: %s (%s)', get_class($node), $node) + ), }; } @@ -90,7 +93,7 @@ private static function fromTuple(TupleNode $node, Scope $scope): TupleType } /** - * @param list $members + * @param array $members */ private static function fromStruct(array $members, Scope $scope): StructType { diff --git a/tests/functional/CompatibilityTest.php b/tests/functional/CompatibilityTest.php index ebddcb1..d1c9a4c 100644 --- a/tests/functional/CompatibilityTest.php +++ b/tests/functional/CompatibilityTest.php @@ -42,7 +42,7 @@ private static function compatibleTypes(): array { $types = []; foreach (self::filesInDirectory(__DIR__ . '/compatible-types/') as $file) { - foreach (explode("\n", file_get_contents($file)) as $line) { + foreach (explode("\n", \Safe\file_get_contents($file)) as $line) { $isMatch = \Safe\preg_match('/^- `(?.+)` is a subtype of `(?.+)`/', $line, $matches); if ($isMatch === 0) { continue; @@ -73,7 +73,7 @@ private static function filesInDirectory(string $directory): iterable private static function types(): iterable { foreach (self::typeFiles() as $file) { - foreach (explode("\n", file_get_contents($file)) as $line) { + foreach (explode("\n", \Safe\file_get_contents($file)) as $line) { if ($line === '') { continue; } @@ -96,7 +96,7 @@ private static function typeFiles(): iterable private static function aliases(): array { $aliases = []; - foreach (explode("\n", file_get_contents(__DIR__ . '/aliases.md')) as $line) { + foreach (explode("\n", \Safe\file_get_contents(__DIR__ . '/aliases.md')) as $line) { $isMatch = \Safe\preg_match('/^- `(?.+)` is an alias of `(?.+)`/', $line, $matches); if ($isMatch === 0) { continue; @@ -168,7 +168,7 @@ public function testEveryTypeIsCompatibleWithMixed(string $type): void } /** - * @return iterable + * @return iterable */ public function allTypes(): iterable { From cf2edb34b2a909d05254aaa95b080e2edb94f23e Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 16 Aug 2022 20:08:33 +0200 Subject: [PATCH 16/53] Remove an unused function import --- tests/functional/CompatibilityTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/functional/CompatibilityTest.php b/tests/functional/CompatibilityTest.php index d1c9a4c..ae76026 100644 --- a/tests/functional/CompatibilityTest.php +++ b/tests/functional/CompatibilityTest.php @@ -16,7 +16,6 @@ use function array_map; use function array_search; use function explode; -use function file_get_contents; use function in_array; use function sort; use function sprintf; From d24210049000f463a30ad4a0c237c49ad2b12831 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 16 Aug 2022 20:13:08 +0200 Subject: [PATCH 17/53] Don't fail fast --- .github/workflows/phpunit.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index d141c3a..f05ec42 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -11,6 +11,7 @@ jobs: matrix: php: [ '8.1' ] prefer-lowest: [ '--prefer-lowest', '' ] + fail-fast: false name: PHPUnit on PHP ${{ matrix.php }} From e47f7f1a01b0f8a7ee53858dce26d94b6fdff703 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 16 Aug 2022 20:14:44 +0200 Subject: [PATCH 18/53] Try adding a blank line --- tests/functional/compatible-types/list.md | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/functional/compatible-types/list.md b/tests/functional/compatible-types/list.md index 272ba3f..9b35bce 100644 --- a/tests/functional/compatible-types/list.md +++ b/tests/functional/compatible-types/list.md @@ -1,3 +1,4 @@ + - `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` From 60e64f24dfc74328fa9fc50a3ff895f159110f84 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 16 Aug 2022 20:17:37 +0200 Subject: [PATCH 19/53] Try duplicating the line --- tests/functional/compatible-types/list.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/compatible-types/list.md b/tests/functional/compatible-types/list.md index 9b35bce..5048d94 100644 --- a/tests/functional/compatible-types/list.md +++ b/tests/functional/compatible-types/list.md @@ -1,4 +1,4 @@ - +- `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` From 07027fd1b4585051cf954686a6eb7d7b693e1f5f Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 16 Aug 2022 20:19:00 +0200 Subject: [PATCH 20/53] Only run CI on pull requests --- .github/workflows/infection.yml | 2 +- .github/workflows/phpcs.yml | 2 +- .github/workflows/phpstan.yml | 2 +- .github/workflows/phpunit.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/infection.yml b/.github/workflows/infection.yml index 5eb7dce..70b5111 100644 --- a/.github/workflows/infection.yml +++ b/.github/workflows/infection.yml @@ -1,6 +1,6 @@ name: Infection -on: [ pull_request, push ] +on: [ pull_request ] jobs: build: diff --git a/.github/workflows/phpcs.yml b/.github/workflows/phpcs.yml index 2ff0e1b..bedf0c7 100644 --- a/.github/workflows/phpcs.yml +++ b/.github/workflows/phpcs.yml @@ -1,6 +1,6 @@ name: PHPCS -on: [ pull_request, push ] +on: [ pull_request ] jobs: build: diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index c273ebc..c95af5d 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -1,6 +1,6 @@ name: PHPStan -on: [ pull_request, push ] +on: [ pull_request ] jobs: build: diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index f05ec42..20f81aa 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -1,6 +1,6 @@ name: PHPUnit -on: [ pull_request, push ] +on: [ pull_request ] jobs: build: From fc13758911b4ff7231dfdcf50b4786f72a9c6a71 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 16 Aug 2022 20:20:57 +0200 Subject: [PATCH 21/53] Delete a performance optimization (until we really need it) --- src/BoolType.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/BoolType.php b/src/BoolType.php index 35b6e08..7cc9e54 100644 --- a/src/BoolType.php +++ b/src/BoolType.php @@ -13,15 +13,6 @@ public function __construct(public readonly ?bool $value = null) { } - public function __toString(): string - { - return match ($this->value) { - null => 'bool', - true => 'true', - false => 'false', - }; - } - public function toNode(): NodeInterface { return new IdentifierNode( From 9d245d98d0d2dcdda7f4491889effd6be9d4bc01 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 16 Aug 2022 20:44:43 +0200 Subject: [PATCH 22/53] Clean up --- src/IntType.php | 5 -- src/ListType.php | 5 -- src/MapType.php | 2 +- src/Scope.php | 39 ---------------- src/StringType.php | 8 ---- src/StructType.php | 2 +- src/UnionType.php | 52 +-------------------- tests/functional/compatible-types/list.md | 1 - tests/functional/compatible-types/struct.md | 12 +++++ tests/functional/types/struct.txt | 1 + 10 files changed, 16 insertions(+), 111 deletions(-) diff --git a/src/IntType.php b/src/IntType.php index 07995f2..5d801d2 100644 --- a/src/IntType.php +++ b/src/IntType.php @@ -14,11 +14,6 @@ public function __construct(public readonly int|null $min = null, public readonl { } - public static function minMax(int $min, int $max): self - { - return new self($min, $max); - } - public static function min(int $min): self { return new self($min, null); diff --git a/src/ListType.php b/src/ListType.php index 29443fa..83745f9 100644 --- a/src/ListType.php +++ b/src/ListType.php @@ -15,11 +15,6 @@ public function __construct(public readonly AbstractType $type, public readonly { } - public function nonEmpty(AbstractType $type): AbstractType - { - return new ListType($type, true); - } - public function toNode(): NodeInterface { return new IdentifierNode($this->nonEmpty ? 'non-empty-list' : 'list', [$this->type->toNode()]); diff --git a/src/MapType.php b/src/MapType.php index 877ec40..ebcf214 100644 --- a/src/MapType.php +++ b/src/MapType.php @@ -19,7 +19,7 @@ public function __construct( ) { } - public function nonEmpty(AbstractType $keyType, AbstractType $valueType): AbstractType + public static function nonEmpty(AbstractType $keyType, AbstractType $valueType): self { return new self($keyType, $valueType, true); } diff --git a/src/Scope.php b/src/Scope.php index cbfb388..64c7c1e 100644 --- a/src/Scope.php +++ b/src/Scope.php @@ -24,7 +24,6 @@ public static function global(): self $scope->register('bool', new BoolType()); $scope->register('false', new BoolType(false)); $scope->register('float', new FloatType()); - $scope->register('int', self::int(...)); $scope->register('iterable', $scope->iterable(...)); $scope->register('list', self::list(...)); $scope->register('mixed', new MixedType()); @@ -44,44 +43,6 @@ public static function global(): self return $scope; } - /** - * @param list $typeParameters - */ - private static function int(array $typeParameters): IntType - { - switch (count($typeParameters)) { - case 0: - return new IntType(); - case 2: - if ( - $typeParameters[0] instanceof IntLiteralType - && $typeParameters[1] instanceof IntLiteralType - ) { - return IntType::minMax( - $typeParameters[0]->value, - $typeParameters[1]->value, - ); - } - throw new RuntimeException( - sprintf( - 'Integer types must take one of the following forms: ' . - 'int, `int<23, 42>`, `int`, `int<23, max>`. ' . - 'Got: int<%s>', - implode(', ', $typeParameters), - ) - ); - default: - throw new RuntimeException( - sprintf( - 'Integer types must take one of the following forms: ' . - 'int, `int<23, 42>`, `int`, `int<23, max>`. ' . - 'Got: int<%s>', - implode(', ', $typeParameters), - ) - ); - } - } - /** * @param list $types */ diff --git a/src/StringType.php b/src/StringType.php index a530a5c..cc09eba 100644 --- a/src/StringType.php +++ b/src/StringType.php @@ -23,14 +23,6 @@ public static function numeric(): AbstractType return new StringType(true, true); } - public function __toString(): string - { - if ($this->numeric) { - return 'numeric-string'; - } - return $this->nonEmpty ? 'non-empty-string' : 'string'; - } - public function toNode(): NodeInterface { if ($this->numeric) { diff --git a/src/StructType.php b/src/StructType.php index a7f34d4..eec3cbd 100644 --- a/src/StructType.php +++ b/src/StructType.php @@ -31,7 +31,7 @@ public function toNode(): NodeInterface public function toMap(): MapType { $types = $this->keyAndValueType(); - return new MapType($types[0], $types[1], true); + return MapType::nonEmpty($types[0], $types[1]); } public function toIterable(): IterableType diff --git a/src/UnionType.php b/src/UnionType.php index 9b1d516..d73753d 100644 --- a/src/UnionType.php +++ b/src/UnionType.php @@ -6,10 +6,8 @@ use PhpTypes\Ast\Node\NodeInterface; use PhpTypes\Ast\Node\UnionNode; -use PhpTypes\Types\Conversion\ToIterableInterface; -use PhpTypes\Types\Conversion\ToMapInterface; -final class UnionType extends AbstractType implements ToIterableInterface, ToMapInterface +final class UnionType extends AbstractType { public function __construct(public readonly AbstractType $left, public readonly AbstractType $right) { @@ -30,52 +28,4 @@ public function flatten(): array $this->right instanceof UnionType ? $this->right->flatten() : [$this->right], ); } - - public function toIterable(): IterableType|NeverType - { - $left = $this->left instanceof ToIterableInterface ? $this->left->toIterable() : $this->left; - if (!$left instanceof IterableType) { - return new NeverType(); - } - $right = $this->right instanceof ToIterableInterface ? $this->right->toIterable() : $this->right; - if (!$right instanceof IterableType) { - return new NeverType(); - } - return new IterableType( - new UnionType($left->keyType, $right->keyType), - new UnionType($left->valueType, $right->valueType), - ); - } - - public function toMap(): MapType|NeverType - { - $left = $this->left instanceof ToMapInterface ? $this->left->toMap() : $this->left; - if (!$left instanceof MapType) { - return new NeverType(); - } - $right = $this->right instanceof ToMapInterface ? $this->right->toMap() : $this->right; - if (!$right instanceof MapType) { - return new NeverType(); - } - return new MapType( - new UnionType($left->keyType, $right->keyType), - new UnionType($left->valueType, $right->valueType), - ); - } - - public function toList(): ListType|NeverType - { - $left = $this->left instanceof self ? $this->left->toList() : $this->left; - if (!$left instanceof ListType) { - return new NeverType(); - } - $right = $this->right instanceof self ? $this->right->toList() : $this->right; - if (!$right instanceof ListType) { - return new NeverType(); - } - return new ListType( - new UnionType($left->type, $right->type), - $left->nonEmpty || $right->nonEmpty, - ); - } } diff --git a/tests/functional/compatible-types/list.md b/tests/functional/compatible-types/list.md index 5048d94..272ba3f 100644 --- a/tests/functional/compatible-types/list.md +++ b/tests/functional/compatible-types/list.md @@ -1,5 +1,4 @@ - `list` is a subtype of `array` -- `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` diff --git a/tests/functional/compatible-types/struct.md b/tests/functional/compatible-types/struct.md index f7e1bd3..8338903 100644 --- a/tests/functional/compatible-types/struct.md +++ b/tests/functional/compatible-types/struct.md @@ -10,6 +10,18 @@ - `array{age?: int, name: string}` is a subtype of `iterable` - `array{age?: int, name: string}` is a subtype of `iterable` +- `array{name: int}` is a subtype of `array` +- `array{name: int}` is a subtype of `array<'name' | 'age', string | int>` +- `array{name: int}` is a subtype of `array` +- `array{name: int}` is a subtype of `array` +- `array{name: int}` is a subtype of `array` +- `array{name: int}` is a subtype of `array` +- `array{name: int}` is a subtype of `array` +- `array{name: int}` is a subtype of `array{name: int}` +- `array{name: int}` is a subtype of `iterable` +- `array{name: int}` is a subtype of `iterable` +- `array{name: int}` is a subtype of `iterable` + - `array{name: string}` is a subtype of `array` - `array{name: string}` is a subtype of `array<'name' | 'age', string | int>` - `array{name: string}` is a subtype of `array` diff --git a/tests/functional/types/struct.txt b/tests/functional/types/struct.txt index a58b185..4587949 100644 --- a/tests/functional/types/struct.txt +++ b/tests/functional/types/struct.txt @@ -1,4 +1,5 @@ array{age?: int, name: string} +array{name: int} array{name: string} array{name: string, age: int} array{name: string, age?: int} From eae9f231841ccd2ccb894f94eee5fb45646f36a2 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 17:01:18 +0200 Subject: [PATCH 23/53] Add tests for invalid types --- src/MapType.php | 13 +++-- tests/functional/CompatibilityTest.php | 24 +++++++-- tests/functional/InvalidTypesTest.php | 49 +++++++++++++++++++ tests/functional/aliases.md | 1 + tests/functional/compatible-types/list.md | 7 ++- .../compatible-types/string-literal.md | 8 +++ tests/functional/compatible-types/string.md | 12 +++++ tests/functional/compatible-types/tuple.md | 2 + tests/functional/compatible-types/union.md | 10 ++++ tests/functional/types/union.txt | 2 + 10 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 tests/functional/InvalidTypesTest.php diff --git a/src/MapType.php b/src/MapType.php index ebcf214..a9824bc 100644 --- a/src/MapType.php +++ b/src/MapType.php @@ -7,8 +7,10 @@ use PhpTypes\Ast\Node\IdentifierNode; use PhpTypes\Ast\Node\NodeInterface; use PhpTypes\Types\Conversion\ToIterableInterface; +use RuntimeException; use function in_array; +use function sprintf; final class MapType extends AbstractType implements ToIterableInterface { @@ -17,11 +19,12 @@ public function __construct( public readonly AbstractType $valueType, public readonly bool $nonEmpty = false, ) { - } - - public static function nonEmpty(AbstractType $keyType, AbstractType $valueType): self - { - return new self($keyType, $valueType, true); + if (Compatibility::check(new UnionType(new StringType(), new IntType()), $keyType)) { + return; + } + throw new RuntimeException( + sprintf('Can\'t use %s as array key. Only strings and integers are allowed.', $keyType), + ); } private static function isArrayKey(AbstractType $type): bool diff --git a/tests/functional/CompatibilityTest.php b/tests/functional/CompatibilityTest.php index ae76026..d71a0b5 100644 --- a/tests/functional/CompatibilityTest.php +++ b/tests/functional/CompatibilityTest.php @@ -6,6 +6,7 @@ use DirectoryIterator; use LogicException; +use PhpTypes\Types\AbstractType; use PhpTypes\Types\ClassLikeType; use PhpTypes\Types\Compatibility; use PhpTypes\Types\MixedType; @@ -22,6 +23,9 @@ final class CompatibilityTest extends TestCase { + /** @var array */ + private static array $cache = []; + private static Scope $scope; public static function setUpBeforeClass(): void @@ -107,13 +111,23 @@ private static function aliases(): array return $aliases; } + private static function fromString(string $typeString, Scope $scope): AbstractType + { + $type = self::$cache[$typeString] ?? null; + if ($type === null) { + $type = Type::fromString($typeString, $scope); + self::$cache[$typeString] = $type; + } + return $type; + } + /** * @dataProvider compatibilityCases */ public function testCompatibility(string $super, string $sub, bool $expected): void { - $superType = Type::fromString($super, self::$scope); - $subType = Type::fromString($sub, self::$scope); + $superType = self::fromString($super, self::$scope); + $subType = self::fromString($sub, self::$scope); $message = $expected ? sprintf('Expected "%s" to be a subtype of "%s", but it is not', $sub, $super) @@ -161,7 +175,7 @@ public function compatibilityCases(): iterable public function testEveryTypeIsCompatibleWithMixed(string $type): void { self::assertTrue( - Compatibility::check(new MixedType(), Type::fromString($type, self::$scope)), + Compatibility::check(new MixedType(), self::fromString($type, self::$scope)), sprintf('Expected "%s" to be a subtype of "mixed", but it is not', $type), ); } @@ -181,8 +195,8 @@ public function allTypes(): iterable */ public function testAliases(string $a, string $b, bool $expected): void { - $aType = Type::fromString($a, self::$scope); - $bType = Type::fromString($b, self::$scope); + $aType = self::fromString($a, self::$scope); + $bType = self::fromString($b, self::$scope); $isAlias = Compatibility::check($aType, $bType) && Compatibility::check($bType, $aType); diff --git a/tests/functional/InvalidTypesTest.php b/tests/functional/InvalidTypesTest.php new file mode 100644 index 0000000..3728b91 --- /dev/null +++ b/tests/functional/InvalidTypesTest.php @@ -0,0 +1,49 @@ +register('Foo', new ClassLikeType('Foo')); + $scope->register('Bar', new ClassLikeType('Bar')); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage($expectedMessage); + + Type::fromString($typeString, $scope); + } + + /** + * @return iterable + */ + public function invalidTypes(): iterable + { + yield 'Boolean array key' => [ + 'array', + 'Can\'t use bool as array key. Only strings and integers are allowed.', + ]; + yield 'Iterable with three type parameters' => [ + 'iterable', + 'Iterable types must take one of the following forms: ' . + 'iterable, iterable, iterable', + ]; + yield 'Class string with two type parameters' => [ + 'class-string', + 'class-string takes zero or one type parameters', + ]; + } +} diff --git a/tests/functional/aliases.md b/tests/functional/aliases.md index 7f7bbd3..86729d8 100644 --- a/tests/functional/aliases.md +++ b/tests/functional/aliases.md @@ -18,3 +18,4 @@ - `bool | true` is an alias of `true | false` - `bool | true` is an alias of `false | true` - `string | 'foo'` is an alias of `string` +- `string | list` is an alias of `list | string` diff --git a/tests/functional/compatible-types/list.md b/tests/functional/compatible-types/list.md index 272ba3f..0fe4de4 100644 --- a/tests/functional/compatible-types/list.md +++ b/tests/functional/compatible-types/list.md @@ -1,3 +1,4 @@ + - `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` @@ -32,6 +33,7 @@ - `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` +- `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `iterable` - `list` is a subtype of `iterable` @@ -40,7 +42,8 @@ - `list` is a subtype of `list` - `list` is a subtype of `list` - `list` is a subtype of `list | list` -- `list` is a subtype of `array` +- `list` is a subtype of `list | string` +- `list` is a subtype of `string | list` - `non-empty-list` is a subtype of `array` - `non-empty-list` is a subtype of `array` @@ -54,5 +57,7 @@ - `non-empty-list` is a subtype of `list` - `non-empty-list` is a subtype of `list` - `non-empty-list` is a subtype of `list | list` +- `non-empty-list` is a subtype of `list | string` +- `non-empty-list` is a subtype of `string | list` - `non-empty-list` is a subtype of `non-empty-array` - `non-empty-list` is a subtype of `non-empty-list` diff --git a/tests/functional/compatible-types/string-literal.md b/tests/functional/compatible-types/string-literal.md index f533436..4ecb704 100644 --- a/tests/functional/compatible-types/string-literal.md +++ b/tests/functional/compatible-types/string-literal.md @@ -1,13 +1,16 @@ - `''` is a subtype of `''` - `''` is a subtype of `int | string` +- `''` is a subtype of `list | string` - `''` is a subtype of `scalar` - `''` is a subtype of `string` - `''` is a subtype of `string | 'foo'` - `''` is a subtype of `string | int` - `''` is a subtype of `string | int | bool` +- `''` is a subtype of `string | list` - `'42'` is a subtype of `'42'` - `'42'` is a subtype of `int | string` +- `'42'` is a subtype of `list | string` - `'42'` is a subtype of `non-empty-string` - `'42'` is a subtype of `numeric-string` - `'42'` is a subtype of `scalar` @@ -15,23 +18,28 @@ - `'42'` is a subtype of `string | 'foo'` - `'42'` is a subtype of `string | int` - `'42'` is a subtype of `string | int | bool` +- `'42'` is a subtype of `string | list` - `'foo'` is a subtype of `'foo'` - `'foo'` is a subtype of `'foo' | 'bar'` - `'foo'` is a subtype of `int | string` +- `'foo'` is a subtype of `list | string` - `'foo'` is a subtype of `non-empty-string` - `'foo'` is a subtype of `scalar` - `'foo'` is a subtype of `string` - `'foo'` is a subtype of `string | 'foo'` - `'foo'` is a subtype of `string | int` - `'foo'` is a subtype of `string | int | bool` +- `'foo'` is a subtype of `string | list` - `'bar'` is a subtype of `'bar'` - `'bar'` is a subtype of `'foo' | 'bar'` - `'bar'` is a subtype of `int | string` +- `'bar'` is a subtype of `list | string` - `'bar'` is a subtype of `non-empty-string` - `'bar'` is a subtype of `scalar` - `'bar'` is a subtype of `string` - `'bar'` is a subtype of `string | 'foo'` - `'bar'` is a subtype of `string | int` - `'bar'` is a subtype of `string | int | bool` +- `'bar'` is a subtype of `string | list` diff --git a/tests/functional/compatible-types/string.md b/tests/functional/compatible-types/string.md index 41f5730..88e6725 100644 --- a/tests/functional/compatible-types/string.md +++ b/tests/functional/compatible-types/string.md @@ -1,42 +1,51 @@ - `class-string` is a subtype of `class-string` - `class-string` is a subtype of `int | string` +- `class-string` is a subtype of `list | string` - `class-string` is a subtype of `non-empty-string` - `class-string` is a subtype of `scalar` - `class-string` is a subtype of `string` - `class-string` is a subtype of `string | 'foo'` - `class-string` is a subtype of `string | int` - `class-string` is a subtype of `string | int | bool` +- `class-string` is a subtype of `string | list` - `class-string` is a subtype of `class-string` - `class-string` is a subtype of `class-string` - `class-string` is a subtype of `class-string` - `class-string` is a subtype of `int | string` +- `class-string` is a subtype of `list | string` - `class-string` is a subtype of `non-empty-string` - `class-string` is a subtype of `scalar` - `class-string` is a subtype of `string` - `class-string` is a subtype of `string | 'foo'` - `class-string` is a subtype of `string | int` - `class-string` is a subtype of `string | int | bool` +- `class-string` is a subtype of `string | list` - `class-string` is a subtype of `class-string` - `class-string` is a subtype of `class-string` - `class-string` is a subtype of `int | string` +- `class-string` is a subtype of `list | string` - `class-string` is a subtype of `non-empty-string` - `class-string` is a subtype of `scalar` - `class-string` is a subtype of `string` - `class-string` is a subtype of `string | 'foo'` - `class-string` is a subtype of `string | int` - `class-string` is a subtype of `string | int | bool` +- `class-string` is a subtype of `string | list` - `non-empty-string` is a subtype of `int | string` +- `non-empty-string` is a subtype of `list | string` - `non-empty-string` is a subtype of `non-empty-string` - `non-empty-string` is a subtype of `scalar` - `non-empty-string` is a subtype of `string` - `non-empty-string` is a subtype of `string | 'foo'` - `non-empty-string` is a subtype of `string | int` - `non-empty-string` is a subtype of `string | int | bool` +- `non-empty-string` is a subtype of `string | list` - `numeric-string` is a subtype of `int | string` +- `numeric-string` is a subtype of `list | string` - `numeric-string` is a subtype of `non-empty-string` - `numeric-string` is a subtype of `numeric-string` - `numeric-string` is a subtype of `scalar` @@ -44,10 +53,13 @@ - `numeric-string` is a subtype of `string | 'foo'` - `numeric-string` is a subtype of `string | int` - `numeric-string` is a subtype of `string | int | bool` +- `numeric-string` is a subtype of `string | list` - `string` is a subtype of `int | string` +- `string` is a subtype of `list | string` - `string` is a subtype of `scalar` - `string` is a subtype of `string` - `string` is a subtype of `string | 'foo'` - `string` is a subtype of `string | int` - `string` is a subtype of `string | int | bool` +- `string` is a subtype of `string | list` diff --git a/tests/functional/compatible-types/tuple.md b/tests/functional/compatible-types/tuple.md index 6cdfba6..7ae9506 100644 --- a/tests/functional/compatible-types/tuple.md +++ b/tests/functional/compatible-types/tuple.md @@ -42,4 +42,6 @@ - `array{string, string}` is a subtype of `list` - `array{string, string}` is a subtype of `list` - `array{string, string}` is a subtype of `list | list` +- `array{string, string}` is a subtype of `list | string` - `array{string, string}` is a subtype of `non-empty-list` +- `array{string, string}` is a subtype of `string | list` diff --git a/tests/functional/compatible-types/union.md b/tests/functional/compatible-types/union.md index 240160e..7987a48 100644 --- a/tests/functional/compatible-types/union.md +++ b/tests/functional/compatible-types/union.md @@ -1,11 +1,13 @@ - `'foo' | 'bar'` is a subtype of `'foo' | 'bar'` - `'foo' | 'bar'` is a subtype of `int | string` +- `'foo' | 'bar'` is a subtype of `list | string` - `'foo' | 'bar'` is a subtype of `non-empty-string` - `'foo' | 'bar'` is a subtype of `scalar` - `'foo' | 'bar'` is a subtype of `string` - `'foo' | 'bar'` is a subtype of `string | 'foo'` - `'foo' | 'bar'` is a subtype of `string | int` - `'foo' | 'bar'` is a subtype of `string | int | bool` +- `'foo' | 'bar'` is a subtype of `string | list` - `bool | true` is a subtype of `bool` - `bool | true` is a subtype of `bool | true` @@ -36,12 +38,17 @@ - `list | list` is a subtype of `iterable` - `list | list` is a subtype of `iterable` +- `list | string` is a subtype of `list | string` +- `list | string` is a subtype of `string | list` + - `string | 'foo'` is a subtype of `int | string` +- `string | 'foo'` is a subtype of `list | string` - `string | 'foo'` is a subtype of `scalar` - `string | 'foo'` is a subtype of `string` - `string | 'foo'` is a subtype of `string | 'foo'` - `string | 'foo'` is a subtype of `string | int` - `string | 'foo'` is a subtype of `string | int | bool` +- `string | 'foo'` is a subtype of `string | list` - `string | int` is a subtype of `int | string` - `string | int` is a subtype of `scalar` @@ -51,6 +58,9 @@ - `string | int | bool` is a subtype of `scalar` - `string | int | bool` is a subtype of `string | int | bool` +- `string | list` is a subtype of `list | string` +- `string | list` is a subtype of `string | list` + - `true | false` is a subtype of `bool` - `true | false` is a subtype of `bool | true` - `true | false` is a subtype of `false | true` diff --git a/tests/functional/types/union.txt b/tests/functional/types/union.txt index ab39885..bd7396a 100644 --- a/tests/functional/types/union.txt +++ b/tests/functional/types/union.txt @@ -7,3 +7,5 @@ false | true 'foo' | 'bar' bool | true string | 'foo' +list | string +string | list From 5b954bf57a3b806554f33eaf5295a2358da01ed9 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 17:11:25 +0200 Subject: [PATCH 24/53] Add a missing constructor --- src/MapType.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/MapType.php b/src/MapType.php index a9824bc..0396a65 100644 --- a/src/MapType.php +++ b/src/MapType.php @@ -27,6 +27,11 @@ public function __construct( ); } + public static function nonEmpty(AbstractType $keyType, AbstractType $valueType): self + { + return new self($keyType, $valueType, true); + } + private static function isArrayKey(AbstractType $type): bool { if (!$type instanceof UnionType) { From 2df5e7fb2828f3024102ddc79d85a6541b127a32 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 17:29:04 +0200 Subject: [PATCH 25/53] Try not including the "string start" matcher in regex --- tests/functional/CompatibilityTest.php | 2 +- tests/functional/compatible-types/list.md | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/functional/CompatibilityTest.php b/tests/functional/CompatibilityTest.php index d71a0b5..5898df5 100644 --- a/tests/functional/CompatibilityTest.php +++ b/tests/functional/CompatibilityTest.php @@ -46,7 +46,7 @@ private static function compatibleTypes(): array $types = []; foreach (self::filesInDirectory(__DIR__ . '/compatible-types/') as $file) { foreach (explode("\n", \Safe\file_get_contents($file)) as $line) { - $isMatch = \Safe\preg_match('/^- `(?.+)` is a subtype of `(?.+)`/', $line, $matches); + $isMatch = \Safe\preg_match('/- `(?.+)` is a subtype of `(?.+)`/', $line, $matches); if ($isMatch === 0) { continue; } diff --git a/tests/functional/compatible-types/list.md b/tests/functional/compatible-types/list.md index 0fe4de4..31e9abe 100644 --- a/tests/functional/compatible-types/list.md +++ b/tests/functional/compatible-types/list.md @@ -1,4 +1,3 @@ - - `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` From ec38208e77d5b83db2a7771781150afd4d33f4be Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 17:30:16 +0200 Subject: [PATCH 26/53] Try trimming the line --- tests/functional/CompatibilityTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/functional/CompatibilityTest.php b/tests/functional/CompatibilityTest.php index 5898df5..8ecbfe7 100644 --- a/tests/functional/CompatibilityTest.php +++ b/tests/functional/CompatibilityTest.php @@ -20,6 +20,7 @@ use function in_array; use function sort; use function sprintf; +use function trim; final class CompatibilityTest extends TestCase { @@ -46,7 +47,7 @@ private static function compatibleTypes(): array $types = []; foreach (self::filesInDirectory(__DIR__ . '/compatible-types/') as $file) { foreach (explode("\n", \Safe\file_get_contents($file)) as $line) { - $isMatch = \Safe\preg_match('/- `(?.+)` is a subtype of `(?.+)`/', $line, $matches); + $isMatch = \Safe\preg_match('/- `(?.+)` is a subtype of `(?.+)`/', trim($line), $matches); if ($isMatch === 0) { continue; } From 1f90768063d9628449bb38705ccef4fbfbea42fa Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 17:34:31 +0200 Subject: [PATCH 27/53] Try logging non-matches --- tests/functional/CompatibilityTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/CompatibilityTest.php b/tests/functional/CompatibilityTest.php index 8ecbfe7..ec67297 100644 --- a/tests/functional/CompatibilityTest.php +++ b/tests/functional/CompatibilityTest.php @@ -20,7 +20,6 @@ use function in_array; use function sort; use function sprintf; -use function trim; final class CompatibilityTest extends TestCase { @@ -47,8 +46,9 @@ private static function compatibleTypes(): array $types = []; foreach (self::filesInDirectory(__DIR__ . '/compatible-types/') as $file) { foreach (explode("\n", \Safe\file_get_contents($file)) as $line) { - $isMatch = \Safe\preg_match('/- `(?.+)` is a subtype of `(?.+)`/', trim($line), $matches); + $isMatch = \Safe\preg_match('/- `(?.+)` is a subtype of `(?.+)`/', $line, $matches); if ($isMatch === 0) { + echo sprintf('Line "%s" in file "%s" is not a match', $line, $file); continue; } $types[] = [$matches['super'], $matches['sub']]; From 1693032572b9021366162a04c01589ab637b812d Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 17:40:02 +0200 Subject: [PATCH 28/53] Try inserting a blank line again --- tests/functional/CompatibilityTest.php | 1 - tests/functional/compatible-types/list.md | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/CompatibilityTest.php b/tests/functional/CompatibilityTest.php index ec67297..5898df5 100644 --- a/tests/functional/CompatibilityTest.php +++ b/tests/functional/CompatibilityTest.php @@ -48,7 +48,6 @@ private static function compatibleTypes(): array foreach (explode("\n", \Safe\file_get_contents($file)) as $line) { $isMatch = \Safe\preg_match('/- `(?.+)` is a subtype of `(?.+)`/', $line, $matches); if ($isMatch === 0) { - echo sprintf('Line "%s" in file "%s" is not a match', $line, $file); continue; } $types[] = [$matches['super'], $matches['sub']]; diff --git a/tests/functional/compatible-types/list.md b/tests/functional/compatible-types/list.md index 31e9abe..0fe4de4 100644 --- a/tests/functional/compatible-types/list.md +++ b/tests/functional/compatible-types/list.md @@ -1,3 +1,4 @@ + - `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` From 59115ca54081363dd27fca2baac7f336f4e4c697 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 17:41:46 +0200 Subject: [PATCH 29/53] Try moving the blank line after the bad case --- tests/functional/compatible-types/list.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/compatible-types/list.md b/tests/functional/compatible-types/list.md index 0fe4de4..5186474 100644 --- a/tests/functional/compatible-types/list.md +++ b/tests/functional/compatible-types/list.md @@ -1,5 +1,5 @@ - - `list` is a subtype of `array` + - `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` From 8e54719ed018171f10eae72f27e6f63a7ff3dd76 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 17:44:50 +0200 Subject: [PATCH 30/53] Blank lines didn't work --- tests/functional/compatible-types/list.md | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/functional/compatible-types/list.md b/tests/functional/compatible-types/list.md index 5186474..31e9abe 100644 --- a/tests/functional/compatible-types/list.md +++ b/tests/functional/compatible-types/list.md @@ -1,5 +1,4 @@ - `list` is a subtype of `array` - - `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` From 02ae70767e411bff76eede720f8c91868315ab60 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 17:56:07 +0200 Subject: [PATCH 31/53] Try outputting the list of compatible types --- tests/functional/CompatibilityTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/functional/CompatibilityTest.php b/tests/functional/CompatibilityTest.php index 5898df5..a402bcf 100644 --- a/tests/functional/CompatibilityTest.php +++ b/tests/functional/CompatibilityTest.php @@ -17,6 +17,7 @@ use function array_map; use function array_search; use function explode; +use function implode; use function in_array; use function sort; use function sprintf; @@ -141,6 +142,12 @@ public function testCompatibility(string $super, string $sub, bool $expected): v public function compatibilityCases(): iterable { $compatibleTypes = self::compatibleTypes(); + echo implode( + "\n", + array_map(static function (array $tuple): string { + return sprintf('- `%s` is a subtype of `%s`', $tuple[1], $tuple[0]); + }, $compatibleTypes), + ) . "\n"; foreach (self::types() as $super) { foreach (self::types() as $sub) { $compatibleTypesKey = array_search([$super, $sub], $compatibleTypes, true); From 090dcd7c537e202d12d6b1b9065047d76306364a Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 20:33:52 +0200 Subject: [PATCH 32/53] Try dumping everything --- tests/functional/CompatibilityTest.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/functional/CompatibilityTest.php b/tests/functional/CompatibilityTest.php index a402bcf..5cd4d9d 100644 --- a/tests/functional/CompatibilityTest.php +++ b/tests/functional/CompatibilityTest.php @@ -21,6 +21,7 @@ use function in_array; use function sort; use function sprintf; +use function var_dump; final class CompatibilityTest extends TestCase { @@ -142,12 +143,6 @@ public function testCompatibility(string $super, string $sub, bool $expected): v public function compatibilityCases(): iterable { $compatibleTypes = self::compatibleTypes(); - echo implode( - "\n", - array_map(static function (array $tuple): string { - return sprintf('- `%s` is a subtype of `%s`', $tuple[1], $tuple[0]); - }, $compatibleTypes), - ) . "\n"; foreach (self::types() as $super) { foreach (self::types() as $sub) { $compatibleTypesKey = array_search([$super, $sub], $compatibleTypes, true); @@ -155,6 +150,12 @@ public function compatibilityCases(): iterable $name = $expected ? sprintf('%s is a subtype of %s', $sub, $super) : sprintf('%s is not a subtype of %s', $sub, $super); + if ($name === 'list is not a subtype of array') { + var_dump($compatibleTypes); + var_dump($compatibleTypesKey); + var_dump([$super, $sub]); + var_dump($compatibleTypes[$compatibleTypesKey]); + } yield $name => [$super, $sub, $expected]; unset($compatibleTypes[$compatibleTypesKey]); } From f453b91b0a6decf6bed8fc1f00f10db367f60cfe Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 20:42:59 +0200 Subject: [PATCH 33/53] Check for "is file" instead of "is dot" --- tests/functional/CompatibilityTest.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/functional/CompatibilityTest.php b/tests/functional/CompatibilityTest.php index 5cd4d9d..83852a2 100644 --- a/tests/functional/CompatibilityTest.php +++ b/tests/functional/CompatibilityTest.php @@ -64,7 +64,7 @@ private static function compatibleTypes(): array private static function filesInDirectory(string $directory): iterable { foreach (new DirectoryIterator($directory) as $file) { - if ($file->isDot()) { + if (!$file->isFile()) { continue; } @@ -150,11 +150,10 @@ public function compatibilityCases(): iterable $name = $expected ? sprintf('%s is a subtype of %s', $sub, $super) : sprintf('%s is not a subtype of %s', $sub, $super); - if ($name === 'list is not a subtype of array') { + if ($name === 'list is a subtype of array') { var_dump($compatibleTypes); var_dump($compatibleTypesKey); var_dump([$super, $sub]); - var_dump($compatibleTypes[$compatibleTypesKey]); } yield $name => [$super, $sub, $expected]; unset($compatibleTypes[$compatibleTypesKey]); From ae7a6dbf28bc7a86a29d800d105667fcf85b009b Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 20:48:36 +0200 Subject: [PATCH 34/53] Try dumping the compatible types --- tests/functional/CompatibilityTest.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/functional/CompatibilityTest.php b/tests/functional/CompatibilityTest.php index 83852a2..9e3fa7c 100644 --- a/tests/functional/CompatibilityTest.php +++ b/tests/functional/CompatibilityTest.php @@ -64,7 +64,7 @@ private static function compatibleTypes(): array private static function filesInDirectory(string $directory): iterable { foreach (new DirectoryIterator($directory) as $file) { - if (!$file->isFile()) { + if ($file->isDot()) { continue; } @@ -143,6 +143,7 @@ public function testCompatibility(string $super, string $sub, bool $expected): v public function compatibilityCases(): iterable { $compatibleTypes = self::compatibleTypes(); + var_dump($compatibleTypes); foreach (self::types() as $super) { foreach (self::types() as $sub) { $compatibleTypesKey = array_search([$super, $sub], $compatibleTypes, true); @@ -150,11 +151,6 @@ public function compatibilityCases(): iterable $name = $expected ? sprintf('%s is a subtype of %s', $sub, $super) : sprintf('%s is not a subtype of %s', $sub, $super); - if ($name === 'list is a subtype of array') { - var_dump($compatibleTypes); - var_dump($compatibleTypesKey); - var_dump([$super, $sub]); - } yield $name => [$super, $sub, $expected]; unset($compatibleTypes[$compatibleTypesKey]); } From 083a5b491ab41dbdaa1290a91f5a547bb90562df Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 20:52:29 +0200 Subject: [PATCH 35/53] Try using is_int() instead of !== false --- tests/functional/CompatibilityTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/functional/CompatibilityTest.php b/tests/functional/CompatibilityTest.php index 9e3fa7c..8b8ea2e 100644 --- a/tests/functional/CompatibilityTest.php +++ b/tests/functional/CompatibilityTest.php @@ -19,6 +19,7 @@ use function explode; use function implode; use function in_array; +use function is_int; use function sort; use function sprintf; use function var_dump; @@ -147,7 +148,7 @@ public function compatibilityCases(): iterable foreach (self::types() as $super) { foreach (self::types() as $sub) { $compatibleTypesKey = array_search([$super, $sub], $compatibleTypes, true); - $expected = $compatibleTypesKey !== false; + $expected = is_int($compatibleTypesKey); $name = $expected ? sprintf('%s is a subtype of %s', $sub, $super) : sprintf('%s is not a subtype of %s', $sub, $super); From 708598a327fc25d0e8242645c7a84a77f08b307f Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 21:26:23 +0200 Subject: [PATCH 36/53] Fix a bug where the first compatible type was sometimes removed --- tests/functional/CompatibilityTest.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/functional/CompatibilityTest.php b/tests/functional/CompatibilityTest.php index 8b8ea2e..cc8390f 100644 --- a/tests/functional/CompatibilityTest.php +++ b/tests/functional/CompatibilityTest.php @@ -17,12 +17,9 @@ use function array_map; use function array_search; use function explode; -use function implode; use function in_array; -use function is_int; use function sort; use function sprintf; -use function var_dump; final class CompatibilityTest extends TestCase { @@ -144,15 +141,17 @@ public function testCompatibility(string $super, string $sub, bool $expected): v public function compatibilityCases(): iterable { $compatibleTypes = self::compatibleTypes(); - var_dump($compatibleTypes); foreach (self::types() as $super) { foreach (self::types() as $sub) { $compatibleTypesKey = array_search([$super, $sub], $compatibleTypes, true); - $expected = is_int($compatibleTypesKey); + $expected = $compatibleTypesKey !== false; $name = $expected ? sprintf('%s is a subtype of %s', $sub, $super) : sprintf('%s is not a subtype of %s', $sub, $super); yield $name => [$super, $sub, $expected]; + if ($compatibleTypesKey === false) { + continue; + } unset($compatibleTypes[$compatibleTypesKey]); } } From 60212e2b72bb879afe7ec7b987a0ceec04baf9b0 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 21:38:23 +0200 Subject: [PATCH 37/53] Test the `never` type, remove an unnecessary condition --- src/Compatibility.php | 2 +- tests/functional/types/misc.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Compatibility.php b/src/Compatibility.php index b0ca5bf..cc9c105 100644 --- a/src/Compatibility.php +++ b/src/Compatibility.php @@ -23,7 +23,7 @@ private function __construct() public static function check(AbstractType $super, AbstractType $sub): bool { - if (!$super instanceof UnionType && $sub instanceof UnionType) { + if ($sub instanceof UnionType) { return self::checkSubUnion($super, $sub); } return match (true) { diff --git a/tests/functional/types/misc.txt b/tests/functional/types/misc.txt index a840023..1211185 100644 --- a/tests/functional/types/misc.txt +++ b/tests/functional/types/misc.txt @@ -1,3 +1,4 @@ float null scalar +never From 5d5e5c0108885a25ec069843c82bfa0ee0a0861f Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 21:52:47 +0200 Subject: [PATCH 38/53] Kill some mutants --- composer.json | 3 ++- src/Compatibility.php | 27 ++------------------------- tests/unit/CompatibilityTest.php | 30 ++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 26 deletions(-) create mode 100644 tests/unit/CompatibilityTest.php diff --git a/composer.json b/composer.json index 3133f95..ab7e754 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,8 @@ }, "autoload-dev": { "psr-4": { - "PhpTypes\\Types\\Tests\\Functional\\": "tests/functional" + "PhpTypes\\Types\\Tests\\Functional\\": "tests/functional", + "PhpTypes\\Types\\Tests\\Unit\\": "tests/unit" } }, "config": { diff --git a/src/Compatibility.php b/src/Compatibility.php index cc9c105..074eca9 100644 --- a/src/Compatibility.php +++ b/src/Compatibility.php @@ -8,6 +8,7 @@ use PhpTypes\Types\Conversion\ToIterableInterface; use PhpTypes\Types\Conversion\ToMapInterface; +use function array_key_exists; use function count; use function get_class; use function is_numeric; @@ -154,33 +155,9 @@ private static function checkFloat(AbstractType $sub): bool private static function checkUnion(UnionType $super, AbstractType $sub): bool { - if ($sub instanceof UnionType) { - $superTypes = $super->flatten(); - foreach ($sub->flatten() as $type) { - if (self::isSubtypeOfAny($type, $superTypes)) { - continue; - } - return false; - } - return true; - } return self::check($super->left, $sub) || self::check($super->right, $sub); } - /** - * @param list $haystack - */ - private static function isSubtypeOfAny(AbstractType $type, array $haystack): bool - { - foreach ($haystack as $item) { - if (!self::check($item, $type)) { - continue; - } - return true; - } - return false; - } - private static function checkScalar(AbstractType $sub): bool { return $sub instanceof ScalarType @@ -250,7 +227,7 @@ private static function checkStruct(StructType $super, AbstractType $sub): bool return false; } foreach ($super->members as $name => $member) { - $subMember = $sub->members[$name] ?? null; + $subMember = array_key_exists($name, $sub->members) ? $sub->members[$name] : null; if ($subMember === null) { return false; } diff --git a/tests/unit/CompatibilityTest.php b/tests/unit/CompatibilityTest.php new file mode 100644 index 0000000..b796961 --- /dev/null +++ b/tests/unit/CompatibilityTest.php @@ -0,0 +1,30 @@ +expectException(LogicException::class); + + Compatibility::check($super, new IntType()); + } +} From 6218f8e61ea55fca305a4a43108538311c3ae393 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 21:56:56 +0200 Subject: [PATCH 39/53] Add unit test suite --- phpunit.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/phpunit.xml b/phpunit.xml index 00e8e9d..9f975db 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,6 +7,9 @@ ./tests/functional + + ./tests/unit + From 6a2dcccf5c96ffce8ff9a3a4de0dec7953f360a4 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 22:00:32 +0200 Subject: [PATCH 40/53] Kill another mutant --- tests/functional/compatible-types/callable.md | 5 +++++ tests/functional/types/callable.txt | 1 + 2 files changed, 6 insertions(+) diff --git a/tests/functional/compatible-types/callable.md b/tests/functional/compatible-types/callable.md index 6556d94..8e96806 100644 --- a/tests/functional/compatible-types/callable.md +++ b/tests/functional/compatible-types/callable.md @@ -7,16 +7,21 @@ - `callable(string): int` is a subtype of `callable(string): float` - `callable(string): int` is a subtype of `callable(string): int` +- `callable(string): int` is a subtype of `callable(string, bool): int` - `callable(string): int` is a subtype of `callable(string, int): int` - `callable(string=): int` is a subtype of `callable(string): float` - `callable(string=): int` is a subtype of `callable(string): int` - `callable(string=): int` is a subtype of `callable(string=): int` +- `callable(string=): int` is a subtype of `callable(string, bool): int` - `callable(string=): int` is a subtype of `callable(string, int): int` +- `callable(string, bool): int` is a subtype of `callable(string, bool): int` + - `callable(string, int): int` is a subtype of `callable(string, int): int` - `callable(string | int): int` is a subtype of `callable(string): float` - `callable(string | int): int` is a subtype of `callable(string): int` +- `callable(string | int): int` is a subtype of `callable(string, bool): int` - `callable(string | int): int` is a subtype of `callable(string, int): int` - `callable(string | int): int` is a subtype of `callable(string | int): int` diff --git a/tests/functional/types/callable.txt b/tests/functional/types/callable.txt index 6b5c983..362fe63 100644 --- a/tests/functional/types/callable.txt +++ b/tests/functional/types/callable.txt @@ -3,5 +3,6 @@ callable(): string callable(string): float callable(string): int callable(string=): int +callable(string, bool): int callable(string, int): int callable(string | int): int From 627dab3069c8dcdf68fe3ef4d4e6462e854d0d8a Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 22:25:29 +0200 Subject: [PATCH 41/53] Kill even more mutants --- src/StructType.php | 2 +- src/TupleType.php | 3 ++- tests/functional/compatible-types/bool.md | 1 + tests/functional/compatible-types/list.md | 2 ++ tests/functional/compatible-types/tuple.md | 1 + tests/functional/compatible-types/union.md | 2 ++ tests/functional/types/union.txt | 1 + 7 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/StructType.php b/src/StructType.php index eec3cbd..365aecc 100644 --- a/src/StructType.php +++ b/src/StructType.php @@ -43,7 +43,7 @@ public function toIterable(): IterableType /** * @return array{AbstractType, AbstractType} */ - public function keyAndValueType(): array + private function keyAndValueType(): array { if ($this->members === []) { return [StringType::nonEmpty(), new MixedType()]; diff --git a/src/TupleType.php b/src/TupleType.php index 4d4f153..60014e0 100644 --- a/src/TupleType.php +++ b/src/TupleType.php @@ -37,7 +37,7 @@ public function toIterable(): IterableType return new IterableType(new IntType(), $this->valueType()); } - public function valueType(): AbstractType + private function valueType(): AbstractType { if ($this->elements === []) { return new MixedType(); @@ -48,6 +48,7 @@ public function valueType(): AbstractType $valueType = $element; continue; } + // @infection-ignore-all This isn't really required, it's just an optimization. if (Compatibility::check($valueType, $element)) { continue; } diff --git a/tests/functional/compatible-types/bool.md b/tests/functional/compatible-types/bool.md index 08872ca..4ad68cd 100644 --- a/tests/functional/compatible-types/bool.md +++ b/tests/functional/compatible-types/bool.md @@ -8,6 +8,7 @@ - `false` is a subtype of `bool` - `false` is a subtype of `bool | true` - `false` is a subtype of `false` +- `false` is a subtype of `false | list` - `false` is a subtype of `false | true` - `false` is a subtype of `scalar` - `false` is a subtype of `string | int | bool` diff --git a/tests/functional/compatible-types/list.md b/tests/functional/compatible-types/list.md index 31e9abe..791f3cd 100644 --- a/tests/functional/compatible-types/list.md +++ b/tests/functional/compatible-types/list.md @@ -34,6 +34,7 @@ - `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` +- `list` is a subtype of `false | list` - `list` is a subtype of `iterable` - `list` is a subtype of `iterable` - `list` is a subtype of `iterable` @@ -49,6 +50,7 @@ - `non-empty-list` is a subtype of `array` - `non-empty-list` is a subtype of `array` - `non-empty-list` is a subtype of `array` +- `non-empty-list` is a subtype of `false | list` - `non-empty-list` is a subtype of `iterable` - `non-empty-list` is a subtype of `iterable` - `non-empty-list` is a subtype of `iterable` diff --git a/tests/functional/compatible-types/tuple.md b/tests/functional/compatible-types/tuple.md index 7ae9506..1762fce 100644 --- a/tests/functional/compatible-types/tuple.md +++ b/tests/functional/compatible-types/tuple.md @@ -35,6 +35,7 @@ - `array{string, string}` is a subtype of `array` - `array{string, string}` is a subtype of `array` - `array{string, string}` is a subtype of `array{string, string}` +- `array{string, string}` is a subtype of `false | list` - `array{string, string}` is a subtype of `iterable` - `array{string, string}` is a subtype of `iterable` - `array{string, string}` is a subtype of `iterable` diff --git a/tests/functional/compatible-types/union.md b/tests/functional/compatible-types/union.md index 7987a48..1584a95 100644 --- a/tests/functional/compatible-types/union.md +++ b/tests/functional/compatible-types/union.md @@ -16,6 +16,8 @@ - `bool | true` is a subtype of `string | int | bool` - `bool | true` is a subtype of `true | false` +- `false | list` is a subtype of `false | list` + - `false | true` is a subtype of `bool` - `false | true` is a subtype of `bool | true` - `false | true` is a subtype of `false | true` diff --git a/tests/functional/types/union.txt b/tests/functional/types/union.txt index bd7396a..3db2d25 100644 --- a/tests/functional/types/union.txt +++ b/tests/functional/types/union.txt @@ -4,6 +4,7 @@ string | int | bool list | list true | false false | true +false | list 'foo' | 'bar' bool | true string | 'foo' From 90298fedf8905b02b3a3b3cafde1c218f84b00b9 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 22:41:00 +0200 Subject: [PATCH 42/53] Kill some mutants related to booleans --- src/Type.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Type.php b/src/Type.php index 0f1f7b5..d36ac9d 100644 --- a/src/Type.php +++ b/src/Type.php @@ -73,12 +73,7 @@ private static function fromUnion(UnionNode $node, Scope $scope): AbstractType return $right; } if ($left instanceof BoolType && $right instanceof BoolType) { - if ( - ($left->value === true && $right->value === false) - || ($left->value === false && $right->value === true) - ) { - return new BoolType(); - } + return new BoolType($left->value === $right->value ? $left->value : null); } return new UnionType($left, $right); } From eafdae92313370116dd8bc24dddda1c425cebca2 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 22:59:51 +0200 Subject: [PATCH 43/53] Try to kill another mutant --- tests/functional/compatible-types/struct.md | 8 ++++++++ tests/functional/types/struct.txt | 1 + 2 files changed, 9 insertions(+) diff --git a/tests/functional/compatible-types/struct.md b/tests/functional/compatible-types/struct.md index 8338903..b3d89c1 100644 --- a/tests/functional/compatible-types/struct.md +++ b/tests/functional/compatible-types/struct.md @@ -57,3 +57,11 @@ - `array{name: string, age?: int}` is a subtype of `iterable` - `array{name: string, age?: int}` is a subtype of `iterable` - `array{name: string, age?: int}` is a subtype of `iterable` + +- `array{obj: Foo}` is a subtype of `array` +- `array{obj: Foo}` is a subtype of `array` +- `array{obj: Foo}` is a subtype of `array` +- `array{obj: Foo}` is a subtype of `array{obj: Foo}` +- `array{obj: Foo}` is a subtype of `iterable` +- `array{obj: Foo}` is a subtype of `iterable` +- `array{obj: Foo}` is a subtype of `iterable` diff --git a/tests/functional/types/struct.txt b/tests/functional/types/struct.txt index 4587949..6be54f1 100644 --- a/tests/functional/types/struct.txt +++ b/tests/functional/types/struct.txt @@ -3,3 +3,4 @@ array{name: int} array{name: string} array{name: string, age: int} array{name: string, age?: int} +array{obj: Foo} From 3daf422d906f1ad1293dd0e55f097365e76a04a8 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 23:08:02 +0200 Subject: [PATCH 44/53] Try to kill another mutant, part II --- tests/functional/compatible-types/iterable.md | 5 +++++ tests/functional/types/iterable.txt | 1 + 2 files changed, 6 insertions(+) diff --git a/tests/functional/compatible-types/iterable.md b/tests/functional/compatible-types/iterable.md index 894e91c..f0943f3 100644 --- a/tests/functional/compatible-types/iterable.md +++ b/tests/functional/compatible-types/iterable.md @@ -8,6 +8,11 @@ - `iterable` is a subtype of `iterable` - `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` +- `iterable` is a subtype of `iterable` + - `iterable` is a subtype of `iterable` - `iterable` is a subtype of `iterable` - `iterable` is a subtype of `iterable` diff --git a/tests/functional/types/iterable.txt b/tests/functional/types/iterable.txt index 71aa2e5..bd74d86 100644 --- a/tests/functional/types/iterable.txt +++ b/tests/functional/types/iterable.txt @@ -3,6 +3,7 @@ iterable iterable iterable iterable +iterable iterable iterable iterable From 2bd69af08df1ef633d94d5309ba4f0c133eeba89 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 30 Aug 2022 23:59:39 +0200 Subject: [PATCH 45/53] Kill all the mutants, I guess --- src/Scope.php | 6 ++++-- src/TupleType.php | 5 +---- tests/functional/InvalidTypesTest.php | 18 ++++++++++++++++++ tests/functional/compatible-types/misc.md | 8 ++++++++ tests/functional/compatible-types/struct.md | 6 ++++++ tests/functional/types/misc.txt | 1 + 6 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/Scope.php b/src/Scope.php index 64c7c1e..9146bef 100644 --- a/src/Scope.php +++ b/src/Scope.php @@ -76,7 +76,8 @@ public function getType(string $name, array $typeParameters = []): AbstractType { $type = $this->types[$name] ?? null; if ($type === null) { - throw new RuntimeException(sprintf('Unknown type %s', $name)); + $typeString = $name . ($typeParameters !== [] ? '<' . implode(', ', $typeParameters) . '>' : ''); + throw new RuntimeException(sprintf('Unknown type %s', $typeString)); } return $type instanceof AbstractType ? $type : $type($typeParameters); } @@ -101,7 +102,8 @@ private function map(array $typeParameters, bool $nonEmpty = false): MapType 2 => [$typeParameters[0], $typeParameters[1]], default => throw new RuntimeException( 'Array types must take one of the following forms: ' . - 'array, array, array', + 'array, array, array. ' . + 'Got array<' . implode(', ', $typeParameters) . '>', ), }; return new MapType($keyType, $valueType, $nonEmpty); diff --git a/src/TupleType.php b/src/TupleType.php index 60014e0..118e9c0 100644 --- a/src/TupleType.php +++ b/src/TupleType.php @@ -39,9 +39,6 @@ public function toIterable(): IterableType private function valueType(): AbstractType { - if ($this->elements === []) { - return new MixedType(); - } $valueType = null; foreach ($this->elements as $element) { if ($valueType === null) { @@ -54,6 +51,6 @@ private function valueType(): AbstractType } $valueType = new UnionType($valueType, $element); } - return $valueType; + return $valueType ?? new NeverType(); } } diff --git a/tests/functional/InvalidTypesTest.php b/tests/functional/InvalidTypesTest.php index 3728b91..084ecd8 100644 --- a/tests/functional/InvalidTypesTest.php +++ b/tests/functional/InvalidTypesTest.php @@ -45,5 +45,23 @@ public function invalidTypes(): iterable 'class-string', 'class-string takes zero or one type parameters', ]; + yield 'List with two type parameters' => [ + 'list', + 'The list type takes exactly one type parameter, 2 (string, int) given', + ]; + yield 'Unknown identifier' => [ + 'my-imaginary-type', + 'Unknown type my-imaginary-type', + ]; + yield 'Unknown identifier with type parameters' => [ + 'my-imaginary-type', + 'Unknown type my-imaginary-type', + ]; + yield 'Map with three type parameters' => [ + 'array', + 'Array types must take one of the following forms: ' + . 'array, array, array. ' + . 'Got array', + ]; } } diff --git a/tests/functional/compatible-types/misc.md b/tests/functional/compatible-types/misc.md index f8b4d91..a0edafc 100644 --- a/tests/functional/compatible-types/misc.md +++ b/tests/functional/compatible-types/misc.md @@ -1,3 +1,11 @@ +- `array{}` is a subtype of `array` +- `array{}` is a subtype of `array{}` +- `array{}` is a subtype of `array` +- `array{}` is a subtype of `array` +- `array{}` is a subtype of `iterable` +- `array{}` is a subtype of `iterable` +- `array{}` is a subtype of `iterable` + - `float` is a subtype of `float` - `float` is a subtype of `scalar` diff --git a/tests/functional/compatible-types/struct.md b/tests/functional/compatible-types/struct.md index b3d89c1..e32dcf5 100644 --- a/tests/functional/compatible-types/struct.md +++ b/tests/functional/compatible-types/struct.md @@ -3,6 +3,7 @@ - `array{age?: int, name: string}` is a subtype of `array` - `array{age?: int, name: string}` is a subtype of `array` - `array{age?: int, name: string}` is a subtype of `array` +- `array{age?: int, name: string}` is a subtype of `array{}` - `array{age?: int, name: string}` is a subtype of `array{age?: int, name: string}` - `array{age?: int, name: string}` is a subtype of `array{name: string}` - `array{age?: int, name: string}` is a subtype of `array{name: string, age?: int}` @@ -17,6 +18,7 @@ - `array{name: int}` is a subtype of `array` - `array{name: int}` is a subtype of `array` - `array{name: int}` is a subtype of `array` +- `array{name: int}` is a subtype of `array{}` - `array{name: int}` is a subtype of `array{name: int}` - `array{name: int}` is a subtype of `iterable` - `array{name: int}` is a subtype of `iterable` @@ -27,6 +29,7 @@ - `array{name: string}` is a subtype of `array` - `array{name: string}` is a subtype of `array` - `array{name: string}` is a subtype of `array` +- `array{name: string}` is a subtype of `array{}` - `array{name: string}` is a subtype of `array{name: string}` - `array{name: string}` is a subtype of `iterable` - `array{name: string}` is a subtype of `iterable` @@ -38,6 +41,7 @@ - `array{name: string, age: int}` is a subtype of `array` - `array{name: string, age: int}` is a subtype of `array` - `array{name: string, age: int}` is a subtype of `array` +- `array{name: string, age: int}` is a subtype of `array{}` - `array{name: string, age: int}` is a subtype of `array{age?: int, name: string}` - `array{name: string, age: int}` is a subtype of `array{name: string}` - `array{name: string, age: int}` is a subtype of `array{name: string, age: int}` @@ -51,6 +55,7 @@ - `array{name: string, age?: int}` is a subtype of `array` - `array{name: string, age?: int}` is a subtype of `array` - `array{name: string, age?: int}` is a subtype of `array` +- `array{name: string, age?: int}` is a subtype of `array{}` - `array{name: string, age?: int}` is a subtype of `array{age?: int, name: string}` - `array{name: string, age?: int}` is a subtype of `array{name: string}` - `array{name: string, age?: int}` is a subtype of `array{name: string, age?: int}` @@ -61,6 +66,7 @@ - `array{obj: Foo}` is a subtype of `array` - `array{obj: Foo}` is a subtype of `array` - `array{obj: Foo}` is a subtype of `array` +- `array{obj: Foo}` is a subtype of `array{}` - `array{obj: Foo}` is a subtype of `array{obj: Foo}` - `array{obj: Foo}` is a subtype of `iterable` - `array{obj: Foo}` is a subtype of `iterable` diff --git a/tests/functional/types/misc.txt b/tests/functional/types/misc.txt index 1211185..6922243 100644 --- a/tests/functional/types/misc.txt +++ b/tests/functional/types/misc.txt @@ -1,3 +1,4 @@ +array{} float null scalar From 28acda96bae605d1760c1ab309e0c592e47c7c8a Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Wed, 31 Aug 2022 00:13:55 +0200 Subject: [PATCH 46/53] Remove an unused method --- src/UnionType.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/UnionType.php b/src/UnionType.php index d73753d..9077066 100644 --- a/src/UnionType.php +++ b/src/UnionType.php @@ -17,15 +17,4 @@ public function toNode(): NodeInterface { return new UnionNode($this->left->toNode(), $this->right->toNode()); } - - /** - * @return list - */ - public function flatten(): array - { - return array_merge( - $this->left instanceof UnionType ? $this->left->flatten() : [$this->left], - $this->right instanceof UnionType ? $this->right->flatten() : [$this->right], - ); - } } From 509219db2c385af2190a0b6a2a595816cdc9db16 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Wed, 31 Aug 2022 18:53:59 +0200 Subject: [PATCH 47/53] Try storing the Infection log as an artifact --- .github/workflows/infection.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/infection.yml b/.github/workflows/infection.yml index 70b5111..a36e5a2 100644 --- a/.github/workflows/infection.yml +++ b/.github/workflows/infection.yml @@ -52,3 +52,9 @@ jobs: --min-msi=100 \ --min-covered-msi=100 \ --ignore-msi-with-no-mutations + + - name: Archive Infection log + uses: actions/upload-artifact@v3 + with: + name: infection-log + path: infection.log From b241b5092850d285ce4221543dcd423c12721311 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Wed, 31 Aug 2022 18:59:28 +0200 Subject: [PATCH 48/53] Add `if: always()`` to Infection log upload --- .github/workflows/infection.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/infection.yml b/.github/workflows/infection.yml index a36e5a2..c01fb1f 100644 --- a/.github/workflows/infection.yml +++ b/.github/workflows/infection.yml @@ -55,6 +55,7 @@ jobs: - name: Archive Infection log uses: actions/upload-artifact@v3 + if: always() with: name: infection-log path: infection.log From 8c7a2bd3a236bf7ee6386f0be668293f095d68a7 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Thu, 24 Nov 2022 20:35:16 +0100 Subject: [PATCH 49/53] Improve intersections of structs --- src/Compatibility.php | 17 +++++++ src/Dto/StructMember.php | 6 +++ src/IntersectionType.php | 49 ++++++++++++++++++- src/StructType.php | 18 +++++++ src/Type.php | 2 +- tests/functional/CompatibilityTest.php | 10 +++- tests/functional/aliases.md | 7 +++ .../functional/compatible-types/class-like.md | 2 + .../compatible-types/intersection.md | 30 ++++++++++++ tests/functional/compatible-types/struct.md | 7 +++ tests/functional/types/class-like.txt | 1 + tests/functional/types/intersection.txt | 3 ++ 12 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 tests/functional/compatible-types/class-like.md create mode 100644 tests/functional/compatible-types/intersection.md create mode 100644 tests/functional/types/class-like.txt create mode 100644 tests/functional/types/intersection.txt diff --git a/src/Compatibility.php b/src/Compatibility.php index 074eca9..c44042e 100644 --- a/src/Compatibility.php +++ b/src/Compatibility.php @@ -27,6 +27,9 @@ public static function check(AbstractType $super, AbstractType $sub): bool if ($sub instanceof UnionType) { return self::checkSubUnion($super, $sub); } + if ($sub instanceof IntersectionType) { + return self::checkSubIntersection($super, $sub); + } return match (true) { $super instanceof BoolType => self::checkBool($super, $sub), $super instanceof CallableType => self::checkCallable($super, $sub), @@ -34,6 +37,7 @@ public static function check(AbstractType $super, AbstractType $sub): bool $super instanceof ClassStringType => self::checkClassString($super, $sub), $super instanceof FloatType => self::checkFloat($sub), $super instanceof IntLiteralType => self::checkIntLiteral($super, $sub), + $super instanceof IntersectionType => self::checkIntersection($super, $sub), $super instanceof IntType => self::checkInt($super, $sub), $super instanceof IterableType => self::checkIterable($super, $sub), $super instanceof ListType => self::checkList($super, $sub), @@ -229,6 +233,9 @@ private static function checkStruct(StructType $super, AbstractType $sub): bool foreach ($super->members as $name => $member) { $subMember = array_key_exists($name, $sub->members) ? $sub->members[$name] : null; if ($subMember === null) { + if ($member->optional) { + continue; + } return false; } if (!self::check($member->type, $subMember->type)) { @@ -286,4 +293,14 @@ private static function checkSubUnion(AbstractType $super, UnionType $sub): bool { return self::check($super, $sub->left) && self::check($super, $sub->right); } + + private static function checkIntersection(IntersectionType $super, AbstractType $sub): bool + { + return self::check($super->left, $sub) && self::check($super->right, $sub); + } + + private static function checkSubIntersection(AbstractType $super, IntersectionType $sub): bool + { + return self::check($super, $sub->left) && self::check($super, $sub->right); + } } diff --git a/src/Dto/StructMember.php b/src/Dto/StructMember.php index 769ca6a..cebf9a9 100644 --- a/src/Dto/StructMember.php +++ b/src/Dto/StructMember.php @@ -6,6 +6,7 @@ use PhpTypes\Ast\Node\Dto\StructMember as StructMemberNode; use PhpTypes\Types\AbstractType; +use PhpTypes\Types\IntersectionType; final class StructMember { @@ -31,4 +32,9 @@ public function toNode(): StructMemberNode ? StructMemberNode::optional($this->type->toNode()) : StructMemberNode::required($this->type->toNode()); } + + public function intersect(self $other): self + { + return new self(IntersectionType::create($this->type, $other->type), $this->optional && $other->optional); + } } diff --git a/src/IntersectionType.php b/src/IntersectionType.php index 48cb49a..2f0a94b 100644 --- a/src/IntersectionType.php +++ b/src/IntersectionType.php @@ -7,14 +7,61 @@ use PhpTypes\Ast\Node\IntersectionNode; use PhpTypes\Ast\Node\NodeInterface; +use function array_shift; +use function assert; +use function count; + final class IntersectionType extends AbstractType { - public function __construct(public readonly AbstractType $left, public readonly AbstractType $right) + private function __construct(public readonly AbstractType $left, public readonly AbstractType $right) + { + } + + public static function create(AbstractType $left, AbstractType $right): AbstractType + { + $parts = array_merge( + $left instanceof self ? $left->flatten() : [$left], + $right instanceof self ? $right->flatten() : [$right], + ); + $structs = []; + foreach ($parts as $index => $part) { + if (!$part instanceof StructType) { + continue; + } + $structs[] = $part; + unset($parts[$index]); + } + if ($structs !== []) { + $parts[] = StructType::merge($structs); + } + assert($parts !== []); + return self::unflatten($parts); + } + + /** + * @param non-empty-array $types + */ + private static function unflatten(array $types): AbstractType { + return match (count($types)) { + 1 => array_shift($types), + 2 => new self(array_shift($types), array_shift($types)), + default => new self(array_shift($types), self::unflatten($types)), + }; } public function toNode(): NodeInterface { return new IntersectionNode($this->left->toNode(), $this->right->toNode()); } + + /** + * @return non-empty-list + */ + private function flatten(): array + { + $leftParts = $this->left instanceof self ? $this->left->flatten() : [$this->left]; + $rightParts = $this->right instanceof self ? $this->right->flatten() : [$this->right]; + return array_merge($leftParts, $rightParts); + } } diff --git a/src/StructType.php b/src/StructType.php index 365aecc..f2a2a07 100644 --- a/src/StructType.php +++ b/src/StructType.php @@ -19,6 +19,24 @@ public function __construct(public readonly array $members) { } + /** + * @param iterable $structs + */ + public static function merge(iterable $structs): self + { + $members = []; + foreach ($structs as $struct) { + foreach ($struct->members as $name => $member) { + if (!isset($members[$name])) { + $members[$name] = $member; + continue; + } + $members[$name] = $members[$name]->intersect($member); + } + } + return new self($members); + } + public function toNode(): NodeInterface { $members = []; diff --git a/src/Type.php b/src/Type.php index d36ac9d..25125fc 100644 --- a/src/Type.php +++ b/src/Type.php @@ -117,7 +117,7 @@ private static function fromCallable(CallableNode $node, Scope $scope): Callable private static function fromIntersection(IntersectionNode $node, Scope $scope): AbstractType { - return new IntersectionType( + return IntersectionType::create( self::fromNode($node->left, $scope), self::fromNode($node->right, $scope), ); diff --git a/tests/functional/CompatibilityTest.php b/tests/functional/CompatibilityTest.php index cc8390f..75fe69c 100644 --- a/tests/functional/CompatibilityTest.php +++ b/tests/functional/CompatibilityTest.php @@ -36,6 +36,12 @@ public static function setUpBeforeClass(): void $fooInterface = new ClassLikeType('FooInterface'); self::$scope->register('FooInterface', $fooInterface); self::$scope->register('Foo', new ClassLikeType('Foo', parents: [$fooInterface])); + $runnable = new ClassLikeType('Runnable'); + self::$scope->register('Runnable', $runnable); + $loggable = new ClassLikeType('Loggable'); + self::$scope->register('Loggable', $loggable); + $runnableAndLoggable = new ClassLikeType('RunnableAndLoggable', parents: [$runnable, $loggable]); + self::$scope->register('RunnableAndLoggable', $runnableAndLoggable); } /** @@ -201,7 +207,9 @@ public function testAliases(string $a, string $b, bool $expected): void $aType = self::fromString($a, self::$scope); $bType = self::fromString($b, self::$scope); - $isAlias = Compatibility::check($aType, $bType) && Compatibility::check($bType, $aType); + $aContainsB = Compatibility::check($aType, $bType); + $bContainsA = Compatibility::check($bType, $aType); + $isAlias = $aContainsB && $bContainsA; $message = $expected ? sprintf('Expected "%s" to be an alias of "%s", but it is not', $b, $a) diff --git a/tests/functional/aliases.md b/tests/functional/aliases.md index 86729d8..8d444ac 100644 --- a/tests/functional/aliases.md +++ b/tests/functional/aliases.md @@ -2,6 +2,8 @@ - `array` is an alias of `array` - `array` is an alias of `array` - `array` is an alias of `array` +- `array{name: string}` is an alias of `array{name: string} & array{name?: string}` +- `array{name: string, age: int}` is an alias of `array{name: string} & array{age: int}` - `array{name: string, age?: int}` is an alias of `array{age?: int, name: string}` - `int<23, 23>` is an alias of `23` - `int<1, max>` is an alias of `positive-int` @@ -19,3 +21,8 @@ - `bool | true` is an alias of `false | true` - `string | 'foo'` is an alias of `string` - `string | list` is an alias of `list | string` + +- `array{age?: int, name: string}` is an alias of `array{name: string} & array{name?: string}` +- `array{name: string, age?: int}` is an alias of `array{name: string} & array{name?: string}` +- `array{name: string}` is an alias of `array{age?: int, name: string}` +- `array{name: string, age?: int}` is an alias of `array{name: string}` diff --git a/tests/functional/compatible-types/class-like.md b/tests/functional/compatible-types/class-like.md new file mode 100644 index 0000000..ebdb95c --- /dev/null +++ b/tests/functional/compatible-types/class-like.md @@ -0,0 +1,2 @@ +- `RunnableAndLoggable` is a subtype of `RunnableAndLoggable` +- `RunnableAndLoggable` is a subtype of `Runnable & Loggable` diff --git a/tests/functional/compatible-types/intersection.md b/tests/functional/compatible-types/intersection.md new file mode 100644 index 0000000..decf5a8 --- /dev/null +++ b/tests/functional/compatible-types/intersection.md @@ -0,0 +1,30 @@ +- `array{name: string} & array{age: int}` is a subtype of `array` +- `array{name: string} & array{age: int}` is a subtype of `array` +- `array{name: string} & array{age: int}` is a subtype of `array` +- `array{name: string} & array{age: int}` is a subtype of `array<'name' | 'age', string | int>` +- `array{name: string} & array{age: int}` is a subtype of `array{}` +- `array{name: string} & array{age: int}` is a subtype of `array{age?: int, name: string}` +- `array{name: string} & array{age: int}` is a subtype of `array{name: string, age: int}` +- `array{name: string} & array{age: int}` is a subtype of `array{name: string, age?: int}` +- `array{name: string} & array{age: int}` is a subtype of `array{name: string}` +- `array{name: string} & array{age: int}` is a subtype of `array{name: string} & array{age: int}` +- `array{name: string} & array{age: int}` is a subtype of `array{name: string} & array{name?: string}` +- `array{name: string} & array{age: int}` is a subtype of `array` +- `array{name: string} & array{age: int}` is a subtype of `iterable` +- `array{name: string} & array{age: int}` is a subtype of `iterable` +- `array{name: string} & array{age: int}` is a subtype of `iterable` + +- `array{name: string} & array{name?: string}` is a subtype of `array` +- `array{name: string} & array{name?: string}` is a subtype of `array` +- `array{name: string} & array{name?: string}` is a subtype of `array` +- `array{name: string} & array{name?: string}` is a subtype of `array<'name' | 'age', string | int>` +- `array{name: string} & array{name?: string}` is a subtype of `array{}` +- `array{name: string} & array{name?: string}` is a subtype of `array{age?: int, name: string}` +- `array{name: string} & array{name?: string}` is a subtype of `array{name: string, age?: int}` +- `array{name: string} & array{name?: string}` is a subtype of `array{name: string}` +- `array{name: string} & array{name?: string}` is a subtype of `array{name: string} & array{name?: string}` +- `array{name: string} & array{name?: string}` is a subtype of `array` +- `array{name: string} & array{name?: string}` is a subtype of `iterable` +- `array{name: string} & array{name?: string}` is a subtype of `iterable` +- `array{name: string} & array{name?: string}` is a subtype of `iterable` +- `array{name: string} & array{name?: string}` is a subtype of `iterable` diff --git a/tests/functional/compatible-types/struct.md b/tests/functional/compatible-types/struct.md index e32dcf5..a994566 100644 --- a/tests/functional/compatible-types/struct.md +++ b/tests/functional/compatible-types/struct.md @@ -7,6 +7,7 @@ - `array{age?: int, name: string}` is a subtype of `array{age?: int, name: string}` - `array{age?: int, name: string}` is a subtype of `array{name: string}` - `array{age?: int, name: string}` is a subtype of `array{name: string, age?: int}` +- `array{age?: int, name: string}` is a subtype of `array{name: string} & array{name?: string}` - `array{age?: int, name: string}` is a subtype of `iterable` - `array{age?: int, name: string}` is a subtype of `iterable` - `array{age?: int, name: string}` is a subtype of `iterable` @@ -30,7 +31,10 @@ - `array{name: string}` is a subtype of `array` - `array{name: string}` is a subtype of `array` - `array{name: string}` is a subtype of `array{}` +- `array{name: string}` is a subtype of `array{age?: int, name: string}` - `array{name: string}` is a subtype of `array{name: string}` +- `array{name: string}` is a subtype of `array{name: string} & array{name?: string}` +- `array{name: string}` is a subtype of `array{name: string, age?: int}` - `array{name: string}` is a subtype of `iterable` - `array{name: string}` is a subtype of `iterable` - `array{name: string}` is a subtype of `iterable` @@ -44,6 +48,8 @@ - `array{name: string, age: int}` is a subtype of `array{}` - `array{name: string, age: int}` is a subtype of `array{age?: int, name: string}` - `array{name: string, age: int}` is a subtype of `array{name: string}` +- `array{name: string, age: int}` is a subtype of `array{name: string} & array{age: int}` +- `array{name: string, age: int}` is a subtype of `array{name: string} & array{name?: string}` - `array{name: string, age: int}` is a subtype of `array{name: string, age: int}` - `array{name: string, age: int}` is a subtype of `array{name: string, age?: int}` - `array{name: string, age: int}` is a subtype of `iterable` @@ -59,6 +65,7 @@ - `array{name: string, age?: int}` is a subtype of `array{age?: int, name: string}` - `array{name: string, age?: int}` is a subtype of `array{name: string}` - `array{name: string, age?: int}` is a subtype of `array{name: string, age?: int}` +- `array{name: string, age?: int}` is a subtype of `array{name: string} & array{name?: string}` - `array{name: string, age?: int}` is a subtype of `iterable` - `array{name: string, age?: int}` is a subtype of `iterable` - `array{name: string, age?: int}` is a subtype of `iterable` diff --git a/tests/functional/types/class-like.txt b/tests/functional/types/class-like.txt new file mode 100644 index 0000000..d2c2c2a --- /dev/null +++ b/tests/functional/types/class-like.txt @@ -0,0 +1 @@ +RunnableAndLoggable diff --git a/tests/functional/types/intersection.txt b/tests/functional/types/intersection.txt new file mode 100644 index 0000000..9b61472 --- /dev/null +++ b/tests/functional/types/intersection.txt @@ -0,0 +1,3 @@ +array{name: string} & array{age: int} +array{name: string} & array{name?: string} +Runnable & Loggable From a08e18198f4a4a5d8b75a80378a7d8ddfab65ded Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Wed, 22 Feb 2023 20:05:30 +0100 Subject: [PATCH 50/53] Test more --- src/Compatibility.php | 9 +++++++++ tests/functional/aliases.md | 9 +++++++++ tests/functional/compatible-types/list.md | 5 +++++ tests/functional/compatible-types/map.md | 10 ++++++++++ tests/functional/compatible-types/tuple.md | 13 +++++++++++++ tests/functional/compatible-types/union.md | 1 + 6 files changed, 47 insertions(+) diff --git a/src/Compatibility.php b/src/Compatibility.php index c44042e..41075d1 100644 --- a/src/Compatibility.php +++ b/src/Compatibility.php @@ -177,6 +177,9 @@ private static function checkScalar(AbstractType $sub): bool private static function checkList(ListType $super, AbstractType $sub): bool { if ($sub instanceof TupleType) { + if ($super->nonEmpty && $sub->elements === []) { + return false; + } foreach ($sub->elements as $element) { if (self::check($super->type, $element)) { continue; @@ -210,6 +213,12 @@ private static function checkMap(MapType $super, AbstractType $sub): bool private static function checkTuple(TupleType $super, AbstractType $sub): bool { + if ($super->elements === []) { + return $sub instanceof ListType + || $sub instanceof TupleType + || $sub instanceof MapType + || $sub instanceof StructType; + } if (!$sub instanceof TupleType) { return false; } diff --git a/tests/functional/aliases.md b/tests/functional/aliases.md index 8d444ac..3130e40 100644 --- a/tests/functional/aliases.md +++ b/tests/functional/aliases.md @@ -26,3 +26,12 @@ - `array{name: string, age?: int}` is an alias of `array{name: string} & array{name?: string}` - `array{name: string}` is an alias of `array{age?: int, name: string}` - `array{name: string, age?: int}` is an alias of `array{name: string}` + +- `array{}` is an alias of `array` +- `array{}` is an alias of `array` +- `array{}` is an alias of `array` +- `array{}` is an alias of `list` +- `array{}` is an alias of `list` +- `array{}` is an alias of `list` +- `array{}` is an alias of `list` +- `array{}` is an alias of `list | list` diff --git a/tests/functional/compatible-types/list.md b/tests/functional/compatible-types/list.md index 791f3cd..68f53b7 100644 --- a/tests/functional/compatible-types/list.md +++ b/tests/functional/compatible-types/list.md @@ -4,6 +4,7 @@ - `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` +- `list` is a subtype of `array{}` - `list` is a subtype of `iterable` - `list` is a subtype of `iterable` - `list` is a subtype of `iterable` @@ -15,6 +16,7 @@ - `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` +- `list` is a subtype of `array{}` - `list` is a subtype of `iterable` - `list` is a subtype of `iterable` - `list` is a subtype of `iterable` @@ -24,6 +26,7 @@ - `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` +- `list` is a subtype of `array{}` - `list` is a subtype of `iterable` - `list` is a subtype of `iterable` - `list` is a subtype of `iterable` @@ -34,6 +37,7 @@ - `list` is a subtype of `array` - `list` is a subtype of `array` - `list` is a subtype of `array` +- `list` is a subtype of `array{}` - `list` is a subtype of `false | list` - `list` is a subtype of `iterable` - `list` is a subtype of `iterable` @@ -50,6 +54,7 @@ - `non-empty-list` is a subtype of `array` - `non-empty-list` is a subtype of `array` - `non-empty-list` is a subtype of `array` +- `non-empty-list` is a subtype of `array{}` - `non-empty-list` is a subtype of `false | list` - `non-empty-list` is a subtype of `iterable` - `non-empty-list` is a subtype of `iterable` diff --git a/tests/functional/compatible-types/map.md b/tests/functional/compatible-types/map.md index 13bf143..f1cca0b 100644 --- a/tests/functional/compatible-types/map.md +++ b/tests/functional/compatible-types/map.md @@ -1,6 +1,7 @@ - `array` is a subtype of `array` - `array` is a subtype of `array` - `array` is a subtype of `array` +- `array` is a subtype of `array{}` - `array` is a subtype of `iterable` - `array` is a subtype of `iterable` - `array` is a subtype of `iterable` @@ -10,6 +11,7 @@ - `array<'name' | 'age', string | int>` is a subtype of `array` - `array<'name' | 'age', string | int>` is a subtype of `array` - `array<'name' | 'age', string | int>` is a subtype of `array` +- `array<'name' | 'age', string | int>` is a subtype of `array{}` - `array<'name' | 'age', string | int>` is a subtype of `iterable` - `array<'name' | 'age', string | int>` is a subtype of `iterable` - `array<'name' | 'age', string | int>` is a subtype of `iterable` @@ -19,6 +21,7 @@ - `array` is a subtype of `array` - `array` is a subtype of `array` - `array` is a subtype of `array` +- `array` is a subtype of `array{}` - `array` is a subtype of `iterable` - `array` is a subtype of `iterable` - `array` is a subtype of `iterable` @@ -26,6 +29,7 @@ - `array` is a subtype of `array` - `array` is a subtype of `array` - `array` is a subtype of `array` +- `array` is a subtype of `array{}` - `array` is a subtype of `iterable` - `array` is a subtype of `iterable` - `array` is a subtype of `iterable` @@ -34,6 +38,7 @@ - `array` is a subtype of `array` - `array` is a subtype of `array` - `array` is a subtype of `array` +- `array` is a subtype of `array{}` - `array` is a subtype of `iterable` - `array` is a subtype of `iterable` - `array` is a subtype of `iterable` @@ -43,6 +48,7 @@ - `array` is a subtype of `array` - `array` is a subtype of `array` - `array` is a subtype of `array` +- `array` is a subtype of `array{}` - `array` is a subtype of `iterable` - `array` is a subtype of `iterable` - `array` is a subtype of `iterable` @@ -52,6 +58,7 @@ - `array` is a subtype of `array` - `array` is a subtype of `array` - `array` is a subtype of `array` +- `array` is a subtype of `array{}` - `array` is a subtype of `iterable` - `array` is a subtype of `iterable` - `array` is a subtype of `iterable` @@ -60,6 +67,7 @@ - `array` is a subtype of `array` - `array` is a subtype of `array` - `array` is a subtype of `array` +- `array` is a subtype of `array{}` - `array` is a subtype of `iterable` - `array` is a subtype of `iterable` - `array` is a subtype of `iterable` @@ -68,6 +76,7 @@ - `array` is a subtype of `array` - `array` is a subtype of `array` - `array` is a subtype of `array` +- `array` is a subtype of `array{}` - `array` is a subtype of `iterable` - `array` is a subtype of `iterable` - `array` is a subtype of `iterable` @@ -79,6 +88,7 @@ - `non-empty-array` is a subtype of `array` - `non-empty-array` is a subtype of `array` - `non-empty-array` is a subtype of `array` +- `non-empty-array` is a subtype of `array{}` - `non-empty-array` is a subtype of `iterable` - `non-empty-array` is a subtype of `iterable` - `non-empty-array` is a subtype of `iterable` diff --git a/tests/functional/compatible-types/tuple.md b/tests/functional/compatible-types/tuple.md index 1762fce..96acc93 100644 --- a/tests/functional/compatible-types/tuple.md +++ b/tests/functional/compatible-types/tuple.md @@ -1,7 +1,17 @@ +- `array{}` is a subtype of `false | list` +- `array{}` is a subtype of `list` +- `array{}` is a subtype of `list` +- `array{}` is a subtype of `list` +- `array{}` is a subtype of `list` +- `array{}` is a subtype of `list | list` +- `array{}` is a subtype of `list | string` +- `array{}` is a subtype of `string | list` + - `array{int, string}` is a subtype of `array` - `array{int, string}` is a subtype of `array` - `array{int, string}` is a subtype of `array` - `array{int, string}` is a subtype of `array` +- `array{int, string}` is a subtype of `array{}` - `array{int, string}` is a subtype of `array{int, string}` - `array{int, string}` is a subtype of `iterable` - `array{int, string}` is a subtype of `iterable` @@ -12,6 +22,7 @@ - `array{string, int}` is a subtype of `array` - `array{string, int}` is a subtype of `array` - `array{string, int}` is a subtype of `array` +- `array{string, int}` is a subtype of `array{}` - `array{string, int}` is a subtype of `array{string, int}` - `array{string, int}` is a subtype of `iterable` - `array{string, int}` is a subtype of `iterable` @@ -22,6 +33,7 @@ - `array{string, int, string}` is a subtype of `array` - `array{string, int, string}` is a subtype of `array` - `array{string, int, string}` is a subtype of `array` +- `array{string, int, string}` is a subtype of `array{}` - `array{string, int, string}` is a subtype of `array{string, int}` - `array{string, int, string}` is a subtype of `array{string, int, string}` - `array{string, int, string}` is a subtype of `iterable` @@ -34,6 +46,7 @@ - `array{string, string}` is a subtype of `array` - `array{string, string}` is a subtype of `array` - `array{string, string}` is a subtype of `array` +- `array{string, string}` is a subtype of `array{}` - `array{string, string}` is a subtype of `array{string, string}` - `array{string, string}` is a subtype of `false | list` - `array{string, string}` is a subtype of `iterable` diff --git a/tests/functional/compatible-types/union.md b/tests/functional/compatible-types/union.md index 1584a95..f8181b0 100644 --- a/tests/functional/compatible-types/union.md +++ b/tests/functional/compatible-types/union.md @@ -34,6 +34,7 @@ - `list | list` is a subtype of `array` - `list | list` is a subtype of `array` - `list | list` is a subtype of `array` +- `list | list` is a subtype of `array{}` - `list | list` is a subtype of `list` - `list | list` is a subtype of `list | list` - `list | list` is a subtype of `iterable` From ab58095411ee02e58f52ab3432a6eb79e6c75145 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Wed, 22 Feb 2023 20:10:11 +0100 Subject: [PATCH 51/53] Help out PHPStan --- src/IntersectionType.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/IntersectionType.php b/src/IntersectionType.php index 2f0a94b..af3ca1a 100644 --- a/src/IntersectionType.php +++ b/src/IntersectionType.php @@ -43,10 +43,11 @@ public static function create(AbstractType $left, AbstractType $right): Abstract */ private static function unflatten(array $types): AbstractType { + $first = array_shift($types); return match (count($types)) { - 1 => array_shift($types), - 2 => new self(array_shift($types), array_shift($types)), - default => new self(array_shift($types), self::unflatten($types)), + 0 => $first, + 1 => new self($first, array_shift($types)), + default => new self($first, self::unflatten($types)), }; } From 814a55de25d0e17f94d98095c7bd1699014c20d7 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Wed, 22 Feb 2023 20:12:49 +0100 Subject: [PATCH 52/53] Require PHPStan 1.9 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ab7e754..be22bbe 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "require-dev": { "infection/infection": "^0.26.13", "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", + "phpstan/phpstan": "^1.9", "phpstan/phpstan-strict-rules": "^1.3", "phpunit/phpunit": "^9.5", "slevomat/coding-standard": "^8.1", From 6c9502c509f4dfb0b1261e1d87f97460d6d2f368 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Wed, 22 Feb 2023 20:49:02 +0100 Subject: [PATCH 53/53] Kill some mutants --- infection.json | 10 ++++++--- src/StructType.php | 7 ++----- src/Type.php | 29 +++++++++++++++++++++++---- tests/functional/InvalidTypesTest.php | 16 +++++++++++++++ 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/infection.json b/infection.json index e2ab411..0466e3b 100644 --- a/infection.json +++ b/infection.json @@ -9,6 +9,10 @@ "text": "infection.log" }, "mutators": { - "@default": true - } -} \ No newline at end of file + "@default": true, + "global-ignoreSourceCodeByRegex": [ + "assert\\(.+\\);" + ] + }, + "timeout": 1 +} diff --git a/src/StructType.php b/src/StructType.php index f2a2a07..aa84d30 100644 --- a/src/StructType.php +++ b/src/StructType.php @@ -13,14 +13,14 @@ final class StructType extends AbstractType implements ToIterableInterface, ToMapInterface { /** - * @param array $members + * @param non-empty-array $members */ public function __construct(public readonly array $members) { } /** - * @param iterable $structs + * @param non-empty-array $structs */ public static function merge(iterable $structs): self { @@ -63,9 +63,6 @@ public function toIterable(): IterableType */ private function keyAndValueType(): array { - if ($this->members === []) { - return [StringType::nonEmpty(), new MixedType()]; - } /** @var array{AbstractType, AbstractType}|null $types */ $types = null; foreach ($this->members as $name => $member) { diff --git a/src/Type.php b/src/Type.php index 25125fc..727e228 100644 --- a/src/Type.php +++ b/src/Type.php @@ -20,6 +20,8 @@ use RuntimeException; use function count; +use function implode; +use function sprintf; final class Type { @@ -90,8 +92,11 @@ private static function fromTuple(TupleNode $node, Scope $scope): TupleType /** * @param array $members */ - private static function fromStruct(array $members, Scope $scope): StructType + private static function fromStruct(array $members, Scope $scope): StructType|TupleType { + if (count($members) === 0) { + return new TupleType([]); + } $typeMembers = []; foreach ($members as $name => $member) { $typeMembers[$name] = $member->optional @@ -130,7 +135,13 @@ private static function fromInt(IdentifierNode $node): IntType return new IntType(); } if ($numberOfParams !== 2) { - throw new RuntimeException('Invalid number of type parameters'); + throw new RuntimeException( + sprintf( + 'The int type takes exactly zero or two type parameters, %d (%s) given', + $numberOfParams, + implode(', ', $node->typeParameters), + ), + ); } $min = (static function () use ($node) { if ($node->typeParameters[0] instanceof IdentifierNode && $node->typeParameters[0]->name === 'min') { @@ -139,7 +150,12 @@ private static function fromInt(IdentifierNode $node): IntType if ($node->typeParameters[0] instanceof IntLiteralNode) { return $node->typeParameters[0]->value; } - throw new RuntimeException('Invalid int type'); + throw new RuntimeException( + sprintf( + "Invalid minimum value for int type: %s. Must be an integer or \"min\".", + $node->typeParameters[0], + ) + ); })(); $max = (static function () use ($node) { if ($node->typeParameters[1] instanceof IdentifierNode && $node->typeParameters[1]->name === 'max') { @@ -148,7 +164,12 @@ private static function fromInt(IdentifierNode $node): IntType if ($node->typeParameters[1] instanceof IntLiteralNode) { return $node->typeParameters[1]->value; } - throw new RuntimeException('Invalid int type'); + throw new RuntimeException( + sprintf( + "Invalid maximum value for int type: %s. Must be an integer or \"max\".", + $node->typeParameters[1], + ) + ); })(); return new IntType($min, $max); } diff --git a/tests/functional/InvalidTypesTest.php b/tests/functional/InvalidTypesTest.php index 084ecd8..89f65d6 100644 --- a/tests/functional/InvalidTypesTest.php +++ b/tests/functional/InvalidTypesTest.php @@ -63,5 +63,21 @@ public function invalidTypes(): iterable . 'array, array, array. ' . 'Got array', ]; + yield 'Int with invalid minimum' => [ + 'int', + 'Invalid minimum value for int type: Foo. Must be an integer or "min".', + ]; + yield 'Int with invalid maximum' => [ + 'int<10, Foo>', + 'Invalid maximum value for int type: Foo. Must be an integer or "max".', + ]; + yield 'Int with a single type parameter' => [ + 'int<10>', + 'The int type takes exactly zero or two type parameters, 1 (10) given', + ]; + yield 'Int with three type parameters' => [ + 'int<10, 20, 30>', + 'The int type takes exactly zero or two type parameters, 3 (10, 20, 30) given', + ]; } }