From d8b7878560d7e715530c0ec3b2411858122a582c Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Wed, 31 Aug 2022 10:46:24 -0400 Subject: [PATCH] [feature] support intersection type support --- .github/workflows/ci.yml | 2 +- src/Callback/Argument.php | 36 +++++++++++-- tests/ArgumentTest.php | 105 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 tests/ArgumentTest.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28aa1c8..03cfa41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: code-coverage: uses: zenstruck/.github/.github/workflows/php-coverage-codecov.yml@main with: - php: 8 + php: 8.1 phpunit: simple-phpunit composer-validate: diff --git a/src/Callback/Argument.php b/src/Callback/Argument.php index 64c31c1..6844dd6 100644 --- a/src/Callback/Argument.php +++ b/src/Callback/Argument.php @@ -65,13 +65,18 @@ public function __construct(\ReflectionParameter $parameter) $this->reflectionType = $parameter->getType(); } + public function __toString(): string + { + return (string) $this->type(); + } + public function type(): ?string { if (isset($this->type)) { return $this->type; } - return $this->type = $this->hasType() ? \implode('|', $this->types()) : null; + return $this->type = $this->hasType() ? \implode($this->isIntersectionType() ? '&' : '|', $this->types()) : null; } /** @@ -101,12 +106,22 @@ function(\ReflectionNamedType $type) { public function hasType(): bool { - return !empty($this->types()); + return (bool) $this->reflectionType; + } + + public function isNamedType(): bool + { + return $this->reflectionType instanceof \ReflectionNamedType; } public function isUnionType(): bool { - return \count($this->types()) > 1; + return $this->reflectionType instanceof \ReflectionUnionType; + } + + public function isIntersectionType(): bool + { + return $this->reflectionType instanceof \ReflectionIntersectionType; } public function isOptional(): bool @@ -129,11 +144,24 @@ public function defaultValue() */ public function supports(string $type, int $options = self::EXACT|self::COVARIANCE): bool { - if (!$this->hasType()) { + if (!$this->reflectionType) { // no type-hint so any type is supported return true; } + if ($this->reflectionType instanceof \ReflectionIntersectionType) { + foreach ($this->reflectionType->getTypes() as $refType) { + $arg = clone $this; + $arg->reflectionType = $refType; + + if (!$arg->supports($type)) { + return false; + } + } + + return true; + } + if ('null' === \mb_strtolower($type) && $this->parameter->allowsNull()) { return true; } diff --git a/tests/ArgumentTest.php b/tests/ArgumentTest.php new file mode 100644 index 0000000..dd4b6b3 --- /dev/null +++ b/tests/ArgumentTest.php @@ -0,0 +1,105 @@ + + */ +final class ArgumentTest extends TestCase +{ + /** + * @test + * @requires PHP >= 8.0 + */ + public function union_type(): void + { + eval('$callback = fn(int|string $arg) => null;'); + $arg = Callback::createFor($callback)->argument(0); + + $this->assertSame('string|int', $arg->type()); + $this->assertSame('string|int', (string) $arg); + $this->assertTrue($arg->hasType()); + $this->assertFalse($arg->isNamedType()); + $this->assertTrue($arg->isUnionType()); + $this->assertFalse($arg->isIntersectionType()); + } + + /** + * @test + */ + public function named_type(): void + { + $arg = Callback::createFor(function(string $foo) {})->argument(0); + + $this->assertSame('string', $arg->type()); + $this->assertSame('string', (string) $arg); + $this->assertTrue($arg->hasType()); + $this->assertTrue($arg->isNamedType()); + $this->assertFalse($arg->isUnionType()); + $this->assertFalse($arg->isIntersectionType()); + } + + /** + * @test + */ + public function no_type(): void + { + $arg = Callback::createFor(function($foo) {})->argument(0); + + $this->assertNull($arg->type()); + $this->assertSame('', (string) $arg); + $this->assertFalse($arg->hasType()); + $this->assertFalse($arg->isNamedType()); + $this->assertFalse($arg->isUnionType()); + $this->assertFalse($arg->isIntersectionType()); + } + + /** + * @test + * @requires PHP >= 8.1 + */ + public function intersection_type(): void + { + eval('$callback = fn(\Countable&\Iterator $arg) => null;'); + $arg = Callback::createFor($callback)->argument(0); + + $this->assertSame('Countable&Iterator', $arg->type()); + $this->assertSame('Countable&Iterator', (string) $arg); + $this->assertTrue($arg->hasType()); + $this->assertFalse($arg->isNamedType()); + $this->assertFalse($arg->isUnionType()); + $this->assertTrue($arg->isIntersectionType()); + } + + /** + * @test + * @requires PHP >= 8.1 + */ + public function supports_intersection(): void + { + eval('$callback = fn(\Countable&\IteratorAggregate $arg) => null;'); + $arg = Callback::createFor($callback)->argument(0); + + $this->assertFalse($arg->supports('string')); + $this->assertFalse($arg->supports(\get_class(new class() implements \Countable { + public function count(): int + { + return 0; + } + }))); + $this->assertTrue($arg->supports(\get_class(new class() implements \Countable, \IteratorAggregate { + public function count(): int + { + return 0; + } + + public function getIterator(): \Traversable + { + return new \ArrayIterator(); + } + }))); + } +}