Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Infer object shape when array or scalar is cast to object #7935

Merged
merged 2 commits into from May 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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])]);
theodorejb marked this conversation as resolved.
Show resolved Hide resolved
$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