diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index c5ef1f4f3f..bd88539070 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -121,7 +121,6 @@ use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypehintHelper; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; @@ -1277,7 +1276,8 @@ private function resolveType(string $exprString, Expr $node): Type } else { $returnType = $arrowScope->getKeepVoidType($node->expr); if ($node->returnType !== null) { - $returnType = TypehintHelper::decideType($this->getFunctionType($node->returnType, false, false), $returnType); + $nativeReturnType = $this->getFunctionType($node->returnType, false, false); + $returnType = self::intersectButNotNever($nativeReturnType, $returnType); } } @@ -1434,7 +1434,10 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu $returnType, ]); } else { - $returnType = TypehintHelper::decideType($this->getFunctionType($node->returnType, false, false), $returnType); + if ($node->returnType !== null) { + $nativeReturnType = $this->getFunctionType($node->returnType, false, false); + $returnType = self::intersectButNotNever($nativeReturnType, $returnType); + } } $usedVariables = []; @@ -3213,16 +3216,16 @@ private function enterAnonymousFunctionWithoutReflection( $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic); if ($callableParameters !== null) { if (isset($callableParameters[$i])) { - $parameterType = TypehintHelper::decideType($parameterType, $callableParameters[$i]->getType()); + $parameterType = self::intersectButNotNever($parameterType, $callableParameters[$i]->getType()); } elseif (count($callableParameters) > 0) { $lastParameter = $callableParameters[count($callableParameters) - 1]; if ($lastParameter->isVariadic()) { - $parameterType = TypehintHelper::decideType($parameterType, $lastParameter->getType()); + $parameterType = self::intersectButNotNever($parameterType, $lastParameter->getType()); } else { - $parameterType = TypehintHelper::decideType($parameterType, new MixedType()); + $parameterType = self::intersectButNotNever($parameterType, new MixedType()); } } else { - $parameterType = TypehintHelper::decideType($parameterType, new MixedType()); + $parameterType = self::intersectButNotNever($parameterType, new MixedType()); } } $holder = ExpressionTypeHolder::createYes($parameter->var, $parameterType); @@ -3389,16 +3392,16 @@ private function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFu if ($callableParameters !== null) { if (isset($callableParameters[$i])) { - $parameterType = TypehintHelper::decideType($parameterType, $callableParameters[$i]->getType()); + $parameterType = self::intersectButNotNever($parameterType, $callableParameters[$i]->getType()); } elseif (count($callableParameters) > 0) { $lastParameter = $callableParameters[count($callableParameters) - 1]; if ($lastParameter->isVariadic()) { - $parameterType = TypehintHelper::decideType($parameterType, $lastParameter->getType()); + $parameterType = self::intersectButNotNever($parameterType, $lastParameter->getType()); } else { - $parameterType = TypehintHelper::decideType($parameterType, new MixedType()); + $parameterType = self::intersectButNotNever($parameterType, new MixedType()); } } else { - $parameterType = TypehintHelper::decideType($parameterType, new MixedType()); + $parameterType = self::intersectButNotNever($parameterType, new MixedType()); } } @@ -3483,6 +3486,20 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type return ParserNodeTypeToPHPStanType::resolve($type, $this->isInClass() ? $this->getClassReflection() : null); } + private static function intersectButNotNever(Type $nativeType, Type $inferredType): Type + { + if ($nativeType->isSuperTypeOf($inferredType)->no()) { + return $nativeType; + } + + $result = TypeCombinator::intersect($nativeType, $inferredType); + if (TypeCombinator::containsNull($nativeType)) { + return TypeCombinator::addNull($result); + } + + return $result; + } + public function enterMatch(Expr\Match_ $expr): self { if ($expr->cond instanceof Variable) { diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index fbb7291d0d..e0768742d1 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -85,6 +85,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2443.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5508.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10254.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7281.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2750.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2850.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2863.php'); diff --git a/tests/PHPStan/Analyser/data/bug-7281.php b/tests/PHPStan/Analyser/data/bug-7281.php new file mode 100644 index 0000000000..83b9014e45 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7281.php @@ -0,0 +1,101 @@ + $array + * @param (callable(T, K): U) $fn + * + * @return array + */ +function map(array $array, callable $fn): array +{ + /** @phpstan-ignore-next-line */ + return array_map($fn, $array); +} + +function (): void { + /** + * @var array> $timelines + */ + $timelines = []; + + assertType('array>', map( + $timelines, + static function (Timeline $timeline): Timeline { + return $timeline; + }, + )); + assertType('array>', map( + $timelines, + static function ($timeline) { + return $timeline; + }, + )); + + assertType('array>', map( + $timelines, + static fn (Timeline $timeline): Timeline => $timeline, + )); + assertType('array>', map( + $timelines, + static fn ($timeline) => $timeline, + )); + + assertType('array>', array_map( + static function (Timeline $timeline): Timeline { + return $timeline; + }, + $timelines, + )); + assertType('array>', array_map( + static function ($timeline) { + return $timeline; + }, + $timelines, + )); + + assertType('array>', array_map( + static fn (Timeline $timeline): Timeline => $timeline, + $timelines, + )); + assertType('array>', array_map( + static fn ($timeline) => $timeline, + $timelines, + )); + + assertType('array>', array_map( + static function (Timeline $timeline) { + return $timeline; + }, + $timelines, + )); + assertType('array>', array_map( + static function ($timeline): Timeline { + return $timeline; + }, + $timelines, + )); + + assertType('array>', array_map( + static fn (Timeline $timeline) => $timeline, + $timelines, + )); + assertType('array>', array_map( + static fn ($timeline): Timeline => $timeline, + $timelines, + )); +}; diff --git a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php index a62e35ea03..020000b38a 100644 --- a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php @@ -31,15 +31,15 @@ public function testClosureReturnTypeRule(): void 28, ], [ - 'Anonymous function should return ClosureReturnTypes\Foo but returns ClosureReturnTypes\Bar.', + 'Anonymous function should return ClosureReturnTypes\Bar&ClosureReturnTypes\Foo but returns ClosureReturnTypes\Bar.', 35, ], [ - 'Anonymous function should return SomeOtherNamespace\Foo but returns ClosureReturnTypes\Foo.', + 'Anonymous function should return ClosureReturnTypes\Foo&SomeOtherNamespace\Foo but returns ClosureReturnTypes\Foo.', 39, ], [ - 'Anonymous function should return SomeOtherNamespace\Baz but returns ClosureReturnTypes\Foo.', + 'Anonymous function should return ClosureReturnTypes\Foo&SomeOtherNamespace\Baz but returns ClosureReturnTypes\Foo.', 46, ], [ diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index f60d015e6c..27718dc9c7 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -706,6 +706,10 @@ public function dataMixed(): array 'Parameter #1 $cb of static method CallStaticMethodMixed\CallableMixed::callReturnsInt() expects callable(): int, Closure(): mixed given.', 161, ], + [ + 'Parameter #1 $cb of static method CallStaticMethodMixed\CallableMixed::callReturnsInt() expects callable(): int, Closure(): mixed given.', + 168, + ], ]; $implicitOnlyErrors = [ [ @@ -720,10 +724,6 @@ public function dataMixed(): array 'Only iterables can be unpacked, mixed given in argument #1.', 51, ], - [ - 'Parameter #1 $cb of static method CallStaticMethodMixed\CallableMixed::callReturnsInt() expects callable(): int, Closure(): mixed given.', - 168, - ], ]; $combinedErrors = array_merge($explicitOnlyErrors, $implicitOnlyErrors); usort($combinedErrors, static fn (array $a, array $b): int => $a[1] <=> $b[1]);