Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/Expectation.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, string>|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.
*
Expand Down
42 changes: 42 additions & 0 deletions src/Expectations/OppositeExpectation.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, string>|string $returnType
*/
public function toHaveMethodWithReturnType(string $method, array|string $returnType): ArchExpectation
{
$returnTypes = is_array($returnType) ? $returnType : [$returnType];

/** @var Expectation<array<int, string>|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.
*/
Expand Down
69 changes: 69 additions & 0 deletions tests/Features/Expect/toHaveMethodWithReturnType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

use Pest\Arch\Exceptions\ArchExpectationFailedException;

test('class has method with return type')
->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');
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Tests\Fixtures\Arch\ToHaveMethodWithReturnType\HasMethodWithReturnType;

class HasMethodWithReturnType
{
public function foo(): string {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Tests\Fixtures\Arch\ToHaveMethodWithReturnType\HasNoMethod;

class HasNoMethodClass
{
public function bar(): string {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Tests\Fixtures\Arch\ToHaveMethodWithReturnType\HasNoReturnType;

class HasNoReturnType
{
public function foo()
{
return 'hello';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Tests\Fixtures\Arch\ToHaveMethodWithReturnType\HasNullableReturnType;

class HasNullableReturnType
{
public function foo(): ?array {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Tests\Fixtures\Arch\ToHaveMethodWithReturnType\HasPartialUnionMatch;

class HasPartialUnionMatch
{
public function foo(): string|int|null {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Tests\Fixtures\Arch\ToHaveMethodWithReturnType\HasUnionReturnType;

class HasUnionReturnType
{
public function foo(): string|int {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Tests\Fixtures\Arch\ToHaveMethodWithReturnType\WrongReturnType;

class WrongReturnType
{
public function foo(): int {}
}