diff --git a/src/Reflection/FunctionVariant.php b/src/Reflection/FunctionVariant.php index 9936ae9244..4970bb6ce3 100644 --- a/src/Reflection/FunctionVariant.php +++ b/src/Reflection/FunctionVariant.php @@ -4,9 +4,10 @@ use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Type; +use PHPStan\Type\TypeUtils; /** @api */ -class FunctionVariant implements ParametersAcceptor +class FunctionVariant implements ParametersAcceptor, SingleParametersAcceptor { /** @@ -51,4 +52,21 @@ public function getReturnType(): Type return $this->returnType; } + /** + * @return static + */ + public function flattenConditionalsInReturnType(): SingleParametersAcceptor + { + /** @var static $result */ + $result = new self( + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, + $this->parameters, + $this->isVariadic, + TypeUtils::flattenConditionals($this->returnType), + ); + + return $result; + } + } diff --git a/src/Reflection/FunctionVariantWithPhpDocs.php b/src/Reflection/FunctionVariantWithPhpDocs.php index aae15cb864..2de7029ade 100644 --- a/src/Reflection/FunctionVariantWithPhpDocs.php +++ b/src/Reflection/FunctionVariantWithPhpDocs.php @@ -4,6 +4,7 @@ use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Type; +use PHPStan\Type\TypeUtils; /** @api */ class FunctionVariantWithPhpDocs extends FunctionVariant implements ParametersAcceptorWithPhpDocs @@ -53,4 +54,23 @@ public function getNativeReturnType(): Type return $this->nativeReturnType; } + /** + * @return static + */ + public function flattenConditionalsInReturnType(): SingleParametersAcceptor + { + /** @var static $result */ + $result = new self( + $this->getTemplateTypeMap(), + $this->getResolvedTemplateTypeMap(), + $this->getParameters(), + $this->isVariadic(), + TypeUtils::flattenConditionals($this->getReturnType()), + TypeUtils::flattenConditionals($this->phpDocReturnType), + $this->nativeReturnType, + ); + + return $result; + } + } diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 15d13d5f63..cdfa607ec9 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -48,7 +48,13 @@ public static function selectSingle( throw new ShouldNotHappenException('Multiple variants - use selectFromArgs() instead.'); } - return $parametersAcceptors[0]; + $parametersAcceptor = $parametersAcceptors[0]; + + if ($parametersAcceptor instanceof SingleParametersAcceptor) { + $parametersAcceptor = $parametersAcceptor->flattenConditionalsInReturnType(); + } + + return $parametersAcceptor; } /** diff --git a/src/Reflection/ResolvedFunctionVariant.php b/src/Reflection/ResolvedFunctionVariant.php index c0e6d27c4a..c15f218c6e 100644 --- a/src/Reflection/ResolvedFunctionVariant.php +++ b/src/Reflection/ResolvedFunctionVariant.php @@ -8,10 +8,11 @@ use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; +use PHPStan\Type\TypeUtils; use function array_key_exists; use function array_map; -class ResolvedFunctionVariant implements ParametersAcceptor +class ResolvedFunctionVariant implements ParametersAcceptor, SingleParametersAcceptor { /** @var ParameterReflection[]|null */ @@ -88,6 +89,22 @@ public function getReturnType(): Type return $type; } + /** + * @return static + */ + public function flattenConditionalsInReturnType(): SingleParametersAcceptor + { + /** @var static $result */ + $result = new self( + $this->parametersAcceptor, + $this->resolvedTemplateTypeMap, + $this->passedArgs, + ); + $result->returnType = TypeUtils::flattenConditionals($this->getReturnType()); + + return $result; + } + private function resolveConditionalTypes(Type $type): Type { return TypeTraverser::map($type, function (Type $type, callable $traverse): Type { diff --git a/src/Reflection/SingleParametersAcceptor.php b/src/Reflection/SingleParametersAcceptor.php new file mode 100644 index 0000000000..ccc938b34b --- /dev/null +++ b/src/Reflection/SingleParametersAcceptor.php @@ -0,0 +1,13 @@ +isResolved()) { - return TypeCombinator::union($this->if, $this->else); + return $this->getResult(); } return $this; diff --git a/src/Type/Traits/ConditionalTypeTrait.php b/src/Type/Traits/ConditionalTypeTrait.php index 0231649aa5..62cb645557 100644 --- a/src/Type/Traits/ConditionalTypeTrait.php +++ b/src/Type/Traits/ConditionalTypeTrait.php @@ -288,7 +288,7 @@ public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic return $otherType->isSmallerThanOrEqual($result); } - private function getResult(): Type + public function getResult(): Type { if ($this->result === null) { return $this->result = TypeCombinator::union($this->if, $this->else); diff --git a/src/Type/TypeUtils.php b/src/Type/TypeUtils.php index 91dcc50bb0..baa70858b6 100644 --- a/src/Type/TypeUtils.php +++ b/src/Type/TypeUtils.php @@ -328,4 +328,15 @@ public static function containsCallable(Type $type): bool return false; } + public static function flattenConditionals(Type $type): Type + { + return TypeTraverser::map($type, static function (Type $type, callable $traverse) { + while ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) { + $type = $type->getResult(); + } + + return $traverse($type); + }); + } + } diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 40396e50fb..3d7d92affc 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -622,7 +622,7 @@ public function testBug6896(): void } $errors = $this->runAnalyse(__DIR__ . '/data/bug-6896.php'); - $this->assertCount(4, $errors); + $this->assertCount(2, $errors); } public function testBug6940(): void diff --git a/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php b/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php index 85eebb189f..f6272e75dc 100644 --- a/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php +++ b/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php @@ -11,6 +11,7 @@ public function dataAsserts(): iterable { yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-return-types.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-return-compound-types.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-return-getsingle-conditional.php'); } /** diff --git a/tests/PHPStan/Analyser/data/TestDynamicReturnTypeExtensions.php b/tests/PHPStan/Analyser/data/TestDynamicReturnTypeExtensions.php index 3fd9f8b0b4..a86f0eaaf7 100644 --- a/tests/PHPStan/Analyser/data/TestDynamicReturnTypeExtensions.php +++ b/tests/PHPStan/Analyser/data/TestDynamicReturnTypeExtensions.php @@ -189,3 +189,23 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } } + + +class ConditionalGetSingle implements DynamicMethodReturnTypeExtension { + + public function getClass(): string + { + return \DynamicMethodReturnGetSingleConditional\Foo::class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'get'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + } + +} diff --git a/tests/PHPStan/Analyser/data/dynamic-method-return-getsingle-conditional.php b/tests/PHPStan/Analyser/data/dynamic-method-return-getsingle-conditional.php new file mode 100644 index 0000000000..270658e7ef --- /dev/null +++ b/tests/PHPStan/Analyser/data/dynamic-method-return-getsingle-conditional.php @@ -0,0 +1,20 @@ +get(0)); + assertType('bool', $this->get(1)); + assertType('bool', $this->get(2)); + } +} diff --git a/tests/PHPStan/Analyser/dynamic-return-type.neon b/tests/PHPStan/Analyser/dynamic-return-type.neon index b8bd815fb4..6d781ca77f 100644 --- a/tests/PHPStan/Analyser/dynamic-return-type.neon +++ b/tests/PHPStan/Analyser/dynamic-return-type.neon @@ -27,3 +27,7 @@ services: class: PHPStan\Tests\FooGetSelf tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Tests\ConditionalGetSingle + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension