Skip to content
Merged
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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"phpunit"
],
"phpstan": "vendor/bin/phpstan analyse --ansi",
"duplicated-code": "vendor/bin/swiss-knife duplicated-code src rules --min-tokens 150 --min-lines 5",
"duplicated-code": "vendor/bin/swiss-knife duplicated-code src rules --min-tokens 100 --min-lines 5",
"check-cs": "vendor/bin/ecs check --ansi",
"class-leak": "vendor/bin/class-leak check config src rules --skip-suffix \"Rector\"",
"fix-cs": "vendor/bin/ecs check --fix --ansi",
Expand Down
114 changes: 114 additions & 0 deletions rules/CodeQuality/NodeAnalyser/MockedMethodTypeResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

declare(strict_types=1);

namespace Rector\PHPUnit\CodeQuality\NodeAnalyser;

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Scalar\String_;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use Rector\Enum\ClassName;
use Rector\NodeNameResolver\NodeNameResolver;
use Rector\NodeTypeResolver\NodeTypeResolver;
use Rector\PHPUnit\CodeQuality\ValueObject\MockedMethod;
use Rector\PHPUnit\Enum\PHPUnitClassName;

final readonly class MockedMethodTypeResolver
{
public function __construct(
private NodeTypeResolver $nodeTypeResolver,
private NodeNameResolver $nodeNameResolver,
) {
}

/**
* Resolves the mocked ->method('name') caller intersection type and method name, or null when it is not a mock.
*
* @param array<string, string> $propertyNameToMockedTypes
*/
public function resolve(MethodCall $methodMethodCall, array $propertyNameToMockedTypes): ?MockedMethod
{
if (! $this->nodeNameResolver->isName($methodMethodCall->name, 'method')) {
return null;
}

$methodNameExpr = $methodMethodCall->getArgs()[0]
->value;
if (! $methodNameExpr instanceof String_) {
return null;
}

$methodName = $methodNameExpr->value;
$callerType = $this->nodeTypeResolver->getType($methodMethodCall->var);
$callerExpr = $methodMethodCall;

if ($callerType instanceof ObjectType && in_array(
$callerType->getClassName(),
[
PHPUnitClassName::INVOCATION_MOCKER,
PHPUnitClassName::INVOCATION_MOCKER_INTERFACE,
PHPUnitClassName::INVOCATION_STUBBER,
],
true
)) {
$callerExpr = $methodMethodCall->var;

if ($callerExpr instanceof MethodCall) {
$callerType = $this->nodeTypeResolver->getType($callerExpr->var);
}
}

$callerType = $this->fallbackMockedObjectInSetUp($callerType, $callerExpr, $propertyNameToMockedTypes);
if (! $callerType instanceof IntersectionType) {
return null;
}

return new MockedMethod($methodName, $callerType);
}

/**
* @param array<string, string> $propertyNameToMockedTypes
*/
private function fallbackMockedObjectInSetUp(
Type $callerType,
Expr $expr,
array $propertyNameToMockedTypes
): Type {
if (! $callerType instanceof ObjectType && ! $callerType instanceof NeverType) {
return $callerType;
}

if (! $expr instanceof MethodCall) {
return $callerType;
}

if ($callerType instanceof ObjectType && $callerType->getClassName() !== ClassName::MOCK_OBJECT) {
return $callerType;
}

// type is missing, because of "final" keyword on mocked class
// resolve from setUp assignment instead
if (! $expr->var instanceof PropertyFetch && ! $expr->var instanceof Variable) {
return $callerType;
}

if ($expr->var instanceof Variable) {
$propertyOrVariableName = $this->nodeNameResolver->getName($expr->var);
} else {
$propertyOrVariableName = $this->nodeNameResolver->getName($expr->var->name);
}

if ($propertyOrVariableName !== null && isset($propertyNameToMockedTypes[$propertyOrVariableName])) {
$mockedType = $propertyNameToMockedTypes[$propertyOrVariableName];
return new IntersectionType([$callerType, new ObjectType($mockedType)]);
}

return $callerType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,21 @@
namespace Rector\PHPUnit\CodeQuality\Rector\Class_;

use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ArrowFunction;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Return_;
use PhpParser\NodeFinder;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use Rector\Enum\ClassName;
use Rector\PHPStanStaticTypeMapper\Enum\TypeKind;
use Rector\PHPUnit\CodeQuality\NodeAnalyser\MockedMethodTypeResolver;
use Rector\PHPUnit\CodeQuality\NodeAnalyser\SetUpAssignedMockTypesResolver;
use Rector\PHPUnit\CodeQuality\Reflection\MethodParametersAndReturnTypesResolver;
use Rector\PHPUnit\CodeQuality\ValueObject\MockedMethod;
use Rector\PHPUnit\CodeQuality\ValueObject\ParamTypesAndReturnType;
use Rector\PHPUnit\Enum\PHPUnitClassName;
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
use Rector\Rector\AbstractRector;
use Rector\Reflection\ReflectionResolver;
Expand All @@ -46,7 +39,8 @@ public function __construct(
private readonly StaticTypeMapper $staticTypeMapper,
private readonly SetUpAssignedMockTypesResolver $setUpAssignedMockTypesResolver,
private readonly MethodParametersAndReturnTypesResolver $methodParametersAndReturnTypesResolver,
private readonly ReflectionResolver $reflectionResolver
private readonly ReflectionResolver $reflectionResolver,
private readonly MockedMethodTypeResolver $mockedMethodTypeResolver
) {
}

Expand Down Expand Up @@ -159,46 +153,18 @@ public function refactor(Node $node): ?Class_
return null;
}

$methodNameExpr = $parentMethodCall->getArgs()[0]
->value;
if (! $methodNameExpr instanceof String_) {
$mockedMethod = $this->mockedMethodTypeResolver->resolve($parentMethodCall, $propertyNameToMockedTypes);
if (! $mockedMethod instanceof MockedMethod) {
return null;
}

$methodName = $methodNameExpr->value;
$callerType = $this->getType($parentMethodCall->var);

if ($callerType instanceof ObjectType && in_array(
$callerType->getClassName(),
[
PHPUnitClassName::INVOCATION_MOCKER,
PHPUnitClassName::INVOCATION_MOCKER_INTERFACE,
PHPUnitClassName::INVOCATION_STUBBER,
],
true
)) {
$parentMethodCall = $parentMethodCall->var;

if ($parentMethodCall instanceof MethodCall) {
$callerType = $this->getType($parentMethodCall->var);
}
}

$callerType = $this->fallbackMockedObjectInSetUp(
$callerType,
$parentMethodCall,
$propertyNameToMockedTypes
);

// we need mocks
if (! $callerType instanceof IntersectionType) {
return null;
}
$methodName = $mockedMethod->getMethodName();
$intersectionType = $mockedMethod->getCallerType();

$hasChanged = false;

$parameterTypesAndReturnType = $this->methodParametersAndReturnTypesResolver->resolveFromReflection(
$callerType,
$intersectionType,
$methodName,
$currentClassReflection
);
Expand Down Expand Up @@ -297,46 +263,6 @@ public function matchInnerClosure(MethodCall $methodCall): null|ArrowFunction|Cl
return null;
}

/**
* @param array<string, string> $propertyNameToMockedTypes
*/
private function fallbackMockedObjectInSetUp(
Type $callerType,
Expr $expr,
array $propertyNameToMockedTypes
): mixed {
if (! $callerType instanceof ObjectType && ! $callerType instanceof NeverType) {
return $callerType;
}

if (! $expr instanceof MethodCall) {
return $callerType;
}

if ($callerType instanceof ObjectType && $callerType->getClassName() !== ClassName::MOCK_OBJECT) {
return $callerType;
}

// type is missing, because of "final" keyword on mocked class
// resolve from constructor instead
if (! $expr->var instanceof PropertyFetch && ! $expr->var instanceof Variable) {
return $callerType;
}

if ($expr->var instanceof Variable) {
$propertyOrVariableName = $this->getName($expr->var);
} else {
$propertyOrVariableName = $this->getName($expr->var->name);
}

if (isset($propertyNameToMockedTypes[$propertyOrVariableName])) {
$mockedType = $propertyNameToMockedTypes[$propertyOrVariableName];
return new IntersectionType([$callerType, new ObjectType($mockedType)]);
}

return $callerType;
}

private function shouldSkipReturnForConflictWithReturnedNodeType(
Closure|ArrowFunction $functionLike,
Type $returnType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,22 @@
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\Throw_;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PhpParser\Node\Scalar;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Return_;
use PhpParser\NodeFinder;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\VoidType;
use Rector\Enum\ClassName;
use Rector\PHPUnit\CodeQuality\NodeAnalyser\MockedMethodTypeResolver;
use Rector\PHPUnit\CodeQuality\NodeAnalyser\SetUpAssignedMockTypesResolver;
use Rector\PHPUnit\CodeQuality\Reflection\MethodParametersAndReturnTypesResolver;
use Rector\PHPUnit\CodeQuality\ValueObject\MockedMethod;
use Rector\PHPUnit\CodeQuality\ValueObject\ParamTypesAndReturnType;
use Rector\PHPUnit\Enum\PHPUnitClassName;
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
use Rector\Rector\AbstractRector;
use Rector\Reflection\ReflectionResolver;
Expand All @@ -49,6 +43,7 @@ public function __construct(
private readonly SetUpAssignedMockTypesResolver $setUpAssignedMockTypesResolver,
private readonly MethodParametersAndReturnTypesResolver $methodParametersAndReturnTypesResolver,
private readonly ReflectionResolver $reflectionResolver,
private readonly MockedMethodTypeResolver $mockedMethodTypeResolver,
) {
}

Expand Down Expand Up @@ -221,44 +216,14 @@ private function isMockedVoidMethodCall(
return false;
}

$parentMethodCall = $methodCall->var;
if (! $this->isName($parentMethodCall->name, 'method')) {
return false;
}

$methodNameExpr = $parentMethodCall->getArgs()[0]
->value;
if (! $methodNameExpr instanceof String_) {
return false;
}

$methodName = $methodNameExpr->value;
$callerType = $this->getType($parentMethodCall->var);

if ($callerType instanceof ObjectType && in_array(
$callerType->getClassName(),
[
PHPUnitClassName::INVOCATION_MOCKER,
PHPUnitClassName::INVOCATION_MOCKER_INTERFACE,
PHPUnitClassName::INVOCATION_STUBBER,
],
true
)) {
$parentMethodCall = $parentMethodCall->var;

if ($parentMethodCall instanceof MethodCall) {
$callerType = $this->getType($parentMethodCall->var);
}
}

$callerType = $this->fallbackMockedObjectInSetUp($callerType, $parentMethodCall, $propertyNameToMockedTypes);
if (! $callerType instanceof IntersectionType) {
$mockedMethod = $this->mockedMethodTypeResolver->resolve($methodCall->var, $propertyNameToMockedTypes);
if (! $mockedMethod instanceof MockedMethod) {
return false;
}

$paramTypesAndReturnType = $this->methodParametersAndReturnTypesResolver->resolveFromReflection(
$callerType,
$methodName,
$mockedMethod->getCallerType(),
$mockedMethod->getMethodName(),
$currentClassReflection
);

Expand Down Expand Up @@ -342,44 +307,4 @@ private function isPureValue(Expr $expr): bool
{
return $expr instanceof Scalar || $expr instanceof ConstFetch || $expr instanceof Variable;
}

/**
* @param array<string, string> $propertyNameToMockedTypes
*/
private function fallbackMockedObjectInSetUp(
Type $callerType,
Expr $expr,
array $propertyNameToMockedTypes
): Type {
if (! $callerType instanceof ObjectType && ! $callerType instanceof NeverType) {
return $callerType;
}

if (! $expr instanceof MethodCall) {
return $callerType;
}

if ($callerType instanceof ObjectType && $callerType->getClassName() !== ClassName::MOCK_OBJECT) {
return $callerType;
}

// type is missing, because of "final" keyword on mocked class
// resolve from setUp assignment instead
if (! $expr->var instanceof PropertyFetch && ! $expr->var instanceof Variable) {
return $callerType;
}

if ($expr->var instanceof Variable) {
$propertyOrVariableName = $this->getName($expr->var);
} else {
$propertyOrVariableName = $this->getName($expr->var->name);
}

if (isset($propertyNameToMockedTypes[$propertyOrVariableName])) {
$mockedType = $propertyNameToMockedTypes[$propertyOrVariableName];
return new IntersectionType([$callerType, new ObjectType($mockedType)]);
}

return $callerType;
}
}
Loading
Loading