From da8612665dbb78496a06e322bcb5a53a0f827dcf Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 23 Sep 2025 21:59:55 +0200 Subject: [PATCH 1/2] add fixture --- ....php.inc => cover_mixed_and_known.php.inc} | 0 ...iterable_param_from_another_method.php.inc | 44 +++++++++++++++++++ 2 files changed, 44 insertions(+) rename rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/{cover_mixex_and_known.php.inc => cover_mixed_and_known.php.inc} (100%) create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/iterable_param_from_another_method.php.inc diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/cover_mixex_and_known.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/cover_mixed_and_known.php.inc similarity index 100% rename from rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/cover_mixex_and_known.php.inc rename to rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/cover_mixed_and_known.php.inc diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/iterable_param_from_another_method.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/iterable_param_from_another_method.php.inc new file mode 100644 index 00000000000..e1e82377225 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/iterable_param_from_another_method.php.inc @@ -0,0 +1,44 @@ +run($items); + } + + private function run(array $items) + { + } +} + +?> +----- +run($items); + } + + /** + * @param string[] $items + */ + private function run(array $items) + { + } +} + +?> From 739fe995d739d5a2e3f971a2b538da3a08ec7b68 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 23 Sep 2025 22:09:13 +0200 Subject: [PATCH 2/2] allow non-strict types as array is never strict, in ClassMethodArrayDocblockParamFromLocalCallsRector --- .../Fixture/skip_param_array_override.php.inc | 18 ++++ .../NodeAnalyzer/CallTypesResolver.php | 97 ++++++++++++++++--- ...ArrayDocblockParamFromLocalCallsRector.php | 2 +- 3 files changed, 101 insertions(+), 16 deletions(-) create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/skip_param_array_override.php.inc diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/skip_param_array_override.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/skip_param_array_override.php.inc new file mode 100644 index 00000000000..2748a6dc9c6 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/skip_param_array_override.php.inc @@ -0,0 +1,18 @@ +run(['item1', 'item2']); + } + + /** + * @param int[]|string[] $items + */ + private function run(array $items) + { + } +} diff --git a/rules/TypeDeclaration/NodeAnalyzer/CallTypesResolver.php b/rules/TypeDeclaration/NodeAnalyzer/CallTypesResolver.php index 32f5e3a3db6..a29dbc2fc20 100644 --- a/rules/TypeDeclaration/NodeAnalyzer/CallTypesResolver.php +++ b/rules/TypeDeclaration/NodeAnalyzer/CallTypesResolver.php @@ -5,10 +5,12 @@ namespace Rector\TypeDeclaration\NodeAnalyzer; use PhpParser\Node\Arg; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Identifier; +use PhpParser\Node\VariadicPlaceholder; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; @@ -38,13 +40,12 @@ public function resolveStrictTypesFromCalls(array $calls): array foreach ($calls as $call) { foreach ($call->args as $position => $arg) { - // there is first class callable usage, or argument unpack, or named arg - // simply returns array marks as unknown as can be anything and in any position - if (! $arg instanceof Arg || $arg->unpack || $arg->name instanceof Identifier) { + if ($this->shouldSkipArg($arg)) { return []; } - if ($arg->value instanceof Array_ && $arg->value->items === []) { + /** @var Arg $arg */ + if ($this->isEmptyArray($arg->value)) { // skip empty array, as it doesn't add any value continue; } @@ -57,23 +58,46 @@ public function resolveStrictTypesFromCalls(array $calls): array return $this->unionToSingleType($staticTypesByArgumentPosition); } - private function resolveStrictArgValueType(Arg $arg): Type + /** + * @param MethodCall[]|StaticCall[] $calls + * @return array + */ + public function resolveTypesFromCalls(array $calls): array { - $argValueType = $this->nodeTypeResolver->getNativeType($arg->value); + $staticTypesByArgumentPosition = []; - // "self" in another object is not correct, this make it independent - $argValueType = $this->correctSelfType($argValueType); + foreach ($calls as $call) { + foreach ($call->args as $position => $arg) { + if ($this->shouldSkipArg($arg)) { + return []; + } - if (! $argValueType instanceof ObjectType) { - return $argValueType; - } + /** @var Arg $arg */ + if ($this->isEmptyArray($arg->value)) { + // skip empty array, as it doesn't add any value + continue; + } - // fix false positive generic type on string - if (! $this->reflectionProvider->hasClass($argValueType->getClassName())) { - return new MixedType(); + $staticTypesByArgumentPosition[$position][] = $this->resolveArgValueType($arg); + } } - return $argValueType; + // unite to single type + return $this->unionToSingleType($staticTypesByArgumentPosition); + } + + private function resolveStrictArgValueType(Arg $arg): Type + { + $argValueType = $this->nodeTypeResolver->getNativeType($arg->value); + + return $this->normalizeType($argValueType); + } + + private function resolveArgValueType(Arg $arg): Type + { + $argValueType = $this->nodeTypeResolver->getType($arg->value); + + return $this->normalizeType($argValueType); } private function correctSelfType(Type $argValueType): Type @@ -149,4 +173,47 @@ private function isTypeWithClassNameOnly(UnionType $unionType): bool return true; } + + private function normalizeType(Type $argValueType): MixedType|ObjectType|Type + { + // "self" in another object is not correct, this make it independent + $argValueType = $this->correctSelfType($argValueType); + + if (! $argValueType instanceof ObjectType) { + return $argValueType; + } + + // fix false positive generic type on string + if (! $this->reflectionProvider->hasClass($argValueType->getClassName())) { + return new MixedType(); + } + + return $argValueType; + } + + /** + * There is first class callable usage, or argument unpack, or named expr + * simply returns array marks as unknown as can be anything and in any position + */ + private function shouldSkipArg(Arg|VariadicPlaceholder $arg): bool + { + if ($arg instanceof VariadicPlaceholder) { + return true; + } + + if ($arg->unpack) { + return true; + } + + return $arg->name instanceof Identifier; + } + + private function isEmptyArray(Expr $expr): bool + { + if (! $expr instanceof Array_) { + return false; + } + + return $expr->items === []; + } } diff --git a/rules/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector.php b/rules/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector.php index 3b87d293b59..b7d668973d5 100644 --- a/rules/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector.php +++ b/rules/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector.php @@ -88,7 +88,7 @@ public function refactor(Node $node): ?Node $classMethodPhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($classMethod); $methodCalls = $this->localMethodCallFinder->match($node, $classMethod); - $classMethodParameterTypes = $this->callTypesResolver->resolveStrictTypesFromCalls($methodCalls); + $classMethodParameterTypes = $this->callTypesResolver->resolveTypesFromCalls($methodCalls); foreach ($classMethod->getParams() as $parameterPosition => $param) { if ($param->type === null) {