diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 35400570ab9..021b466ecf3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -183,22 +183,42 @@ public static function analyze( } if ($stmt instanceof PhpParser\Node\Expr\Cast\Object_) { - $was_inside_general_use = $context->inside_general_use; - $context->inside_general_use = true; - if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context) === false) { - $context->inside_general_use = $was_inside_general_use; - + if (!self::checkExprGeneralUse($statements_analyzer, $stmt, $context)) { return false; } - $context->inside_general_use = $was_inside_general_use; - $type = new Union([new TNamedObject('stdClass')]); + $permissible_atomic_types = []; + $all_permissible = false; - $maybe_type = $statements_analyzer->node_data->getType($stmt->expr); + if ($stmt_expr_type = $statements_analyzer->node_data->getType($stmt->expr)) { + if ($stmt_expr_type->isObjectType()) { + self::handleRedundantCast($stmt_expr_type, $statements_analyzer, $stmt); + } + + $all_permissible = true; + + foreach ($stmt_expr_type->getAtomicTypes() as $type) { + if ($type instanceof Scalar) { + $objWithProps = new TObjectWithProperties(['scalar' => new Union([$type])]); + $permissible_atomic_types[] = $objWithProps; + } elseif ($type instanceof TKeyedArray) { + $permissible_atomic_types[] = new TObjectWithProperties($type->properties); + } else { + $all_permissible = false; + break; + } + } + } + + if ($permissible_atomic_types && $all_permissible) { + $type = TypeCombiner::combine($permissible_atomic_types); + } else { + $type = Type::getObject(); + } if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph ) { - $type->parent_nodes = $maybe_type->parent_nodes ?? []; + $type->parent_nodes = $stmt_expr_type->parent_nodes ?? []; } $statements_analyzer->node_data->setType($stmt, $type); @@ -207,14 +227,9 @@ public static function analyze( } if ($stmt instanceof PhpParser\Node\Expr\Cast\Array_) { - $was_inside_general_use = $context->inside_general_use; - $context->inside_general_use = true; - if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context) === false) { - $context->inside_general_use = $was_inside_general_use; - + if (!self::checkExprGeneralUse($statements_analyzer, $stmt, $context)) { return false; } - $context->inside_general_use = $was_inside_general_use; $permissible_atomic_types = []; $all_permissible = false; @@ -457,6 +472,18 @@ public static function castStringAttempt( return $str_type; } + private static function checkExprGeneralUse( + StatementsAnalyzer $statements_analyzer, + PhpParser\Node\Expr\Cast $stmt, + Context $context + ): bool { + $was_inside_general_use = $context->inside_general_use; + $context->inside_general_use = true; + $retVal = ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context); + $context->inside_general_use = $was_inside_general_use; + return $retVal; + } + private static function handleRedundantCast( Union $maybe_type, StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 91378836e53..47d8e573ade 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -9,7 +9,6 @@ use Psalm\Internal\Type\TypeAlias; use Psalm\Internal\Type\TypeAlias\LinkableTypeAlias; use Psalm\Type; -use Psalm\Type\Atomic; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TArrayKey; use Psalm\Type\Atomic\TAssertionFalsy; diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index c37c07d5fd0..15b0de859c4 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -10,13 +10,6 @@ use Psalm\Internal\Type\TypeCombiner; use Psalm\Type; use Psalm\Type\Atomic; -use Psalm\Type\Atomic\TArray; -use Psalm\Type\Atomic\TKeyedArray; -use Psalm\Type\Atomic\TLiteralClassString; -use Psalm\Type\Atomic\TLiteralInt; -use Psalm\Type\Atomic\TLiteralString; -use Psalm\Type\Atomic\TNonEmptyArray; -use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Union; use UnexpectedValueException; diff --git a/src/Psalm/Type/Atomic/TList.php b/src/Psalm/Type/Atomic/TList.php index 3d5136530d4..e6a7a478174 100644 --- a/src/Psalm/Type/Atomic/TList.php +++ b/src/Psalm/Type/Atomic/TList.php @@ -9,11 +9,6 @@ use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Type; use Psalm\Type\Atomic; -use Psalm\Type\Atomic\TArray; -use Psalm\Type\Atomic\TGenericObject; -use Psalm\Type\Atomic\TIterable; -use Psalm\Type\Atomic\TKeyedArray; -use Psalm\Type\Atomic\TList; use Psalm\Type\Union; use function get_class; diff --git a/tests/ArgTest.php b/tests/ArgTest.php index 4354f308f2b..de57435f0e9 100644 --- a/tests/ArgTest.php +++ b/tests/ArgTest.php @@ -702,6 +702,19 @@ function takesObject($_o): void {} ', 'error_message' => 'ArgumentTypeCoercion', ], + 'objectRedundantCast' => [ + ' 42]; + } + + function takesObject(object $_o): void {} + + takesObject((object)makeObj()); // expected: RedundantCast + ', + 'error_message' => 'RedundantCast', + ], 'MissingMandatoryParamWithNamedParams' => [ ' [ + ' 'array{scalar: true}'], + ]; + yield 'propertiesOfPOPO' => [ - // todo: fix object cast so that it results in `object{a:1}` instead ' 1]);', - ['$ret' => 'array'], + ['$ret' => 'array{a: int}'], ]; } } diff --git a/tests/ReturnTypeTest.php b/tests/ReturnTypeTest.php index 53be10b286e..492044b7436 100644 --- a/tests/ReturnTypeTest.php +++ b/tests/ReturnTypeTest.php @@ -855,6 +855,33 @@ function map(callable $predicate): callable { '$res' => 'iterable', ], ], + 'infersObjectShapeOfCastScalar' => [ + ' [ + '$obj' => 'object{scalar:int}', + ], + ], + 'infersObjectShapeOfCastArray' => [ + ' 1]; + } + + $obj = (object)returnsArray(); + ', + 'assertions' => [ + '$obj' => 'object{a:int}', + ], + ], 'mixedAssignmentWithUnderscore' => [ ' 'LessSpecificReturnStatement', ], + 'objectCastFromArrayWithMissingKey' => [ + ' "failed", + ]; + } + ', + 'error_message' => 'InvalidReturnStatement', + ], 'lessSpecificImplementedReturnTypeFromTemplatedTraitMethod' => [ '