diff --git a/src/Type/CombiningType.php b/src/Type/CombiningType.php index e143ba94..de08fd16 100644 --- a/src/Type/CombiningType.php +++ b/src/Type/CombiningType.php @@ -5,7 +5,7 @@ namespace CuyZ\Valinor\Type; /** @api */ -interface CombiningType extends Type +interface CombiningType extends CompositeType { public function isMatchedBy(Type $other): bool; diff --git a/src/Type/CompositeTraversableType.php b/src/Type/CompositeTraversableType.php index 0546b17a..46fb1313 100644 --- a/src/Type/CompositeTraversableType.php +++ b/src/Type/CompositeTraversableType.php @@ -7,7 +7,7 @@ use CuyZ\Valinor\Type\Types\ArrayKeyType; /** @api */ -interface CompositeTraversableType extends Type +interface CompositeTraversableType extends CompositeType { public function keyType(): ArrayKeyType; diff --git a/src/Type/CompositeType.php b/src/Type/CompositeType.php new file mode 100644 index 00000000..49a9d049 --- /dev/null +++ b/src/Type/CompositeType.php @@ -0,0 +1,14 @@ + + */ + public function traverse(): iterable; +} diff --git a/src/Type/TraversableType.php b/src/Type/TraversableType.php deleted file mode 100644 index 08b2bb17..00000000 --- a/src/Type/TraversableType.php +++ /dev/null @@ -1,10 +0,0 @@ -subType; } + public function traverse(): iterable + { + yield $this->subType; + + if ($this->subType instanceof CompositeType) { + yield from $this->subType->traverse(); + } + } + public function __toString(): string { return $this->signature; diff --git a/src/Type/Types/ClassType.php b/src/Type/Types/ClassType.php index 4f96092a..823dd164 100644 --- a/src/Type/Types/ClassType.php +++ b/src/Type/Types/ClassType.php @@ -5,12 +5,13 @@ namespace CuyZ\Valinor\Type\Types; use CuyZ\Valinor\Type\ObjectType; +use CuyZ\Valinor\Type\CompositeType; use CuyZ\Valinor\Type\Type; use function is_a; /** @api */ -final class ClassType implements ObjectType +final class ClassType implements ObjectType, CompositeType { /** @var class-string */ private string $className; @@ -64,6 +65,17 @@ public function matches(Type $other): bool return is_a($this->className, $other->className(), true); } + public function traverse(): iterable + { + foreach ($this->generics as $type) { + yield $type; + + if ($type instanceof CompositeType) { + yield from $type->traverse(); + } + } + } + public function __toString(): string { return empty($this->generics) diff --git a/src/Type/Types/IntersectionType.php b/src/Type/Types/IntersectionType.php index fec90561..e6e4839c 100644 --- a/src/Type/Types/IntersectionType.php +++ b/src/Type/Types/IntersectionType.php @@ -6,6 +6,7 @@ use CuyZ\Valinor\Type\CombiningType; use CuyZ\Valinor\Type\ObjectType; +use CuyZ\Valinor\Type\CompositeType; use CuyZ\Valinor\Type\Type; use function implode; @@ -65,6 +66,17 @@ public function isMatchedBy(Type $other): bool return true; } + public function traverse(): iterable + { + foreach ($this->types as $type) { + yield $type; + + if ($type instanceof CompositeType) { + yield from $type->traverse(); + } + } + } + /** * @return ObjectType[] */ diff --git a/src/Type/Types/IterableType.php b/src/Type/Types/IterableType.php index 97d06a46..48147e0d 100644 --- a/src/Type/Types/IterableType.php +++ b/src/Type/Types/IterableType.php @@ -5,6 +5,7 @@ namespace CuyZ\Valinor\Type\Types; use CuyZ\Valinor\Type\CompositeTraversableType; +use CuyZ\Valinor\Type\CompositeType; use CuyZ\Valinor\Type\Type; use function is_iterable; @@ -84,6 +85,15 @@ public function subType(): Type return $this->subType; } + public function traverse(): iterable + { + yield $this->subType; + + if ($this->subType instanceof CompositeType) { + yield from $this->subType->traverse(); + } + } + public function __toString(): string { return $this->signature; diff --git a/src/Type/Types/ListType.php b/src/Type/Types/ListType.php index 7e799ddc..17083e5c 100644 --- a/src/Type/Types/ListType.php +++ b/src/Type/Types/ListType.php @@ -5,6 +5,7 @@ namespace CuyZ\Valinor\Type\Types; use CuyZ\Valinor\Type\CompositeTraversableType; +use CuyZ\Valinor\Type\CompositeType; use CuyZ\Valinor\Type\Type; use function is_array; @@ -91,6 +92,15 @@ public function subType(): Type return $this->subType; } + public function traverse(): iterable + { + yield $this->subType; + + if ($this->subType instanceof CompositeType) { + yield from $this->subType->traverse(); + } + } + public function __toString(): string { return $this->signature; diff --git a/src/Type/Types/NonEmptyArrayType.php b/src/Type/Types/NonEmptyArrayType.php index aa2897c9..7c7f834a 100644 --- a/src/Type/Types/NonEmptyArrayType.php +++ b/src/Type/Types/NonEmptyArrayType.php @@ -5,6 +5,7 @@ namespace CuyZ\Valinor\Type\Types; use CuyZ\Valinor\Type\CompositeTraversableType; +use CuyZ\Valinor\Type\CompositeType; use CuyZ\Valinor\Type\Type; use function is_array; @@ -94,6 +95,15 @@ public function subType(): Type return $this->subType; } + public function traverse(): iterable + { + yield $this->subType; + + if ($this->subType instanceof CompositeType) { + yield from $this->subType->traverse(); + } + } + public function __toString(): string { return $this->signature; diff --git a/src/Type/Types/NonEmptyListType.php b/src/Type/Types/NonEmptyListType.php index eb628470..83697f58 100644 --- a/src/Type/Types/NonEmptyListType.php +++ b/src/Type/Types/NonEmptyListType.php @@ -5,6 +5,7 @@ namespace CuyZ\Valinor\Type\Types; use CuyZ\Valinor\Type\CompositeTraversableType; +use CuyZ\Valinor\Type\CompositeType; use CuyZ\Valinor\Type\Type; use function count; @@ -99,6 +100,15 @@ public function subType(): Type return $this->subType; } + public function traverse(): iterable + { + yield $this->subType; + + if ($this->subType instanceof CompositeType) { + yield from $this->subType->traverse(); + } + } + public function __toString(): string { return $this->signature; diff --git a/src/Type/Types/ShapedArrayType.php b/src/Type/Types/ShapedArrayType.php index 7f7af2ae..44e39e05 100644 --- a/src/Type/Types/ShapedArrayType.php +++ b/src/Type/Types/ShapedArrayType.php @@ -6,7 +6,7 @@ use CuyZ\Valinor\Type\CompositeTraversableType; use CuyZ\Valinor\Type\Parser\Exception\Iterable\ShapedArrayElementDuplicatedKey; -use CuyZ\Valinor\Type\TraversableType; +use CuyZ\Valinor\Type\CompositeType; use CuyZ\Valinor\Type\Type; use function array_diff; @@ -18,7 +18,7 @@ use function is_array; /** @api */ -final class ShapedArrayType implements TraversableType +final class ShapedArrayType implements CompositeType { /** @var ShapedArrayElement[] */ private array $elements; @@ -120,6 +120,17 @@ public function matches(Type $other): bool return true; } + public function traverse(): iterable + { + foreach ($this->elements as $element) { + yield $type = $element->type(); + + if ($type instanceof CompositeType) { + yield from $type->traverse(); + } + } + } + /** * @return ShapedArrayElement[] */ diff --git a/src/Type/Types/UnionType.php b/src/Type/Types/UnionType.php index b5e74087..7f8193f4 100644 --- a/src/Type/Types/UnionType.php +++ b/src/Type/Types/UnionType.php @@ -5,6 +5,7 @@ namespace CuyZ\Valinor\Type\Types; use CuyZ\Valinor\Type\CombiningType; +use CuyZ\Valinor\Type\CompositeType; use CuyZ\Valinor\Type\Type; use CuyZ\Valinor\Type\Types\Exception\ForbiddenMixedType; @@ -82,6 +83,17 @@ public function isMatchedBy(Type $other): bool return false; } + public function traverse(): iterable + { + foreach ($this->types as $type) { + yield $type; + + if ($type instanceof CompositeType) { + yield from $type->traverse(); + } + } + } + public function types(): array { return $this->types; diff --git a/tests/Fake/Type/FakeCompositeType.php b/tests/Fake/Type/FakeCompositeType.php new file mode 100644 index 00000000..a24d425d --- /dev/null +++ b/tests/Fake/Type/FakeCompositeType.php @@ -0,0 +1,39 @@ +types = $types; + } + + public function traverse(): iterable + { + yield from $this->types; + } + + public function accepts($value): bool + { + return true; + } + + public function matches(Type $other): bool + { + return true; + } + + public function __toString(): string + { + return 'FakeCompositeType'; + } +} diff --git a/tests/Fake/Type/FakeObjectCompositeType.php b/tests/Fake/Type/FakeObjectCompositeType.php new file mode 100644 index 00000000..5a83e7bd --- /dev/null +++ b/tests/Fake/Type/FakeObjectCompositeType.php @@ -0,0 +1,59 @@ + */ + private array $generics; + + /** + * @param class-string $className + * @param array $generics + */ + public function __construct(string $className = stdClass::class, array $generics = []) + { + $this->className = $className; + $this->generics = $generics; + } + + public function className(): string + { + return $this->className; + } + + public function generics(): array + { + return $this->generics; + } + + public function accepts($value): bool + { + return true; + } + + public function matches(Type $other): bool + { + return true; + } + + public function traverse(): iterable + { + yield from $this->generics; + } + + public function __toString(): string + { + return $this->className; + } +} diff --git a/tests/Unit/Type/Types/ArrayTypeTest.php b/tests/Unit/Type/Types/ArrayTypeTest.php index dbb5ecdf..55eae671 100644 --- a/tests/Unit/Type/Types/ArrayTypeTest.php +++ b/tests/Unit/Type/Types/ArrayTypeTest.php @@ -4,6 +4,7 @@ namespace CuyZ\Valinor\Tests\Unit\Type\Types; +use CuyZ\Valinor\Tests\Fake\Type\FakeCompositeType; use CuyZ\Valinor\Tests\Fake\Type\FakeType; use CuyZ\Valinor\Type\Types\ArrayKeyType; use CuyZ\Valinor\Type\Types\ArrayType; @@ -134,6 +135,7 @@ public function test_matches_valid_iterable_type(): void self::assertTrue($arrayType->matches($iterableType)); } + public function test_does_not_match_invalid_iterable_type(): void { $typeA = new FakeType(); @@ -180,4 +182,26 @@ public function test_does_not_match_union_containing_invalid_type(): void self::assertFalse(ArrayType::native()->matches($unionType)); } + + public function test_traverse_type_yields_sub_type(): void + { + $subType = new FakeType(); + + $type = new ArrayType(ArrayKeyType::default(), $subType); + + self::assertCount(1, $type->traverse()); + self::assertContains($subType, $type->traverse()); + } + + public function test_traverse_type_yields_types_recursively(): void + { + $subType = new FakeType(); + $compositeType = new FakeCompositeType($subType); + + $type = new ArrayType(ArrayKeyType::default(), $compositeType); + + self::assertCount(2, $type->traverse()); + self::assertContains($subType, $type->traverse()); + self::assertContains($compositeType, $type->traverse()); + } } diff --git a/tests/Unit/Type/Types/ClassTypeTest.php b/tests/Unit/Type/Types/ClassTypeTest.php index b770c7da..c0c93a18 100644 --- a/tests/Unit/Type/Types/ClassTypeTest.php +++ b/tests/Unit/Type/Types/ClassTypeTest.php @@ -4,6 +4,7 @@ namespace CuyZ\Valinor\Tests\Unit\Type\Types; +use CuyZ\Valinor\Tests\Fake\Type\FakeCompositeType; use CuyZ\Valinor\Tests\Fake\Type\FakeType; use CuyZ\Valinor\Type\Types\ClassType; use CuyZ\Valinor\Type\Types\MixedType; @@ -106,4 +107,31 @@ public function test_does_not_match_union_containing_invalid_type(): void self::assertFalse($classType->matches($unionType)); } + + public function test_traverse_type_yields_sub_types(): void + { + $subTypeA = new FakeType(); + $subTypeB = new FakeType(); + + $type = new ClassType(stdClass::class, [ + 'TemplateA' => $subTypeA, + 'TemplateB' => $subTypeB, + ]); + + self::assertCount(2, $type->traverse()); + self::assertContains($subTypeA, $type->traverse()); + self::assertContains($subTypeB, $type->traverse()); + } + + public function test_traverse_type_yields_types_recursively(): void + { + $subType = new FakeType(); + $compositeType = new FakeCompositeType($subType); + + $type = new ClassType(stdClass::class, ['Template' => $compositeType]); + + self::assertCount(2, $type->traverse()); + self::assertContains($subType, $type->traverse()); + self::assertContains($compositeType, $type->traverse()); + } } diff --git a/tests/Unit/Type/Types/IntersectionTypeTest.php b/tests/Unit/Type/Types/IntersectionTypeTest.php index 538194fc..f79d1435 100644 --- a/tests/Unit/Type/Types/IntersectionTypeTest.php +++ b/tests/Unit/Type/Types/IntersectionTypeTest.php @@ -4,6 +4,7 @@ namespace CuyZ\Valinor\Tests\Unit\Type\Types; +use CuyZ\Valinor\Tests\Fake\Type\FakeObjectCompositeType; use CuyZ\Valinor\Tests\Fake\Type\FakeObjectType; use CuyZ\Valinor\Tests\Fake\Type\FakeType; use CuyZ\Valinor\Type\Types\IntersectionType; @@ -133,4 +134,32 @@ public function test_does_not_match_union_containing_invalid_type(): void self::assertFalse($intersectionType->matches($unionType)); } + + public function test_traverse_type_yields_sub_types(): void + { + $objectTypeA = new FakeObjectType(); + $objectTypeB = new FakeObjectType(); + + $type = new IntersectionType($objectTypeA, $objectTypeB); + + self::assertCount(2, $type->traverse()); + self::assertContains($objectTypeA, $type->traverse()); + self::assertContains($objectTypeB, $type->traverse()); + } + + public function test_traverse_type_yields_types_recursively(): void + { + $subTypeA = new FakeType(); + $subTypeB = new FakeType(); + $objectTypeA = new FakeObjectCompositeType(stdClass::class, ['Template' => $subTypeA]); + $objectTypeB = new FakeObjectCompositeType(stdClass::class, ['Template' => $subTypeB]); + + $type = new IntersectionType($objectTypeA, $objectTypeB); + + self::assertCount(4, $type->traverse()); + self::assertContains($subTypeA, $type->traverse()); + self::assertContains($subTypeB, $type->traverse()); + self::assertContains($objectTypeA, $type->traverse()); + self::assertContains($objectTypeB, $type->traverse()); + } } diff --git a/tests/Unit/Type/Types/IterableTypeTest.php b/tests/Unit/Type/Types/IterableTypeTest.php index a49ac054..59d56801 100644 --- a/tests/Unit/Type/Types/IterableTypeTest.php +++ b/tests/Unit/Type/Types/IterableTypeTest.php @@ -4,6 +4,7 @@ namespace CuyZ\Valinor\Tests\Unit\Type\Types; +use CuyZ\Valinor\Tests\Fake\Type\FakeCompositeType; use CuyZ\Valinor\Tests\Fake\Type\FakeType; use CuyZ\Valinor\Type\Types\ArrayKeyType; use CuyZ\Valinor\Type\Types\NativeStringType; @@ -146,4 +147,26 @@ public function test_does_not_match_union_containing_invalid_type(): void self::assertFalse(IterableType::native()->matches($unionType)); } + + public function test_traverse_type_yields_sub_type(): void + { + $subType = new FakeType(); + + $type = new IterableType(ArrayKeyType::default(), $subType); + + self::assertCount(1, $type->traverse()); + self::assertContains($subType, $type->traverse()); + } + + public function test_traverse_type_yields_types_recursively(): void + { + $subType = new FakeType(); + $compositeType = new FakeCompositeType($subType); + + $type = new IterableType(ArrayKeyType::default(), $compositeType); + + self::assertCount(2, $type->traverse()); + self::assertContains($subType, $type->traverse()); + self::assertContains($compositeType, $type->traverse()); + } } diff --git a/tests/Unit/Type/Types/ListTypeTest.php b/tests/Unit/Type/Types/ListTypeTest.php index 0c877815..20292fa1 100644 --- a/tests/Unit/Type/Types/ListTypeTest.php +++ b/tests/Unit/Type/Types/ListTypeTest.php @@ -4,6 +4,7 @@ namespace CuyZ\Valinor\Tests\Unit\Type\Types; +use CuyZ\Valinor\Tests\Fake\Type\FakeCompositeType; use CuyZ\Valinor\Tests\Fake\Type\FakeType; use CuyZ\Valinor\Type\Types\ArrayKeyType; use CuyZ\Valinor\Type\Types\ArrayType; @@ -188,4 +189,26 @@ public function test_does_not_match_union_containing_invalid_type(): void self::assertFalse(ListType::native()->matches($unionType)); } + + public function test_traverse_type_yields_sub_type(): void + { + $subType = new FakeType(); + + $type = new ListType($subType); + + self::assertCount(1, $type->traverse()); + self::assertContains($subType, $type->traverse()); + } + + public function test_traverse_type_yields_types_recursively(): void + { + $subType = new FakeType(); + $compositeType = new FakeCompositeType($subType); + + $type = new ListType($compositeType); + + self::assertCount(2, $type->traverse()); + self::assertContains($subType, $type->traverse()); + self::assertContains($compositeType, $type->traverse()); + } } diff --git a/tests/Unit/Type/Types/NonEmptyArrayTypeTest.php b/tests/Unit/Type/Types/NonEmptyArrayTypeTest.php index 13e73084..5910c872 100644 --- a/tests/Unit/Type/Types/NonEmptyArrayTypeTest.php +++ b/tests/Unit/Type/Types/NonEmptyArrayTypeTest.php @@ -4,6 +4,7 @@ namespace CuyZ\Valinor\Tests\Unit\Type\Types; +use CuyZ\Valinor\Tests\Fake\Type\FakeCompositeType; use CuyZ\Valinor\Tests\Fake\Type\FakeType; use CuyZ\Valinor\Type\Types\ArrayKeyType; use CuyZ\Valinor\Type\Types\MixedType; @@ -149,4 +150,26 @@ public function test_does_not_match_union_containing_invalid_type(): void self::assertFalse(NonEmptyArrayType::native()->matches($unionType)); } + + public function test_traverse_type_yields_sub_type(): void + { + $subType = new FakeType(); + + $type = new NonEmptyArrayType(ArrayKeyType::default(), $subType); + + self::assertCount(1, $type->traverse()); + self::assertContains($subType, $type->traverse()); + } + + public function test_traverse_type_yields_types_recursively(): void + { + $subType = new FakeType(); + $compositeType = new FakeCompositeType($subType); + + $type = new NonEmptyArrayType(ArrayKeyType::default(), $compositeType); + + self::assertCount(2, $type->traverse()); + self::assertContains($subType, $type->traverse()); + self::assertContains($compositeType, $type->traverse()); + } } diff --git a/tests/Unit/Type/Types/NonEmptyListTypeTest.php b/tests/Unit/Type/Types/NonEmptyListTypeTest.php index a19e612a..aaa1d7cf 100644 --- a/tests/Unit/Type/Types/NonEmptyListTypeTest.php +++ b/tests/Unit/Type/Types/NonEmptyListTypeTest.php @@ -4,6 +4,7 @@ namespace CuyZ\Valinor\Tests\Unit\Type\Types; +use CuyZ\Valinor\Tests\Fake\Type\FakeCompositeType; use CuyZ\Valinor\Tests\Fake\Type\FakeType; use CuyZ\Valinor\Type\Types\ArrayKeyType; use CuyZ\Valinor\Type\Types\ArrayType; @@ -235,4 +236,26 @@ public function test_does_not_match_union_containing_invalid_type(): void self::assertFalse(NonEmptyListType::native()->matches($unionType)); } + + public function test_traverse_type_yields_sub_type(): void + { + $subType = new FakeType(); + + $type = new NonEmptyListType($subType); + + self::assertCount(1, $type->traverse()); + self::assertContains($subType, $type->traverse()); + } + + public function test_traverse_type_yields_types_recursively(): void + { + $subType = new FakeType(); + $compositeType = new FakeCompositeType($subType); + + $type = new NonEmptyListType($compositeType); + + self::assertCount(2, $type->traverse()); + self::assertContains($subType, $type->traverse()); + self::assertContains($compositeType, $type->traverse()); + } } diff --git a/tests/Unit/Type/Types/ShapedArrayTypeTest.php b/tests/Unit/Type/Types/ShapedArrayTypeTest.php index 2d4c7e22..c1ebef06 100644 --- a/tests/Unit/Type/Types/ShapedArrayTypeTest.php +++ b/tests/Unit/Type/Types/ShapedArrayTypeTest.php @@ -4,6 +4,7 @@ namespace CuyZ\Valinor\Tests\Unit\Type\Types; +use CuyZ\Valinor\Tests\Fake\Type\FakeCompositeType; use CuyZ\Valinor\Tests\Fake\Type\FakeType; use CuyZ\Valinor\Type\Parser\Exception\Iterable\ShapedArrayElementDuplicatedKey; use CuyZ\Valinor\Type\Types\ArrayKeyType; @@ -168,4 +169,38 @@ public function test_does_not_match_union_containing_invalid_type(): void self::assertFalse($this->type->matches($unionType)); } + + public function test_traverse_type_yields_sub_types(): void + { + $subTypeA = new FakeType(); + $subTypeB = new FakeType(); + + $type = new ShapedArrayType( + new ShapedArrayElement(new StringValueType('foo'), $subTypeA), + new ShapedArrayElement(new StringValueType('bar'), $subTypeB), + ); + + self::assertCount(2, $type->traverse()); + self::assertContains($subTypeA, $type->traverse()); + self::assertContains($subTypeB, $type->traverse()); + } + + public function test_traverse_type_yields_types_recursively(): void + { + $subTypeA = new FakeType(); + $subTypeB = new FakeType(); + $compositeTypeA = new FakeCompositeType($subTypeA); + $compositeTypeB = new FakeCompositeType($subTypeB); + + $type = new ShapedArrayType( + new ShapedArrayElement(new StringValueType('foo'), $compositeTypeA), + new ShapedArrayElement(new StringValueType('bar'), $compositeTypeB), + ); + + self::assertCount(4, $type->traverse()); + self::assertContains($subTypeA, $type->traverse()); + self::assertContains($subTypeB, $type->traverse()); + self::assertContains($compositeTypeA, $type->traverse()); + self::assertContains($compositeTypeB, $type->traverse()); + } } diff --git a/tests/Unit/Type/Types/UnionTypeTest.php b/tests/Unit/Type/Types/UnionTypeTest.php index e911494f..93d6849d 100644 --- a/tests/Unit/Type/Types/UnionTypeTest.php +++ b/tests/Unit/Type/Types/UnionTypeTest.php @@ -4,6 +4,7 @@ namespace CuyZ\Valinor\Tests\Unit\Type\Types; +use CuyZ\Valinor\Tests\Fake\Type\FakeCompositeType; use CuyZ\Valinor\Tests\Fake\Type\FakeType; use CuyZ\Valinor\Type\Types\Exception\ForbiddenMixedType; use CuyZ\Valinor\Type\Types\MixedType; @@ -135,4 +136,32 @@ public function test_does_not_match_other_not_matching_union(): void self::assertFalse($unionTypeA->matches($unionTypeB)); } + + public function test_traverse_type_yields_sub_types(): void + { + $subTypeA = new FakeType(); + $subTypeB = new FakeType(); + + $type = new UnionType($subTypeA, $subTypeB); + + self::assertCount(2, $type->traverse()); + self::assertContains($subTypeA, $type->traverse()); + self::assertContains($subTypeB, $type->traverse()); + } + + public function test_traverse_type_yields_types_recursively(): void + { + $subTypeA = new FakeType(); + $subTypeB = new FakeType(); + $compositeTypeA = new FakeCompositeType($subTypeA); + $compositeTypeB = new FakeCompositeType($subTypeB); + + $type = new UnionType($compositeTypeA, $compositeTypeB); + + self::assertCount(4, $type->traverse()); + self::assertContains($subTypeA, $type->traverse()); + self::assertContains($subTypeB, $type->traverse()); + self::assertContains($compositeTypeA, $type->traverse()); + self::assertContains($compositeTypeB, $type->traverse()); + } }