Skip to content

Fix phpstan/phpstan#8031: Union of constant string type lost when used with generics.#5167

Open
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-k3o610u
Open

Fix phpstan/phpstan#8031: Union of constant string type lost when used with generics.#5167
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-k3o610u

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

When constructing a generic class with an array containing constant string keys (e.g., new Collection(['one' => 1, 'two' => 2])), PHPStan was generalizing the key type from 'one'|'two' to string, losing the constant type information. This fix preserves constant string types during template type inference.

Changes

  • Modified src/Type/Generic/TemplateTypeHelper.phpgeneralizeInferredTemplateType(): changed the first condition from $type->isScalar()->yes() to $type->isInteger()->yes() so that only integer types are generalized when the template bound is array-key. Removed the || $isArrayKey clause from the second condition so constant string values with scalar bounds are preserved.
  • Updated tests/PHPStan/Analyser/nsrt/bug-13144.php — adjusted expected type from ArrayObject<string, int> to ArrayObject<'a'|'b', int> to reflect the more precise inference.
  • Updated tests/PHPStan/Analyser/nsrt/native-reflection-default-values.php — adjusted expected type from ArrayObject<string, int> to ArrayObject<'key', int>.
  • Added tests/PHPStan/Analyser/nsrt/bug-8031.php — regression test.

Root cause

In TemplateTypeHelper::generalizeInferredTemplateType(), the condition $type->isScalar()->yes() && $isArrayKey was too broad — it matched all scalar types (including unions of constant strings) when the template bound was array-key, causing them to be generalized via GeneralizePrecision::templateArgument() which turns constant strings into plain string. The fix narrows this condition to only generalize integer types with array-key bounds (since integer keys typically come from sequential array indices and should be generalized to int), while preserving constant string keys which are always explicitly provided.

Test

Added tests/PHPStan/Analyser/nsrt/bug-8031.php which creates a generic Collection<TKey, TValue> with ['one' => 1, 'two' => 2] and asserts the inferred type is Collection<'one'|'two', int> rather than Collection<string, int>.

Fixes phpstan/phpstan#8031

- Changed TemplateTypeHelper::generalizeInferredTemplateType() to only
  generalize integer types (not strings) when template bound is array-key
- Previously, all scalar constant values were generalized when the bound
  was array-key, turning 'one'|'two' into string
- Updated test expectations in bug-13144.php and
  native-reflection-default-values.php to reflect the more precise types
- New regression test in tests/PHPStan/Analyser/nsrt/bug-8031.php

Closes phpstan/phpstan#8031
if (!$templateType->getVariance()->covariant()) {
$isArrayKey = $templateType->getBound()->describe(VerbosityLevel::precise()) === '(int|string)';
if ($type->isScalar()->yes() && $isArrayKey) {
if ($type->isInteger()->yes() && $isArrayKey) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont see a reason to have a different behavior for int and string


$arr = new ArrayObject(['a' => 1, 'b' => 2]);

assertType('ArrayObject<string, int>', $arr); // correctly inferred as `ArrayObject<string, int>`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dunno if this will create issues...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure either.

related issue was comment to be blocked on bidrectional narrowing:
phpstan/phpstan#8031 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants