From b46e49638ab78c7b9a770792367513daba0a7ae2 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 12 May 2025 13:04:52 +0200 Subject: [PATCH 1/4] Validate checkedExceptionClasses config --- build/phpstan.neon | 1 - src/Rules/Exceptions/DefaultExceptionTypeResolver.php | 11 +++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/build/phpstan.neon b/build/phpstan.neon index b285f56e12..b8979d63b7 100644 --- a/build/phpstan.neon +++ b/build/phpstan.neon @@ -39,7 +39,6 @@ parameters: - 'PHPStan\ShouldNotHappenException' - 'Symfony\Component\Console\Exception\InvalidArgumentException' - 'PHPStan\BetterReflection\SourceLocator\Exception\InvalidFileLocation' - - 'PHPStan\BetterReflection\SourceLocator\Exception\InvalidArgumentException' - 'Symfony\Component\Finder\Exception\DirectoryNotFoundException' - 'InvalidArgumentException' - 'PHPStan\DependencyInjection\ParameterNotFoundException' diff --git a/src/Rules/Exceptions/DefaultExceptionTypeResolver.php b/src/Rules/Exceptions/DefaultExceptionTypeResolver.php index f428436b48..40b5a4095d 100644 --- a/src/Rules/Exceptions/DefaultExceptionTypeResolver.php +++ b/src/Rules/Exceptions/DefaultExceptionTypeResolver.php @@ -5,6 +5,7 @@ use Nette\Utils\Strings; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; use function count; /** @@ -27,6 +28,16 @@ public function __construct( private array $checkedExceptionClasses, ) { + foreach ($this->checkedExceptionClasses as $checkedExceptionClass) { + if (!$this->reflectionProvider->hasClass($checkedExceptionClass)) { + throw new ShouldNotHappenException('Class ' . $checkedExceptionClass . ' used in \'checkedExceptionClasses\' does not exist.'); + } + } + foreach ($this->uncheckedExceptionClasses as $uncheckedExceptionClass) { + if (!$this->reflectionProvider->hasClass($uncheckedExceptionClass)) { + throw new ShouldNotHappenException('Class ' . $uncheckedExceptionClass . ' used in \'uncheckedExceptionClasses\' does not exist.'); + } + } } public function isCheckedException(string $className, Scope $scope): bool From 7142ddc0ec51f2b109fd066897572e2cf66b6614 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 12 May 2025 13:10:39 +0200 Subject: [PATCH 2/4] Fix tests --- ...rowsVoidFunctionWithExplicitThrowPointRuleTest.php | 11 +++++++++-- ...VoidPropertyHookWithExplicitThrowPointRuleTest.php | 4 ++-- .../Rules/Exceptions/data/throws-void-function.php | 6 +++--- .../Exceptions/data/throws-void-property-hook.php | 6 +++--- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php index ff6c4416a6..4a50e673da 100644 --- a/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use ThrowsVoidFunction\DifferentException; use ThrowsVoidFunction\MyException; /** @@ -38,7 +39,13 @@ public function dataRule(): array ], [ false, - ['DifferentException'], + [DifferentException::class], + [ + [ + 'Function ThrowsVoidFunction\foo() throws exception ThrowsVoidFunction\MyException but the PHPDoc contains @throws void.', + 15, + ], + ], [ [ 'Function ThrowsVoidFunction\foo() throws exception ThrowsVoidFunction\MyException but the PHPDoc contains @throws void.', @@ -53,7 +60,7 @@ public function dataRule(): array ], [ true, - ['DifferentException'], + [DifferentException::class], [ [ 'Function ThrowsVoidFunction\foo() throws exception ThrowsVoidFunction\MyException but the PHPDoc contains @throws void.', diff --git a/tests/PHPStan/Rules/Exceptions/ThrowsVoidPropertyHookWithExplicitThrowPointRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowsVoidPropertyHookWithExplicitThrowPointRuleTest.php index 6072db088b..757e47c6ca 100644 --- a/tests/PHPStan/Rules/Exceptions/ThrowsVoidPropertyHookWithExplicitThrowPointRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/ThrowsVoidPropertyHookWithExplicitThrowPointRuleTest.php @@ -38,7 +38,7 @@ public function dataRule(): array ], [ false, - ['DifferentException'], + ['ThrowsVoidPropertyHook\\DifferentException'], [ [ 'Get hook for property ThrowsVoidPropertyHook\Foo::$i throws exception ThrowsVoidPropertyHook\MyException but the PHPDoc contains @throws void.', @@ -57,7 +57,7 @@ public function dataRule(): array ], [ true, - ['DifferentException'], + ['ThrowsVoidPropertyHook\\DifferentException'], [ [ 'Get hook for property ThrowsVoidPropertyHook\Foo::$i throws exception ThrowsVoidPropertyHook\MyException but the PHPDoc contains @throws void.', diff --git a/tests/PHPStan/Rules/Exceptions/data/throws-void-function.php b/tests/PHPStan/Rules/Exceptions/data/throws-void-function.php index d5a407ec13..c9e0e566a7 100644 --- a/tests/PHPStan/Rules/Exceptions/data/throws-void-function.php +++ b/tests/PHPStan/Rules/Exceptions/data/throws-void-function.php @@ -2,10 +2,10 @@ namespace ThrowsVoidFunction; -class MyException extends \Exception -{ +class MyException extends \Exception {} + +class DifferentException extends \Exception {} -} /** * @throws void diff --git a/tests/PHPStan/Rules/Exceptions/data/throws-void-property-hook.php b/tests/PHPStan/Rules/Exceptions/data/throws-void-property-hook.php index 08e3f10940..e7abb0976f 100644 --- a/tests/PHPStan/Rules/Exceptions/data/throws-void-property-hook.php +++ b/tests/PHPStan/Rules/Exceptions/data/throws-void-property-hook.php @@ -2,10 +2,10 @@ namespace ThrowsVoidPropertyHook; -class MyException extends \Exception -{ +class MyException extends \Exception {} + +class DifferentException extends \Exception {} -} class Foo { From e1aeb926d54710ff77d1681cc5ec7c40c17dd07c Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 12 May 2025 13:29:39 +0200 Subject: [PATCH 3/4] fix one more test --- .../ThrowsVoidMethodWithExplicitThrowPointRuleTest.php | 5 +++-- tests/PHPStan/Rules/Exceptions/data/throws-void-method.php | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php index 5a2dcb0429..059bcb0bba 100644 --- a/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use ThrowsVoidMethod\DifferentException; use ThrowsVoidMethod\MyException; use UnhandledMatchError; use const PHP_VERSION_ID; @@ -40,7 +41,7 @@ public function dataRule(): array ], [ false, - ['DifferentException'], + [DifferentException::class], [ [ 'Method ThrowsVoidMethod\Foo::doFoo() throws exception ThrowsVoidMethod\MyException but the PHPDoc contains @throws void.', @@ -55,7 +56,7 @@ public function dataRule(): array ], [ true, - ['DifferentException'], + [DifferentException::class], [ [ 'Method ThrowsVoidMethod\Foo::doFoo() throws exception ThrowsVoidMethod\MyException but the PHPDoc contains @throws void.', diff --git a/tests/PHPStan/Rules/Exceptions/data/throws-void-method.php b/tests/PHPStan/Rules/Exceptions/data/throws-void-method.php index fec5c30a9a..084168dce1 100644 --- a/tests/PHPStan/Rules/Exceptions/data/throws-void-method.php +++ b/tests/PHPStan/Rules/Exceptions/data/throws-void-method.php @@ -2,10 +2,10 @@ namespace ThrowsVoidMethod; -class MyException extends \Exception -{ +class MyException extends \Exception {} + +class DifferentException extends \Exception {} -} class Foo { From 1986cf0dbd70f8b0ecb2960b9f3c4942c21bb1a5 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 28 Jul 2025 16:32:06 +0200 Subject: [PATCH 4/4] Move exception class validation to ContainerFactory as requested MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Moved validation from DefaultExceptionTypeResolver constructor to ContainerFactory::validateParameters() - Added bleeding edge feature flag protection to avoid BC break - Use InvalidConfigurationException for proper error handling - Note: The required placement in validateParameters() complicates the code as we cannot use BleedingEdgeToggle::isBleedingEdge() since it's not initialized yet, requiring manual parameter checking 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/DependencyInjection/ContainerFactory.php | 29 +++++++++++++++++++ .../DefaultExceptionTypeResolver.php | 11 ------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/DependencyInjection/ContainerFactory.php b/src/DependencyInjection/ContainerFactory.php index c28e08a77c..6789691adb 100644 --- a/src/DependencyInjection/ContainerFactory.php +++ b/src/DependencyInjection/ContainerFactory.php @@ -7,6 +7,7 @@ use Nette\DI\Definitions\Statement; use Nette\DI\Extensions\ExtensionsExtension; use Nette\DI\Helpers; +use Nette\DI\InvalidConfigurationException; use Nette\Schema\Context as SchemaContext; use Nette\Schema\Elements\AnyOf; use Nette\Schema\Elements\Structure; @@ -323,6 +324,34 @@ private function validateParameters(array $parameters, array $parametersSchema): if ($phpVersion['max'] < $phpVersion['min']) { throw new InvalidPhpVersionException('Invalid PHP version range: phpVersion.max should be greater or equal to phpVersion.min.'); } + + if (array_key_exists('featureToggles', $parameters) && is_array($parameters['featureToggles']) && (bool) $parameters['featureToggles']['bleedingEdge']) { + $this->validateExceptionClasses($parameters); + } + } + + /** + * @param array $parameters + */ + private function validateExceptionClasses(array $parameters): void + { + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + + if (array_key_exists('checkedExceptionClasses', $parameters) && is_array($parameters['checkedExceptionClasses'])) { + foreach ($parameters['checkedExceptionClasses'] as $checkedExceptionClass) { + if (!$reflectionProvider->hasClass($checkedExceptionClass)) { + throw new InvalidConfigurationException('Class ' . $checkedExceptionClass . ' used in \'checkedExceptionClasses\' does not exist.'); + } + } + } + + if (array_key_exists('uncheckedExceptionClasses', $parameters) && is_array($parameters['uncheckedExceptionClasses'])) { + foreach ($parameters['uncheckedExceptionClasses'] as $uncheckedExceptionClass) { + if (!$reflectionProvider->hasClass($uncheckedExceptionClass)) { + throw new InvalidConfigurationException('Class ' . $uncheckedExceptionClass . ' used in \'uncheckedExceptionClasses\' does not exist.'); + } + } + } } /** diff --git a/src/Rules/Exceptions/DefaultExceptionTypeResolver.php b/src/Rules/Exceptions/DefaultExceptionTypeResolver.php index 40b5a4095d..f428436b48 100644 --- a/src/Rules/Exceptions/DefaultExceptionTypeResolver.php +++ b/src/Rules/Exceptions/DefaultExceptionTypeResolver.php @@ -5,7 +5,6 @@ use Nette\Utils\Strings; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\ShouldNotHappenException; use function count; /** @@ -28,16 +27,6 @@ public function __construct( private array $checkedExceptionClasses, ) { - foreach ($this->checkedExceptionClasses as $checkedExceptionClass) { - if (!$this->reflectionProvider->hasClass($checkedExceptionClass)) { - throw new ShouldNotHappenException('Class ' . $checkedExceptionClass . ' used in \'checkedExceptionClasses\' does not exist.'); - } - } - foreach ($this->uncheckedExceptionClasses as $uncheckedExceptionClass) { - if (!$this->reflectionProvider->hasClass($uncheckedExceptionClass)) { - throw new ShouldNotHappenException('Class ' . $uncheckedExceptionClass . ' used in \'uncheckedExceptionClasses\' does not exist.'); - } - } } public function isCheckedException(string $className, Scope $scope): bool