Skip to content

Commit

Permalink
Merge pull request #7935 from theodorejb/improve-object-casts
Browse files Browse the repository at this point in the history
Infer object shape when array or scalar is cast to object
  • Loading branch information
orklah committed May 14, 2022
2 parents b37da4a + 4eef964 commit 3929d73
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 30 deletions.
57 changes: 42 additions & 15 deletions src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php
Expand Up @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
1 change: 0 additions & 1 deletion src/Psalm/Type/Atomic.php
Expand Up @@ -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;
Expand Down
7 changes: 0 additions & 7 deletions src/Psalm/Type/Atomic/TKeyedArray.php
Expand Up @@ -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;

Expand Down
5 changes: 0 additions & 5 deletions src/Psalm/Type/Atomic/TList.php
Expand Up @@ -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;
Expand Down
13 changes: 13 additions & 0 deletions tests/ArgTest.php
Expand Up @@ -702,6 +702,19 @@ function takesObject($_o): void {}
',
'error_message' => 'ArgumentTypeCoercion',
],
'objectRedundantCast' => [
'<?php
function makeObj(): object {
return (object)["a" => 42];
}
function takesObject(object $_o): void {}
takesObject((object)makeObj()); // expected: RedundantCast
',
'error_message' => 'RedundantCast',
],
'MissingMandatoryParamWithNamedParams' => [
'<?php
class User
Expand Down
8 changes: 6 additions & 2 deletions tests/ReturnTypeProvider/GetObjectVarsTest.php
Expand Up @@ -90,10 +90,14 @@ function f(object $p): array {
[],
];

yield 'propertiesOfCastScalar' => [
'<?php $ret = get_object_vars((object)true);',
['$ret' => 'array{scalar: true}'],
];

yield 'propertiesOfPOPO' => [
// todo: fix object cast so that it results in `object{a:1}` instead
'<?php $ret = get_object_vars((object)["a" => 1]);',
['$ret' => 'array<string, mixed>'],
['$ret' => 'array{a: int}'],
];
}
}
38 changes: 38 additions & 0 deletions tests/ReturnTypeTest.php
Expand Up @@ -855,6 +855,33 @@ function map(callable $predicate): callable {
'$res' => 'iterable<int, numeric-string>',
],
],
'infersObjectShapeOfCastScalar' => [
'<?php
function returnsInt(): int {
return 1;
}
$obj = (object)returnsInt();
',
'assertions' => [
'$obj' => 'object{scalar:int}',
],
],
'infersObjectShapeOfCastArray' => [
'<?php
/**
* @return array{a:1}
*/
function returnsArray(): array {
return ["a" => 1];
}
$obj = (object)returnsArray();
',
'assertions' => [
'$obj' => 'object{a:int}',
],
],
'mixedAssignmentWithUnderscore' => [
'<?php
$gen = (function (): Generator {
Expand Down Expand Up @@ -1542,6 +1569,17 @@ function f(): object {
',
'error_message' => 'LessSpecificReturnStatement',
],
'objectCastFromArrayWithMissingKey' => [
'<?php
/** @return object{status: string} */
function foo(): object {
return (object) [
"notstatus" => "failed",
];
}
',
'error_message' => 'InvalidReturnStatement',
],
'lessSpecificImplementedReturnTypeFromTemplatedTraitMethod' => [
'<?php
/** @template T */
Expand Down

0 comments on commit 3929d73

Please sign in to comment.