1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 04:45:20 +01:00

Infer object shape when array or scalar is cast to object

Also detect redundant object casts.

Fixes #7916, fixes #7934
This commit is contained in:
Theodore Brown 2022-05-08 23:40:04 -05:00
parent 6f3ceea7d0
commit 4eef964048
4 changed files with 99 additions and 17 deletions

View File

@ -183,22 +183,42 @@ class CastAnalyzer
}
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 @@ class CastAnalyzer
}
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 @@ class CastAnalyzer
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,

View File

@ -702,6 +702,19 @@ class ArgTest extends TestCase
',
'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

View File

@ -90,10 +90,14 @@ class GetObjectVarsTest extends TestCase
[],
];
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}'],
];
}
}

View File

@ -855,6 +855,33 @@ class ReturnTypeTest extends TestCase
'$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 {
@ -1542,6 +1569,17 @@ class ReturnTypeTest extends TestCase
',
'error_message' => 'LessSpecificReturnStatement',
],
'objectCastFromArrayWithMissingKey' => [
'<?php
/** @return object{status: string} */
function foo(): object {
return (object) [
"notstatus" => "failed",
];
}
',
'error_message' => 'InvalidReturnStatement',
],
'lessSpecificImplementedReturnTypeFromTemplatedTraitMethod' => [
'<?php
/** @template T */