From b0b3c9287e29bed3f18712fedea95f0eca74038d Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Sat, 17 Mar 2018 19:03:46 -0400 Subject: [PATCH] Fix array_key_exists calls on possibly undefined objectlike --- .../Statements/Expression/AssertionFinder.php | 34 +++++++++++++++++ src/Psalm/Type/Reconciler.php | 10 ++++- tests/ArrayAssignmentTest.php | 38 +++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Checker/Statements/Expression/AssertionFinder.php b/src/Psalm/Checker/Statements/Expression/AssertionFinder.php index dbf789f35..9bbd35738 100644 --- a/src/Psalm/Checker/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Checker/Statements/Expression/AssertionFinder.php @@ -875,6 +875,26 @@ class AssertionFinder if ($first_var_name) { $if_types[$first_var_name] = $prefix . 'callable'; } + } elseif (self::hasArrayKeyExistsCheck($expr)) { + $array_root = isset($expr->args[1]->value) + ? ExpressionChecker::getArrayVarId( + $expr->args[1]->value, + $this_class_name, + $source + ) + : null; + + if ($first_var_name === null && isset($expr->args[0]->value)) { + if ($expr->args[0]->value instanceof PhpParser\Node\Scalar\String_) { + $first_var_name = '"' . $expr->args[0]->value->value . '"'; + } elseif ($expr->args[0]->value instanceof PhpParser\Node\Scalar\LNumber) { + $first_var_name = (string) $expr->args[0]->value->value; + } + } + + if ($first_var_name !== null && $array_root) { + $if_types[$array_root . '[' . $first_var_name . ']'] = $prefix . 'array-key-exists'; + } } return $if_types; @@ -1263,4 +1283,18 @@ class AssertionFinder return false; } + + /** + * @param PhpParser\Node\Expr\FuncCall $stmt + * + * @return bool + */ + protected static function hasArrayKeyExistsCheck(PhpParser\Node\Expr\FuncCall $stmt) + { + if ($stmt->name instanceof PhpParser\Node\Name && $stmt->name->parts === ['array_key_exists']) { + return true; + } + + return false; + } } diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 51d4be0d5..4eb57cc20 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -176,7 +176,7 @@ class Reconciler return Type::getMixed(); } - if ($new_var_type === 'isset' || $new_var_type === '!empty') { + if ($new_var_type === 'isset' || $new_var_type === '!empty' || $new_var_type === 'array-key-exists') { return Type::getMixed(); } @@ -205,7 +205,7 @@ class Reconciler return $existing_var_type; } - if ($new_var_type === '!isset') { + if ($new_var_type === '!isset' || $new_var_type === '!array-key-exists') { return Type::getNull(); } @@ -489,6 +489,12 @@ class Reconciler return $existing_var_type; } + if ($new_var_type === 'array-key-exists') { + $existing_var_type->possibly_undefined = false; + + return $existing_var_type; + } + if ($new_var_type === '^!empty') { $existing_var_type->removeType('null'); $existing_var_type->removeType('false'); diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 4a9433179..1558c6e9e 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -850,6 +850,18 @@ class ArrayAssignmentTest extends TestCase echo $a[0]; }', ], + 'possiblyUndefinedArrayAccessWithArrayKeyExists' => [ + ' 1]; + } else { + $a = [2, 3]; + } + + if (array_key_exists(0, $a)) { + echo $a[0]; + }', + ], ]; } @@ -929,6 +941,32 @@ class ArrayAssignmentTest extends TestCase }', 'error_message' => 'InvalidPropertyAssignmentValue', ], + 'possiblyUndefinedArrayAccessWithArrayKeyExistsOnWrongKey' => [ + ' 1]; + } else { + $a = [2, 3]; + } + + if (array_key_exists("a", $a)) { + echo $a[0]; + }', + 'error_message' => 'PossiblyUndefinedGlobalVariable', + ], + 'possiblyUndefinedArrayAccessWithArrayKeyExistsOnMissingKey' => [ + ' 1]; + } else { + $a = [2, 3]; + } + + if (array_key_exists("b", $a)) { + echo $a[0]; + }', + 'error_message' => 'PossiblyUndefinedGlobalVariable', + ], ]; } }