From 2bf30bff0b91e2c8144171ce6892fd9098183eff Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 25 Apr 2021 10:32:10 +0200 Subject: [PATCH] Dynamic throw type extensions --- conf/config.neon | 9 + src/Analyser/NodeScopeResolver.php | 175 +++++++++++++----- .../DynamicThrowTypeExtensionProvider.php | 21 +++ .../LazyDynamicThrowTypeExtensionProvider.php | 36 ++++ src/Testing/RuleTestCase.php | 2 + src/Testing/TypeInferenceTestCase.php | 2 + .../DynamicFunctionThrowTypeExtension.php | 16 ++ src/Type/DynamicMethodThrowTypeExtension.php | 16 ++ .../DynamicStaticMethodThrowTypeExtension.php | 16 ++ ...ThrowOnErrorDynamicReturnTypeExtension.php | 15 +- src/Type/Php/JsonThrowTypeExtension.php | 100 ++++++++++ tests/PHPStan/Analyser/AnalyserTest.php | 2 + .../DynamicMethodThrowTypeExtensionTest.php | 37 ++++ .../Analyser/NodeScopeResolverTest.php | 1 + tests/PHPStan/Analyser/data/bug-4814.php | 40 ++++ .../dynamic-method-throw-type-extension.php | 123 ++++++++++++ .../dynamic-throw-type-extension.neon | 10 + .../CatchWithUnthrownExceptionRuleTest.php | 10 + .../Rules/Exceptions/data/bug-4814.php | 19 ++ 19 files changed, 596 insertions(+), 54 deletions(-) create mode 100644 src/DependencyInjection/Type/DynamicThrowTypeExtensionProvider.php create mode 100644 src/DependencyInjection/Type/LazyDynamicThrowTypeExtensionProvider.php create mode 100644 src/Type/DynamicFunctionThrowTypeExtension.php create mode 100644 src/Type/DynamicMethodThrowTypeExtension.php create mode 100644 src/Type/DynamicStaticMethodThrowTypeExtension.php create mode 100644 src/Type/Php/JsonThrowTypeExtension.php create mode 100644 tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php create mode 100644 tests/PHPStan/Analyser/data/bug-4814.php create mode 100644 tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension.php create mode 100644 tests/PHPStan/Analyser/dynamic-throw-type-extension.neon create mode 100644 tests/PHPStan/Rules/Exceptions/data/bug-4814.php diff --git a/conf/config.neon b/conf/config.neon index be12f34692..f97152b01c 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -515,6 +515,10 @@ services: class: PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider factory: PHPStan\DependencyInjection\Type\LazyOperatorTypeSpecifyingExtensionRegistryProvider + - + class: PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider + factory: PHPStan\DependencyInjection\Type\LazyDynamicThrowTypeExtensionProvider + - class: PHPStan\File\FileHelper arguments: @@ -1021,6 +1025,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\JsonThrowTypeExtension + tags: + - phpstan.dynamicFunctionThrowTypeExtension + - class: PHPStan\Type\Php\SimpleXMLElementClassPropertyReflectionExtension tags: diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index da1627ddb2..ecc2753fd8 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -51,6 +51,7 @@ use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider; +use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; use PHPStan\File\FileHelper; use PHPStan\File\FileReader; use PHPStan\Node\BooleanAndNode; @@ -145,6 +146,8 @@ class NodeScopeResolver private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider; + private bool $polluteScopeWithLoopInitialAssignments; private bool $polluteCatchScopeWithTryAssignments; @@ -190,6 +193,7 @@ public function __construct( PhpDocInheritanceResolver $phpDocInheritanceResolver, FileHelper $fileHelper, TypeSpecifier $typeSpecifier, + DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider, bool $polluteScopeWithLoopInitialAssignments, bool $polluteCatchScopeWithTryAssignments, bool $polluteScopeWithAlwaysIterableForeach, @@ -208,6 +212,7 @@ public function __construct( $this->phpDocInheritanceResolver = $phpDocInheritanceResolver; $this->fileHelper = $fileHelper; $this->typeSpecifier = $typeSpecifier; + $this->dynamicThrowTypeExtensionProvider = $dynamicThrowTypeExtensionProvider; $this->polluteScopeWithLoopInitialAssignments = $polluteScopeWithLoopInitialAssignments; $this->polluteCatchScopeWithTryAssignments = $polluteCatchScopeWithTryAssignments; $this->polluteScopeWithAlwaysIterableForeach = $polluteScopeWithAlwaysIterableForeach; @@ -1780,34 +1785,9 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); if (isset($functionReflection)) { - if ($functionReflection->getThrowType() !== null) { - $throwType = $functionReflection->getThrowType(); - if (!$throwType instanceof VoidType) { - $throwPoints[] = ThrowPoint::createExplicit($scope, $throwType, true); - } - } elseif ($this->implicitThrows) { - $requiredParameters = null; - if ($parametersAcceptor !== null) { - $requiredParameters = 0; - foreach ($parametersAcceptor->getParameters() as $parameter) { - if ($parameter->isOptional()) { - continue; - } - - $requiredParameters++; - } - } - if ( - !$functionReflection->isBuiltin() - || $requiredParameters === null - || $requiredParameters > 0 - || count($expr->args) > 0 - ) { - $functionReturnedType = $scope->getType($expr); - if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($functionReturnedType)->yes()) { - $throwPoints[] = ThrowPoint::createImplicit($scope); - } - } + $functionThrowPoint = $this->getFunctionThrowPoint($functionReflection, $parametersAcceptor, $expr, $scope); + if ($functionThrowPoint !== null) { + $throwPoints[] = $functionThrowPoint; } } else { $throwPoints[] = ThrowPoint::createImplicit($scope); @@ -1975,16 +1955,9 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $expr->args, $methodReflection->getVariants() ); - if ($methodReflection->getThrowType() !== null) { - $throwType = $methodReflection->getThrowType(); - if (!$throwType instanceof VoidType) { - $throwPoints[] = ThrowPoint::createExplicit($scope, $methodReflection->getThrowType(), true); - } - } elseif ($this->implicitThrows) { - $methodReturnedType = $scope->getType($expr); - if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { - $throwPoints[] = ThrowPoint::createImplicit($scope); - } + $methodThrowPoint = $this->getMethodThrowPoint($methodReflection, $expr, $scope); + if ($methodThrowPoint !== null) { + $throwPoints[] = $methodThrowPoint; } } else { $throwPoints[] = ThrowPoint::createImplicit($scope); @@ -2056,16 +2029,9 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $expr->args, $methodReflection->getVariants() ); - if ($methodReflection->getThrowType() !== null) { - $throwType = $methodReflection->getThrowType(); - if (!$throwType instanceof VoidType) { - $throwPoints[] = ThrowPoint::createExplicit($scope, $throwType, true); - } - } elseif ($this->implicitThrows) { - $methodReturnedType = $scope->getType($expr); - if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { - $throwPoints[] = ThrowPoint::createImplicit($scope); - } + $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $expr, $scope); + if ($methodThrowPoint !== null) { + $throwPoints[] = $methodThrowPoint; } if ( $classReflection->getName() === 'Closure' @@ -2627,6 +2593,119 @@ static function () use ($scope, $expr): MutatingScope { ); } + private function getFunctionThrowPoint( + FunctionReflection $functionReflection, + ?ParametersAcceptor $parametersAcceptor, + FuncCall $funcCall, + MutatingScope $scope + ): ?ThrowPoint + { + foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicFunctionThrowTypeExtensions() as $extension) { + if (!$extension->isFunctionSupported($functionReflection)) { + continue; + } + + $throwType = $extension->getThrowTypeFromFunctionCall($functionReflection, $funcCall, $scope); + if ($throwType === null) { + return null; + } + + return ThrowPoint::createExplicit($scope, $throwType, false); + } + + if ($functionReflection->getThrowType() !== null) { + $throwType = $functionReflection->getThrowType(); + if (!$throwType instanceof VoidType) { + return ThrowPoint::createExplicit($scope, $throwType, true); + } + } elseif ($this->implicitThrows) { + $requiredParameters = null; + if ($parametersAcceptor !== null) { + $requiredParameters = 0; + foreach ($parametersAcceptor->getParameters() as $parameter) { + if ($parameter->isOptional()) { + continue; + } + + $requiredParameters++; + } + } + if ( + !$functionReflection->isBuiltin() + || $requiredParameters === null + || $requiredParameters > 0 + || count($funcCall->args) > 0 + ) { + $functionReturnedType = $scope->getType($funcCall); + if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($functionReturnedType)->yes()) { + return ThrowPoint::createImplicit($scope); + } + } + } + + return null; + } + + private function getMethodThrowPoint(MethodReflection $methodReflection, MethodCall $methodCall, MutatingScope $scope): ?ThrowPoint + { + foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicMethodThrowTypeExtensions() as $extension) { + if (!$extension->isMethodSupported($methodReflection)) { + continue; + } + + $throwType = $extension->getThrowTypeFromMethodCall($methodReflection, $methodCall, $scope); + if ($throwType === null) { + return null; + } + + return ThrowPoint::createExplicit($scope, $throwType, false); + } + + if ($methodReflection->getThrowType() !== null) { + $throwType = $methodReflection->getThrowType(); + if (!$throwType instanceof VoidType) { + return ThrowPoint::createExplicit($scope, $throwType, true); + } + } elseif ($this->implicitThrows) { + $methodReturnedType = $scope->getType($methodCall); + if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { + return ThrowPoint::createImplicit($scope); + } + } + + return null; + } + + private function getStaticMethodThrowPoint(MethodReflection $methodReflection, StaticCall $methodCall, MutatingScope $scope): ?ThrowPoint + { + foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) { + if (!$extension->isStaticMethodSupported($methodReflection)) { + continue; + } + + $throwType = $extension->getThrowTypeFromStaticMethodCall($methodReflection, $methodCall, $scope); + if ($throwType === null) { + return null; + } + + return ThrowPoint::createExplicit($scope, $throwType, false); + } + + if ($methodReflection->getThrowType() !== null) { + $throwType = $methodReflection->getThrowType(); + if (!$throwType instanceof VoidType) { + return ThrowPoint::createExplicit($scope, $throwType, true); + } + } elseif ($this->implicitThrows) { + $methodReturnedType = $scope->getType($methodCall); + if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { + return ThrowPoint::createImplicit($scope); + } + } + + return null; + } + /** * @param Expr $expr * @return string[] diff --git a/src/DependencyInjection/Type/DynamicThrowTypeExtensionProvider.php b/src/DependencyInjection/Type/DynamicThrowTypeExtensionProvider.php new file mode 100644 index 0000000000..9f67221cf1 --- /dev/null +++ b/src/DependencyInjection/Type/DynamicThrowTypeExtensionProvider.php @@ -0,0 +1,21 @@ +container = $container; + } + + public function getDynamicFunctionThrowTypeExtensions(): array + { + return $this->container->getServicesByTag(self::FUNCTION_TAG); + } + + public function getDynamicMethodThrowTypeExtensions(): array + { + return $this->container->getServicesByTag(self::METHOD_TAG); + } + + public function getDynamicStaticMethodThrowTypeExtensions(): array + { + return $this->container->getServicesByTag(self::STATIC_METHOD_TAG); + } + +} diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index 63dec544f7..5a1c9ca897 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -11,6 +11,7 @@ use PHPStan\Cache\Cache; use PHPStan\Dependency\DependencyResolver; use PHPStan\Dependency\ExportedNodeResolver; +use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; use PHPStan\File\FileHelper; use PHPStan\File\SimpleRelativePathHelper; use PHPStan\Php\PhpVersion; @@ -79,6 +80,7 @@ private function getAnalyser(): Analyser $phpDocInheritanceResolver, $fileHelper, $typeSpecifier, + self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), $this->shouldPolluteScopeWithLoopInitialAssignments(), $this->shouldPolluteCatchScopeWithTryAssignments(), $this->shouldPolluteScopeWithAlwaysIterableForeach(), diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index e9084e02da..50205e2e7e 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -12,6 +12,7 @@ use PHPStan\Broker\AnonymousClassNameHelper; use PHPStan\Broker\Broker; use PHPStan\Cache\Cache; +use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; use PHPStan\File\FileHelper; use PHPStan\File\SimpleRelativePathHelper; use PHPStan\Php\PhpVersion; @@ -71,6 +72,7 @@ public function processFile( $phpDocInheritanceResolver, $fileHelper, $typeSpecifier, + self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), true, $this->polluteCatchScopeWithTryAssignments, true, diff --git a/src/Type/DynamicFunctionThrowTypeExtension.php b/src/Type/DynamicFunctionThrowTypeExtension.php new file mode 100644 index 0000000000..0aaac67c14 --- /dev/null +++ b/src/Type/DynamicFunctionThrowTypeExtension.php @@ -0,0 +1,16 @@ +args[$argumentPosition]->value; $constrictedReturnType = TypeCombinator::remove($defaultReturnType, new ConstantBooleanType(false)); - if ($this->isBitwiseOrWithJsonThrowOnError($optionsExpr)) { + if ($this->isBitwiseOrWithJsonThrowOnError($optionsExpr, $scope)) { return $constrictedReturnType; } @@ -83,18 +83,21 @@ public function getTypeFromFunctionCall( return $constrictedReturnType; } - private function isBitwiseOrWithJsonThrowOnError(Expr $expr): bool + private function isBitwiseOrWithJsonThrowOnError(Expr $expr, Scope $scope): bool { - if ($expr instanceof ConstFetch && $expr->name->toCodeString() === '\JSON_THROW_ON_ERROR') { - return true; + if ($expr instanceof ConstFetch) { + $constant = $this->reflectionProvider->resolveConstantName($expr->name, $scope); + if ($constant === 'JSON_THROW_ON_ERROR') { + return true; + } } if (!$expr instanceof BitwiseOr) { return false; } - return $this->isBitwiseOrWithJsonThrowOnError($expr->left) || - $this->isBitwiseOrWithJsonThrowOnError($expr->right); + return $this->isBitwiseOrWithJsonThrowOnError($expr->left, $scope) || + $this->isBitwiseOrWithJsonThrowOnError($expr->right, $scope); } } diff --git a/src/Type/Php/JsonThrowTypeExtension.php b/src/Type/Php/JsonThrowTypeExtension.php new file mode 100644 index 0000000000..bcb7bd4ee3 --- /dev/null +++ b/src/Type/Php/JsonThrowTypeExtension.php @@ -0,0 +1,100 @@ + */ + private array $argumentPositions = [ + 'json_encode' => 1, + 'json_decode' => 3, + ]; + + private ReflectionProvider $reflectionProvider; + + public function __construct(ReflectionProvider $reflectionProvider) + { + $this->reflectionProvider = $reflectionProvider; + } + + public function isFunctionSupported( + FunctionReflection $functionReflection + ): bool + { + return $this->reflectionProvider->hasConstant(new Name\FullyQualified('JSON_THROW_ON_ERROR'), null) && in_array( + $functionReflection->getName(), + [ + 'json_encode', + 'json_decode', + ], + true + ); + } + + public function getThrowTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope + ): ?Type + { + $argumentPosition = $this->argumentPositions[$functionReflection->getName()]; + if (!isset($functionCall->args[$argumentPosition])) { + return null; + } + + $optionsExpr = $functionCall->args[$argumentPosition]->value; + if ($this->isBitwiseOrWithJsonThrowOnError($optionsExpr, $scope)) { + return new ObjectType('JsonException'); + } + + $valueType = $scope->getType($optionsExpr); + if (!$valueType instanceof ConstantIntegerType) { + return null; + } + + $value = $valueType->getValue(); + $throwOnErrorType = $this->reflectionProvider->getConstant(new Name\FullyQualified('JSON_THROW_ON_ERROR'), null)->getValueType(); + if (!$throwOnErrorType instanceof ConstantIntegerType) { + return null; + } + + $throwOnErrorValue = $throwOnErrorType->getValue(); + if (($value & $throwOnErrorValue) !== $throwOnErrorValue) { + return null; + } + + return new ObjectType('JsonException'); + } + + private function isBitwiseOrWithJsonThrowOnError(Expr $expr, Scope $scope): bool + { + if ($expr instanceof ConstFetch) { + $constant = $this->reflectionProvider->resolveConstantName($expr->name, $scope); + if ($constant === 'JSON_THROW_ON_ERROR') { + return true; + } + } + + if (!$expr instanceof BitwiseOr) { + return false; + } + + return $this->isBitwiseOrWithJsonThrowOnError($expr->left, $scope) || + $this->isBitwiseOrWithJsonThrowOnError($expr->right, $scope); + } + +} diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index 352f2fa326..2547c6985e 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -8,6 +8,7 @@ use PHPStan\Command\IgnoredRegexValidator; use PHPStan\Dependency\DependencyResolver; use PHPStan\Dependency\ExportedNodeResolver; +use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; use PHPStan\File\RelativePathHelper; use PHPStan\NodeVisitor\StatementOrderVisitor; use PHPStan\Parser\RichParser; @@ -510,6 +511,7 @@ private function createAnalyser(bool $reportUnmatchedIgnoredErrors): \PHPStan\An $phpDocInheritanceResolver, $fileHelper, $typeSpecifier, + self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), false, false, true, diff --git a/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php b/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php new file mode 100644 index 0000000000..bdb8094fe9 --- /dev/null +++ b/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php @@ -0,0 +1,37 @@ +gatherAssertTypes(__DIR__ . '/data/dynamic-method-throw-type-extension.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param string $assertType + * @param string $file + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/dynamic-throw-type-extension.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 19210ed2c7..c06793dc78 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -389,6 +389,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4820.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4822.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4816.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4814.php'); } /** diff --git a/tests/PHPStan/Analyser/data/bug-4814.php b/tests/PHPStan/Analyser/data/bug-4814.php new file mode 100644 index 0000000000..4e8607a793 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4814.php @@ -0,0 +1,40 @@ +sendRequest($request); + $body = (string) $response->getBody(); + $decodedResponseBody = json_decode($body, true, 512, JSON_THROW_ON_ERROR); + } catch (\Throwable $exception) { + assertType('string|null', $body); + assertType('array()', $decodedResponseBody); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension.php b/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension.php new file mode 100644 index 0000000000..9d4aaf48a3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension.php @@ -0,0 +1,123 @@ +getDeclaringClass()->getName() === Foo::class && $methodReflection->getName() === 'throwOrNot'; + } + + public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->args) < 1) { + return $methodReflection->getThrowType(); + } + + $argType = $scope->getType($methodCall->args[0]->value); + if ((new ConstantBooleanType(true))->isSuperTypeOf($argType)->yes()) { + return $methodReflection->getThrowType(); + } + + return null; + } + +} + +class StaticMethodThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension +{ + + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getDeclaringClass()->getName() === Foo::class && $methodReflection->getName() === 'staticThrowOrNot'; + } + + public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->args) < 1) { + return $methodReflection->getThrowType(); + } + + $argType = $scope->getType($methodCall->args[0]->value); + if ((new ConstantBooleanType(true))->isSuperTypeOf($argType)->yes()) { + return $methodReflection->getThrowType(); + } + + return null; + } + +} + +class Foo +{ + + /** @throws \Exception */ + public function throwOrNot(bool $need): int + { + if ($need) { + throw new \Exception(); + } + + return 1; + } + + /** @throws \Exception */ + public static function staticThrowOrNot(bool $need): int + { + if ($need) { + throw new \Exception(); + } + + return 1; + } + + public function doFoo1() + { + try { + $result = $this->throwOrNot(true); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $result); + } + } + + public function doFoo2() + { + try { + $result = $this->throwOrNot(false); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $result); + } + } + + public function doFoo3() + { + try { + $result = self::staticThrowOrNot(true); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $result); + } + } + + public function doFoo4() + { + try { + $result = self::staticThrowOrNot(false); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $result); + } + } + +} diff --git a/tests/PHPStan/Analyser/dynamic-throw-type-extension.neon b/tests/PHPStan/Analyser/dynamic-throw-type-extension.neon new file mode 100644 index 0000000000..7e42b64ac0 --- /dev/null +++ b/tests/PHPStan/Analyser/dynamic-throw-type-extension.neon @@ -0,0 +1,10 @@ +services: + - + class: DynamicMethodThrowTypeExtension\MethodThrowTypeExtension + tags: + - phpstan.dynamicMethodThrowTypeExtension + + - + class: DynamicMethodThrowTypeExtension\StaticMethodThrowTypeExtension + tags: + - phpstan.dynamicStaticMethodThrowTypeExtension diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 4bcd58cab2..4978f8ddc8 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -87,4 +87,14 @@ public function testBug4863(): void $this->analyse([__DIR__ . '/data/bug-4863.php'], []); } + public function testBug4814(): void + { + $this->analyse([__DIR__ . '/data/bug-4814.php'], [ + [ + 'Dead catch - JsonException is never thrown in the try block.', + 16, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-4814.php b/tests/PHPStan/Rules/Exceptions/data/bug-4814.php new file mode 100644 index 0000000000..9959138f6e --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-4814.php @@ -0,0 +1,19 @@ +