diff --git a/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php b/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php index 88eed9df7fe..77220d78f34 100644 --- a/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php +++ b/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php @@ -16,6 +16,7 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\Type\UnionTypeMethodReflection; use PHPStan\ShouldNotHappenException; use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\DynamicMethodThrowTypeExtension; @@ -23,9 +24,11 @@ use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use ReflectionFunction; use ReflectionMethod; use Throwable; +use function count; use function in_array; #[AutowiredService] @@ -63,6 +66,42 @@ public function getThrowPoint( Type $methodCallReturnType, ): ?InternalThrowPoint { + if ($methodReflection instanceof UnionTypeMethodReflection) { + $throwPoints = []; + foreach ($methodReflection->getMethods() as $innerMethodReflection) { + $throwPoint = $this->getThrowPoint($innerMethodReflection, $parametersAcceptor, $normalizedMethodCall, $scope, $context, $methodCallReturnType); + if ($throwPoint === null) { + continue; + } + + $throwPoints[] = $throwPoint; + } + + if (count($throwPoints) === 0) { + return null; + } + + $throwTypes = []; + $canContainAnyThrowable = false; + $isExplicit = false; + foreach ($throwPoints as $throwPoint) { + $throwTypes[] = $throwPoint->getType(); + if ($throwPoint->canContainAnyThrowable()) { + $canContainAnyThrowable = true; + } + if (!$throwPoint->isExplicit()) { + continue; + } + $isExplicit = true; + } + + if (!$isExplicit) { + return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall); + } + + return InternalThrowPoint::createExplicit($scope, TypeCombinator::union(...$throwTypes), $normalizedMethodCall, $canContainAnyThrowable); + } + if ($normalizedMethodCall instanceof MethodCall) { foreach ($this->dynamicMethodThrowTypeExtensions->getAll() as $extension) { if (!$extension->isMethodSupported($methodReflection)) { diff --git a/src/Reflection/Type/UnionTypeMethodReflection.php b/src/Reflection/Type/UnionTypeMethodReflection.php index 8daaf43cb0d..bc7b5b70ff1 100644 --- a/src/Reflection/Type/UnionTypeMethodReflection.php +++ b/src/Reflection/Type/UnionTypeMethodReflection.php @@ -11,6 +11,7 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function array_map; @@ -19,6 +20,7 @@ use function implode; use function is_bool; +#[ReferencedByTurboExtension(key: 'unionTypeMethodReflection')] final class UnionTypeMethodReflection implements ExtendedMethodReflection { @@ -34,6 +36,14 @@ public function getDeclaringClass(): ClassReflection return $this->methods[0]->getDeclaringClass(); } + /** + * @return ExtendedMethodReflection[] + */ + public function getMethods(): array + { + return $this->methods; + } + public function isStatic(): bool { foreach ($this->methods as $method) { diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 43633d859c5..c827f2b8950 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -783,6 +783,12 @@ public function testBug14479(): void $this->analyse([__DIR__ . '/data/bug-14479.php'], []); } + #[RequiresPhp('>= 8.3.0')] + public function testBug14467(): void + { + $this->analyse([__DIR__ . '/data/bug-14467.php'], []); + } + public function testBug5952(): void { $this->analyse([__DIR__ . '/data/bug-5952.php'], [ diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-14467.php b/tests/PHPStan/Rules/Exceptions/data/bug-14467.php new file mode 100644 index 00000000000..5694276654b --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-14467.php @@ -0,0 +1,38 @@ += 8.3 + +// FQN must sort after \DateTime so methods[0] resolves to DateTime. +namespace TestBug14467; + +class Foo +{ + + /** @throws \DateMalformedStringException */ + public function modify(string $s): self + { + return $this; + } + +} + +class Bar +{ + + /** @param \DateTime|Foo $obj */ + public function caseDateTimeFirst($obj): void + { + try { + $obj->modify('+1 day'); + } catch (\DateMalformedStringException $e) { + } + } + + /** @param Foo|\DateTime $obj */ + public function caseFooFirst($obj): void + { + try { + $obj->modify('+1 day'); + } catch (\DateMalformedStringException $e) { + } + } + +} diff --git a/turbo-ext/src/MethodThrowPointHelper.cpp b/turbo-ext/src/MethodThrowPointHelper.cpp index 98afc5655cd..638d458763e 100644 --- a/turbo-ext/src/MethodThrowPointHelper.cpp +++ b/turbo-ext/src/MethodThrowPointHelper.cpp @@ -26,6 +26,7 @@ namespace sigs = ptdecl::MethodThrowPointHelper::sig; #include "TypeOps.h" #include "AcceptorValues.h" #include "ParserVisitors.h" +#include "AnalyserValues.h" static zend_class_entry *pt_ce_method_throw_point_helper; @@ -104,6 +105,12 @@ class MethodThrowPointHelper * exception */ zv::Val getThrowPoint(zval *methodReflection, zval *parametersAcceptor, zval *normalizedMethodCall, zval *scope, zval *context, zval *methodCallReturnType) const { + zend_class_entry *unionTypeMethodReflectionCe = pt_class(PT_CLASS_UNION_TYPE_METHOD_REFLECTION); + if (UNEXPECTED(unionTypeMethodReflectionCe == NULL)) return zv::Val(); + if (instanceof_function(Z_OBJCE_P(methodReflection), unionTypeMethodReflectionCe)) { + return getUnionThrowPoint(methodReflection, parametersAcceptor, normalizedMethodCall, scope, context, methodCallReturnType); + } + zend_class_entry *methodCallCe = pt_class(PT_CLASS_METHOD_CALL); if (UNEXPECTED(methodCallCe == NULL)) return zv::Val(); bool isMethodCall = instanceof_function(Z_OBJCE_P(normalizedMethodCall), methodCallCe); @@ -258,6 +265,52 @@ class MethodThrowPointHelper private: zend_object *self; + /* Mirrors getThrowPoint()'s UnionTypeMethodReflection arm: the inner + * methods' throw points, combined */ + zv::Val getUnionThrowPoint(zval *methodReflection, zval *parametersAcceptor, zval *normalizedMethodCall, zval *scope, zval *context, zval *methodCallReturnType) const + { + zv::Val methods = pt_type_call(Z_OBJ_P(methodReflection), PT_LC("getmethods"), 0, NULL); + if (UNEXPECTED(methods.isUndef())) return zv::Val(); + + zv::Arr throwTypes = zv::Arr::create(0); + bool canContainAnyThrowable = false; + bool isExplicit = false; + if (EXPECTED(Z_TYPE_P(methods.raw()) == IS_ARRAY)) { + for (zv::ArrayEntry entry : zv::ArrRef(methods.raw())) { + zval *innerMethodReflection = entry.value().deref().raw(); + if (UNEXPECTED(Z_TYPE_P(innerMethodReflection) != IS_OBJECT)) { + zend_type_error("PHPStan\\Analyser\\ExprHandler\\Helper\\MethodThrowPointHelper::getThrowPoint(): Argument #1 ($methodReflection) must be of type PHPStan\\Reflection\\MethodReflection, %s given", zend_zval_value_name(innerMethodReflection)); + return zv::Val(); + } + zv::Val throwPoint = getThrowPoint(innerMethodReflection, parametersAcceptor, normalizedMethodCall, scope, context, methodCallReturnType); + if (UNEXPECTED(throwPoint.isUndef())) return zv::Val(); + if (Z_TYPE_P(throwPoint.raw()) == IS_NULL) continue; + + zv::Val typeHold; + zval *type = pt_internal_throw_point_type(throwPoint.raw(), typeHold); + if (UNEXPECTED(type == NULL)) return zv::Val(); + throwTypes.push(zv::Val::copyOf(zv::Ref(type))); + bool innerCanContainAnyThrowable; + if (UNEXPECTED(!pt_internal_throw_point_can_contain_any_throwable(throwPoint.raw(), innerCanContainAnyThrowable))) return zv::Val(); + if (innerCanContainAnyThrowable) canContainAnyThrowable = true; + bool innerIsExplicit; + if (UNEXPECTED(!pt_internal_throw_point_is_explicit(throwPoint.raw(), innerIsExplicit))) return zv::Val(); + if (innerIsExplicit) isExplicit = true; + } + } + + HashTable *table = throwTypes.table(); + uint32_t count = zend_hash_num_elements(table); + if (count == 0) return zv::Val::null(); + if (!isExplicit) return createImplicit(scope, normalizedMethodCall); + + zv::Val throwType = EXPECTED(HT_IS_PACKED(table) && table->nNumUsed == count) + ? pt_type_combinator_union(count, table->arPacked) + : pt_type_combinator_call_spread(PT_LC("union"), table); + if (UNEXPECTED(throwType.isUndef())) return zv::Val(); + return createExplicit(scope, throwType.raw(), normalizedMethodCall, canContainAnyThrowable); + } + zval *slot(uint32_t index) const { return OBJ_PROP_NUM(self, index); } /* the engine's Error for reading a never-written typed property */ diff --git a/turbo-ext/src/support.cpp b/turbo-ext/src/support.cpp index e34c9b87288..7ec102350d1 100644 --- a/turbo-ext/src/support.cpp +++ b/turbo-ext/src/support.cpp @@ -415,6 +415,7 @@ static const pt_class_template pt_class_templates[PT_CLASS_COUNT] = { /* PT_CLASS_NATIVE_PHP_TYPE_NODE_HELPER */ {"nativePhpTypeNodeHelper", "PHPStan\\Type\\NativePhpTypeNodeHelper"}, /* PT_CLASS_FULLY_QUALIFIED_NAME_NODE */ {"fullyQualifiedNameNode", "PHPStan\\PhpDocParser\\Ast\\Type\\FullyQualifiedNameNode"}, /* PT_CLASS_TYPE_NODE_DESCRIBER */ {"typeNodeDescriber", "PHPStan\\Type\\TypeNodeDescriber"}, + /* PT_CLASS_UNION_TYPE_METHOD_REFLECTION */ {"unionTypeMethodReflection", "PHPStan\\Reflection\\Type\\UnionTypeMethodReflection"}, }; zend_class_entry *pt_class(int idx) diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index 6c0e26f9c3a..584cb20b125 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -519,6 +519,8 @@ enum { /* the class names of Type::toPhpDocNode() and how describe() prints them */ PT_CLASS_FULLY_QUALIFIED_NAME_NODE, PT_CLASS_TYPE_NODE_DESCRIBER, + /* MethodThrowPointHelper's union arm */ + PT_CLASS_UNION_TYPE_METHOD_REFLECTION, PT_CLASS_COUNT };