From ed82443bc7c8831c2d41bc228aaf4120dde571e6 Mon Sep 17 00:00:00 2001 From: Theodore Brown Date: Sun, 8 May 2022 23:40:04 -0500 Subject: [PATCH] Infer object shape when array or scalar is cast to object Also detect redundant object casts. Fixes #7916, fixes #7934 --- .../Statements/Expression/CastAnalyzer.php | 57 ++++++++++++++----- tests/ArgTest.php | 13 +++++ .../ReturnTypeProvider/GetObjectVarsTest.php | 8 ++- tests/ReturnTypeTest.php | 38 +++++++++++++ 4 files changed, 101 insertions(+), 15 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 35400570ab9..9ffea8807c3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -183,22 +183,44 @@ 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; + $result = self::checkExprGeneralUse($statements_analyzer, $stmt, $context); + if (!$result) { 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 +229,11 @@ 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; + $result = self::checkExprGeneralUse($statements_analyzer, $stmt, $context); + if (!$result) { return false; } - $context->inside_general_use = $was_inside_general_use; $permissible_atomic_types = []; $all_permissible = false; @@ -457,6 +476,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/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' => [ '