From d3725faf543773ec0c831bcb015ae15145883632 Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Tue, 12 May 2026 12:19:22 +0000 Subject: [PATCH 01/12] Validate `define()` and `const` values against explicit types in `dynamicConstantNames` - Add `getExplicitGlobalConstantType()` and `getExplicitClassConstantType()` methods to `ConstantResolver` to retrieve the configured type from `dynamicConstantNames` without applying it - Add `ValueAssignedToDefineRule` (level 2) that validates `define()` call values against the explicit type configured in `dynamicConstantNames` - Add `ValueAssignedToGlobalConstantRule` (level 2) that validates file-level `const` statement values against the explicit type configured in `dynamicConstantNames` - Extend `ValueAssignedToClassConstantRule` to also validate class constant values against explicit types in `dynamicConstantNames` when no PHPDoc or native type is present --- src/Analyser/ConstantResolver.php | 27 +++++++ .../ValueAssignedToClassConstantRule.php | 21 +++++ .../Constants/ValueAssignedToDefineRule.php | 77 +++++++++++++++++++ .../ValueAssignedToGlobalConstantRule.php | 65 ++++++++++++++++ .../ValueAssignedToClassConstantRuleTest.php | 5 +- ...oClassConstantWithDynamicNamesRuleTest.php | 39 ++++++++++ .../ValueAssignedToDefineRuleTest.php | 43 +++++++++++ .../ValueAssignedToGlobalConstantRuleTest.php | 39 ++++++++++ ...signed-to-class-constant-dynamic-names.php | 9 +++ .../data/value-assigned-to-define.php | 11 +++ .../value-assigned-to-global-constant.php | 4 + .../value-assigned-dynamic-constant.neon | 7 ++ 12 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 src/Rules/Constants/ValueAssignedToDefineRule.php create mode 100644 src/Rules/Constants/ValueAssignedToGlobalConstantRule.php create mode 100644 tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantWithDynamicNamesRuleTest.php create mode 100644 tests/PHPStan/Rules/Constants/ValueAssignedToDefineRuleTest.php create mode 100644 tests/PHPStan/Rules/Constants/ValueAssignedToGlobalConstantRuleTest.php create mode 100644 tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant-dynamic-names.php create mode 100644 tests/PHPStan/Rules/Constants/data/value-assigned-to-define.php create mode 100644 tests/PHPStan/Rules/Constants/data/value-assigned-to-global-constant.php create mode 100644 tests/PHPStan/Rules/Constants/value-assigned-dynamic-constant.neon diff --git a/src/Analyser/ConstantResolver.php b/src/Analyser/ConstantResolver.php index d304ef5a64d..47577ac3dc5 100644 --- a/src/Analyser/ConstantResolver.php +++ b/src/Analyser/ConstantResolver.php @@ -414,6 +414,33 @@ private function getMaxPhpVersion(): ?PhpVersion return $this->composerPhpVersionFactory->getMaxVersion(); } + public function getExplicitGlobalConstantType(string $constantName): ?Type + { + if (array_key_exists($constantName, $this->dynamicConstantNames)) { + $phpdocTypes = $this->dynamicConstantNames[$constantName]; + if ($this->container !== null) { + $typeStringResolver = $this->container->getByType(TypeStringResolver::class); + return $typeStringResolver->resolve($phpdocTypes, new NameScope(null, [], className: null)); + } + } + + return null; + } + + public function getExplicitClassConstantType(string $className, string $constantName): ?Type + { + $lookupConstantName = sprintf('%s::%s', $className, $constantName); + if (array_key_exists($lookupConstantName, $this->dynamicConstantNames)) { + $phpdocTypes = $this->dynamicConstantNames[$lookupConstantName]; + if ($this->container !== null) { + $typeStringResolver = $this->container->getByType(TypeStringResolver::class); + return $typeStringResolver->resolve($phpdocTypes, new NameScope(null, [], $className)); + } + } + + return null; + } + public function resolveConstantType(string $constantName, Type $constantType): Type { if ($constantType->isConstantValue()->yes()) { diff --git a/src/Rules/Constants/ValueAssignedToClassConstantRule.php b/src/Rules/Constants/ValueAssignedToClassConstantRule.php index 9041a60ef0b..4cc87c1d5aa 100644 --- a/src/Rules/Constants/ValueAssignedToClassConstantRule.php +++ b/src/Rules/Constants/ValueAssignedToClassConstantRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Constants; use PhpParser\Node; +use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ClassReflection; @@ -23,6 +24,10 @@ final class ValueAssignedToClassConstantRule implements Rule { + public function __construct(private ConstantResolver $constantResolver) + { + } + public function getNodeType(): string { return Node\Stmt\ClassConst::class; @@ -62,6 +67,22 @@ private function processSingleConstant(ClassReflection $classReflection, string $phpDocType = $constantReflection->getPhpDocType(); if ($phpDocType === null) { if ($nativeType === null) { + $configuredType = $this->constantResolver->getExplicitClassConstantType($classReflection->getName(), $constantName); + if ($configuredType !== null) { + $accepts = $configuredType->accepts($valueExprType, true); + if (!$accepts->yes()) { + $verbosity = VerbosityLevel::getRecommendedLevelByType($configuredType, $valueExprType); + return [ + RuleErrorBuilder::message(sprintf( + 'Constant %s::%s (%s) does not accept value %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $configuredType->describe(VerbosityLevel::typeOnly()), + $valueExprType->describe($verbosity), + ))->acceptsReasonsTip($accepts->reasons)->identifier('classConstant.value')->build(), + ]; + } + } return []; } diff --git a/src/Rules/Constants/ValueAssignedToDefineRule.php b/src/Rules/Constants/ValueAssignedToDefineRule.php new file mode 100644 index 00000000000..042babe4ef3 --- /dev/null +++ b/src/Rules/Constants/ValueAssignedToDefineRule.php @@ -0,0 +1,77 @@ + + */ +#[RegisteredRule(level: 2)] +final class ValueAssignedToDefineRule implements Rule +{ + + public function __construct(private ConstantResolver $constantResolver) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (strtolower((string) $node->name) !== 'define') { + return []; + } + + $args = $node->getArgs(); + if (count($args) < 2) { + return []; + } + + $constantNameStrings = $scope->getType($args[0]->value)->getConstantStrings(); + if (count($constantNameStrings) !== 1 || $constantNameStrings[0]->getValue() === '') { + return []; + } + + $constantName = $constantNameStrings[0]->getValue(); + $configuredType = $this->constantResolver->getExplicitGlobalConstantType($constantName); + if ($configuredType === null) { + return []; + } + + $valueType = $scope->getType($args[1]->value); + $accepts = $configuredType->accepts($valueType, true); + if ($accepts->yes()) { + return []; + } + + $verbosity = VerbosityLevel::getRecommendedLevelByType($configuredType, $valueType); + + return [ + RuleErrorBuilder::message(sprintf( + 'Constant %s (%s) does not accept value %s.', + $constantName, + $configuredType->describe(VerbosityLevel::typeOnly()), + $valueType->describe($verbosity), + ))->acceptsReasonsTip($accepts->reasons)->identifier('constant.value')->build(), + ]; + } + +} diff --git a/src/Rules/Constants/ValueAssignedToGlobalConstantRule.php b/src/Rules/Constants/ValueAssignedToGlobalConstantRule.php new file mode 100644 index 00000000000..e812f4ebc6f --- /dev/null +++ b/src/Rules/Constants/ValueAssignedToGlobalConstantRule.php @@ -0,0 +1,65 @@ + + */ +#[RegisteredRule(level: 2)] +final class ValueAssignedToGlobalConstantRule implements Rule +{ + + public function __construct(private ConstantResolver $constantResolver) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Const_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $errors = []; + + foreach ($node->consts as $const) { + if ($const->namespacedName !== null) { + $constantName = $const->namespacedName->toString(); + } else { + $constantName = $const->name->toString(); + } + + $configuredType = $this->constantResolver->getExplicitGlobalConstantType($constantName); + if ($configuredType === null) { + continue; + } + + $valueType = $scope->getType($const->value); + $accepts = $configuredType->accepts($valueType, true); + if ($accepts->yes()) { + continue; + } + + $verbosity = VerbosityLevel::getRecommendedLevelByType($configuredType, $valueType); + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Constant %s (%s) does not accept value %s.', + $constantName, + $configuredType->describe(VerbosityLevel::typeOnly()), + $valueType->describe($verbosity), + ))->acceptsReasonsTip($accepts->reasons)->identifier('constant.value')->build(); + } + + return $errors; + } + +} diff --git a/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantRuleTest.php b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantRuleTest.php index f36fa8dd808..2953b0a4734 100644 --- a/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Constants; +use PHPStan\Analyser\ConstantResolver; use PHPStan\Rules\Rule as TRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -14,7 +15,9 @@ class ValueAssignedToClassConstantRuleTest extends RuleTestCase protected function getRule(): TRule { - return new ValueAssignedToClassConstantRule(); + return new ValueAssignedToClassConstantRule( + self::getContainer()->getByType(ConstantResolver::class), + ); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantWithDynamicNamesRuleTest.php b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantWithDynamicNamesRuleTest.php new file mode 100644 index 00000000000..36a991c88f1 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantWithDynamicNamesRuleTest.php @@ -0,0 +1,39 @@ + + */ +class ValueAssignedToClassConstantWithDynamicNamesRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new ValueAssignedToClassConstantRule( + self::getContainer()->getByType(ConstantResolver::class), + ); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/value-assigned-dynamic-constant.neon', + ]; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/value-assigned-to-class-constant-dynamic-names.php'], [ + [ + 'Constant ValueAssignedToClassConstantDynamicNames\Foo::BAR (int|string|null) does not accept value false.', + 7, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Constants/ValueAssignedToDefineRuleTest.php b/tests/PHPStan/Rules/Constants/ValueAssignedToDefineRuleTest.php new file mode 100644 index 00000000000..26ee8ba8d30 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/ValueAssignedToDefineRuleTest.php @@ -0,0 +1,43 @@ + + */ +class ValueAssignedToDefineRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new ValueAssignedToDefineRule( + self::getContainer()->getByType(ConstantResolver::class), + ); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/value-assigned-dynamic-constant.neon', + ]; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/value-assigned-to-define.php'], [ + [ + 'Constant BAR_CONSTANT (int|string|null) does not accept value false.', + 5, + ], + [ + 'Constant BAR_CONSTANT (int|string|null) does not accept value int|false.', + 6, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Constants/ValueAssignedToGlobalConstantRuleTest.php b/tests/PHPStan/Rules/Constants/ValueAssignedToGlobalConstantRuleTest.php new file mode 100644 index 00000000000..7b209d79cf7 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/ValueAssignedToGlobalConstantRuleTest.php @@ -0,0 +1,39 @@ + + */ +class ValueAssignedToGlobalConstantRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new ValueAssignedToGlobalConstantRule( + self::getContainer()->getByType(ConstantResolver::class), + ); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/value-assigned-dynamic-constant.neon', + ]; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/value-assigned-to-global-constant.php'], [ + [ + 'Constant BAR_CONSTANT (int|string|null) does not accept value false.', + 3, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant-dynamic-names.php b/tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant-dynamic-names.php new file mode 100644 index 00000000000..cb4d2e535a8 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant-dynamic-names.php @@ -0,0 +1,9 @@ + Date: Tue, 12 May 2026 12:51:31 +0000 Subject: [PATCH 02/12] Gate dynamic constant name value validation behind bleeding edge The new rules (ValueAssignedToDefineRule, ValueAssignedToGlobalConstantRule) and the dynamicConstantNames check in ValueAssignedToClassConstantRule are now only enabled when bleeding edge is active, via the new checkDynamicConstantNameValues feature toggle. Co-Authored-By: Claude Opus 4.6 --- conf/bleedingEdge.neon | 1 + conf/config.level2.neon | 10 ++++++++++ conf/config.neon | 1 + conf/parametersSchema.neon | 1 + .../Constants/ValueAssignedToClassConstantRule.php | 10 +++++++++- src/Rules/Constants/ValueAssignedToDefineRule.php | 2 -- .../Constants/ValueAssignedToGlobalConstantRule.php | 2 -- .../Constants/ValueAssignedToClassConstantRuleTest.php | 1 + ...AssignedToClassConstantWithDynamicNamesRuleTest.php | 1 + 9 files changed, 24 insertions(+), 5 deletions(-) diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 76afb8bdd54..ccb34eea665 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -18,3 +18,4 @@ parameters: curlSetOptArrayTypes: true checkDateIntervalConstructor: true reportMethodPurityOverride: true + checkDynamicConstantNameValues: true diff --git a/conf/config.level2.neon b/conf/config.level2.neon index dd50138b541..bb615c07c34 100644 --- a/conf/config.level2.neon +++ b/conf/config.level2.neon @@ -15,6 +15,10 @@ conditionalTags: phpstan.restrictedPropertyUsageExtension: %featureToggles.internalTag% PHPStan\Rules\InternalTag\RestrictedInternalMethodUsageExtension: phpstan.restrictedMethodUsageExtension: %featureToggles.internalTag% + PHPStan\Rules\Constants\ValueAssignedToDefineRule: + phpstan.rules.rule: %featureToggles.checkDynamicConstantNameValues% + PHPStan\Rules\Constants\ValueAssignedToGlobalConstantRule: + phpstan.rules.rule: %featureToggles.checkDynamicConstantNameValues% services: - @@ -22,3 +26,9 @@ services: - class: PHPStan\Rules\InternalTag\RestrictedInternalMethodUsageExtension + + - + class: PHPStan\Rules\Constants\ValueAssignedToDefineRule + + - + class: PHPStan\Rules\Constants\ValueAssignedToGlobalConstantRule diff --git a/conf/config.neon b/conf/config.neon index 1628b4b83a0..f0ae51d29dc 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -45,6 +45,7 @@ parameters: curlSetOptArrayTypes: false checkDateIntervalConstructor: false reportMethodPurityOverride: false + checkDynamicConstantNameValues: false fileExtensions: - php checkAdvancedIsset: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 15e6e02c215..46f7f6a04e0 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -47,6 +47,7 @@ parametersSchema: curlSetOptArrayTypes: bool() checkDateIntervalConstructor: bool() reportMethodPurityOverride: bool() + checkDynamicConstantNameValues: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/src/Rules/Constants/ValueAssignedToClassConstantRule.php b/src/Rules/Constants/ValueAssignedToClassConstantRule.php index 4cc87c1d5aa..0a314c33e77 100644 --- a/src/Rules/Constants/ValueAssignedToClassConstantRule.php +++ b/src/Rules/Constants/ValueAssignedToClassConstantRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ClassReflection; use PHPStan\Rules\IdentifierRuleError; @@ -24,7 +25,11 @@ final class ValueAssignedToClassConstantRule implements Rule { - public function __construct(private ConstantResolver $constantResolver) + public function __construct( + private ConstantResolver $constantResolver, + #[AutowiredParameter(ref: '%featureToggles.checkDynamicConstantNameValues%')] + private bool $checkDynamicConstantNameValues, + ) { } @@ -67,6 +72,9 @@ private function processSingleConstant(ClassReflection $classReflection, string $phpDocType = $constantReflection->getPhpDocType(); if ($phpDocType === null) { if ($nativeType === null) { + if (!$this->checkDynamicConstantNameValues) { + return []; + } $configuredType = $this->constantResolver->getExplicitClassConstantType($classReflection->getName(), $constantName); if ($configuredType !== null) { $accepts = $configuredType->accepts($valueExprType, true); diff --git a/src/Rules/Constants/ValueAssignedToDefineRule.php b/src/Rules/Constants/ValueAssignedToDefineRule.php index 042babe4ef3..13c44020b8f 100644 --- a/src/Rules/Constants/ValueAssignedToDefineRule.php +++ b/src/Rules/Constants/ValueAssignedToDefineRule.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\Scope; -use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; @@ -17,7 +16,6 @@ /** * @implements Rule */ -#[RegisteredRule(level: 2)] final class ValueAssignedToDefineRule implements Rule { diff --git a/src/Rules/Constants/ValueAssignedToGlobalConstantRule.php b/src/Rules/Constants/ValueAssignedToGlobalConstantRule.php index e812f4ebc6f..2e762bcf6b6 100644 --- a/src/Rules/Constants/ValueAssignedToGlobalConstantRule.php +++ b/src/Rules/Constants/ValueAssignedToGlobalConstantRule.php @@ -5,7 +5,6 @@ use PhpParser\Node; use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\Scope; -use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; @@ -14,7 +13,6 @@ /** * @implements Rule */ -#[RegisteredRule(level: 2)] final class ValueAssignedToGlobalConstantRule implements Rule { diff --git a/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantRuleTest.php b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantRuleTest.php index 2953b0a4734..2aa9ceba8d8 100644 --- a/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantRuleTest.php @@ -17,6 +17,7 @@ protected function getRule(): TRule { return new ValueAssignedToClassConstantRule( self::getContainer()->getByType(ConstantResolver::class), + false, ); } diff --git a/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantWithDynamicNamesRuleTest.php b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantWithDynamicNamesRuleTest.php index 36a991c88f1..3b51c26ba7f 100644 --- a/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantWithDynamicNamesRuleTest.php +++ b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantWithDynamicNamesRuleTest.php @@ -16,6 +16,7 @@ protected function getRule(): TRule { return new ValueAssignedToClassConstantRule( self::getContainer()->getByType(ConstantResolver::class), + true, ); } From ef0d121347ac02cca4c60d0170ba583c197bfb7a Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 12 May 2026 13:26:28 +0000 Subject: [PATCH 03/12] DRY resolveConstantType and resolveClassConstantType by reusing getExplicit*Type methods Co-Authored-By: Claude Opus 4.6 --- src/Analyser/ConstantResolver.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Analyser/ConstantResolver.php b/src/Analyser/ConstantResolver.php index 47577ac3dc5..a47a7baa2ce 100644 --- a/src/Analyser/ConstantResolver.php +++ b/src/Analyser/ConstantResolver.php @@ -444,12 +444,11 @@ public function getExplicitClassConstantType(string $className, string $constant public function resolveConstantType(string $constantName, Type $constantType): Type { if ($constantType->isConstantValue()->yes()) { + $explicitType = $this->getExplicitGlobalConstantType($constantName); + if ($explicitType !== null) { + return $explicitType; + } if (array_key_exists($constantName, $this->dynamicConstantNames)) { - $phpdocTypes = $this->dynamicConstantNames[$constantName]; - if ($this->container !== null) { - $typeStringResolver = $this->container->getByType(TypeStringResolver::class); - return $typeStringResolver->resolve($phpdocTypes, new NameScope(null, [], className: null)); - } return $constantType; } if (in_array($constantName, $this->dynamicConstantNames, true)) { @@ -465,10 +464,9 @@ public function resolveClassConstantType(string $className, string $constantName $lookupConstantName = sprintf('%s::%s', $className, $constantName); if (array_key_exists($lookupConstantName, $this->dynamicConstantNames)) { if ($constantType->isConstantValue()->yes()) { - $phpdocTypes = $this->dynamicConstantNames[$lookupConstantName]; - if ($this->container !== null) { - $typeStringResolver = $this->container->getByType(TypeStringResolver::class); - return $typeStringResolver->resolve($phpdocTypes, new NameScope(null, [], $className)); + $explicitType = $this->getExplicitClassConstantType($className, $constantName); + if ($explicitType !== null) { + return $explicitType; } } From 1d465d80922dcbf1e832a9a8e8cad572453b8700 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 12 May 2026 13:32:05 +0000 Subject: [PATCH 04/12] DRY constant type resolution and support multiple define() name strings Refactor resolveConstantType/resolveClassConstantType to reuse getExplicitGlobalConstantType/getExplicitClassConstantType instead of duplicating the type string resolution logic. Update ValueAssignedToDefineRule to iterate over all constant name strings instead of bailing out when count !== 1, so define() calls with union-typed names are checked too. Co-Authored-By: Claude Opus 4.6 --- .../Constants/ValueAssignedToDefineRule.php | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/Rules/Constants/ValueAssignedToDefineRule.php b/src/Rules/Constants/ValueAssignedToDefineRule.php index 13c44020b8f..b2f96d17f55 100644 --- a/src/Rules/Constants/ValueAssignedToDefineRule.php +++ b/src/Rules/Constants/ValueAssignedToDefineRule.php @@ -44,32 +44,40 @@ public function processNode(Node $node, Scope $scope): array } $constantNameStrings = $scope->getType($args[0]->value)->getConstantStrings(); - if (count($constantNameStrings) !== 1 || $constantNameStrings[0]->getValue() === '') { - return []; - } - - $constantName = $constantNameStrings[0]->getValue(); - $configuredType = $this->constantResolver->getExplicitGlobalConstantType($constantName); - if ($configuredType === null) { + if (count($constantNameStrings) === 0) { return []; } $valueType = $scope->getType($args[1]->value); - $accepts = $configuredType->accepts($valueType, true); - if ($accepts->yes()) { - return []; - } + $errors = []; - $verbosity = VerbosityLevel::getRecommendedLevelByType($configuredType, $valueType); + foreach ($constantNameStrings as $constantNameString) { + if ($constantNameString->getValue() === '') { + continue; + } - return [ - RuleErrorBuilder::message(sprintf( + $constantName = $constantNameString->getValue(); + $configuredType = $this->constantResolver->getExplicitGlobalConstantType($constantName); + if ($configuredType === null) { + continue; + } + + $accepts = $configuredType->accepts($valueType, true); + if ($accepts->yes()) { + continue; + } + + $verbosity = VerbosityLevel::getRecommendedLevelByType($configuredType, $valueType); + + $errors[] = RuleErrorBuilder::message(sprintf( 'Constant %s (%s) does not accept value %s.', $constantName, $configuredType->describe(VerbosityLevel::typeOnly()), $valueType->describe($verbosity), - ))->acceptsReasonsTip($accepts->reasons)->identifier('constant.value')->build(), - ]; + ))->acceptsReasonsTip($accepts->reasons)->identifier('constant.value')->build(); + } + + return $errors; } } From 10de40cee54a0a3975683989bae8f634d8b3392d Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 12 May 2026 13:32:55 +0000 Subject: [PATCH 05/12] DRY ConstantResolver and handle multiple constant names in ValueAssignedToDefineRule - Refactor resolveConstantType() and resolveClassConstantType() to reuse getExplicitGlobalConstantType() and getExplicitClassConstantType() instead of duplicating the TypeStringResolver lookup logic. - Update ValueAssignedToDefineRule to iterate over all constant name strings from the define() call, not just when there is exactly one. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/ConstantResolver.php | 6 +----- src/Rules/Constants/ValueAssignedToDefineRule.php | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Analyser/ConstantResolver.php b/src/Analyser/ConstantResolver.php index a47a7baa2ce..6bcc451a4fd 100644 --- a/src/Analyser/ConstantResolver.php +++ b/src/Analyser/ConstantResolver.php @@ -444,12 +444,8 @@ public function getExplicitClassConstantType(string $className, string $constant public function resolveConstantType(string $constantName, Type $constantType): Type { if ($constantType->isConstantValue()->yes()) { - $explicitType = $this->getExplicitGlobalConstantType($constantName); - if ($explicitType !== null) { - return $explicitType; - } if (array_key_exists($constantName, $this->dynamicConstantNames)) { - return $constantType; + return $this->getExplicitGlobalConstantType($constantName) ?? $constantType; } if (in_array($constantName, $this->dynamicConstantNames, true)) { return $this->generalizeDynamicConstantType($constantType); diff --git a/src/Rules/Constants/ValueAssignedToDefineRule.php b/src/Rules/Constants/ValueAssignedToDefineRule.php index b2f96d17f55..3039581cd99 100644 --- a/src/Rules/Constants/ValueAssignedToDefineRule.php +++ b/src/Rules/Constants/ValueAssignedToDefineRule.php @@ -52,11 +52,11 @@ public function processNode(Node $node, Scope $scope): array $errors = []; foreach ($constantNameStrings as $constantNameString) { - if ($constantNameString->getValue() === '') { + $constantName = $constantNameString->getValue(); + if ($constantName === '') { continue; } - $constantName = $constantNameString->getValue(); $configuredType = $this->constantResolver->getExplicitGlobalConstantType($constantName); if ($configuredType === null) { continue; From c3524c1efbabcdc967dc00bcad340ccd5a370575 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 12 May 2026 17:33:26 +0200 Subject: [PATCH 06/12] Update ValueAssignedToDefineRule.php --- src/Rules/Constants/ValueAssignedToDefineRule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Rules/Constants/ValueAssignedToDefineRule.php b/src/Rules/Constants/ValueAssignedToDefineRule.php index 3039581cd99..c4dc297b922 100644 --- a/src/Rules/Constants/ValueAssignedToDefineRule.php +++ b/src/Rules/Constants/ValueAssignedToDefineRule.php @@ -74,7 +74,7 @@ public function processNode(Node $node, Scope $scope): array $constantName, $configuredType->describe(VerbosityLevel::typeOnly()), $valueType->describe($verbosity), - ))->acceptsReasonsTip($accepts->reasons)->identifier('constant.value')->build(); + ))->acceptsReasonsTip($accepts->reasons)->identifier('constant.defineValue')->build(); } return $errors; From 5b39181add7d297c6f7f863cf604d34356ce55f9 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 12 May 2026 17:51:15 +0200 Subject: [PATCH 07/12] try to improve message --- src/Rules/Constants/ValueAssignedToClassConstantRule.php | 2 +- src/Rules/Constants/ValueAssignedToDefineRule.php | 2 +- src/Rules/Constants/ValueAssignedToGlobalConstantRule.php | 2 +- .../ValueAssignedToClassConstantWithDynamicNamesRuleTest.php | 2 +- .../PHPStan/Rules/Constants/ValueAssignedToDefineRuleTest.php | 4 ++-- .../Rules/Constants/ValueAssignedToGlobalConstantRuleTest.php | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Rules/Constants/ValueAssignedToClassConstantRule.php b/src/Rules/Constants/ValueAssignedToClassConstantRule.php index 0a314c33e77..c8317a14692 100644 --- a/src/Rules/Constants/ValueAssignedToClassConstantRule.php +++ b/src/Rules/Constants/ValueAssignedToClassConstantRule.php @@ -82,7 +82,7 @@ private function processSingleConstant(ClassReflection $classReflection, string $verbosity = VerbosityLevel::getRecommendedLevelByType($configuredType, $valueExprType); return [ RuleErrorBuilder::message(sprintf( - 'Constant %s::%s (%s) does not accept value %s.', + 'Configuration defined type for constant %s::%s (%s) is incompatible with value %s.', $constantReflection->getDeclaringClass()->getDisplayName(), $constantName, $configuredType->describe(VerbosityLevel::typeOnly()), diff --git a/src/Rules/Constants/ValueAssignedToDefineRule.php b/src/Rules/Constants/ValueAssignedToDefineRule.php index c4dc297b922..0d8fa99a48b 100644 --- a/src/Rules/Constants/ValueAssignedToDefineRule.php +++ b/src/Rules/Constants/ValueAssignedToDefineRule.php @@ -70,7 +70,7 @@ public function processNode(Node $node, Scope $scope): array $verbosity = VerbosityLevel::getRecommendedLevelByType($configuredType, $valueType); $errors[] = RuleErrorBuilder::message(sprintf( - 'Constant %s (%s) does not accept value %s.', + 'Configuration defined type for constant %s (%s) is incompatible with value %s.', $constantName, $configuredType->describe(VerbosityLevel::typeOnly()), $valueType->describe($verbosity), diff --git a/src/Rules/Constants/ValueAssignedToGlobalConstantRule.php b/src/Rules/Constants/ValueAssignedToGlobalConstantRule.php index 2e762bcf6b6..5ea1fd212d1 100644 --- a/src/Rules/Constants/ValueAssignedToGlobalConstantRule.php +++ b/src/Rules/Constants/ValueAssignedToGlobalConstantRule.php @@ -50,7 +50,7 @@ public function processNode(Node $node, Scope $scope): array $verbosity = VerbosityLevel::getRecommendedLevelByType($configuredType, $valueType); $errors[] = RuleErrorBuilder::message(sprintf( - 'Constant %s (%s) does not accept value %s.', + 'Configuration defined type for constant %s (%s) is incompatible with value %s.', $constantName, $configuredType->describe(VerbosityLevel::typeOnly()), $valueType->describe($verbosity), diff --git a/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantWithDynamicNamesRuleTest.php b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantWithDynamicNamesRuleTest.php index 3b51c26ba7f..0fa7288f176 100644 --- a/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantWithDynamicNamesRuleTest.php +++ b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantWithDynamicNamesRuleTest.php @@ -31,7 +31,7 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/value-assigned-to-class-constant-dynamic-names.php'], [ [ - 'Constant ValueAssignedToClassConstantDynamicNames\Foo::BAR (int|string|null) does not accept value false.', + 'Configuration defined type for constant ValueAssignedToClassConstantDynamicNames\Foo::BAR (int|string|null) is incompatible with value false.', 7, ], ]); diff --git a/tests/PHPStan/Rules/Constants/ValueAssignedToDefineRuleTest.php b/tests/PHPStan/Rules/Constants/ValueAssignedToDefineRuleTest.php index 26ee8ba8d30..4aaa03fd79f 100644 --- a/tests/PHPStan/Rules/Constants/ValueAssignedToDefineRuleTest.php +++ b/tests/PHPStan/Rules/Constants/ValueAssignedToDefineRuleTest.php @@ -30,11 +30,11 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/value-assigned-to-define.php'], [ [ - 'Constant BAR_CONSTANT (int|string|null) does not accept value false.', + 'Configuration defined type for constant BAR_CONSTANT (int|string|null) is incompatible with value false.', 5, ], [ - 'Constant BAR_CONSTANT (int|string|null) does not accept value int|false.', + 'Configuration defined type for constant BAR_CONSTANT (int|string|null) is incompatible with value int|false.', 6, ], ]); diff --git a/tests/PHPStan/Rules/Constants/ValueAssignedToGlobalConstantRuleTest.php b/tests/PHPStan/Rules/Constants/ValueAssignedToGlobalConstantRuleTest.php index 7b209d79cf7..2bc97c93f60 100644 --- a/tests/PHPStan/Rules/Constants/ValueAssignedToGlobalConstantRuleTest.php +++ b/tests/PHPStan/Rules/Constants/ValueAssignedToGlobalConstantRuleTest.php @@ -30,7 +30,7 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/value-assigned-to-global-constant.php'], [ [ - 'Constant BAR_CONSTANT (int|string|null) does not accept value false.', + 'Configuration defined type for constant BAR_CONSTANT (int|string|null) is incompatible with value false.', 3, ], ]); From 23610b15befc0190e94d7bb0063ea376f905f1e6 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 16 May 2026 06:53:24 +0000 Subject: [PATCH 08/12] Rename getExplicit*ConstantType to getConfigured*ConstantType Co-Authored-By: Claude Opus 4.6 --- src/Analyser/ConstantResolver.php | 8 ++++---- src/Rules/Constants/ValueAssignedToClassConstantRule.php | 2 +- src/Rules/Constants/ValueAssignedToDefineRule.php | 2 +- src/Rules/Constants/ValueAssignedToGlobalConstantRule.php | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Analyser/ConstantResolver.php b/src/Analyser/ConstantResolver.php index 6bcc451a4fd..5f4849d53bc 100644 --- a/src/Analyser/ConstantResolver.php +++ b/src/Analyser/ConstantResolver.php @@ -414,7 +414,7 @@ private function getMaxPhpVersion(): ?PhpVersion return $this->composerPhpVersionFactory->getMaxVersion(); } - public function getExplicitGlobalConstantType(string $constantName): ?Type + public function getConfiguredGlobalConstantType(string $constantName): ?Type { if (array_key_exists($constantName, $this->dynamicConstantNames)) { $phpdocTypes = $this->dynamicConstantNames[$constantName]; @@ -427,7 +427,7 @@ public function getExplicitGlobalConstantType(string $constantName): ?Type return null; } - public function getExplicitClassConstantType(string $className, string $constantName): ?Type + public function getConfiguredClassConstantType(string $className, string $constantName): ?Type { $lookupConstantName = sprintf('%s::%s', $className, $constantName); if (array_key_exists($lookupConstantName, $this->dynamicConstantNames)) { @@ -445,7 +445,7 @@ public function resolveConstantType(string $constantName, Type $constantType): T { if ($constantType->isConstantValue()->yes()) { if (array_key_exists($constantName, $this->dynamicConstantNames)) { - return $this->getExplicitGlobalConstantType($constantName) ?? $constantType; + return $this->getConfiguredGlobalConstantType($constantName) ?? $constantType; } if (in_array($constantName, $this->dynamicConstantNames, true)) { return $this->generalizeDynamicConstantType($constantType); @@ -460,7 +460,7 @@ public function resolveClassConstantType(string $className, string $constantName $lookupConstantName = sprintf('%s::%s', $className, $constantName); if (array_key_exists($lookupConstantName, $this->dynamicConstantNames)) { if ($constantType->isConstantValue()->yes()) { - $explicitType = $this->getExplicitClassConstantType($className, $constantName); + $explicitType = $this->getConfiguredClassConstantType($className, $constantName); if ($explicitType !== null) { return $explicitType; } diff --git a/src/Rules/Constants/ValueAssignedToClassConstantRule.php b/src/Rules/Constants/ValueAssignedToClassConstantRule.php index c8317a14692..cbef28d6637 100644 --- a/src/Rules/Constants/ValueAssignedToClassConstantRule.php +++ b/src/Rules/Constants/ValueAssignedToClassConstantRule.php @@ -75,7 +75,7 @@ private function processSingleConstant(ClassReflection $classReflection, string if (!$this->checkDynamicConstantNameValues) { return []; } - $configuredType = $this->constantResolver->getExplicitClassConstantType($classReflection->getName(), $constantName); + $configuredType = $this->constantResolver->getConfiguredClassConstantType($classReflection->getName(), $constantName); if ($configuredType !== null) { $accepts = $configuredType->accepts($valueExprType, true); if (!$accepts->yes()) { diff --git a/src/Rules/Constants/ValueAssignedToDefineRule.php b/src/Rules/Constants/ValueAssignedToDefineRule.php index 0d8fa99a48b..0bc595da0e1 100644 --- a/src/Rules/Constants/ValueAssignedToDefineRule.php +++ b/src/Rules/Constants/ValueAssignedToDefineRule.php @@ -57,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array continue; } - $configuredType = $this->constantResolver->getExplicitGlobalConstantType($constantName); + $configuredType = $this->constantResolver->getConfiguredGlobalConstantType($constantName); if ($configuredType === null) { continue; } diff --git a/src/Rules/Constants/ValueAssignedToGlobalConstantRule.php b/src/Rules/Constants/ValueAssignedToGlobalConstantRule.php index 5ea1fd212d1..4af81a0e5c3 100644 --- a/src/Rules/Constants/ValueAssignedToGlobalConstantRule.php +++ b/src/Rules/Constants/ValueAssignedToGlobalConstantRule.php @@ -36,7 +36,7 @@ public function processNode(Node $node, Scope $scope): array $constantName = $const->name->toString(); } - $configuredType = $this->constantResolver->getExplicitGlobalConstantType($constantName); + $configuredType = $this->constantResolver->getConfiguredGlobalConstantType($constantName); if ($configuredType === null) { continue; } From 822b7550ea6028f050b266575cbca9d26b863679 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 16 May 2026 07:42:39 +0000 Subject: [PATCH 09/12] Add test for Maybe accepts path in ValueAssignedToClassConstantRule Co-Authored-By: Claude Opus 4.6 --- ...ValueAssignedToClassConstantWithDynamicNamesRuleTest.php | 6 +++++- .../data/value-assigned-to-class-constant-dynamic-names.php | 6 ++++++ .../Rules/Constants/value-assigned-dynamic-constant.neon | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantWithDynamicNamesRuleTest.php b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantWithDynamicNamesRuleTest.php index 0fa7288f176..d4a8b41f33a 100644 --- a/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantWithDynamicNamesRuleTest.php +++ b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantWithDynamicNamesRuleTest.php @@ -32,7 +32,11 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/value-assigned-to-class-constant-dynamic-names.php'], [ [ 'Configuration defined type for constant ValueAssignedToClassConstantDynamicNames\Foo::BAR (int|string|null) is incompatible with value false.', - 7, + 12, + ], + [ + 'Configuration defined type for constant ValueAssignedToClassConstantDynamicNames\Foo::MAYBE_BAR (int<1, max>) is incompatible with value int.', + 14, ], ]); } diff --git a/tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant-dynamic-names.php b/tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant-dynamic-names.php index cb4d2e535a8..be63b30d9ae 100644 --- a/tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant-dynamic-names.php +++ b/tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant-dynamic-names.php @@ -2,8 +2,14 @@ namespace ValueAssignedToClassConstantDynamicNames; +class Bar +{ + const DYNAMIC = 42; +} + class Foo { const BAR = false; // error - configured as int|string|null const BAR2 = 1; // fine - not in dynamicConstantNames + const MAYBE_BAR = Bar::DYNAMIC; // error (maybe) - positive-int doesn't fully accept int } diff --git a/tests/PHPStan/Rules/Constants/value-assigned-dynamic-constant.neon b/tests/PHPStan/Rules/Constants/value-assigned-dynamic-constant.neon index 494fc6dd66f..852530e17ab 100644 --- a/tests/PHPStan/Rules/Constants/value-assigned-dynamic-constant.neon +++ b/tests/PHPStan/Rules/Constants/value-assigned-dynamic-constant.neon @@ -5,3 +5,5 @@ parameters: dynamicConstantNames: BAR_CONSTANT: 'int|string|null' ValueAssignedToClassConstantDynamicNames\Foo::BAR: 'int|string|null' + ValueAssignedToClassConstantDynamicNames\Bar::DYNAMIC: 'int' + ValueAssignedToClassConstantDynamicNames\Foo::MAYBE_BAR: 'positive-int' From 6cb0618023accdf4e72bba816019bc75447424eb Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 16 May 2026 08:48:06 +0000 Subject: [PATCH 10/12] Use 'does not accept value' wording in configured constant type errors Co-Authored-By: Claude Opus 4.6 --- src/Rules/Constants/ValueAssignedToClassConstantRule.php | 2 +- src/Rules/Constants/ValueAssignedToDefineRule.php | 2 +- src/Rules/Constants/ValueAssignedToGlobalConstantRule.php | 2 +- .../ValueAssignedToClassConstantWithDynamicNamesRuleTest.php | 4 ++-- .../PHPStan/Rules/Constants/ValueAssignedToDefineRuleTest.php | 4 ++-- .../Rules/Constants/ValueAssignedToGlobalConstantRuleTest.php | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Rules/Constants/ValueAssignedToClassConstantRule.php b/src/Rules/Constants/ValueAssignedToClassConstantRule.php index cbef28d6637..d7a75f98f26 100644 --- a/src/Rules/Constants/ValueAssignedToClassConstantRule.php +++ b/src/Rules/Constants/ValueAssignedToClassConstantRule.php @@ -82,7 +82,7 @@ private function processSingleConstant(ClassReflection $classReflection, string $verbosity = VerbosityLevel::getRecommendedLevelByType($configuredType, $valueExprType); return [ RuleErrorBuilder::message(sprintf( - 'Configuration defined type for constant %s::%s (%s) is incompatible with value %s.', + 'Configuration defined type for constant %s::%s (%s) does not accept value %s.', $constantReflection->getDeclaringClass()->getDisplayName(), $constantName, $configuredType->describe(VerbosityLevel::typeOnly()), diff --git a/src/Rules/Constants/ValueAssignedToDefineRule.php b/src/Rules/Constants/ValueAssignedToDefineRule.php index 0bc595da0e1..04c6f49f34d 100644 --- a/src/Rules/Constants/ValueAssignedToDefineRule.php +++ b/src/Rules/Constants/ValueAssignedToDefineRule.php @@ -70,7 +70,7 @@ public function processNode(Node $node, Scope $scope): array $verbosity = VerbosityLevel::getRecommendedLevelByType($configuredType, $valueType); $errors[] = RuleErrorBuilder::message(sprintf( - 'Configuration defined type for constant %s (%s) is incompatible with value %s.', + 'Configuration defined type for constant %s (%s) does not accept value %s.', $constantName, $configuredType->describe(VerbosityLevel::typeOnly()), $valueType->describe($verbosity), diff --git a/src/Rules/Constants/ValueAssignedToGlobalConstantRule.php b/src/Rules/Constants/ValueAssignedToGlobalConstantRule.php index 4af81a0e5c3..e9f17ab9b12 100644 --- a/src/Rules/Constants/ValueAssignedToGlobalConstantRule.php +++ b/src/Rules/Constants/ValueAssignedToGlobalConstantRule.php @@ -50,7 +50,7 @@ public function processNode(Node $node, Scope $scope): array $verbosity = VerbosityLevel::getRecommendedLevelByType($configuredType, $valueType); $errors[] = RuleErrorBuilder::message(sprintf( - 'Configuration defined type for constant %s (%s) is incompatible with value %s.', + 'Configuration defined type for constant %s (%s) does not accept value %s.', $constantName, $configuredType->describe(VerbosityLevel::typeOnly()), $valueType->describe($verbosity), diff --git a/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantWithDynamicNamesRuleTest.php b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantWithDynamicNamesRuleTest.php index d4a8b41f33a..dd74f58562d 100644 --- a/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantWithDynamicNamesRuleTest.php +++ b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantWithDynamicNamesRuleTest.php @@ -31,11 +31,11 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/value-assigned-to-class-constant-dynamic-names.php'], [ [ - 'Configuration defined type for constant ValueAssignedToClassConstantDynamicNames\Foo::BAR (int|string|null) is incompatible with value false.', + 'Configuration defined type for constant ValueAssignedToClassConstantDynamicNames\Foo::BAR (int|string|null) does not accept value false.', 12, ], [ - 'Configuration defined type for constant ValueAssignedToClassConstantDynamicNames\Foo::MAYBE_BAR (int<1, max>) is incompatible with value int.', + 'Configuration defined type for constant ValueAssignedToClassConstantDynamicNames\Foo::MAYBE_BAR (int<1, max>) does not accept value int.', 14, ], ]); diff --git a/tests/PHPStan/Rules/Constants/ValueAssignedToDefineRuleTest.php b/tests/PHPStan/Rules/Constants/ValueAssignedToDefineRuleTest.php index 4aaa03fd79f..37afefc7de3 100644 --- a/tests/PHPStan/Rules/Constants/ValueAssignedToDefineRuleTest.php +++ b/tests/PHPStan/Rules/Constants/ValueAssignedToDefineRuleTest.php @@ -30,11 +30,11 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/value-assigned-to-define.php'], [ [ - 'Configuration defined type for constant BAR_CONSTANT (int|string|null) is incompatible with value false.', + 'Configuration defined type for constant BAR_CONSTANT (int|string|null) does not accept value false.', 5, ], [ - 'Configuration defined type for constant BAR_CONSTANT (int|string|null) is incompatible with value int|false.', + 'Configuration defined type for constant BAR_CONSTANT (int|string|null) does not accept value int|false.', 6, ], ]); diff --git a/tests/PHPStan/Rules/Constants/ValueAssignedToGlobalConstantRuleTest.php b/tests/PHPStan/Rules/Constants/ValueAssignedToGlobalConstantRuleTest.php index 2bc97c93f60..3278703c23f 100644 --- a/tests/PHPStan/Rules/Constants/ValueAssignedToGlobalConstantRuleTest.php +++ b/tests/PHPStan/Rules/Constants/ValueAssignedToGlobalConstantRuleTest.php @@ -30,7 +30,7 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/value-assigned-to-global-constant.php'], [ [ - 'Configuration defined type for constant BAR_CONSTANT (int|string|null) is incompatible with value false.', + 'Configuration defined type for constant BAR_CONSTANT (int|string|null) does not accept value false.', 3, ], ]); From b6ad16593383e839bce613e3252abcf7bb009521 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 16 May 2026 09:12:45 +0000 Subject: [PATCH 11/12] Cache resolved configured constant types in ConstantResolver Co-Authored-By: Claude Opus 4.6 --- src/Analyser/ConstantResolver.php | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/Analyser/ConstantResolver.php b/src/Analyser/ConstantResolver.php index 5f4849d53bc..fab878f3e82 100644 --- a/src/Analyser/ConstantResolver.php +++ b/src/Analyser/ConstantResolver.php @@ -45,6 +45,9 @@ final class ConstantResolver /** @var array */ private array $currentlyResolving = []; + /** @var array */ + private array $configuredTypesCache = []; + /** * @param string[] $dynamicConstantNames * @param int|array{min: int, max: int}|null $phpVersion @@ -416,29 +419,43 @@ private function getMaxPhpVersion(): ?PhpVersion public function getConfiguredGlobalConstantType(string $constantName): ?Type { + if (array_key_exists($constantName, $this->configuredTypesCache)) { + return $this->configuredTypesCache[$constantName]; + } + + $result = null; if (array_key_exists($constantName, $this->dynamicConstantNames)) { $phpdocTypes = $this->dynamicConstantNames[$constantName]; if ($this->container !== null) { $typeStringResolver = $this->container->getByType(TypeStringResolver::class); - return $typeStringResolver->resolve($phpdocTypes, new NameScope(null, [], className: null)); + $result = $typeStringResolver->resolve($phpdocTypes, new NameScope(null, [], className: null)); } } - return null; + $this->configuredTypesCache[$constantName] = $result; + + return $result; } public function getConfiguredClassConstantType(string $className, string $constantName): ?Type { $lookupConstantName = sprintf('%s::%s', $className, $constantName); + if (array_key_exists($lookupConstantName, $this->configuredTypesCache)) { + return $this->configuredTypesCache[$lookupConstantName]; + } + + $result = null; if (array_key_exists($lookupConstantName, $this->dynamicConstantNames)) { $phpdocTypes = $this->dynamicConstantNames[$lookupConstantName]; if ($this->container !== null) { $typeStringResolver = $this->container->getByType(TypeStringResolver::class); - return $typeStringResolver->resolve($phpdocTypes, new NameScope(null, [], $className)); + $result = $typeStringResolver->resolve($phpdocTypes, new NameScope(null, [], $className)); } } - return null; + $this->configuredTypesCache[$lookupConstantName] = $result; + + return $result; } public function resolveConstantType(string $constantName, Type $constantType): Type From 12b7d5e34af0ebf3b072aa6b9f21a1c8f67797a3 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 16 May 2026 12:02:53 +0000 Subject: [PATCH 12/12] Add test for Maybe accepts path in ValueAssignedToGlobalConstantRule Co-Authored-By: Claude Opus 4.6 --- .../Constants/ValueAssignedToGlobalConstantRuleTest.php | 6 +++++- .../Constants/data/dynamic-int-constant-definition.php | 3 +++ .../Constants/data/value-assigned-to-global-constant.php | 1 + .../Rules/Constants/value-assigned-dynamic-constant.neon | 2 ++ 4 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Constants/data/dynamic-int-constant-definition.php diff --git a/tests/PHPStan/Rules/Constants/ValueAssignedToGlobalConstantRuleTest.php b/tests/PHPStan/Rules/Constants/ValueAssignedToGlobalConstantRuleTest.php index 3278703c23f..3c64355ec61 100644 --- a/tests/PHPStan/Rules/Constants/ValueAssignedToGlobalConstantRuleTest.php +++ b/tests/PHPStan/Rules/Constants/ValueAssignedToGlobalConstantRuleTest.php @@ -28,11 +28,15 @@ public static function getAdditionalConfigFiles(): array public function testRule(): void { - $this->analyse([__DIR__ . '/data/value-assigned-to-global-constant.php'], [ + $this->analyse([__DIR__ . '/data/dynamic-int-constant-definition.php', __DIR__ . '/data/value-assigned-to-global-constant.php'], [ [ 'Configuration defined type for constant BAR_CONSTANT (int|string|null) does not accept value false.', 3, ], + [ + 'Configuration defined type for constant MAYBE_CONSTANT (int<1, max>) does not accept value int.', + 5, + ], ]); } diff --git a/tests/PHPStan/Rules/Constants/data/dynamic-int-constant-definition.php b/tests/PHPStan/Rules/Constants/data/dynamic-int-constant-definition.php new file mode 100644 index 00000000000..406b0730d39 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/dynamic-int-constant-definition.php @@ -0,0 +1,3 @@ +