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

namespace PHPStan\Type\PHPUnit;

use PhpParser\Node\Arg;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
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 strtolower;

class MockForIntersectionDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension, DynamicStaticMethodReturnTypeExtension
{

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

public function isMethodSupported(MethodReflection $methodReflection): bool
{
return strtolower($methodReflection->getName()) === 'createmockforintersectionofinterfaces';
}

public function isStaticMethodSupported(MethodReflection $methodReflection): bool
{
return strtolower($methodReflection->getName()) === 'createstubforintersectionofinterfaces';
}

public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type
{
return $this->getTypeFromCall($methodReflection, $methodCall->getArgs(), $scope);
}

public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
{
return $this->getTypeFromCall($methodReflection, $methodCall->getArgs(), $scope);
}

/**
* @param array<Arg> $args
*/
private function getTypeFromCall(MethodReflection $methodReflection, array $args, Scope $scope): ?Type
{
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 = static::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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\PHPUnit;

use PHPStan\Testing\TypeInferenceTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use function method_exists;

class MockForIntersectionDynamicReturnTypeExtensionTest extends TypeInferenceTestCase
{

/** @return mixed[] */
public static function dataFileAsserts(): iterable
{
if (method_exists(TestCase::class, 'createMockForIntersectionOfInterfaces')) { // @phpstan-ignore-line
yield from self::gatherAssertTypes(__DIR__ . '/data/mock-for-intersection.php');
}

return [];
}

/**
* @dataProvider dataFileAsserts
* @param mixed ...$args
*/
#[DataProvider('dataFileAsserts')]
public function testFileAsserts(
string $assertType,
string $file,
...$args
): void
{
$this->assertFileAsserts($assertType, $file, ...$args);
}

public static function getAdditionalConfigFiles(): array
{
return [__DIR__ . '/../../../extension.neon'];
}

}
37 changes: 37 additions & 0 deletions tests/Type/PHPUnit/data/mock-for-intersection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace MockForIntersection;

use PHPUnit\Framework\TestCase;

use function PHPStan\Testing\assertType;

class Foo extends TestCase
{

public function testFoo(bool $bool): void
{
assertType(
'MockForIntersection\BarInterface&MockForIntersection\FooInterface&PHPUnit\Framework\MockObject\MockObject',
$this->createMockForIntersectionOfInterfaces([FooInterface::class, BarInterface::class]),
);
assertType(
'MockForIntersection\BarInterface&MockForIntersection\FooInterface&PHPUnit\Framework\MockObject\Stub',
self::createStubForIntersectionOfInterfaces([FooInterface::class, BarInterface::class]),
);


assertType(
'PHPUnit\Framework\MockObject\MockObject',
$this->createMockForIntersectionOfInterfaces($bool ? [FooInterface::class, BarInterface::class] : [FooInterface::class]),
);
assertType(
'PHPUnit\Framework\MockObject\MockObject',
$this->createMockForIntersectionOfInterfaces($bool ? [FooInterface::class] : [BarInterface::class]),
);
}

}

interface FooInterface {}
interface BarInterface {}