Widen empty ConstantArrayType to array when resolving dynamic constant types#5606
Open
phpstan-bot wants to merge 6 commits intophpstan:2.1.xfrom
Open
Widen empty ConstantArrayType to array when resolving dynamic constant types#5606phpstan-bot wants to merge 6 commits intophpstan:2.1.xfrom
ConstantArrayType to array when resolving dynamic constant types#5606phpstan-bot wants to merge 6 commits intophpstan:2.1.xfrom
Conversation
…stant types
- `ConstantArrayType::generalize(lessSpecific)` returns `$this` for empty
arrays (0 keys), so `resolveConstantType()` and `resolveClassConstantType()`
were returning `array{}` for dynamic constants defined as `[]`
- Add `generalizeDynamicConstantType()` helper in `ConstantResolver` that
falls back to `ArrayType(MixedType, MixedType)` when `generalize()` returns
a type that is still a constant value and is an array
- Fixes false positive "Offset X on array{} in isset() does not exist" for
dynamic array constants
- Covers both global constants (`define('FOO', [])`) and class constants
(`const FOO = []`) listed in `dynamicConstantNames`
Contributor
VincentLanglet
left a comment
There was a problem hiding this comment.
I don't think you really understood the issue.
Let's start with a (possibly failing test).
Use
dynamicConstantNames:
- FOO
- DYNAMICARRAY
with
define('FOO', true);
define('DYNAMICARRAY', []);
if (isset(DYNAMICARRAY['MyKey'])){
..
}
if (FOO) {
echo 'foo';
}
and ensure that there is no
- Offset 'MyKey' on array{} in isset() does not exist.
- If condition is always true
errors
…ynamic constants
Tests the actual user scenario: define('FOO', true) and define('DYNAMICARRAY', [])
with dynamicConstantNames should not report "Offset on array{} in isset() does not
exist" or "If condition is always true".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Collaborator
Author
|
Done. Added an integration test (
I verified the test fails without the fix (showing the "Offset 'MyKey' on array{} in isset() does not exist" error) and passes with the fix. All existing tests and |
The method operates on any constant value type, not specifically dynamic constants. The previous name was misleading about its scope. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Narrow the fallback check from any constant array value to specifically an empty ConstantArrayType using equals(). This is safer against future changes to ConstantArrayType::generalize() and documents the exact edge case being handled. Also restores the generalizeConstantType name since the method generalizes any constant type, not just dynamic ones. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The method is only called in contexts where the constant is in dynamicConstantNames, so the "Dynamic" qualifier is appropriate. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VincentLanglet
approved these changes
May 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When a constant is defined as an empty array (
define('FOO', [])orconst FOO = []) and listed indynamicConstantNames, PHPStan should treat its type asarray(non-constant). Instead, the type remainedarray{}, causing false positives like "Offset 'X' on array{} in isset() does not exist."Changes
generalizeDynamicConstantType()helper method insrc/Analyser/ConstantResolver.phpthat handles the case wheregeneralize(lessSpecific)fails to widen the type (returns a type still satisfyingisConstantValue()->yes())resolveConstantType()andresolveClassConstantType()to use the new helpertests/PHPStan/Analyser/data/dynamic-constant.phpandtests/PHPStan/Analyser/dynamic-constants.neonAnalogous cases probed
resolveClassConstantType()change. Test added.resolveClassConstantType()uses the PHPDoc type before reachinggeneralize(). Existing test confirms (DYNAMIC_EMPTY_ARRAY_WITH_PHPDOC_CONSTANT).generalize(lessSpecific)widenstrue→bool,42→int, etc. No fix needed.generalize()returnsnull(itself), butnullhas no wider type, so this is correct. Users needing a wider type should use the explicit type syntax (FOO: 'string|null').ArrayTypewith generalized key/value types. No fix needed.Root cause
ConstantArrayType::generalize(GeneralizePrecision::lessSpecific())has an early return for empty arrays (count($this->keyTypes) === 0 → return $this). This means an empty constant array "generalizes" to itself — still a constant value. WhenConstantResolver::resolveConstantType()callsgeneralize()to widen a dynamic constant's type, the empty array passes through unchanged.Rather than changing
generalize()globally (which would affect type descriptions in error messages, making unions likearray{}|array{a: int}describe asarrayinstead ofarray<string, int>), the fix is scoped toConstantResolver: after callinggeneralize(), if the result is still a constant array value, fall back toArrayType(MixedType, MixedType).Test
assertType('array', GLOBAL_DYNAMIC_EMPTY_ARRAY)andassertType('array', DynamicConstantClass::DYNAMIC_EMPTY_ARRAY_NO_PHPDOC)intests/PHPStan/Analyser/data/dynamic-constant.php. Both fail without the fix (actual type:array{}), pass with it.Fixes phpstan/phpstan#8526