From f7a1185a3f06cea4eccaea23f7b11285d35dc11d Mon Sep 17 00:00:00 2001 From: Damilare Ogunsiji <62488009+Ogdams@users.noreply.github.com> Date: Fri, 26 Jun 2026 13:58:18 +0100 Subject: [PATCH] add toHaveMethodWithReturnType arch expectation --- src/Expectation.php | 43 ++++++++++++ src/Expectations/OppositeExpectation.php | 42 +++++++++++ .../Expect/toHaveMethodWithReturnType.php | 69 +++++++++++++++++++ .../HasMethodWithReturnType.php | 10 +++ .../HasNoMethod/HasNoMethodClass.php | 10 +++ .../HasNoReturnType/HasNoReturnType.php | 13 ++++ .../HasNullableReturnType.php | 10 +++ .../HasPartialUnionMatch.php | 10 +++ .../HasUnionReturnType/HasUnionReturnType.php | 10 +++ .../WrongReturnType/WrongReturnType.php | 10 +++ 10 files changed, 227 insertions(+) create mode 100644 tests/Features/Expect/toHaveMethodWithReturnType.php create mode 100644 tests/Fixtures/Arch/ToHaveMethodWithReturnType/HasMethodWithReturnType/HasMethodWithReturnType.php create mode 100644 tests/Fixtures/Arch/ToHaveMethodWithReturnType/HasNoMethod/HasNoMethodClass.php create mode 100644 tests/Fixtures/Arch/ToHaveMethodWithReturnType/HasNoReturnType/HasNoReturnType.php create mode 100644 tests/Fixtures/Arch/ToHaveMethodWithReturnType/HasNullableReturnType/HasNullableReturnType.php create mode 100644 tests/Fixtures/Arch/ToHaveMethodWithReturnType/HasPartialUnionMatch/HasPartialUnionMatch.php create mode 100644 tests/Fixtures/Arch/ToHaveMethodWithReturnType/HasUnionReturnType/HasUnionReturnType.php create mode 100644 tests/Fixtures/Arch/ToHaveMethodWithReturnType/WrongReturnType/WrongReturnType.php diff --git a/src/Expectation.php b/src/Expectation.php index b8c0862a7..0881be8af 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -618,6 +618,49 @@ public function toHaveMethod(array|string $method): ArchExpectation ); } + /** + * Asserts that the given expectation target has a specific method with a specific return type. + * + * When a string is given, the return type must match exactly (e.g. 'string|int' matches only 'string|int'). + * When an array is given, the method's return type must contain all of the given types (e.g. ['string', 'int'] + * matches 'string|int', 'string|int|null', etc.). + * + * @param array|string $returnType + */ + public function toHaveMethodWithReturnType(string $method, array|string $returnType): ArchExpectation + { + $returnTypes = is_array($returnType) ? $returnType : [$returnType]; + + return Targeted::make( + $this, + function (ObjectDescription $object) use ($method, $returnTypes, $returnType): bool { + if (! isset($object->reflectionClass) || ! $object->reflectionClass->hasMethod($method)) { + return false; + } + + $reflectionMethod = $object->reflectionClass->getMethod($method); + + if (! $reflectionMethod->hasReturnType()) { + return false; + } + + $actualReturnType = (string) $reflectionMethod->getReturnType(); + + if (! is_array($returnType)) { + return $actualReturnType === $returnType; + } + + $actualTypes = str_starts_with($actualReturnType, '?') + ? [substr($actualReturnType, 1), 'null'] + : explode('|', $actualReturnType); + + return count(array_intersect($returnTypes, $actualTypes)) === count($returnTypes); + }, + sprintf("to have method '%s' with return type%s", $method, is_array($returnType) ? sprintf(" containing '%s'", implode("', '", $returnTypes)) : sprintf(" '%s'", $returnType)), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), + ); + } + /** * Asserts that the given expectation target has a specific methods. * diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index 2713c7244..23ae16663 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -280,6 +280,48 @@ public function toHaveMethod(array|string $method): ArchExpectation ); } + /** + * Asserts that the given expectation target does not have a specific method with a specific return type. + * + * @param array|string $returnType + */ + public function toHaveMethodWithReturnType(string $method, array|string $returnType): ArchExpectation + { + $returnTypes = is_array($returnType) ? $returnType : [$returnType]; + + /** @var Expectation|string> $original */ + $original = $this->original; + + return Targeted::make( + $original, + function (ObjectDescription $object) use ($method, $returnTypes, $returnType): bool { + if (! isset($object->reflectionClass) || ! $object->reflectionClass->hasMethod($method)) { + return true; + } + + $reflectionMethod = $object->reflectionClass->getMethod($method); + + if (! $reflectionMethod->hasReturnType()) { + return true; + } + + $actualReturnType = (string) $reflectionMethod->getReturnType(); + + if (! is_array($returnType)) { + return $actualReturnType !== $returnType; + } + + $actualTypes = str_starts_with($actualReturnType, '?') + ? [substr($actualReturnType, 1), 'null'] + : explode('|', $actualReturnType); + + return count(array_intersect($returnTypes, $actualTypes)) !== count($returnTypes); + }, + sprintf("to not have method '%s' with return type%s", $method, is_array($returnType) ? sprintf(" containing '%s'", implode("', '", $returnTypes)) : sprintf(" '%s'", $returnType)), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), + ); + } + /** * Asserts that the given expectation target does not have suspicious characters. */ diff --git a/tests/Features/Expect/toHaveMethodWithReturnType.php b/tests/Features/Expect/toHaveMethodWithReturnType.php new file mode 100644 index 000000000..1716e7f86 --- /dev/null +++ b/tests/Features/Expect/toHaveMethodWithReturnType.php @@ -0,0 +1,69 @@ +expect('Tests\Fixtures\Arch\ToHaveMethodWithReturnType\HasMethodWithReturnType\HasMethodWithReturnType') + ->toHaveMethodWithReturnType('foo', 'string'); + +test('opposite class has method with return type') + ->throws(ArchExpectationFailedException::class) + ->expect('Tests\Fixtures\Arch\ToHaveMethodWithReturnType\HasMethodWithReturnType\HasMethodWithReturnType') + ->not->toHaveMethodWithReturnType('foo', 'string'); + +test('failure when return type does not match') + ->throws(ArchExpectationFailedException::class) + ->expect('Tests\Fixtures\Arch\ToHaveMethodWithReturnType\WrongReturnType\WrongReturnType') + ->toHaveMethodWithReturnType('foo', 'string'); + +test('opposite passes when return type does not match') + ->expect('Tests\Fixtures\Arch\ToHaveMethodWithReturnType\WrongReturnType\WrongReturnType') + ->not->toHaveMethodWithReturnType('foo', 'string'); + +test('failure when method has no return type') + ->throws(ArchExpectationFailedException::class) + ->expect('Tests\Fixtures\Arch\ToHaveMethodWithReturnType\HasNoReturnType\HasNoReturnType') + ->toHaveMethodWithReturnType('foo', 'string'); + +test('class has method with union return type exact match') + ->expect('Tests\Fixtures\Arch\ToHaveMethodWithReturnType\HasUnionReturnType\HasUnionReturnType') + ->toHaveMethodWithReturnType('foo', 'string|int'); + +test('failure when exact union does not match') + ->throws(ArchExpectationFailedException::class) + ->expect('Tests\Fixtures\Arch\ToHaveMethodWithReturnType\HasUnionReturnType\HasUnionReturnType') + ->toHaveMethodWithReturnType('foo', 'string'); + +test('class has method with union return type containing types') + ->expect('Tests\Fixtures\Arch\ToHaveMethodWithReturnType\HasUnionReturnType\HasUnionReturnType') + ->toHaveMethodWithReturnType('foo', ['string', 'int']); + +test('class has method with partial union match') + ->expect('Tests\Fixtures\Arch\ToHaveMethodWithReturnType\HasPartialUnionMatch\HasPartialUnionMatch') + ->toHaveMethodWithReturnType('foo', ['string', 'int']); + +test('failure when not all array types are present') + ->throws(ArchExpectationFailedException::class) + ->expect('Tests\Fixtures\Arch\ToHaveMethodWithReturnType\HasUnionReturnType\HasUnionReturnType') + ->toHaveMethodWithReturnType('foo', ['string', 'float']); + +test('opposite passes when not all array types are present') + ->expect('Tests\Fixtures\Arch\ToHaveMethodWithReturnType\HasUnionReturnType\HasUnionReturnType') + ->not->toHaveMethodWithReturnType('foo', ['string', 'float']); + +test('failure when class has no method') + ->throws(ArchExpectationFailedException::class) + ->expect('Tests\Fixtures\Arch\ToHaveMethodWithReturnType\HasNoMethod\HasNoMethodClass') + ->toHaveMethodWithReturnType('foo', 'string'); + +test('opposite passes when class has no method') + ->expect('Tests\Fixtures\Arch\ToHaveMethodWithReturnType\HasNoMethod\HasNoMethodClass') + ->not->toHaveMethodWithReturnType('foo', 'string'); + +test('class has method with nullable return type containing types') + ->expect('Tests\Fixtures\Arch\ToHaveMethodWithReturnType\HasNullableReturnType\HasNullableReturnType') + ->toHaveMethodWithReturnType('foo', ['array', 'null']); + +test('class has method with nullable return type exact match') + ->expect('Tests\Fixtures\Arch\ToHaveMethodWithReturnType\HasNullableReturnType\HasNullableReturnType') + ->toHaveMethodWithReturnType('foo', '?array'); diff --git a/tests/Fixtures/Arch/ToHaveMethodWithReturnType/HasMethodWithReturnType/HasMethodWithReturnType.php b/tests/Fixtures/Arch/ToHaveMethodWithReturnType/HasMethodWithReturnType/HasMethodWithReturnType.php new file mode 100644 index 000000000..ef0c77032 --- /dev/null +++ b/tests/Fixtures/Arch/ToHaveMethodWithReturnType/HasMethodWithReturnType/HasMethodWithReturnType.php @@ -0,0 +1,10 @@ +