From 943e691712c2fbf93a1f5097fcad471b88a34ea6 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 13 Oct 2025 10:11:53 +0200 Subject: [PATCH 1/3] [type-declaration] Add AddParamTypeFromStrictMethodCallPassRector --- ...TypeFromStrictMethodCallPassRectorTest.php | 28 +++ .../Fixture/fixture.php.inc | 35 ++++ .../Fixture/skip_if_check.php.inc | 19 ++ .../config/configured_rule.php | 9 + .../Stmt/RemoveConditionExactReturnRector.php | 2 - .../MethodCallParamTypeResolver.php | 39 ++++ ...aramTypeFromStrictMethodCallPassRector.php | 166 ++++++++++++++++++ ...eParamTypeFromIterableMethodCallRector.php | 2 - src/Config/Level/TypeDeclarationLevel.php | 2 + src/Reflection/ReflectionResolver.php | 56 +++--- 10 files changed, 325 insertions(+), 33 deletions(-) create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector/AddParamTypeFromStrictMethodCallPassRectorTest.php create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector/Fixture/fixture.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector/Fixture/skip_if_check.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector/config/configured_rule.php create mode 100644 rules/TypeDeclaration/NodeTypeAnalyzer/MethodCallParamTypeResolver.php create mode 100644 rules/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector.php diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector/AddParamTypeFromStrictMethodCallPassRectorTest.php b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector/AddParamTypeFromStrictMethodCallPassRectorTest.php new file mode 100644 index 00000000000..f61365953ca --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector/AddParamTypeFromStrictMethodCallPassRectorTest.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/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector/Fixture/fixture.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector/Fixture/fixture.php.inc new file mode 100644 index 00000000000..ff53ff1cf3c --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector/Fixture/fixture.php.inc @@ -0,0 +1,35 @@ +checkInt($number); + } + + private function checkInt(int $integer) + { + } +} + +?> +----- +checkInt($number); + } + + private function checkInt(int $integer) + { + } +} + +?> diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector/Fixture/skip_if_check.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector/Fixture/skip_if_check.php.inc new file mode 100644 index 00000000000..d7be8720a5a --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector/Fixture/skip_if_check.php.inc @@ -0,0 +1,19 @@ +checkInt($number); + } + + private function checkInt(int $integer) + { + } +} diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector/config/configured_rule.php b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector/config/configured_rule.php new file mode 100644 index 00000000000..a1329044217 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector/config/configured_rule.php @@ -0,0 +1,9 @@ +withRules([AddParamTypeFromStrictMethodCallPassRector::class]); diff --git a/rules/DeadCode/Rector/Stmt/RemoveConditionExactReturnRector.php b/rules/DeadCode/Rector/Stmt/RemoveConditionExactReturnRector.php index b7ccb574b5a..b9f76999273 100644 --- a/rules/DeadCode/Rector/Stmt/RemoveConditionExactReturnRector.php +++ b/rules/DeadCode/Rector/Stmt/RemoveConditionExactReturnRector.php @@ -8,8 +8,6 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\Equal; use PhpParser\Node\Expr\BinaryOp\Identical; -use PhpParser\Node\Expr\MethodCall; -use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Stmt\If_; use PhpParser\Node\Stmt\Return_; use Rector\Contract\PhpParser\Node\StmtsAwareInterface; diff --git a/rules/TypeDeclaration/NodeTypeAnalyzer/MethodCallParamTypeResolver.php b/rules/TypeDeclaration/NodeTypeAnalyzer/MethodCallParamTypeResolver.php new file mode 100644 index 00000000000..53e096b1981 --- /dev/null +++ b/rules/TypeDeclaration/NodeTypeAnalyzer/MethodCallParamTypeResolver.php @@ -0,0 +1,39 @@ + + */ + public function resolve(MethodCall $methodCall): array + { + $methodReflection = $this->reflectionResolver->resolveMethodReflectionFromMethodCall($methodCall); + if (! $methodReflection instanceof MethodReflection) { + return []; + } + + $extendedParametersAcceptor = ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants()); + + $typeByPosition = []; + foreach ($extendedParametersAcceptor->getParameters() as $position => $parameterReflection) { + $typeByPosition[$position] = $parameterReflection->getNativeType(); + } + + return $typeByPosition; + } +} diff --git a/rules/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector.php b/rules/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector.php new file mode 100644 index 00000000000..9f122ee13d3 --- /dev/null +++ b/rules/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector.php @@ -0,0 +1,166 @@ +resolve($value); + } + + private function resolve(int $value) + { + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +final class SomeClass +{ + public function run(int $value) + { + $this->resolve($value); + } + + private function resolve(int $value) + { + } +} +CODE_SAMPLE + ), + + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [ClassMethod::class, Function_::class, Node\Expr\Closure::class]; + } + + /** + * @param ClassMethod|Function_|Node\Expr\Closure $node + */ + public function refactor(Node $node): ?Node + { + if ($node->getParams() === []) { + return null; + } + + if ($node instanceof ClassMethod && $node->stmts === null) { + return null; + } + + $hasChanged = false; + + $usedVariables = $this->betterNodeFinder->findInstancesOfScoped((array) $node->stmts, Variable::class); + + foreach ($node->getParams() as $param) { + // already known type + if ($param->type instanceof Node) { + continue; + } + + /** @var string $paramName */ + $paramName = $this->getName($param->var); + + $paramVariables = array_filter( + $usedVariables, + fn (Variable $variable): bool => $this->isName($variable, $paramName) + ); + + if (count($paramVariables) >= 2) { + // skip for now, as we look for sole use + continue; + } + + $methodCalls = $this->betterNodeFinder->findInstancesOfScoped((array) $node->stmts, MethodCall::class); + foreach ($methodCalls as $methodCall) { + if ($methodCall->isFirstClassCallable()) { + continue; + } + + $typesByPosition = $this->methodCallParamTypeResolver->resolve($methodCall); + + $usedPosition = $this->matchParamMethodCallUsedPosition($methodCall, $paramName); + if (! is_int($usedPosition)) { + continue; + } + + $paramType = $typesByPosition[$usedPosition] ?? null; + if (! $paramType instanceof Type) { + continue; + } + + $paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($paramType, TypeKind::PARAM); + if (! $paramTypeNode instanceof Node) { + continue; + } + + $param->type = $paramTypeNode; + $hasChanged = true; + + // go to next param + continue 2; + } + } + + return null; + } + + private function matchParamMethodCallUsedPosition(MethodCall $methodCall, string $paramName): int|null + { + foreach ($methodCall->getArgs() as $position => $arg) { + if (! $arg->value instanceof Variable) { + continue; + } + + if (! $this->isName($arg->value, $paramName)) { + continue; + } + + return $position; + } + + return null; + } +} diff --git a/rules/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromIterableMethodCallRector.php b/rules/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromIterableMethodCallRector.php index 6f54f3fe03d..a1a511d096d 100644 --- a/rules/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromIterableMethodCallRector.php +++ b/rules/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromIterableMethodCallRector.php @@ -95,13 +95,11 @@ public function refactor(Node $node): ?Node } $varType = $this->getType($node->var); - if (! $varType instanceof IntersectionType || ! $varType->isIterable()->yes()) { return null; } $className = $varType->getObjectClassNames()[0] ?? null; - if ($className === null) { return null; } diff --git a/src/Config/Level/TypeDeclarationLevel.php b/src/Config/Level/TypeDeclarationLevel.php index f4ae649db3e..d883d511da3 100644 --- a/src/Config/Level/TypeDeclarationLevel.php +++ b/src/Config/Level/TypeDeclarationLevel.php @@ -20,6 +20,7 @@ use Rector\TypeDeclaration\Rector\ClassMethod\AddParamStringTypeFromSprintfUseRector; use Rector\TypeDeclaration\Rector\ClassMethod\AddParamTypeBasedOnPHPUnitDataProviderRector; use Rector\TypeDeclaration\Rector\ClassMethod\AddParamTypeFromPropertyTypeRector; +use Rector\TypeDeclaration\Rector\ClassMethod\AddParamTypeFromStrictMethodCallPassRector; use Rector\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector; use Rector\TypeDeclaration\Rector\ClassMethod\AddReturnTypeFromTryCatchTypeRector; use Rector\TypeDeclaration\Rector\ClassMethod\AddVoidReturnTypeWhereNoReturnRector; @@ -149,6 +150,7 @@ final class TypeDeclarationLevel // array parameter from dim fetch assign inside StrictArrayParamDimFetchRector::class, AddParamFromDimFetchKeyUseRector::class, + AddParamTypeFromStrictMethodCallPassRector::class, AddParamStringTypeFromSprintfUseRector::class, // possibly based on docblocks, but also helpful, intentionally last diff --git a/src/Reflection/ReflectionResolver.php b/src/Reflection/ReflectionResolver.php index 9ec329c1086..1fe92b44ea4 100644 --- a/src/Reflection/ReflectionResolver.php +++ b/src/Reflection/ReflectionResolver.php @@ -15,8 +15,6 @@ use PhpParser\Node\Name; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassLike; -use PhpParser\Node\Stmt\ClassMethod; -use PhpParser\Node\Stmt\Function_; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\FunctionReflection; @@ -204,33 +202,33 @@ public function resolveFunctionLikeReflectionFromCall( return $this->resolveFunctionReflectionFromFuncCall($call); } - public function resolveMethodReflectionFromClassMethod(ClassMethod $classMethod, Scope $scope): ?MethodReflection - { - $classReflection = $scope->getClassReflection(); - if (! $classReflection instanceof ClassReflection) { - return null; - } - - $className = $classReflection->getName(); - $methodName = $this->nodeNameResolver->getName($classMethod); - - return $this->resolveMethodReflection($className, $methodName, $scope); - } - - public function resolveFunctionReflectionFromFunction(Function_ $function): ?FunctionReflection - { - $name = $this->nodeNameResolver->getName($function); - if ($name === null) { - return null; - } - - $functionName = new Name($name); - if ($this->reflectionProvider->hasFunction($functionName, null)) { - return $this->reflectionProvider->getFunction($functionName, null); - } - - return null; - } + // public function resolveMethodReflectionFromClassMethod(ClassMethod $classMethod, Scope $scope): ?MethodReflection + // { + // $classReflection = $scope->getClassReflection(); + // if (! $classReflection instanceof ClassReflection) { + // return null; + // } + // + // $className = $classReflection->getName(); + // $methodName = $this->nodeNameResolver->getName($classMethod); + // + // return $this->resolveMethodReflection($className, $methodName, $scope); + // } + + // public function resolveFunctionReflectionFromFunction(Function_ $function): ?FunctionReflection + // { + // $name = $this->nodeNameResolver->getName($function); + // if ($name === null) { + // return null; + // } + // + // $functionName = new Name($name); + // if ($this->reflectionProvider->hasFunction($functionName, null)) { + // return $this->reflectionProvider->getFunction($functionName, null); + // } + // + // return null; + // } public function resolveMethodReflectionFromNew(New_ $new): ?MethodReflection { From 6385e91349b00b73bb7b9d817ac5a20180fb57e8 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 13 Oct 2025 10:42:35 +0200 Subject: [PATCH 2/3] remove unused resolveMethodReflectionFromClassMethod() and resolveFunctionReflectionFromFunction() --- src/Reflection/ReflectionResolver.php | 54 +++++++++++++-------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Reflection/ReflectionResolver.php b/src/Reflection/ReflectionResolver.php index 1fe92b44ea4..3631c3a9b0d 100644 --- a/src/Reflection/ReflectionResolver.php +++ b/src/Reflection/ReflectionResolver.php @@ -202,33 +202,33 @@ public function resolveFunctionLikeReflectionFromCall( return $this->resolveFunctionReflectionFromFuncCall($call); } - // public function resolveMethodReflectionFromClassMethod(ClassMethod $classMethod, Scope $scope): ?MethodReflection - // { - // $classReflection = $scope->getClassReflection(); - // if (! $classReflection instanceof ClassReflection) { - // return null; - // } - // - // $className = $classReflection->getName(); - // $methodName = $this->nodeNameResolver->getName($classMethod); - // - // return $this->resolveMethodReflection($className, $methodName, $scope); - // } - - // public function resolveFunctionReflectionFromFunction(Function_ $function): ?FunctionReflection - // { - // $name = $this->nodeNameResolver->getName($function); - // if ($name === null) { - // return null; - // } - // - // $functionName = new Name($name); - // if ($this->reflectionProvider->hasFunction($functionName, null)) { - // return $this->reflectionProvider->getFunction($functionName, null); - // } - // - // return null; - // } + public function resolveMethodReflectionFromClassMethod(ClassMethod $classMethod, Scope $scope): ?MethodReflection + { + $classReflection = $scope->getClassReflection(); + if (! $classReflection instanceof ClassReflection) { + return null; + } + + $className = $classReflection->getName(); + $methodName = $this->nodeNameResolver->getName($classMethod); + + return $this->resolveMethodReflection($className, $methodName, $scope); + } + + public function resolveFunctionReflectionFromFunction(Function_ $function): ?FunctionReflection + { + $name = $this->nodeNameResolver->getName($function); + if ($name === null) { + return null; + } + + $functionName = new Name($name); + if ($this->reflectionProvider->hasFunction($functionName, null)) { + return $this->reflectionProvider->getFunction($functionName, null); + } + + return null; + } public function resolveMethodReflectionFromNew(New_ $new): ?MethodReflection { From f0f269cbafb3bd72711ef1157784a9b95d5f099e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 13 Oct 2025 08:43:28 +0000 Subject: [PATCH 3/3] [ci-review] Rector Rectify --- .../AddParamTypeFromStrictMethodCallPassRector.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rules/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector.php b/rules/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector.php index 9f122ee13d3..10c0e77c84a 100644 --- a/rules/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector.php +++ b/rules/TypeDeclaration/Rector/ClassMethod/AddParamTypeFromStrictMethodCallPassRector.php @@ -4,6 +4,7 @@ namespace Rector\TypeDeclaration\Rector\ClassMethod; +use PhpParser\Node\Expr\Closure; use PhpParser\Node; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\Variable; @@ -74,11 +75,11 @@ private function resolve(int $value) */ public function getNodeTypes(): array { - return [ClassMethod::class, Function_::class, Node\Expr\Closure::class]; + return [ClassMethod::class, Function_::class, Closure::class]; } /** - * @param ClassMethod|Function_|Node\Expr\Closure $node + * @param ClassMethod|Function_|Closure $node */ public function refactor(Node $node): ?Node {