diff --git a/src/Type/Php/UnserializeFunctionThrowTypeExtension.php b/src/Type/Php/UnserializeFunctionThrowTypeExtension.php index 56cfb20301e..939a2afa275 100644 --- a/src/Type/Php/UnserializeFunctionThrowTypeExtension.php +++ b/src/Type/Php/UnserializeFunctionThrowTypeExtension.php @@ -13,14 +13,16 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\VoidType; -use TypeError; +use Throwable; use function count; use function preg_match; /** - * unserialize() throws TypeError and ValueError for invalid $options, and - * TypeError when the data does not fit a typed property. Objects of classes - * that are not allowed are unserialized as __PHP_Incomplete_Class. + * unserialize() throws TypeError and ValueError for invalid $options. When + * classes are allowed, it can also throw anything from autoloaders, __wakeup(), + * __unserialize() or Serializable::unserialize(), and TypeError when the data + * does not fit a typed property. Objects of classes that are not allowed are + * unserialized as __PHP_Incomplete_Class. */ #[AutowiredService] final class UnserializeFunctionThrowTypeExtension implements DynamicFunctionThrowTypeExtension @@ -33,45 +35,41 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type { - if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->no()) { - return new VoidType(); - } - $args = $funcCall->getArgs(); foreach ($args as $arg) { if ($arg->unpack || $arg->name !== null) { - return $functionReflection->getThrowType(); + return new ObjectType(Throwable::class); } } if (count($args) < 2) { - return new ObjectType(TypeError::class); + return new ObjectType(Throwable::class); } $optionsType = $scope->getNativeType($args[1]->value); $constantArrays = $optionsType->getConstantArrays(); if (!$optionsType->isArray()->yes() || count($constantArrays) === 0) { - return $functionReflection->getThrowType(); + return new ObjectType(Throwable::class); } - $allowsNoClasses = true; + $areOptionsValid = true; foreach ($constantArrays as $constantArray) { - if (!$this->areOptionsValid($constantArray)) { - return $functionReflection->getThrowType(); + if (!$this->allowsNoClasses($constantArray)) { + return new ObjectType(Throwable::class); } - if ($this->allowsNoClasses($constantArray)) { + if ($this->areOptionsValid($constantArray)) { continue; } - $allowsNoClasses = false; + $areOptionsValid = false; } - if ($allowsNoClasses) { + if ($areOptionsValid || $scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->no()) { return new VoidType(); } - return new ObjectType(TypeError::class); + return $functionReflection->getThrowType(); } private function areOptionsValid(ConstantArrayType $options): bool diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 43633d859c5..fc15f47a680 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -1426,10 +1426,6 @@ public function testDsMapVoidThrowType(): void public function testUnserializeThrowType(): void { $this->analyse([__DIR__ . '/data/unserialize-throw-type.php'], [ - [ - 'Dead catch - ValueError is never thrown in the try block.', - 15, - ], [ 'Dead catch - TypeError is never thrown in the try block.', 27, @@ -1439,8 +1435,8 @@ public function testUnserializeThrowType(): void 33, ], [ - 'Dead catch - ValueError is never thrown in the try block.', - 39, + 'Dead catch - Exception is never thrown in the try block.', + 93, ], ]); } @@ -1449,14 +1445,6 @@ public function testUnserializeThrowType(): void public function testUnserializeThrowTypeBeforePhp8(): void { $this->analyse([__DIR__ . '/data/unserialize-throw-type.php'], [ - [ - 'Dead catch - ValueError is never thrown in the try block.', - 15, - ], - [ - 'Dead catch - TypeError is never thrown in the try block.', - 21, - ], [ 'Dead catch - TypeError is never thrown in the try block.', 27, @@ -1467,37 +1455,18 @@ public function testUnserializeThrowTypeBeforePhp8(): void ], [ 'Dead catch - ValueError is never thrown in the try block.', - 39, + 87, ], [ - 'Dead catch - TypeError is never thrown in the try block.', - 45, - ], - [ - 'Dead catch - ValueError is never thrown in the try block.', - 51, - ], - [ - 'Dead catch - ValueError is never thrown in the try block.', - 57, - ], - [ - 'Dead catch - TypeError is never thrown in the try block.', - 63, - ], - [ - 'Dead catch - ValueError is never thrown in the try block.', - 69, - ], - [ - 'Dead catch - TypeError is never thrown in the try block.', - 75, - ], - [ - 'Dead catch - ValueError is never thrown in the try block.', - 81, + 'Dead catch - Exception is never thrown in the try block.', + 93, ], ]); } + public function testBug15329(): void + { + $this->analyse([__DIR__ . '/data/bug-15329.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-15329.php b/tests/PHPStan/Rules/Exceptions/data/bug-15329.php new file mode 100644 index 00000000000..0556a287318 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-15329.php @@ -0,0 +1,27 @@ + 'b']; + } + /** @param array $a */ + function __unserialize(array $a) { + if (rand(0,1) == 0) { + throw new Exception("nope"); + } + return; + } +} + +$c = new C(); +$s = serialize($c); + +try { + unserialize($s); +} catch (Exception $e) { + echo "caught " . $e->getMessage(); +} diff --git a/tests/PHPStan/Rules/Exceptions/data/unserialize-throw-type.php b/tests/PHPStan/Rules/Exceptions/data/unserialize-throw-type.php index dc74e0ae155..f1f09d6de1a 100644 --- a/tests/PHPStan/Rules/Exceptions/data/unserialize-throw-type.php +++ b/tests/PHPStan/Rules/Exceptions/data/unserialize-throw-type.php @@ -81,6 +81,24 @@ public function doFoo(string $s, array $options, array $phpDocOptions, int $dept } catch (\ValueError $e) { } + + try { + $a = unserialize($s, ['allowed_classes' => false, 'max_depth' => -1]); + } catch (\ValueError $e) { + + } + + try { + $a = unserialize($s, ['allowed_classes' => [], 'max_depth' => 10]); + } catch (\Exception $e) { + + } + + try { + $a = unserialize($s, ['allowed_classes' => true]); + } catch (\Exception $e) { + + } } }