Skip to content

Fix phpstan/phpstan#11507: Type mismatch when explicitly setting an array key#5317

Open
phpstan-bot wants to merge 2 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-t74y1cu
Open

Fix phpstan/phpstan#11507: Type mismatch when explicitly setting an array key#5317
phpstan-bot wants to merge 2 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-t74y1cu

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When mapping over a Collection<non-empty-array<string>> and setting an explicit array key inside the callback ($item['foo'] = '100'), PHPStan incorrectly reported a type mismatch error: "should return Collection<non-empty-array> but returns Collection<non-empty-array>". The error showed identical types but still failed the invariance check.

Changes

  • Modified src/Type/Generic/TemplateTypeVariance.php: Added a fallback in isValidVariance() for invariant templates. When equals() fails, it strips HasOffsetValueType and HasOffsetType accessory types from both sides and re-checks equality. This ensures types that differ only in offset tracking information are treated as equal for invariance purposes.
  • Added tests/PHPStan/Rules/Functions/data/bug-11507.php: Regression test with a Collection class using an invariant template parameter and a map() callback that sets an explicit array key.
  • Added test method testBug11507 in tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php.

Root cause

When $item['foo'] = '100' is executed on a non-empty-array<string, string>, ArrayType::setOffsetValueType() adds a HasOffsetValueType('foo', '100') to the type's intersection. This makes the type non-empty-array<string, string>&hasOffsetValue('foo', '100') which is structurally different from non-empty-array<string, string> even though both describe as non-empty-array<string> in error messages (AccessoryType instances are hidden in normal descriptions).

The invariance check in TemplateTypeVariance::isValidVariance() used $a->equals($b) which performs strict structural comparison. The extra HasOffsetValueType caused this to return false, triggering a false positive error about template covariance.

The fix uses TypeTraverser to replace HasOffsetValueType and HasOffsetType with MixedType (which gets absorbed in intersections) before comparing, so types that only differ in offset tracking are considered equal for invariance.

Test

Added a regression test (testBug11507) that creates a Collection class with an invariant TValue template parameter and a map() method. The test verifies that mapping over Collection<non-empty-array<string>> with a callback that sets $item['foo'] = '100' and returns the item does not produce a false positive return type error.

Fixes phpstan/phpstan#11507

… HasOffsetValueType

- When setting an explicit array key on a generic array type (e.g. $item['foo'] = '100'),
  PHPStan adds HasOffsetValueType to the intersection, making the type structurally
  unequal to the base array type even though they describe the same type to the user
- The invariance check in TemplateTypeVariance::isValidVariance used strict equals(),
  which failed due to the extra HasOffsetValueType accessory type
- Added a fallback that strips HasOffsetValueType/HasOffsetType before comparing,
  so types differing only in offset tracking are treated as equal for invariance
- New regression test in tests/PHPStan/Rules/Functions/data/bug-11507.php

if ($this->invariant()) {
$result = $a->equals($b);
if (!$result) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Adjust the corresponding equals-method implementation instead

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

And make phpstan also confirmed passing (exit code 0). Both verification runs for the final solution completed successfully. The commit has already been pushed to the PR branch.

…redTemplateType

Instead of working around the equals check in TemplateTypeVariance::isValidVariance(),
strip HasOffsetValueType and HasOffsetType during template type generalization in
TemplateTypeHelper::generalizeInferredTemplateType(). This normalizes inferred
template types before they reach the invariance check, so equals() naturally
returns true without needing special handling.

Changing IntersectionType::equals() globally was investigated but breaks scope
tracking and type narrowing (introduces false positives in PHPStan self-analysis).
The generalization approach is more targeted: it only affects inferred template
type arguments for non-covariant templates, which is the correct normalization
point for this precision.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

2 participants