Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ services:
class: PHPStan\Type\PHPUnit\MockBuilderDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: PHPStan\Type\PHPUnit\MockForIntersectionDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: PHPStan\Rules\PHPUnit\CoversHelper
-
Expand Down
79 changes: 79 additions & 0 deletions src/Type/PHPUnit/MockForIntersectionDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\PHPUnit;

use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\MockObject\Stub;
use PHPUnit\Framework\TestCase;
use function count;
use function in_array;

class MockForIntersectionDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{

public function getClass(): string
{
return TestCase::class;
}

public function isMethodSupported(MethodReflection $methodReflection): bool
{
return in_array(
$methodReflection->getName(),
[
'createMockForIntersectionOfInterfaces',
'createStubForIntersectionOfInterfaces',
],
true,
);
}

public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
{
$args = $methodCall->getArgs();
if (!isset($args[0])) {
return null;
}

$interfaces = $scope->getType($args[0]->value);
$constantArrays = $interfaces->getConstantArrays();
if (count($constantArrays) !== 1) {
return null;
}

$constantArray = $constantArrays[0];
if (count($constantArray->getOptionalKeys()) > 0) {
return null;
}

$result = [];
if ($methodReflection->getName() === 'createMockForIntersectionOfInterfaces') {
$result[] = new ObjectType(MockObject::class);
} else {
$result[] = new ObjectType(Stub::class);
}

foreach ($constantArray->getValueTypes() as $valueType) {
if (!$valueType->isClassString()->yes()) {
return null;
}

$values = $valueType->getConstantScalarValues();
if (count($values) !== 1) {
return null;
}

$result[] = new ObjectType((string) $values[0]);
}

return TypeCombinator::intersect(...$result);
}

}
13 changes: 13 additions & 0 deletions tests/Rules/PHPUnit/MockMethodCallRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\TestCase;
use function method_exists;
use const PHP_VERSION_ID;

/**
Expand Down Expand Up @@ -34,6 +36,17 @@ public function testRule(): void
],
];

if (method_exists(TestCase::class, 'createMockForIntersectionOfInterfaces')) { // @phpstan-ignore-line
$expectedErrors[] = [
'Trying to mock an undefined method bazMethod() on class MockMethodCall\FooInterface&MockMethodCall\BarInterface.',
49,
];
$expectedErrors[] = [
'Trying to mock an undefined method bazMethod() on class MockMethodCall\FooInterface&MockMethodCall\BarInterface.',
57,
];
}

$this->analyse([__DIR__ . '/data/mock-method-call.php'], $expectedErrors);
}

Expand Down
24 changes: 24 additions & 0 deletions tests/Rules/PHPUnit/data/mock-method-call.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ public function testMockObject(\PHPUnit\Framework\MockObject\MockObject $mock)
$mock->method('doFoo');
}

public function testMockForIntersection()
{
$mock = $this->createMockForIntersectionOfInterfaces([FooInterface::class, BarInterface::class]);
$mock->method('fooMethod');
$mock->method('barMethod');
$mock->method('bazMethod');
}

public function testStubForIntersection()
{
$stub = $this->createStubForIntersectionOfInterfaces([FooInterface::class, BarInterface::class]);
$stub->method('fooMethod');
$stub->method('barMethod');
$stub->method('bazMethod');
}

}

class Bar {
Expand Down Expand Up @@ -71,3 +87,11 @@ public function testMockFinalClass()
}

}

interface FooInterface {
public function fooMethod(): int;
}

interface BarInterface {
public function barMethod(): string;
}