From 3b2dd42df20fe22557ad95c383997454d320a8b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Fri, 24 Apr 2026 18:18:49 +0200 Subject: [PATCH] Throw a descriptive exception when `typeString` cannot be parsed A wildcard or other invalid value in `typeString` caused PHPStan to crash at DI container initialisation with a raw `ParserException` and no indication of which config entry was responsible. The new `InvalidTypeStringInConfigException` names the offending `typeString` value, includes the parser's own reason, and appends a wildcard-specific hint when the value contains `*`. Both it and the existing `UnsupportedParamTypeInConfigException` extend the new abstract `InvalidConfigException`, which all factories catch and wrap in `ShouldNotHappenException` with the config identifier prepended. --- src/Allowed/AllowedConfigFactory.php | 14 +++- src/DisallowedAttributeFactory.php | 37 +++++---- src/DisallowedCallFactory.php | 4 +- src/DisallowedConstantFactory.php | 4 +- src/DisallowedKeywordFactory.php | 4 +- src/DisallowedNamespaceFactory.php | 4 +- src/DisallowedPropertyFactory.php | 35 +++++---- src/DisallowedSuperglobalFactory.php | 4 +- src/Exceptions/InvalidConfigException.php | 11 +++ .../InvalidTypeStringInConfigException.php | 16 ++++ .../UnsupportedParamTypeInConfigException.php | 3 +- src/Usages/InstancePropertyUsages.php | 3 +- src/Usages/StaticPropertyUsages.php | 3 +- ...nctionCallsInvalidTypeStringConfigTest.php | 76 +++++++++++++++++++ 14 files changed, 172 insertions(+), 46 deletions(-) create mode 100644 src/Exceptions/InvalidConfigException.php create mode 100644 src/Exceptions/InvalidTypeStringInConfigException.php create mode 100644 tests/Calls/FunctionCallsInvalidTypeStringConfigTest.php diff --git a/src/Allowed/AllowedConfigFactory.php b/src/Allowed/AllowedConfigFactory.php index 25dc54d6..f8d4cc18 100644 --- a/src/Allowed/AllowedConfigFactory.php +++ b/src/Allowed/AllowedConfigFactory.php @@ -4,11 +4,14 @@ namespace Spaze\PHPStan\Rules\Disallowed\Allowed; use PHPStan\PhpDoc\TypeStringResolver; +use PHPStan\PhpDocParser\Parser\ParserException; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\NullType; use PHPStan\Type\VerbosityLevel; +use Spaze\PHPStan\Rules\Disallowed\Exceptions\InvalidConfigException; +use Spaze\PHPStan\Rules\Disallowed\Exceptions\InvalidTypeStringInConfigException; use Spaze\PHPStan\Rules\Disallowed\Exceptions\UnsupportedParamTypeInConfigException; use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer; use Spaze\PHPStan\Rules\Disallowed\Params\ParamValue; @@ -42,7 +45,7 @@ public function __construct( * @param array $allowed * @phpstan-param AllowDirectivesConfig $allowed * @return AllowedConfig - * @throws UnsupportedParamTypeInConfigException + * @throws InvalidConfigException */ public function getConfig(array $allowed): AllowedConfig { @@ -147,7 +150,7 @@ public function getConfig(array $allowed): AllowedConfig * @param int|string $key * @param int|bool|string|null|array{position:int, value?:int|bool|string, typeString?:string, name?:string} $value * @return T - * @throws UnsupportedParamTypeInConfigException + * @throws InvalidConfigException */ private function paramFactory(string $class, $key, $value): ParamValue { @@ -180,7 +183,12 @@ private function paramFactory(string $class, $key, $value): ParamValue } if ($typeString) { - $type = $this->typeStringResolver->resolve($typeString); + try { + $type = $this->typeStringResolver->resolve($typeString); + } catch (ParserException $e) { + $hint = str_contains($typeString, '*') ? ' Wildcards are not supported in typeString.' : ''; + throw new InvalidTypeStringInConfigException($typeString, $e->getMessage() . $hint, $e); + } } elseif (is_int($paramValue)) { $type = new ConstantIntegerType($paramValue); } elseif (is_bool($paramValue)) { diff --git a/src/DisallowedAttributeFactory.php b/src/DisallowedAttributeFactory.php index bc7f55a1..377279b1 100644 --- a/src/DisallowedAttributeFactory.php +++ b/src/DisallowedAttributeFactory.php @@ -3,8 +3,10 @@ namespace Spaze\PHPStan\Rules\Disallowed; +use PHPStan\ShouldNotHappenException; use Spaze\PHPStan\Rules\Disallowed\Allowed\AllowedConfigFactory; -use Spaze\PHPStan\Rules\Disallowed\Exceptions\UnsupportedParamTypeInConfigException; +use Spaze\PHPStan\Rules\Disallowed\Exceptions\InvalidConfigException; +use Spaze\PHPStan\Rules\Disallowed\Formatter\Formatter; use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer; class DisallowedAttributeFactory @@ -14,11 +16,14 @@ class DisallowedAttributeFactory private Normalizer $normalizer; + private Formatter $formatter; - public function __construct(AllowedConfigFactory $allowedConfigFactory, Normalizer $normalizer) + + public function __construct(AllowedConfigFactory $allowedConfigFactory, Normalizer $normalizer, Formatter $formatter) { $this->allowedConfigFactory = $allowedConfigFactory; $this->normalizer = $normalizer; + $this->formatter = $formatter; } @@ -26,27 +31,31 @@ public function __construct(AllowedConfigFactory $allowedConfigFactory, Normaliz * @param array $config * @phpstan-param DisallowedAttributesConfig $config * @return list - * @throws UnsupportedParamTypeInConfigException + * @throws ShouldNotHappenException */ public function createFromConfig(array $config): array { $disallowedAttributes = []; foreach ($config as $disallowed) { - $attributes = $disallowed['attribute']; + $attributes = (array)$disallowed['attribute']; $excludes = []; foreach ((array)($disallowed['exclude'] ?? []) as $exclude) { $excludes[] = $this->normalizer->normalizeAttribute($exclude); } - foreach ((array)$attributes as $attribute) { - $disallowedAttribute = new DisallowedAttribute( - $this->normalizer->normalizeAttribute($attribute), - $excludes, - $disallowed['message'] ?? null, - $this->allowedConfigFactory->getConfig($disallowed), - $disallowed['errorIdentifier'] ?? null, - $disallowed['errorTip'] ?? [] - ); - $disallowedAttributes[$disallowedAttribute->getAttribute()] = $disallowedAttribute; + try { + foreach ($attributes as $attribute) { + $disallowedAttribute = new DisallowedAttribute( + $this->normalizer->normalizeAttribute($attribute), + $excludes, + $disallowed['message'] ?? null, + $this->allowedConfigFactory->getConfig($disallowed), + $disallowed['errorIdentifier'] ?? null, + $disallowed['errorTip'] ?? [] + ); + $disallowedAttributes[$disallowedAttribute->getAttribute()] = $disallowedAttribute; + } + } catch (InvalidConfigException $e) { + throw new ShouldNotHappenException(sprintf('%s: %s', $this->formatter->formatIdentifier($attributes), $e->getMessage())); } } return array_values($disallowedAttributes); diff --git a/src/DisallowedCallFactory.php b/src/DisallowedCallFactory.php index 052ea3b6..2edb17a4 100644 --- a/src/DisallowedCallFactory.php +++ b/src/DisallowedCallFactory.php @@ -5,7 +5,7 @@ use PHPStan\ShouldNotHappenException; use Spaze\PHPStan\Rules\Disallowed\Allowed\AllowedConfigFactory; -use Spaze\PHPStan\Rules\Disallowed\Exceptions\UnsupportedParamTypeInConfigException; +use Spaze\PHPStan\Rules\Disallowed\Exceptions\InvalidConfigException; use Spaze\PHPStan\Rules\Disallowed\Formatter\Formatter; use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer; @@ -61,7 +61,7 @@ public function createFromConfig(array $config): array ); $disallowedCalls[$disallowedCall->getKey()] = $disallowedCall; } - } catch (UnsupportedParamTypeInConfigException $e) { + } catch (InvalidConfigException $e) { throw new ShouldNotHappenException(sprintf('%s: %s', $this->formatter->formatIdentifier($calls), $e->getMessage())); } } diff --git a/src/DisallowedConstantFactory.php b/src/DisallowedConstantFactory.php index 2f2d6ee4..40827272 100644 --- a/src/DisallowedConstantFactory.php +++ b/src/DisallowedConstantFactory.php @@ -5,7 +5,7 @@ use PHPStan\ShouldNotHappenException; use Spaze\PHPStan\Rules\Disallowed\Allowed\AllowedConfigFactory; -use Spaze\PHPStan\Rules\Disallowed\Exceptions\UnsupportedParamTypeInConfigException; +use Spaze\PHPStan\Rules\Disallowed\Exceptions\InvalidConfigException; use Spaze\PHPStan\Rules\Disallowed\Formatter\Formatter; use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer; @@ -54,7 +54,7 @@ public function createFromConfig(array $config): array ); $disallowedConstants[$disallowedConstant->getConstant()] = $disallowedConstant; } - } catch (UnsupportedParamTypeInConfigException $e) { + } catch (InvalidConfigException $e) { throw new ShouldNotHappenException(sprintf('%s: %s', $this->formatter->formatIdentifier($constants), $e->getMessage())); } } diff --git a/src/DisallowedKeywordFactory.php b/src/DisallowedKeywordFactory.php index 17801ac2..b07932a4 100644 --- a/src/DisallowedKeywordFactory.php +++ b/src/DisallowedKeywordFactory.php @@ -5,7 +5,7 @@ use PHPStan\ShouldNotHappenException; use Spaze\PHPStan\Rules\Disallowed\Allowed\AllowedConfigFactory; -use Spaze\PHPStan\Rules\Disallowed\Exceptions\UnsupportedParamTypeInConfigException; +use Spaze\PHPStan\Rules\Disallowed\Exceptions\InvalidConfigException; use Spaze\PHPStan\Rules\Disallowed\Formatter\Formatter; class DisallowedKeywordFactory @@ -84,7 +84,7 @@ public function getDisallowedKeywords(array $config): array ); $disallowedKeywords[$disallowedKeyword->getKeyword()] = $disallowedKeyword; } - } catch (UnsupportedParamTypeInConfigException $e) { + } catch (InvalidConfigException $e) { throw new ShouldNotHappenException(sprintf('%s: %s', $this->formatter->formatIdentifier($keywords), $e->getMessage())); } } diff --git a/src/DisallowedNamespaceFactory.php b/src/DisallowedNamespaceFactory.php index 28fa73ce..303227fc 100644 --- a/src/DisallowedNamespaceFactory.php +++ b/src/DisallowedNamespaceFactory.php @@ -5,7 +5,7 @@ use PHPStan\ShouldNotHappenException; use Spaze\PHPStan\Rules\Disallowed\Allowed\AllowedConfigFactory; -use Spaze\PHPStan\Rules\Disallowed\Exceptions\UnsupportedParamTypeInConfigException; +use Spaze\PHPStan\Rules\Disallowed\Exceptions\InvalidConfigException; use Spaze\PHPStan\Rules\Disallowed\Formatter\Formatter; use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer; @@ -64,7 +64,7 @@ public function createFromConfig(array $config): array ); $disallowedNamespaces[$disallowedNamespace->getNamespace()] = $disallowedNamespace; } - } catch (UnsupportedParamTypeInConfigException $e) { + } catch (InvalidConfigException $e) { throw new ShouldNotHappenException(sprintf('%s: %s', $this->formatter->formatIdentifier($namespaces), $e->getMessage())); } } diff --git a/src/DisallowedPropertyFactory.php b/src/DisallowedPropertyFactory.php index 553c361f..83dd83cd 100644 --- a/src/DisallowedPropertyFactory.php +++ b/src/DisallowedPropertyFactory.php @@ -3,8 +3,10 @@ namespace Spaze\PHPStan\Rules\Disallowed; +use PHPStan\ShouldNotHappenException; use Spaze\PHPStan\Rules\Disallowed\Allowed\AllowedConfigFactory; -use Spaze\PHPStan\Rules\Disallowed\Exceptions\UnsupportedParamTypeInConfigException; +use Spaze\PHPStan\Rules\Disallowed\Exceptions\InvalidConfigException; +use Spaze\PHPStan\Rules\Disallowed\Formatter\Formatter; use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer; class DisallowedPropertyFactory @@ -14,33 +16,40 @@ class DisallowedPropertyFactory private Normalizer $normalizer; + private Formatter $formatter; - public function __construct(AllowedConfigFactory $allowedConfigFactory, Normalizer $normalizer) + + public function __construct(AllowedConfigFactory $allowedConfigFactory, Normalizer $normalizer, Formatter $formatter) { $this->allowedConfigFactory = $allowedConfigFactory; $this->normalizer = $normalizer; + $this->formatter = $formatter; } /** * @param array, message?:string, errorIdentifier?:string, errorTip?:string|list}> $config + AllowDirectivesConfig * @return list - * @throws UnsupportedParamTypeInConfigException + * @throws ShouldNotHappenException */ public function createFromConfig(array $config): array { $disallowedProperties = []; foreach ($config as $disallowed) { - $properties = $disallowed['property']; - foreach ((array)$properties as $property) { - $disallowedProperty = new DisallowedProperty( - $this->normalizer->normalizeProperty($property), - $disallowed['message'] ?? null, - $this->allowedConfigFactory->getConfig($disallowed), - $disallowed['errorIdentifier'] ?? null, - $disallowed['errorTip'] ?? [] - ); - $disallowedProperties[$disallowedProperty->getProperty()] = $disallowedProperty; + $properties = (array)$disallowed['property']; + try { + foreach ($properties as $property) { + $disallowedProperty = new DisallowedProperty( + $this->normalizer->normalizeProperty($property), + $disallowed['message'] ?? null, + $this->allowedConfigFactory->getConfig($disallowed), + $disallowed['errorIdentifier'] ?? null, + $disallowed['errorTip'] ?? [] + ); + $disallowedProperties[$disallowedProperty->getProperty()] = $disallowedProperty; + } + } catch (InvalidConfigException $e) { + throw new ShouldNotHappenException(sprintf('%s: %s', $this->formatter->formatIdentifier($properties), $e->getMessage())); } } return array_values($disallowedProperties); diff --git a/src/DisallowedSuperglobalFactory.php b/src/DisallowedSuperglobalFactory.php index 2d8bd7ab..762b2867 100644 --- a/src/DisallowedSuperglobalFactory.php +++ b/src/DisallowedSuperglobalFactory.php @@ -5,7 +5,7 @@ use PHPStan\ShouldNotHappenException; use Spaze\PHPStan\Rules\Disallowed\Allowed\AllowedConfigFactory; -use Spaze\PHPStan\Rules\Disallowed\Exceptions\UnsupportedParamTypeInConfigException; +use Spaze\PHPStan\Rules\Disallowed\Exceptions\InvalidConfigException; use Spaze\PHPStan\Rules\Disallowed\Formatter\Formatter; class DisallowedSuperglobalFactory @@ -67,7 +67,7 @@ public function getDisallowedVariables(array $config): array ); $disallowedSuperglobals[$disallowedSuperglobal->getVariable()] = $disallowedSuperglobal; } - } catch (UnsupportedParamTypeInConfigException $e) { + } catch (InvalidConfigException $e) { throw new ShouldNotHappenException(sprintf('%s: %s', $this->formatter->formatIdentifier($superglobals), $e->getMessage())); } } diff --git a/src/Exceptions/InvalidConfigException.php b/src/Exceptions/InvalidConfigException.php new file mode 100644 index 00000000..2aa315d0 --- /dev/null +++ b/src/Exceptions/InvalidConfigException.php @@ -0,0 +1,11 @@ +, message?:string, errorIdentifier?:string, errorTip?:string|list}> $disallowedProperties + AllowDirectivesConfig - * @throws UnsupportedParamTypeInConfigException + * @throws ShouldNotHappenException */ public function __construct( DisallowedPropertyFactory $disallowedPropertyFactory, diff --git a/src/Usages/StaticPropertyUsages.php b/src/Usages/StaticPropertyUsages.php index b6e4ffc8..c5570e13 100644 --- a/src/Usages/StaticPropertyUsages.php +++ b/src/Usages/StaticPropertyUsages.php @@ -13,7 +13,6 @@ use PHPStan\ShouldNotHappenException; use Spaze\PHPStan\Rules\Disallowed\DisallowedProperty; use Spaze\PHPStan\Rules\Disallowed\DisallowedPropertyFactory; -use Spaze\PHPStan\Rules\Disallowed\Exceptions\UnsupportedParamTypeInConfigException; use Spaze\PHPStan\Rules\Disallowed\PHPStan1Compatibility; use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedPropertyRuleErrors; @@ -34,7 +33,7 @@ class StaticPropertyUsages implements Rule /** * @param array, message?:string, allowIn?:list, allowExceptIn?:list, disallowIn?:list, errorIdentifier?:string, errorTip?:string|list}> $disallowedProperties + AllowDirectivesConfig - * @throws UnsupportedParamTypeInConfigException + * @throws ShouldNotHappenException */ public function __construct( DisallowedPropertyFactory $disallowedPropertyFactory, diff --git a/tests/Calls/FunctionCallsInvalidTypeStringConfigTest.php b/tests/Calls/FunctionCallsInvalidTypeStringConfigTest.php new file mode 100644 index 00000000..b6cfc957 --- /dev/null +++ b/tests/Calls/FunctionCallsInvalidTypeStringConfigTest.php @@ -0,0 +1,76 @@ +expectException(ShouldNotHappenException::class); + $this->expectExceptionMessageMatches("~foo\\(\\): Invalid typeString 'Foo\\\\\\*':.*Wildcards are not supported in typeString\\.~"); + $container = self::getContainer(); + new FunctionCalls( + $container->getByType(DisallowedFunctionRuleErrors::class), + $container->getByType(DisallowedCallableParameterRuleErrors::class), + $container->getByType(DisallowedCallFactory::class), + [ + [ + 'function' => 'foo()', + 'disallowParamsInAllowed' => [ + 1 => [ + 'position' => 1, + 'typeString' => 'Foo\*', + ], + ], + ], + ] + ); + } + + + /** + * @throws ShouldNotHappenException + */ + public function testInvalidTypeStringWithoutWildcard(): void + { + $this->expectException(ShouldNotHappenException::class); + $this->expectExceptionMessage("foo(): Invalid typeString 'arraygetByType(DisallowedFunctionRuleErrors::class), + $container->getByType(DisallowedCallableParameterRuleErrors::class), + $container->getByType(DisallowedCallFactory::class), + [ + [ + 'function' => 'foo()', + 'disallowParamsInAllowed' => [ + 1 => [ + 'position' => 1, + 'typeString' => 'array