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:
parent
6f3ceea7d0
commit
4eef964048
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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}'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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 */
|
||||
|
Loading…
Reference in New Issue
Block a user