mirror of
https://github.com/danog/psalm.git
synced 2024-12-04 10:38:49 +01:00
Fix automatic array creation checks
This commit is contained in:
parent
e663c1da19
commit
0563f508ca
@ -935,7 +935,7 @@ class StatementsChecker
|
|||||||
$closure_checker->check($use_context, $this->check_methods);
|
$closure_checker->check($use_context, $this->check_methods);
|
||||||
|
|
||||||
} elseif ($stmt instanceof PhpParser\Node\Expr\ArrayDimFetch) {
|
} elseif ($stmt instanceof PhpParser\Node\Expr\ArrayDimFetch) {
|
||||||
return $this->checkArrayAccess($stmt, $context);
|
return $this->checkArrayAccess($stmt, $context, $array_assignment);
|
||||||
|
|
||||||
} elseif ($stmt instanceof PhpParser\Node\Expr\Cast\Int_) {
|
} elseif ($stmt instanceof PhpParser\Node\Expr\Cast\Int_) {
|
||||||
if ($this->checkExpression($stmt->expr, $context) === false) {
|
if ($this->checkExpression($stmt->expr, $context) === false) {
|
||||||
@ -1599,15 +1599,7 @@ class StatementsChecker
|
|||||||
{
|
{
|
||||||
// if the array is empty, this special type allows us to match any other array type against it
|
// if the array is empty, this special type allows us to match any other array type against it
|
||||||
if (empty($stmt->items)) {
|
if (empty($stmt->items)) {
|
||||||
$stmt->inferredType = new Type\Union([
|
$stmt->inferredType = Type::getEmptyArray();
|
||||||
new Type\Generic(
|
|
||||||
'array',
|
|
||||||
[
|
|
||||||
new Type\Union([new Type\Atomic('empty')]),
|
|
||||||
new Type\Union([new Type\Atomic('empty')])
|
|
||||||
]
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2147,7 +2139,7 @@ class StatementsChecker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getVarId(PhpParser\Node\Expr $stmt)
|
public static function getVarId(PhpParser\Node\Expr $stmt, &$nesting = 0)
|
||||||
{
|
{
|
||||||
if ($stmt instanceof PhpParser\Node\Expr\Variable && is_string($stmt->name)) {
|
if ($stmt instanceof PhpParser\Node\Expr\Variable && is_string($stmt->name)) {
|
||||||
return $stmt->name;
|
return $stmt->name;
|
||||||
@ -2164,13 +2156,17 @@ class StatementsChecker
|
|||||||
|
|
||||||
return $object_id . '->' . $stmt->name;
|
return $object_id . '->' . $stmt->name;
|
||||||
}
|
}
|
||||||
|
else if ($stmt instanceof PhpParser\Node\Expr\ArrayDimFetch) {
|
||||||
|
$nesting++;
|
||||||
|
return self::getVarId($stmt->var, $nesting);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function checkArrayAssignment(PhpParser\Node\Expr\ArrayDimFetch $stmt, Context $context, Type\Union $assignment_value_type)
|
protected function checkArrayAssignment(PhpParser\Node\Expr\ArrayDimFetch $stmt, Context $context, Type\Union $assignment_value_type)
|
||||||
{
|
{
|
||||||
if ($stmt->dim && $this->checkExpression($stmt->dim, $context, true) === false) {
|
if ($stmt->dim && $this->checkExpression($stmt->dim, $context, false) === false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2193,13 +2189,19 @@ class StatementsChecker
|
|||||||
$assignment_key_type = Type::getInt();
|
$assignment_key_type = Type::getInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
$var_id = self::getVarId($stmt->var);
|
$nesting = 0;
|
||||||
|
|
||||||
|
$var_id = self::getVarId($stmt->var, $nesting);
|
||||||
|
|
||||||
if (isset($stmt->var->inferredType)) {
|
if (isset($stmt->var->inferredType)) {
|
||||||
$return_type = $stmt->var->inferredType;
|
$return_type = $stmt->var->inferredType;
|
||||||
|
|
||||||
if (!$return_type->isMixed()) {
|
if ($return_type->isEmpty()) {
|
||||||
|
$return_type = Type::getEmptyArray();
|
||||||
|
$return_type->types['array']->type_params[0] = $assignment_key_type;
|
||||||
|
$return_type->types['array']->type_params[1] = $assignment_value_type;
|
||||||
|
}
|
||||||
|
else {
|
||||||
foreach ($return_type->types as &$type) {
|
foreach ($return_type->types as &$type) {
|
||||||
if ($type->isScalarType() && !$type->isString()) {
|
if ($type->isScalarType() && !$type->isString()) {
|
||||||
if (IssueBuffer::accepts(
|
if (IssueBuffer::accepts(
|
||||||
@ -2228,7 +2230,30 @@ class StatementsChecker
|
|||||||
|
|
||||||
$type = $refined_type;
|
$type = $refined_type;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($nesting) {
|
||||||
|
$context_type = clone $context->vars_in_scope[$var_id];
|
||||||
|
|
||||||
|
$array_type = $context_type;
|
||||||
|
|
||||||
|
for ($i = 0; $i < $nesting + 1; $i++) {
|
||||||
|
if ($i < $nesting) {
|
||||||
|
if ($array_type->types['array']->type_params[1]->isEmpty()) {
|
||||||
|
$array_type->types['array']->type_params[1] = $return_type;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$array_type = $array_type->types['array']->type_params[1];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$array_type->types['array']->type_params[1] = $return_type->types['array']->type_params[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$context->vars_in_scope[$var_id] = $context_type;
|
||||||
|
}
|
||||||
|
else {
|
||||||
$context->vars_in_scope[$var_id] = $return_type;
|
$context->vars_in_scope[$var_id] = $return_type;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3468,31 +3493,92 @@ class StatementsChecker
|
|||||||
* @param array &$context->vars_possibly_in_scope
|
* @param array &$context->vars_possibly_in_scope
|
||||||
* @return false|null
|
* @return false|null
|
||||||
*/
|
*/
|
||||||
protected function checkArrayAccess(PhpParser\Node\Expr\ArrayDimFetch $stmt, Context $context)
|
protected function checkArrayAccess(PhpParser\Node\Expr\ArrayDimFetch $stmt, Context $context, $array_assignment = false)
|
||||||
{
|
{
|
||||||
if ($this->checkExpression($stmt->var, $context) === false) {
|
if ($this->checkExpression($stmt->var, $context, $array_assignment) === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stmt->dim && $this->checkExpression($stmt->dim, $context) === false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$var_type = null;
|
$var_type = null;
|
||||||
$key_type = null;
|
$key_type = null;
|
||||||
|
|
||||||
|
$nesting = 0;
|
||||||
|
$var_id = self::getVarId($stmt->var, $nesting);
|
||||||
|
|
||||||
if (isset($stmt->var->inferredType)) {
|
if (isset($stmt->var->inferredType)) {
|
||||||
$var_type = $stmt->var->inferredType;
|
$var_type = $stmt->var->inferredType;
|
||||||
|
|
||||||
if ($var_type instanceof Type\Generic) {
|
foreach ($var_type->types as &$type) {
|
||||||
|
if ($type instanceof Type\Generic) {
|
||||||
// create a union type to pass back to the statement
|
// create a union type to pass back to the statement
|
||||||
$value_index = count($var_type->type_params) - 1;
|
$value_index = count($type->type_params) - 1;
|
||||||
$stmt->inferredType = $var_type->type_params[$value_index];
|
|
||||||
|
|
||||||
if ($value_index) {
|
if ($value_index) {
|
||||||
$key_type = $var_type->type_params[0];
|
// if we're assigning to an empty array with a key offset, refashion that array
|
||||||
|
if ($array_assignment && $type->type_params[0]->isEmpty()) {
|
||||||
|
if (isset($stmt->dim->inferredType)) {
|
||||||
|
$key_type = $stmt->dim->inferredType;
|
||||||
|
$type->type_params[0] = $key_type;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elseif ($var_type->isString()) {
|
else {
|
||||||
|
if ($key_type) {
|
||||||
|
$key_type = Type::combineUnionTypes($key_type, $type->type_params[0]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$key_type = $type->type_params[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($array_assignment && $type->type_params[$value_index]->isEmpty()) {
|
||||||
|
// if in array assignment and the referenced variable does not have
|
||||||
|
// an array at this level, create one
|
||||||
|
$empty_type = Type::getEmptyArray();
|
||||||
|
|
||||||
|
$stmt->inferredType = $empty_type;
|
||||||
|
|
||||||
|
$context_type = clone $context->vars_in_scope[$var_id];
|
||||||
|
|
||||||
|
$array_type = $context_type;
|
||||||
|
|
||||||
|
for ($i = 0; $i < $nesting + 1; $i++) {
|
||||||
|
if ($i < $nesting) {
|
||||||
|
if ($array_type->types['array']->type_params[1]->isEmpty()) {
|
||||||
|
$new_empty = clone $empty_type;
|
||||||
|
$new_empty->types['array']->type_params[0] = $stmt->dim ? $stmt->dim->inferredType : Type::getInt();
|
||||||
|
$array_type->types['array']->type_params[1] = $new_empty;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$array_type = $array_type->types['array']->type_params[1];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$array_type->types['array']->type_params[0] = $stmt->dim ? $stmt->dim->inferredType : Type::getInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$context->vars_in_scope[$var_id] = $context_type;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$stmt->inferredType = $type->type_params[$value_index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($type->isString()) {
|
||||||
|
if ($key_type) {
|
||||||
|
$key_type = Type::combineUnionTypes($key_type, Type::getInt());
|
||||||
|
}
|
||||||
|
else {
|
||||||
$key_type = Type::getInt();
|
$key_type = Type::getInt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!isset($stmt->inferredType)) {
|
if (!isset($stmt->inferredType)) {
|
||||||
$stmt->inferredType = Type::getMixed();
|
$stmt->inferredType = Type::getMixed();
|
||||||
@ -3506,18 +3592,12 @@ class StatementsChecker
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($stmt->dim) {
|
if ($stmt->dim) {
|
||||||
if ($this->checkExpression($stmt->dim, $context) === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($stmt->dim->inferredType) && $key_type) {
|
if (isset($stmt->dim->inferredType) && $key_type) {
|
||||||
foreach ($stmt->dim->inferredType->types as $at) {
|
foreach ($stmt->dim->inferredType->types as $at) {
|
||||||
if ($at->isMixed()) {
|
if ($at->isMixed()) {
|
||||||
// @todo emit issue
|
// @todo emit issue
|
||||||
}
|
}
|
||||||
elseif (!$at->isIn($key_type)) {
|
elseif (!$at->isIn($key_type)) {
|
||||||
$var_id = self::getVarId($stmt->var);
|
|
||||||
|
|
||||||
if (IssueBuffer::accepts(
|
if (IssueBuffer::accepts(
|
||||||
new InvalidArrayAccess(
|
new InvalidArrayAccess(
|
||||||
'Cannot access value on variable $' . $var_id . ' using ' . $at . ' offset',
|
'Cannot access value on variable $' . $var_id . ' using ' . $at . ' offset',
|
||||||
|
@ -232,6 +232,19 @@ abstract class Type
|
|||||||
return new Union([$type]);
|
return new Union([$type]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getEmptyArray()
|
||||||
|
{
|
||||||
|
return new Type\Union([
|
||||||
|
new Type\Generic(
|
||||||
|
'array',
|
||||||
|
[
|
||||||
|
new Type\Union([new Type\Atomic('empty')]),
|
||||||
|
new Type\Union([new Type\Atomic('empty')])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public static function getVoid()
|
public static function getVoid()
|
||||||
{
|
{
|
||||||
$type = new Atomic('void');
|
$type = new Atomic('void');
|
||||||
|
Loading…
Reference in New Issue
Block a user