diff --git a/src/Psalm/Internal/Type/Comparator/KeyedArrayComparator.php b/src/Psalm/Internal/Type/Comparator/KeyedArrayComparator.php index 773237ad3..c9d7aad58 100644 --- a/src/Psalm/Internal/Type/Comparator/KeyedArrayComparator.php +++ b/src/Psalm/Internal/Type/Comparator/KeyedArrayComparator.php @@ -11,8 +11,8 @@ use Psalm\Type\Atomic\TGenericObject; use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TObjectWithProperties; - use Psalm\Type\Union; + use function array_keys; use function is_string; diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 8442696e6..96c960031 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -80,6 +80,134 @@ class FunctionCallTest extends TestCase '$iterable===' => 'iterable', ], ], + 'inferTypeFromAnonymousObjectWithTemplatedProperty' => [ + 'code' => 'value; + } + /** + * @template T + * @param object{value: object{value: T}} $object + * @return T + */ + function getNestedValue(object $object): mixed + { + return $object->value->value; + } + $object = new Value(new Value(42)); + $value = getValue($object); + $nestedValue = getNestedValue($object);', + 'assertions' => [ + '$value===' => 'Value<42>', + '$nestedValue===' => '42', + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'inferTypeFromAnonymousObjectWithTemplatedPropertyFromTemplatedAncestor' => [ + 'code' => ' + */ + final class ConcreteValue extends AbstractValue + { + /** + * @param TValue $value + */ + public function __construct(mixed $value) + { + parent::__construct($value); + } + } + /** + * @template T + * @param object{value: T} $object + * @return T + */ + function getValue(object $object): mixed + { + return $object->value; + } + /** + * @template T + * @param object{value: object{value: T}} $object + * @return T + */ + function getNestedValue(object $object): mixed + { + return $object->value->value; + } + $object = new ConcreteValue(new ConcreteValue(42)); + $value = getValue($object); + $nestedValue = getNestedValue($object);', + 'assertions' => [ + '$value===' => 'ConcreteValue<42>', + '$nestedValue===' => '42', + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'inferTypeFromAnonymousObjectWithTemplatedPropertyFromConcreteAncestor' => [ + 'code' => ' */ + final class IntValue extends AbstractValue {} + final class Nested + { + public function __construct(public readonly IntValue $value) {} + } + /** + * @template T + * @param object{value: T} $object + * @return T + */ + function getValue(object $object): mixed + { + return $object->value; + } + /** + * @template T + * @param object{value: object{value: T}} $object + * @return T + */ + function getNestedValue(object $object): mixed + { + return $object->value->value; + } + $object = new Nested(new IntValue(42)); + $value = getValue($object); + $nestedValue = getNestedValue($object);', + 'assertions' => [ + '$value===' => 'IntValue', + '$nestedValue===' => 'int', + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], 'countShapedArrays' => [ 'code' => '