diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 5d2a084ec0..0544ce76c6 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -58,15 +58,18 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\ErrorType; +use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; +use PHPStan\Type\OffsetAccessType; use PHPStan\Type\StaticTypeFactory; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use TypeError; +use function array_key_exists; use function array_key_last; use function array_merge; use function array_pop; @@ -439,7 +442,28 @@ public function processAssignVar( $offsetValueType = $varType; $offsetNativeValueType = $varNativeType; - [$valueToWrite, $additionalExpressions] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetTypes, $offsetValueType, $valueToWrite, $scope); + $dependentPreserved = false; + if ( + count($dimFetchStack) === 1 + && count($offsetTypes) === 1 + && $offsetTypes[0][0] !== null + && TypeUtils::containsTemplateType($offsetTypes[0][0]) + ) { + $rawValueType = $this->getRawExpressionType($scope, $assignedExpr); + if ( + $rawValueType instanceof OffsetAccessType + && $this->isSameTemplateOffset($rawValueType->getAccessedOffset(), $offsetTypes[0][0]) + && $rawValueType->getAccessedType()->isSuperTypeOf($offsetValueType)->yes() + ) { + $dependentPreserved = true; + $additionalExpressions = [[$dimFetchStack[0], $valueToWrite]]; + $valueToWrite = $offsetValueType; + } + } + + if (!$dependentPreserved) { + [$valueToWrite, $additionalExpressions] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetTypes, $offsetValueType, $valueToWrite, $scope); + } if (!$offsetValueType->equals($offsetNativeValueType) || !$valueToWrite->equals($nativeValueToWrite)) { [$nativeValueToWrite, $additionalNativeExpressions] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope); @@ -1258,4 +1282,27 @@ private function isSameVariable(Expr $a, Expr $b): bool return false; } + private function isSameTemplateOffset(Type $a, Type $b): bool + { + if ($a->equals($b)) { + return true; + } + + if ($a instanceof TemplateType && $b instanceof TemplateType) { + return $a->getScope()->equals($b->getScope()) && $a->getName() === $b->getName(); + } + + return false; + } + + private function getRawExpressionType(MutatingScope $scope, Expr $expr): ?Type + { + $key = $scope->getNodeKey($expr); + if (!array_key_exists($key, $scope->expressionTypes)) { + return null; + } + + return $scope->expressionTypes[$key]->getType(); + } + } diff --git a/src/Type/OffsetAccessType.php b/src/Type/OffsetAccessType.php index 5e4ef1aec3..8b0c011dbb 100644 --- a/src/Type/OffsetAccessType.php +++ b/src/Type/OffsetAccessType.php @@ -24,6 +24,16 @@ public function __construct( { } + public function getAccessedType(): Type + { + return $this->type; + } + + public function getAccessedOffset(): Type + { + return $this->offset; + } + public function getReferencedClasses(): array { return array_merge( diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 803d37fe8c..2cea597e93 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -824,9 +824,8 @@ public function testBug7215(): void public function testBug7094(): void { - // false positive $errors = $this->runAnalyse(__DIR__ . '/data/bug-7094.php'); - $this->assertCount(6, $errors); + $this->assertCount(5, $errors); $this->assertSame('Parameter #2 $val of method Bug7094\Foo::setAttribute() contains unresolvable type.', $errors[0]->getMessage()); $this->assertSame(74, $errors[0]->getLine()); @@ -838,9 +837,6 @@ public function testBug7094(): void $this->assertSame(78, $errors[3]->getLine()); $this->assertSame('Return type of call to method Bug7094\Foo::getAttribute() contains unresolvable type.', $errors[4]->getMessage()); $this->assertSame(79, $errors[4]->getLine()); - - $this->assertSame('Parameter #1 $attr of method Bug7094\Foo::setAttributes() expects array{foo?: string, bar?: 5|6|7, baz?: bool}, non-empty-array<\'bar\'|\'baz\'|\'foo\'|K of string, 5|6|7|bool|string> given.', $errors[5]->getMessage()); - $this->assertSame(29, $errors[5]->getLine()); } #[RequiresPhp('>= 8.0.0')] diff --git a/tests/PHPStan/Analyser/nsrt/bug-7380.php b/tests/PHPStan/Analyser/nsrt/bug-7380.php new file mode 100644 index 0000000000..31a8a84c48 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7380.php @@ -0,0 +1,64 @@ + + * @param K $key + * @param Attrs[K] $val + */ + public function setAttribute(string $key, mixed $val): void + { + $attr = $this->getAttributes(); + $attr[$key] = $val; + assertType('array{foo?: string, bar?: 5|6|7, baz?: bool}', $attr); + $this->setAttributes($attr); + } + + /** @return Attrs */ + public function getAttributes(): array + { + return []; + } + + /** @param Attrs $attr */ + public function setAttributes(array $attr): void + { + } +} + +/** + * @template T of array + */ +final class GenericBar { + + /** + * @template K of key-of + * @param K $key + * @param T[K] $val + */ + public function setAttribute(string $key, mixed $val): void + { + $attr = $this->getAttributes(); + $attr[$key] = $val; + $this->setAttributes($attr); + } + + /** @return T */ + public function getAttributes(): array + { + return []; + } + + /** @param T $attr */ + public function setAttributes(array $attr): void + { + } +}