diff --git a/config/sets/phpunit-code-quality.php b/config/sets/phpunit-code-quality.php index e1f472a0..ad2a786a 100644 --- a/config/sets/phpunit-code-quality.php +++ b/config/sets/phpunit-code-quality.php @@ -37,6 +37,7 @@ use Rector\PHPUnit\CodeQuality\Rector\MethodCall\NarrowIdenticalWithConsecutiveRector; use Rector\PHPUnit\CodeQuality\Rector\MethodCall\NarrowSingleWillReturnCallbackRector; use Rector\PHPUnit\CodeQuality\Rector\MethodCall\RemoveExpectAnyFromMockRector; +use Rector\PHPUnit\CodeQuality\Rector\MethodCall\ScalarArgumentToExpectedParamTypeRector; use Rector\PHPUnit\CodeQuality\Rector\MethodCall\SingleWithConsecutiveToWithRector; use Rector\PHPUnit\CodeQuality\Rector\MethodCall\StringCastAssertStringContainsStringRector; use Rector\PHPUnit\CodeQuality\Rector\MethodCall\UseSpecificWillMethodRector; @@ -74,6 +75,7 @@ StringCastAssertStringContainsStringRector::class, AddParamTypeFromDependsRector::class, AddReturnTypeToDependedRector::class, + ScalarArgumentToExpectedParamTypeRector::class, NarrowUnusedSetUpDefinedPropertyRector::class, diff --git a/rector.php b/rector.php index 2e6487ac..621cc49e 100644 --- a/rector.php +++ b/rector.php @@ -39,7 +39,8 @@ naming: true, earlyReturn: true, rectorPreset: true, - phpunitCodeQuality: true + phpunitCodeQuality: true, + symfonyCodeQuality: true, ) ->withConfiguredRule(StringClassNameToClassConstantRector::class, [ // keep unprefixed to protected from downgrade diff --git a/rules-tests/CodeQuality/Rector/MethodCall/ScalarArgumentToExpectedParamTypeRector/Fixture/simple_method_call.php.inc b/rules-tests/CodeQuality/Rector/MethodCall/ScalarArgumentToExpectedParamTypeRector/Fixture/simple_method_call.php.inc new file mode 100644 index 00000000..6879f188 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/MethodCall/ScalarArgumentToExpectedParamTypeRector/Fixture/simple_method_call.php.inc @@ -0,0 +1,35 @@ +setPhoneNumber(123456); + } +} + +?> +----- +setPhoneNumber('123456'); + } +} + +?> diff --git a/rules-tests/CodeQuality/Rector/MethodCall/ScalarArgumentToExpectedParamTypeRector/Fixture/skip_docblock_type.php.inc b/rules-tests/CodeQuality/Rector/MethodCall/ScalarArgumentToExpectedParamTypeRector/Fixture/skip_docblock_type.php.inc new file mode 100644 index 00000000..7c63da93 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/MethodCall/ScalarArgumentToExpectedParamTypeRector/Fixture/skip_docblock_type.php.inc @@ -0,0 +1,15 @@ +setMagicType(123456); + } +} diff --git a/rules-tests/CodeQuality/Rector/MethodCall/ScalarArgumentToExpectedParamTypeRector/Fixture/skip_union_type.php.inc b/rules-tests/CodeQuality/Rector/MethodCall/ScalarArgumentToExpectedParamTypeRector/Fixture/skip_union_type.php.inc new file mode 100644 index 00000000..206133d9 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/MethodCall/ScalarArgumentToExpectedParamTypeRector/Fixture/skip_union_type.php.inc @@ -0,0 +1,15 @@ +setUnionType(123456); + } +} diff --git a/rules-tests/CodeQuality/Rector/MethodCall/ScalarArgumentToExpectedParamTypeRector/ScalarArgumentToExpectedParamTypeRectorTest.php b/rules-tests/CodeQuality/Rector/MethodCall/ScalarArgumentToExpectedParamTypeRector/ScalarArgumentToExpectedParamTypeRectorTest.php new file mode 100644 index 00000000..2864f044 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/MethodCall/ScalarArgumentToExpectedParamTypeRector/ScalarArgumentToExpectedParamTypeRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/CodeQuality/Rector/MethodCall/ScalarArgumentToExpectedParamTypeRector/Source/SomeClassWithSetter.php b/rules-tests/CodeQuality/Rector/MethodCall/ScalarArgumentToExpectedParamTypeRector/Source/SomeClassWithSetter.php new file mode 100644 index 00000000..824c7620 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/MethodCall/ScalarArgumentToExpectedParamTypeRector/Source/SomeClassWithSetter.php @@ -0,0 +1,25 @@ +rule(ScalarArgumentToExpectedParamTypeRector::class); +}; diff --git a/rules/CodeQuality/Rector/MethodCall/ScalarArgumentToExpectedParamTypeRector.php b/rules/CodeQuality/Rector/MethodCall/ScalarArgumentToExpectedParamTypeRector.php new file mode 100644 index 00000000..fd6b5321 --- /dev/null +++ b/rules/CodeQuality/Rector/MethodCall/ScalarArgumentToExpectedParamTypeRector.php @@ -0,0 +1,158 @@ +setPhone(12345); + } +} + +final class SomeClass +{ + public function setPhone(string $phone) + { + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use PHPUnit\Framework\TestCase; + +class SomeTest extends TestCase +{ + public function test() + { + $someClass = new SomeClass(); + $someClass->setPhone('12345'); + } +} + +final class SomeClass +{ + public function setPhone(string $phone) + { + } +} +CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [MethodCall::class, StaticCall::class]; + } + + /** + * @param MethodCall|StaticCall $node + */ + public function refactor(Node $node): ?Node + { + if (! $this->testsNodeAnalyzer->isInTestClass($node)) { + return null; + } + + if ($node->isFirstClassCallable()) { + return null; + } + + if ($node->getArgs() === []) { + return null; + } + + $hasChanged = false; + + if (! $this->hasStringOrNumberArguments($node)) { + return null; + } + + $callParameterTypes = $this->methodParametersAndReturnTypesResolver->resolveCallParameterTypes($node); + + foreach ($node->getArgs() as $key => $arg) { + if (! $arg->value instanceof Scalar) { + continue; + } + + $knownParameterType = $callParameterTypes[$key] ?? null; + if (! $knownParameterType instanceof Type) { + continue; + } + + if ($knownParameterType instanceof StringType && $arg->value instanceof Int_) { + $arg->value = new String_((string) $arg->value->value); + $hasChanged = true; + } + + if ($knownParameterType instanceof IntegerType && $arg->value instanceof String_) { + $arg->value = new Int_((int) $arg->value->value); + $hasChanged = true; + } + } + + if (! $hasChanged) { + return null; + } + + return $node; + } + + private function hasStringOrNumberArguments(StaticCall|MethodCall $call): bool + { + foreach ($call->getArgs() as $arg) { + if ($arg->value instanceof Int_) { + return true; + } + + if ($arg->value instanceof String_) { + return true; + } + } + + return false; + } +} diff --git a/rules/CodeQuality/Reflection/MethodParametersAndReturnTypesResolver.php b/rules/CodeQuality/Reflection/MethodParametersAndReturnTypesResolver.php index c4f1616f..34d5b403 100644 --- a/rules/CodeQuality/Reflection/MethodParametersAndReturnTypesResolver.php +++ b/rules/CodeQuality/Reflection/MethodParametersAndReturnTypesResolver.php @@ -4,6 +4,9 @@ namespace Rector\PHPUnit\CodeQuality\Reflection; +use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Identifier; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; @@ -13,10 +16,17 @@ use PHPStan\Type\StaticType; use PHPStan\Type\Type; use Rector\Enum\ClassName; +use Rector\NodeTypeResolver\NodeTypeResolver; +use Rector\PHPStan\ScopeFetcher; use Rector\PHPUnit\CodeQuality\ValueObject\ParamTypesAndReturnType; -final class MethodParametersAndReturnTypesResolver +final readonly class MethodParametersAndReturnTypesResolver { + public function __construct( + private NodeTypeResolver $nodeTypeResolver + ) { + } + public function resolveFromReflection( IntersectionType $intersectionType, string $methodName, @@ -51,10 +61,41 @@ public function resolveFromReflection( return null; } + /** + * @return null|Type[] + */ + public function resolveCallParameterTypes(MethodCall|StaticCall $call): ?array + { + if (! $call->name instanceof Identifier) { + return null; + } + + $methodName = $call->name->toString(); + + $callerType = $this->nodeTypeResolver->getType($call instanceof MethodCall ? $call->var : $call->class); + if (! $callerType instanceof ObjectType) { + return null; + } + + $classReflection = $callerType->getClassReflection(); + if (! $classReflection instanceof ClassReflection) { + return null; + } + + if (! $classReflection->hasNativeMethod($methodName)) { + return null; + } + + $scope = ScopeFetcher::fetch($call); + $extendedMethodReflection = $classReflection->getMethod($methodName, $scope); + + return $this->resolveParameterTypes($extendedMethodReflection, $classReflection); + } + /** * @return Type[] */ - private function resolveParameterTypes( + public function resolveParameterTypes( ExtendedMethodReflection $extendedMethodReflection, ClassReflection $currentClassReflection ): array {