diff --git a/composer.json b/composer.json index 49b24972..e5922475 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/rules/CodeQuality/NodeAnalyser/MockedMethodTypeResolver.php b/rules/CodeQuality/NodeAnalyser/MockedMethodTypeResolver.php new file mode 100644 index 00000000..a734e9f2 --- /dev/null +++ b/rules/CodeQuality/NodeAnalyser/MockedMethodTypeResolver.php @@ -0,0 +1,114 @@ +method('name') caller intersection type and method name, or null when it is not a mock. + * + * @param array $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 $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; + } +} diff --git a/rules/CodeQuality/Rector/Class_/TypeWillReturnCallableArrowFunctionRector.php b/rules/CodeQuality/Rector/Class_/TypeWillReturnCallableArrowFunctionRector.php index cadd3539..0a022652 100644 --- a/rules/CodeQuality/Rector/Class_/TypeWillReturnCallableArrowFunctionRector.php +++ b/rules/CodeQuality/Rector/Class_/TypeWillReturnCallableArrowFunctionRector.php @@ -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; @@ -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 ) { } @@ -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 ); @@ -297,46 +263,6 @@ public function matchInnerClosure(MethodCall $methodCall): null|ArrowFunction|Cl return null; } - /** - * @param array $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 diff --git a/rules/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector.php b/rules/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector.php index f24dfcaf..0cef3201 100644 --- a/rules/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector.php +++ b/rules/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector.php @@ -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; @@ -49,6 +43,7 @@ public function __construct( private readonly SetUpAssignedMockTypesResolver $setUpAssignedMockTypesResolver, private readonly MethodParametersAndReturnTypesResolver $methodParametersAndReturnTypesResolver, private readonly ReflectionResolver $reflectionResolver, + private readonly MockedMethodTypeResolver $mockedMethodTypeResolver, ) { } @@ -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 ); @@ -342,44 +307,4 @@ private function isPureValue(Expr $expr): bool { return $expr instanceof Scalar || $expr instanceof ConstFetch || $expr instanceof Variable; } - - /** - * @param array $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; - } } diff --git a/rules/CodeQuality/ValueObject/MockedMethod.php b/rules/CodeQuality/ValueObject/MockedMethod.php new file mode 100644 index 00000000..9c9f036b --- /dev/null +++ b/rules/CodeQuality/ValueObject/MockedMethod.php @@ -0,0 +1,26 @@ +methodName; + } + + public function getCallerType(): IntersectionType + { + return $this->intersectionType; + } +}