Skip to content
Permalink
Browse files

Fix #1830 - infer key type after array_key_exists check

  • Loading branch information
muglug committed Nov 10, 2019
1 parent 065653c commit 2fc7f5fdf737d15c842e853f78b921f6915bc4c4
@@ -53,6 +53,7 @@
use function strpos;
use function substr;
use Psalm\Issue\InvalidDocblock;
use Doctrine\Instantiator\Exception\UnexpectedValueException;
class AssertionReconciler extends \Psalm\Type\Reconciler
{
@@ -191,6 +192,14 @@ public static function reconcile(
return $existing_var_type;
}
if (substr($assertion, 0, 9) === 'in-array-') {
return self::reconcileInArray(
$codebase,
$existing_var_type,
substr($assertion, 9)
);
}
if ($assertion === 'falsy' || $assertion === 'empty') {
return self::reconcileFalsyOrEmpty(
$assertion,
@@ -1371,6 +1380,44 @@ private static function reconcileIterable(
return Type::getMixed();
}
/**
* @param string[] $suppressed_issues
* @param 0|1|2 $failed_reconciliation
*/
private static function reconcileInArray(
Codebase $codebase,
Union $existing_var_type,
string $assertion
) : Union {
if (strpos($assertion, '::')) {
list($fq_classlike_name, $const_name) = explode('::', $assertion);
$class_constant_type = $codebase->classlikes->getConstantForClass(
$fq_classlike_name,
$const_name,
\ReflectionProperty::IS_PRIVATE
);
if ($class_constant_type) {
foreach ($class_constant_type->getTypes() as $const_type_atomic) {
if ($const_type_atomic instanceof Type\Atomic\ObjectLike
|| $const_type_atomic instanceof Type\Atomic\TArray
) {
if ($const_type_atomic instanceof Type\Atomic\ObjectLike) {
$const_type_atomic = $const_type_atomic->getGenericArrayType();
}
return clone $const_type_atomic->type_params[0];
}
}
}
}
$existing_var_type->removeType('null');
return $existing_var_type;
}
/**
* @param string[] $suppressed_issues
* @param 0|1|2 $failed_reconciliation
@@ -91,6 +91,8 @@ public static function reconcile(
return Type::getNull();
} elseif ($assertion === 'array-key-exists') {
return Type::getEmpty();
} elseif (substr($assertion, 0, 9) === 'in-array-') {
return $existing_var_type;
}
}
@@ -84,76 +84,89 @@ public static function reconcileKeyedTypes(
$suppressed_issues = $statements_analyzer->getSuppressedIssues();
foreach ($new_types as $nk => $type) {
if ((strpos($nk, '[') || strpos($nk, '->'))
&& ($type[0][0] === '=isset'
if (strpos($nk, '[') || strpos($nk, '->')) {
if ($type[0][0] === '=isset'
|| $type[0][0] === '!=empty'
|| $type[0][0] === 'isset'
|| $type[0][0] === '!empty')
) {
$isset_or_empty = $type[0][0] === 'isset' || $type[0][0] === '=isset'
? '=isset'
: '!=empty';
|| $type[0][0] === '!empty'
) {
$isset_or_empty = $type[0][0] === 'isset' || $type[0][0] === '=isset'
? '=isset'
: '!=empty';
$key_parts = Reconciler::breakUpPathIntoParts($nk);
$key_parts = Reconciler::breakUpPathIntoParts($nk);
if (!$key_parts) {
throw new \UnexpectedValueException('There should be some key parts');
}
if (!$key_parts) {
throw new \UnexpectedValueException('There should be some key parts');
}
$base_key = array_shift($key_parts);
$base_key = array_shift($key_parts);
if (!isset($existing_types[$base_key]) || $existing_types[$base_key]->isNullable()) {
if (!isset($new_types[$base_key])) {
$new_types[$base_key] = [['=isset']];
} else {
$new_types[$base_key][] = ['=isset'];
if (!isset($existing_types[$base_key]) || $existing_types[$base_key]->isNullable()) {
if (!isset($new_types[$base_key])) {
$new_types[$base_key] = [['=isset']];
} else {
$new_types[$base_key][] = ['=isset'];
}
}
}
while ($key_parts) {
$divider = array_shift($key_parts);
while ($key_parts) {
$divider = array_shift($key_parts);
if ($divider === '[') {
$array_key = array_shift($key_parts);
array_shift($key_parts);
if ($divider === '[') {
$array_key = array_shift($key_parts);
array_shift($key_parts);
$new_base_key = $base_key . '[' . $array_key . ']';
$new_base_key = $base_key . '[' . $array_key . ']';
if (strpos($array_key, '\'') !== false) {
$new_types[$base_key][] = ['=string-array-access'];
} else {
$new_types[$base_key][] = ['=int-or-string-array-access'];
if (strpos($array_key, '\'') !== false) {
$new_types[$base_key][] = ['=string-array-access'];
} else {
$new_types[$base_key][] = ['=int-or-string-array-access'];
}
$base_key = $new_base_key;
continue;
}
$base_key = $new_base_key;
if ($divider === '->') {
$property_name = array_shift($key_parts);
$new_base_key = $base_key . '->' . $property_name;
continue;
}
$base_key = $new_base_key;
} else {
throw new \InvalidArgumentException('Unexpected divider ' . $divider);
}
if ($divider === '->') {
$property_name = array_shift($key_parts);
$new_base_key = $base_key . '->' . $property_name;
if (!$key_parts) {
break;
}
$base_key = $new_base_key;
} else {
throw new \InvalidArgumentException('Unexpected divider ' . $divider);
if (!isset($new_types[$base_key])) {
$new_types[$base_key] = [['!~bool'], ['!~int'], ['=isset']];
} else {
$new_types[$base_key][] = ['!~bool'];
$new_types[$base_key][] = ['!~int'];
$new_types[$base_key][] = ['=isset'];
}
}
if (!$key_parts) {
break;
}
// replace with a less specific check
$new_types[$nk][0][0] = $isset_or_empty;
}
if (!isset($new_types[$base_key])) {
$new_types[$base_key] = [['!~bool'], ['!~int'], ['=isset']];
} else {
$new_types[$base_key][] = ['!~bool'];
$new_types[$base_key][] = ['!~int'];
$new_types[$base_key][] = ['=isset'];
if ($type[0][0] === 'array-key-exists') {
$key_parts = Reconciler::breakUpPathIntoParts($nk);
if (count($key_parts) === 4 && $key_parts[1] === '[') {
if (isset($new_types[$key_parts[2]])) {
$new_types[$key_parts[2]][] = ['=in-array-' . $key_parts[0]];
} else {
$new_types[$key_parts[2]] = [['=in-array-' . $key_parts[0]]];
}
}
}
// replace with a less specific check
$new_types[$nk][0][0] = $isset_or_empty;
}
}
@@ -379,6 +379,34 @@ public function foo() : stdClass {
}
}'
],
'assertArrayKeyExistsRefinesType' => [
'<?php
class Foo {
/** @var array<int,string> */
public const DAYS = [
1 => "mon",
2 => "tue",
3 => "wed",
4 => "thu",
5 => "fri",
6 => "sat",
7 => "sun",
];
/** @param key-of<self::DAYS> $dayNum*/
private static function doGetDayName(int $dayNum): string {
return self::DAYS[$dayNum];
}
/** @throws LogicException */
public static function getDayName(int $dayNum): string {
if (! array_key_exists($dayNum, self::DAYS)) {
throw new \LogicException();
}
return self::doGetDayName($dayNum);
}
}'
],
];
}
}

0 comments on commit 2fc7f5f

Please sign in to comment.
You can’t perform that action at this time.