From 1bf9fe3cac45d85da264631a6e8c28b161a04f63 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 30 Nov 2025 10:43:17 +0100 Subject: [PATCH] Implement CastObjectHandler --- .../ExprHandler/CastObjectHandler.php | 57 ++++++++++++++++++ .../InitializerExprTypeResolver.php | 60 ++++++++++--------- .../PHPStan/Analyser/Generator/data/gnsr.php | 2 +- 3 files changed, 90 insertions(+), 29 deletions(-) create mode 100644 src/Analyser/Generator/ExprHandler/CastObjectHandler.php diff --git a/src/Analyser/Generator/ExprHandler/CastObjectHandler.php b/src/Analyser/Generator/ExprHandler/CastObjectHandler.php new file mode 100644 index 0000000000..1c3a62aa4f --- /dev/null +++ b/src/Analyser/Generator/ExprHandler/CastObjectHandler.php @@ -0,0 +1,57 @@ + + */ +#[AutowiredService] +final class CastObjectHandler implements ExprHandler +{ + + public function __construct(private InitializerExprTypeResolver $initializerExprTypeResolver) + { + } + + public function supports(Expr $expr): bool + { + return $expr instanceof Expr\Cast\Object_; + } + + public function analyseExpr( + Stmt $stmt, + Expr $expr, + GeneratorScope $scope, + ExpressionContext $context, + ?callable $alternativeNodeCallback, + ): Generator + { + $exprResult = yield new ExprAnalysisRequest($stmt, $expr->expr, $scope, $context->enterDeep(), $alternativeNodeCallback); + + return new ExprAnalysisResult( + $this->initializerExprTypeResolver->getCastObjectType($exprResult->type), + $this->initializerExprTypeResolver->getCastObjectType($exprResult->nativeType), + $scope, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + specifiedTruthyTypes: new SpecifiedTypes(), + specifiedFalseyTypes: new SpecifiedTypes(), + specifiedNullTypes: new SpecifiedTypes(), + ); + } + +} diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index f3284672cc..4a4fcdadae 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -702,43 +702,47 @@ public function getCastType(Expr\Cast $expr, callable $getTypeCallback): Type return $getTypeCallback($expr->expr)->toArray(); } if ($expr instanceof Object_) { - $castToObject = static function (Type $type): Type { - $constantArrays = $type->getConstantArrays(); - if (count($constantArrays) > 0) { - $objects = []; - foreach ($constantArrays as $constantArray) { - $properties = []; - $optionalProperties = []; - foreach ($constantArray->getKeyTypes() as $i => $keyType) { - $valueType = $constantArray->getValueTypes()[$i]; - $optional = $constantArray->isOptionalKey($i); - if ($optional) { - $optionalProperties[] = $keyType->getValue(); - } - $properties[$keyType->getValue()] = $valueType; - } + return $this->getCastObjectType($getTypeCallback($expr->expr)); + } - $objects[] = TypeCombinator::intersect(new ObjectShapeType($properties, $optionalProperties), new ObjectType(stdClass::class)); + return new MixedType(); + } + + public function getCastObjectType(Type $exprType): Type + { + $castToObject = static function (Type $type): Type { + $constantArrays = $type->getConstantArrays(); + if (count($constantArrays) > 0) { + $objects = []; + foreach ($constantArrays as $constantArray) { + $properties = []; + $optionalProperties = []; + foreach ($constantArray->getKeyTypes() as $i => $keyType) { + $valueType = $constantArray->getValueTypes()[$i]; + $optional = $constantArray->isOptionalKey($i); + if ($optional) { + $optionalProperties[] = $keyType->getValue(); + } + $properties[$keyType->getValue()] = $valueType; } - return TypeCombinator::union(...$objects); - } - if ($type->isObject()->yes()) { - return $type; + $objects[] = TypeCombinator::intersect(new ObjectShapeType($properties, $optionalProperties), new ObjectType(stdClass::class)); } - return new ObjectType('stdClass'); - }; - - $exprType = $getTypeCallback($expr->expr); - if ($exprType instanceof UnionType) { - return TypeCombinator::union(...array_map($castToObject, $exprType->getTypes())); + return TypeCombinator::union(...$objects); } + if ($type->isObject()->yes()) { + return $type; + } + + return new ObjectType('stdClass'); + }; - return $castToObject($exprType); + if ($exprType instanceof UnionType) { + return TypeCombinator::union(...array_map($castToObject, $exprType->getTypes())); } - return new MixedType(); + return $castToObject($exprType); } /** diff --git a/tests/PHPStan/Analyser/Generator/data/gnsr.php b/tests/PHPStan/Analyser/Generator/data/gnsr.php index 2d3cc50649..a142ab7bed 100644 --- a/tests/PHPStan/Analyser/Generator/data/gnsr.php +++ b/tests/PHPStan/Analyser/Generator/data/gnsr.php @@ -241,7 +241,7 @@ function doCast() { assertType('1', (int) $a); assertType("array{'1'}", (array) $a); - // assertType("array{'1'}", (object) $a); + assertType('stdClass', (object) $a); assertType('1.0', (double) $a); assertType('true', (bool) $a); assertType("'1'", (string) $a);