Skip to content

Commit

Permalink
Report error when trying to configure a non existing method on MockOb…
Browse files Browse the repository at this point in the history
…ject
  • Loading branch information
VincentLanglet authored and ondrejmirtes committed May 30, 2020
1 parent 1648b3d commit 1d3cfe3
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 0 deletions.
9 changes: 9 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ parameters:
- markTestIncomplete
- markTestSkipped
stubFiles:
- stubs/InvocationMocker.stub
- stubs/MockBuilder.stub
- stubs/MockObject.stub
- stubs/TestCase.stub
Expand All @@ -26,7 +27,15 @@ services:
class: PHPStan\Type\PHPUnit\Assert\AssertStaticMethodTypeSpecifyingExtension
tags:
- phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension
-
class: PHPStan\Type\PHPUnit\InvocationMockerDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: PHPStan\Type\PHPUnit\MockBuilderDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: PHPStan\Type\PHPUnit\MockObjectDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
1 change: 1 addition & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ rules:
- PHPStan\Rules\PHPUnit\AssertSameBooleanExpectedRule
- PHPStan\Rules\PHPUnit\AssertSameNullExpectedRule
- PHPStan\Rules\PHPUnit\AssertSameWithCountRule
- PHPStan\Rules\PHPUnit\MockMethodCallRule
85 changes: 85 additions & 0 deletions src/Rules/PHPUnit/MockMethodCallRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\PHPUnit;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\ObjectType;
use PHPUnit\Framework\MockObject\InvocationMocker;
use PHPUnit\Framework\MockObject\MockObject;

/**
* @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\MethodCall>
*/
class MockMethodCallRule implements \PHPStan\Rules\Rule
{

public function getNodeType(): string
{
return Node\Expr\MethodCall::class;
}

public function processNode(Node $node, Scope $scope): array
{
/** @var Node\Expr\MethodCall $node */
$node = $node;

if (!$node->name instanceof Node\Identifier || $node->name->name !== 'method') {
return [];
}

if (count($node->args) < 1) {
return [];
}

$argType = $scope->getType($node->args[0]->value);
if (!($argType instanceof ConstantStringType)) {
return [];
}

$method = $argType->getValue();
$type = $scope->getType($node->var);

if (
$type instanceof IntersectionType
&& in_array(MockObject::class, $type->getReferencedClasses(), true)
&& !$type->hasMethod($method)->yes()
) {
$mockClass = array_filter($type->getReferencedClasses(), function (string $class): bool {
return $class !== MockObject::class;
});

return [
sprintf(
'Trying to mock an undefined method %s() on class %s.',
$method,
\implode('&', $mockClass)
),
];
}

if (
$type instanceof GenericObjectType
&& $type->getClassName() === InvocationMocker::class
&& count($type->getTypes()) > 0
) {
$mockClass = $type->getTypes()[0];

if ($mockClass instanceof ObjectType && !$mockClass->hasMethod($method)->yes()) {
return [
sprintf(
'Trying to mock an undefined method %s() on class %s.',
$method,
$mockClass->getClassName()
),
];
}
}

return [];
}

}
29 changes: 29 additions & 0 deletions src/Type/PHPUnit/InvocationMockerDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?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\Type;
use PHPUnit\Framework\MockObject\Builder\InvocationMocker;

class InvocationMockerDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension
{

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

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

public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
{
return $scope->getType($methodCall->var);
}

}
42 changes: 42 additions & 0 deletions src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?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\Generic\GenericObjectType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeWithClassName;
use PHPUnit\Framework\MockObject\InvocationMocker;
use PHPUnit\Framework\MockObject\MockObject;

class MockObjectDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension
{

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

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

public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
{
$type = $scope->getType($methodCall->var);
if (!($type instanceof IntersectionType)) {
return new GenericObjectType(InvocationMocker::class, []);
}

$mockClasses = array_filter($type->getTypes(), function (Type $type): bool {
return !$type instanceof TypeWithClassName || $type->getClassName() !== MockObject::class;
});

return new GenericObjectType(InvocationMocker::class, $mockClasses);
}

}
13 changes: 13 additions & 0 deletions stubs/InvocationMocker.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace PHPUnit\Framework\MockObject\Builder;

use PHPUnit\Framework\MockObject\Stub;

/**
* @template TMockedClass
*/
class InvocationMocker
{

}
42 changes: 42 additions & 0 deletions tests/Rules/PHPUnit/MockMethodCallRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\PHPUnit;

use PHPStan\Rules\Rule;

/**
* @extends \PHPStan\Testing\RuleTestCase<MockMethodCallRule>
*/
class MockMethodCallRuleTest extends \PHPStan\Testing\RuleTestCase
{

protected function getRule(): Rule
{
return new MockMethodCallRule();
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/mock-method-call.php'], [
[
'Trying to mock an undefined method doBadThing() on class MockMethodCall\Bar.',
15,
],
[
'Trying to mock an undefined method doBadThing() on class MockMethodCall\Bar.',
20,
],
]);
}

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

}
43 changes: 43 additions & 0 deletions tests/Rules/PHPUnit/data/mock-method-call.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php declare(strict_types = 1);

namespace MockMethodCall;

class Foo extends \PHPUnit\Framework\TestCase
{

public function testGoodMethod()
{
$this->createMock(Bar::class)->method('doThing');
}

public function testBadMethod()
{
$this->createMock(Bar::class)->method('doBadThing');
}

public function testBadMethodWithExpectation()
{
$this->createMock(Bar::class)->expects($this->once())->method('doBadThing');
}

public function testWithAnotherObject()
{
$bar = new BarWithMethod();
$bar->method('doBadThing');
}

}

class Bar {
public function doThing()
{
return 1;
}
};

class BarWithMethod {
public function method(string $string)
{
return $string;
}
};

0 comments on commit 1d3cfe3

Please sign in to comment.