1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-21 21:31:13 +01:00

Fix #713 - support offsets of known array types

This commit is contained in:
Matthew Brown 2018-05-05 17:30:18 -04:00
parent 0181fce46f
commit 21261172a8
20 changed files with 273 additions and 135 deletions

View File

@ -96,7 +96,7 @@ class FunctionChecker extends FunctionLikeChecker
&& $atomic_types['array']->sealed
) {
return new Type\Union([
new Type\Atomic\TInt([count($atomic_types['array']->properties) => true])
new Type\Atomic\TLiteralInt([count($atomic_types['array']->properties) => true])
]);
}
}

View File

@ -60,7 +60,7 @@ class ArrayChecker
if ($item->key instanceof PhpParser\Node\Scalar\String_
&& preg_match('/^(0|[1-9][0-9]*)$/', $item->key->value)
) {
$key_type = Type::getInt(false, [$item->key->value => true]);
$key_type = Type::getInt(false, [(int)$item->key->value => true]);
}
if ($item_key_type) {
@ -154,7 +154,7 @@ class ArrayChecker
$item_value_type ?: Type::getMixed(),
]);
$array_type->count = new Type\Atomic\TInt([count($stmt->items) => true]);
$array_type->count = new Type\Atomic\TLiteralInt([count($stmt->items) => true]);
$stmt->inferredType = new Type\Union([
$array_type,

View File

@ -323,7 +323,7 @@ class ArrayAssignmentChecker
} elseif ($atomic_root_types['array'] instanceof ObjectLike
&& $atomic_root_types['array']->sealed
) {
$array_atomic_type->count = new Type\Atomic\TInt([
$array_atomic_type->count = new Type\Atomic\TLiteralInt([
count($atomic_root_types['array']->properties) => true
]);
$from_countable_object_like = true;
@ -350,7 +350,7 @@ class ArrayAssignmentChecker
$new_counts = [];
foreach ($atomic_root_types['array']->count->values as $count => $_) {
$new_counts[(string)((int)$count + 1)] = true;
$new_counts[((int)$count + 1)] = true;
}
$atomic_root_types['array']->count->values = $new_counts;

View File

@ -169,10 +169,16 @@ class MethodCallChecker extends \Psalm\Checker\Statements\Expression\CallChecker
break;
case Type\Atomic\TInt::class:
case Type\Atomic\TLiteralInt::class:
case Type\Atomic\TFloat::class:
case Type\Atomic\TLiteralFloat::class:
case Type\Atomic\TBool::class:
case Type\Atomic\TTrue::class:
case Type\Atomic\TArray::class:
case Type\Atomic\TArray::class:
case Type\Atomic\ObjectLike::class:
case Type\Atomic\TString::class:
case Type\Atomic\TLiteralString::class:
case Type\Atomic\TNumericString::class:
case Type\Atomic\TClassString::class:
$invalid_method_call_types[] = (string)$class_type_part;

View File

@ -29,7 +29,12 @@ use Psalm\Type;
use Psalm\Type\Atomic\ObjectLike;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TEmpty;
use Psalm\Type\Atomic\TLiteralFloat;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TFloat;
use Psalm\Type\Atomic\TGenericParam;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNull;
@ -194,6 +199,26 @@ class ArrayFetchChecker
|| $stmt->dim instanceof PhpParser\Node\Scalar\LNumber
) {
$key_value = $stmt->dim->value;
} elseif (isset($stmt->dim->inferredType)) {
foreach ($stmt->dim->inferredType->getTypes() as $possible_value_type) {
if ($possible_value_type instanceof TLiteralString
|| $possible_value_type instanceof TLiteralFloat
|| $possible_value_type instanceof TLiteralInt
) {
if (!$key_value && count($possible_value_type->values) === 1) {
$key_value = array_keys($possible_value_type->values)[0];
} else {
$key_value = null;
break;
}
} elseif ($possible_value_type instanceof TString
|| $possible_value_type instanceof TFloat
|| $possible_value_type instanceof TInt
) {
$key_value = null;
break;
}
}
}
$array_access_type = null;
@ -305,7 +330,7 @@ class ArrayFetchChecker
true,
$offset_type->ignore_falsable_issues
)) {
$expected_offset_types[] = (string)$type->type_params[0];
$expected_offset_types[] = $type->type_params[0]->getId();
} else {
$has_valid_offset = true;
}
@ -315,7 +340,7 @@ class ArrayFetchChecker
$new_counts = [];
foreach ($type->count->values as $count => $_) {
$new_counts[(string)((int)$count + 1)] = true;
$new_counts[(int)$count + 1] = true;
}
$type->count->values = $new_counts;
@ -388,18 +413,20 @@ class ArrayFetchChecker
);
}
} else {
$object_like_keys = array_keys($type->properties);
if (!$inside_isset || $type->sealed) {
$object_like_keys = array_keys($type->properties);
if (count($object_like_keys) === 1) {
$expected_keys_string = '\'' . $object_like_keys[0] . '\'';
} else {
$last_key = array_pop($object_like_keys);
$expected_keys_string = '\'' . implode('\', \'', $object_like_keys) .
'\' or \'' . $last_key . '\'';
if (count($object_like_keys) === 1) {
$expected_keys_string = '\'' . $object_like_keys[0] . '\'';
} else {
$last_key = array_pop($object_like_keys);
$expected_keys_string = '\'' . implode('\', \'', $object_like_keys) .
'\' or \'' . $last_key . '\'';
}
$expected_offset_types[] = $expected_keys_string;
}
$expected_offset_types[] = $expected_keys_string;
$array_access_type = Type::getMixed();
}
} elseif (TypeChecker::isContainedBy(
@ -430,7 +457,7 @@ class ArrayFetchChecker
if (!$stmt->dim && $property_count) {
++$property_count;
$type->count = new Type\Atomic\TInt([$property_count => true]);
$type->count = new Type\Atomic\TLiteralInt([$property_count => true]);
}
if (!$array_access_type) {
@ -453,8 +480,8 @@ class ArrayFetchChecker
}
$has_valid_offset = true;
} else {
$expected_offset_types[] = (string)$type->getGenericKeyType();
} elseif (!$inside_isset || $type->sealed) {
$expected_offset_types[] = (string)$type->getGenericKeyType()->getId();
$array_access_type = Type::getMixed();
}
@ -627,7 +654,7 @@ class ArrayFetchChecker
if ($expected_offset_types) {
$invalid_offset_type = $expected_offset_types[0];
$used_offset = 'using a ' . $offset_type . ' offset';
$used_offset = 'using a ' . $offset_type->getId() . ' offset';
if ($key_value !== null) {
$used_offset = 'using offset value of '
@ -679,8 +706,7 @@ class ArrayFetchChecker
$offset_atomic_types = $offset_type->getTypes();
if (isset($offset_atomic_types['string'])
&& $offset_atomic_types['string'] instanceof Type\Atomic\TString
&& $offset_atomic_types['string']->values
&& $offset_atomic_types['string'] instanceof Type\Atomic\TLiteralString
) {
$strings = [];
$ints = [];
@ -697,7 +723,7 @@ class ArrayFetchChecker
$offset_type = clone $offset_type;
if ($strings) {
$offset_type->addType(new Type\Atomic\TString($strings));
$offset_type->addType(new Type\Atomic\TLiteralString($strings));
} else {
$offset_type->removeType('string');
}
@ -705,13 +731,13 @@ class ArrayFetchChecker
if (isset($offset_atomic_types['int'])
&& $offset_atomic_types['int'] instanceof Type\Atomic\TInt
) {
if ($offset_atomic_types['int']->values) {
$offset_type->addType(new Type\Atomic\TInt(
if ($offset_atomic_types['int'] instanceof Type\Atomic\TLiteralInt) {
$offset_type->addType(new Type\Atomic\TLiteralInt(
$offset_atomic_types['int']->values + $ints
));
}
} else {
$offset_type->addType(new Type\Atomic\TInt($ints));
$offset_type->addType(new Type\Atomic\TLiteralInt($ints));
}
}
}

View File

@ -201,12 +201,12 @@ class ExpressionChecker
$stmt->inferredType = clone $stmt->var->inferredType;
$stmt->inferredType->from_calculation = true;
foreach ($stmt->inferredType->getTypes() as $atomic_type) {
if ($atomic_type instanceof Type\Atomic\TInt
|| $atomic_type instanceof Type\Atomic\TFloat
) {
if ($context->inside_loop) {
$atomic_type->values = null;
if ($context->inside_loop) {
foreach ($stmt->inferredType->getTypes() as $atomic_type) {
if ($atomic_type instanceof Type\Atomic\TLiteralInt) {
$stmt->inferredType->addType(new Type\Atomic\TInt);
} elseif ($atomic_type instanceof Type\Atomic\TLiteralFloat) {
$stmt->inferredType->addType(new Type\Atomic\TFloat);
}
}
}

View File

@ -4,6 +4,7 @@ namespace Psalm\Checker;
use Psalm\Checker\Statements\ExpressionChecker;
use Psalm\Codebase;
use Psalm\Type;
use Psalm\Type\Atomic\LiteralType;
use Psalm\Type\Atomic\ObjectLike;
use Psalm\Type\Atomic\Scalar;
use Psalm\Type\Atomic\TArray;
@ -823,12 +824,8 @@ class TypeChecker
|| ($input_type_part instanceof TInt && $container_type_part instanceof TInt)
|| ($input_type_part instanceof TFloat && $container_type_part instanceof TFloat)
) {
/**
* @psalm-suppress UndefinedPropertyFetch
* @psalm-suppress MixedArgument
*/
if ($input_type_part->values !== null && $container_type_part->values !== null) {
$all_types_contain = !array_diff_key($input_type_part->values, $container_type_part->values);
if ($input_type_part instanceof LiteralType && $container_type_part instanceof LiteralType) {
$all_types_contain = !array_diff_key($input_type_part->getValues(), $container_type_part->getValues());
$incompatible_values = !$all_types_contain;
}
}

View File

@ -14,6 +14,9 @@ use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TFloat;
use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TLiteralFloat;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNull;
@ -546,13 +549,18 @@ abstract class Type
/**
* @param bool $from_calculation
* @param array<string|int, bool>|null $values
* @param array<int, bool>|null $values
*
* @return Type\Union
*/
public static function getInt($from_calculation = false, array $values = null)
{
$union = new Union([new TInt($values)]);
if ($values) {
$union = new Union([new TLiteralInt($values)]);
} else {
$union = new Union([new TInt()]);
}
$union->from_calculation = $from_calculation;
return $union;
@ -575,7 +583,11 @@ abstract class Type
*/
public static function getString(array $values = null)
{
$type = new TString($values);
if ($values) {
$type = new TLiteralString($values);
} else {
$type = new TString();
}
return new Union([$type]);
}
@ -631,7 +643,11 @@ abstract class Type
*/
public static function getFloat(array $values = null)
{
$type = new TFloat($values);
if ($values) {
$type = new TLiteralFloat($values);
} else {
$type = new TFloat();
}
return new Union([$type]);
}
@ -686,7 +702,7 @@ abstract class Type
/**
* @psalm-suppress InvalidScalarArgument because of a bug
*/
$array_type->count = new TInt([0 => true]);
$array_type->count = new TLiteralInt([0 => true]);
return new Type\Union([
$array_type,
@ -974,7 +990,8 @@ abstract class Type
$array_type = new TArray($generic_type_params);
if ($combination->array_counts) {
$array_type->count = new TInt($combination->array_counts);
/** @psalm-suppress InvalidScalarArgument */
$array_type->count = new TLiteralInt($combination->array_counts);
}
$new_types[] = $array_type;
@ -989,11 +1006,24 @@ abstract class Type
&& !count($new_types))
) {
if ($type instanceof TString) {
$type->values = $combination->strings ?: null;
if ($combination->strings) {
$type = new TLiteralString($combination->strings);
} elseif ($type instanceof TLiteralString) {
$type = new TString();
}
} elseif ($type instanceof TInt) {
$type->values = $combination->ints ?: null;
if ($combination->ints) {
/** @psalm-suppress InvalidScalarArgument */
$type = new TLiteralInt($combination->ints);
} elseif ($type instanceof TLiteralInt) {
$type = new TInt();
}
} elseif ($type instanceof TFloat) {
$type->values = $combination->floats ?: null;
if ($combination->floats) {
$type = new TLiteralFloat($combination->floats);
} elseif ($type instanceof TLiteralFloat) {
$type = new TFloat();
}
}
$new_types[] = $type;
@ -1032,11 +1062,11 @@ abstract class Type
return null;
}
if (get_class($type) === 'Psalm\\Type\\Atomic\\TBool' && isset($combination->value_types['false'])) {
if (get_class($type) === TBool::class && isset($combination->value_types['false'])) {
unset($combination->value_types['false']);
}
if (get_class($type) === 'Psalm\\Type\\Atomic\\TBool' && isset($combination->value_types['true'])) {
if (get_class($type) === TBool::class && isset($combination->value_types['true'])) {
unset($combination->value_types['true']);
}
@ -1102,22 +1132,22 @@ abstract class Type
}
} else {
if ($type instanceof TString && $combination->strings !== null) {
if ($type->values === null) {
$combination->strings = null;
} else {
if ($type instanceof TLiteralString) {
$combination->strings = $combination->strings + $type->values;
} else {
$combination->strings = null;
}
} elseif ($type instanceof TInt && $combination->ints !== null) {
if ($type->values === null) {
$combination->ints = null;
} else {
if ($type instanceof TLiteralInt) {
$combination->ints = $combination->ints + $type->values;
} else {
$combination->ints = null;
}
} elseif ($type instanceof TFloat && $combination->floats !== null) {
if ($type->values === null) {
$combination->ints = null;
if ($type instanceof TLiteralFloat) {
$combination->floats = $combination->floats + $type->values;
} else {
$combination->ints = $combination->floats + $type->values;
$combination->floats = null;
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Psalm\Type\Atomic;
interface LiteralType
{
/**
* @return array<string|int, bool>
*/
public function getValues();
}

View File

@ -153,9 +153,9 @@ class ObjectLike extends \Psalm\Type\Atomic
foreach ($this->properties as $key => $_) {
if (is_int($key)) {
$key_types[] = new Type\Atomic\TInt([$key => true]);
$key_types[] = new Type\Atomic\TLiteralInt([$key => true]);
} else {
$key_types[] = new Type\Atomic\TString([$key => true]);
$key_types[] = new Type\Atomic\TLiteralString([$key => true]);
}
}
@ -196,9 +196,9 @@ class ObjectLike extends \Psalm\Type\Atomic
foreach ($this->properties as $key => $property) {
if (is_int($key)) {
$key_types[] = new Type\Atomic\TInt([$key => true]);
$key_types[] = new Type\Atomic\TLiteralInt([$key => true]);
} else {
$key_types[] = new Type\Atomic\TString([$key => true]);
$key_types[] = new Type\Atomic\TLiteralString([$key => true]);
}
if ($value_type === null) {
@ -215,7 +215,7 @@ class ObjectLike extends \Psalm\Type\Atomic
$value_type->possibly_undefined = false;
$array_type = new TArray([Type::combineTypes($key_types), $value_type]);
$array_type->count = new TInt([count($this->properties) => true]);
$array_type->count = new TLiteralInt([count($this->properties) => true]);
return $array_type;
}

View File

@ -14,7 +14,7 @@ class TArray extends \Psalm\Type\Atomic
public $value = 'array';
/**
* @var TInt|null
* @var TLiteralInt|null
*/
public $count;

View File

@ -3,17 +3,6 @@ namespace Psalm\Type\Atomic;
class TFloat extends Scalar
{
/** @var array<string, bool>|null */
public $values;
/**
* @param array<string, bool>|null $values
*/
public function __construct(array $values = null)
{
$this->values = $values;
}
public function __toString()
{
return 'float';
@ -26,12 +15,4 @@ class TFloat extends Scalar
{
return 'float';
}
/**
* @return string
*/
public function getId()
{
return $this->values ? 'float(' . implode(',', array_keys($this->values)) . ')' : 'float';
}
}

View File

@ -3,17 +3,6 @@ namespace Psalm\Type\Atomic;
class TInt extends Scalar
{
/** @var array<string|int, bool>|null */
public $values;
/**
* @param array<string|int, bool>|null $values
*/
public function __construct(array $values = null)
{
$this->values = $values;
}
public function __toString()
{
return 'int';
@ -26,12 +15,4 @@ class TInt extends Scalar
{
return 'int';
}
/**
* @return string
*/
public function getId()
{
return $this->values ? 'int(' . implode(',', array_keys($this->values)) . ')' : 'int';
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Psalm\Type\Atomic;
class TLiteralFloat extends TFloat implements LiteralType
{
/** @var array<string, bool> */
public $values;
/**
* @param array<string, bool> $values
*/
public function __construct(array $values)
{
$this->values = $values;
}
/**
* @return string
*/
public function getId()
{
return $this->values ? 'float(' . implode(',', array_keys($this->values)) . ')' : 'float';
}
/**
* @return array<string, bool>
*/
public function getValues()
{
return $this->values;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Psalm\Type\Atomic;
class TLiteralInt extends TInt implements LiteralType
{
/** @var array<int, bool> */
public $values;
/**
* @param array<int, bool> $values
*/
public function __construct(array $values)
{
$this->values = $values;
}
/**
* @return string
*/
public function getId()
{
return $this->values ? 'int(' . implode(',', array_keys($this->values)) . ')' : 'int';
}
/**
* @return array<int, bool>
*/
public function getValues()
{
return $this->values;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Psalm\Type\Atomic;
class TLiteralString extends TString implements LiteralType
{
/** @var array<string|int, bool> */
public $values;
/**
* @param array<string|int, bool> $values
*/
public function __construct(array $values)
{
$this->values = $values;
}
/**
* @return string
*/
public function getId()
{
return $this->values ? 'string(\'' . implode('\',\'', array_keys($this->values)) . '\')' : 'string';
}
/**
* @return array<string|int, bool>
*/
public function getValues()
{
return $this->values;
}
}

View File

@ -3,17 +3,6 @@ namespace Psalm\Type\Atomic;
class TString extends Scalar
{
/** @var array<string|int, bool>|null */
public $values;
/**
* @param array<string|int, bool>|null $values
*/
public function __construct(array $values = null)
{
$this->values = $values;
}
public function __toString()
{
return 'string';
@ -26,12 +15,4 @@ class TString extends Scalar
{
return 'string';
}
/**
* @return string
*/
public function getId()
{
return $this->values ? 'string(\'' . implode('\',\'', array_keys($this->values)) . '\')' : 'string';
}
}

View File

@ -241,8 +241,7 @@ class Reconciler
$ints = array_flip(explode(',', $bracketed));
if (isset($existing_var_atomic_types['int'])
&& $existing_var_atomic_types['int'] instanceof Type\Atomic\TInt
&& $existing_var_atomic_types['int']->values
&& $existing_var_atomic_types['int'] instanceof Type\Atomic\TLiteralInt
) {
$current_count = count($existing_var_atomic_types['int']->values);
@ -275,8 +274,7 @@ class Reconciler
$strings = array_flip(explode('\',\'', substr($bracketed, 1, -1)));
if (isset($existing_var_atomic_types['string'])
&& $existing_var_atomic_types['string'] instanceof Type\Atomic\TString
&& $existing_var_atomic_types['string']->values
&& $existing_var_atomic_types['string'] instanceof Type\Atomic\TLiteralString
) {
$current_count = count($existing_var_atomic_types['string']->values);
@ -309,8 +307,7 @@ class Reconciler
$floats = array_flip(explode(',', $bracketed));
if (isset($existing_var_atomic_types['float'])
&& $existing_var_atomic_types['float'] instanceof Type\Atomic\TFloat
&& $existing_var_atomic_types['float']->values
&& $existing_var_atomic_types['float'] instanceof Type\Atomic\TLiteralFloat
) {
$current_count = count($existing_var_atomic_types['float']->values);
@ -965,8 +962,7 @@ class Reconciler
$ints = array_flip(explode(',', $bracketed));
if (isset($existing_var_atomic_types['int'])
&& $existing_var_atomic_types['int'] instanceof Type\Atomic\TInt
&& $existing_var_atomic_types['int']->values
&& $existing_var_atomic_types['int'] instanceof Type\Atomic\TLiteralInt
) {
$current_count = count($existing_var_atomic_types['int']->values);
@ -999,8 +995,7 @@ class Reconciler
$strings = array_flip(explode('\',\'', substr($bracketed, 1, -1)));
if (isset($existing_var_atomic_types['string'])
&& $existing_var_atomic_types['string'] instanceof Type\Atomic\TString
&& $existing_var_atomic_types['string']->values
&& $existing_var_atomic_types['string'] instanceof Type\Atomic\TLiteralString
) {
$current_count = count($existing_var_atomic_types['string']->values);
@ -1033,8 +1028,7 @@ class Reconciler
$floats = array_flip(explode(',', $bracketed));
if (isset($existing_var_atomic_types['float'])
&& $existing_var_atomic_types['float'] instanceof Type\Atomic\TFloat
&& $existing_var_atomic_types['float']->values
&& $existing_var_atomic_types['float'] instanceof Type\Atomic\TLiteralFloat
) {
$current_count = count($existing_var_atomic_types['float']->values);

View File

@ -267,6 +267,26 @@ class IssetTest extends TestCase
echo $arr["bar"];
}',
],
'issetAdditionalVar' => [
'<?php
class Example {
const FOO = "foo";
/**
* @param array{bar:string} $params
*/
public function test(array $params) : bool {
if (isset($params[self::FOO])) {
return true;
}
if (isset($params["bat"])) {
return true;
}
return false;
}
}'
],
];
}
@ -291,7 +311,23 @@ class IssetTest extends TestCase
$b = 1;
echo $arr[$b][$c];
}',
'error_message' => 'PossiblyNullArrayAccess',
'error_message' => 'NullArrayAccess',
],
'issetAdditionalVarWithSealedObjectLike' => [
'<?php
class Example {
const FOO = "foo";
public function test() : bool {
$params = ["bar" => "bat"];
if (isset($params[self::FOO])) {
return true;
}
return false;
}
}',
'error_message' => 'InvalidArrayOffset',
],
];
}

View File

@ -176,7 +176,7 @@ class MethodCallTest extends TestCase
}
/** @param A1|string $x */
function example($x, bool $isObject) {
function example($x, bool $isObject) : void {
if ($isObject) {
$x->methodOfA();
}