1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

Support simple list assignment in foreach

Ref #4741
This commit is contained in:
Matt Brown 2020-12-06 19:14:52 -05:00 committed by Daniil Gentili
parent ce9d100908
commit ae8aaaf1d8
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
7 changed files with 128 additions and 10 deletions

View File

@ -414,12 +414,22 @@ class ForeachAnalyzer
} }
$iterator_atomic_type = $iterator_atomic_type->getGenericArrayType(); $iterator_atomic_type = $iterator_atomic_type->getGenericArrayType();
} elseif ($iterator_atomic_type instanceof Type\Atomic\TList) { } elseif ($iterator_atomic_type instanceof Type\Atomic\TList) {
$list_var_id = ExpressionIdentifier::getArrayVarId(
$stmt->expr,
$statements_analyzer->getFQCLN(),
$statements_analyzer
);
if (!$iterator_atomic_type instanceof Type\Atomic\TNonEmptyList) { if (!$iterator_atomic_type instanceof Type\Atomic\TNonEmptyList) {
$always_non_empty_array = false; $always_non_empty_array = false;
} }
$iterator_atomic_type = new Type\Atomic\TArray([ $iterator_atomic_type = new Type\Atomic\TArray([
Type::getInt(), $list_var_id
? new Type\Union([
new Type\Atomic\TDependentListKey($list_var_id)
])
: Type::getInt(),
$iterator_atomic_type->type_param $iterator_atomic_type->type_param
]); ]);
} elseif (!$iterator_atomic_type instanceof Type\Atomic\TNonEmptyArray) { } elseif (!$iterator_atomic_type instanceof Type\Atomic\TNonEmptyArray) {

View File

@ -437,6 +437,16 @@ class ArrayAssignmentAnalyzer
$current_dim_type = Type::getArrayKey(); $current_dim_type = Type::getArrayKey();
} }
if ($current_dim_type->isSingle()) {
$current_dim_type_type = \array_values($current_dim_type->getAtomicTypes())[0];
if ($current_dim_type_type instanceof Type\Atomic\TDependentListKey
&& $current_dim_type_type->getVarId() === $parent_var_id
) {
$offset_already_existed = true;
}
}
$array_atomic_key_type = ArrayFetchAnalyzer::replaceOffsetTypeWithInts( $array_atomic_key_type = ArrayFetchAnalyzer::replaceOffsetTypeWithInts(
$current_dim_type $current_dim_type
); );
@ -580,6 +590,7 @@ class ArrayAssignmentAnalyzer
$atomic_root_types['array']->count++; $atomic_root_types['array']->count++;
} }
} }
return $new_child_type; return $new_child_type;
} }
@ -604,6 +615,7 @@ class ArrayAssignmentAnalyzer
$reversed_child_stmts = []; $reversed_child_stmts = [];
$var_id_additions = []; $var_id_additions = [];
$full_var_id = true; $full_var_id = true;
$child_stmt = null; $child_stmt = null;
// First go from the root element up, and go as far as we can to figure out what // First go from the root element up, and go as far as we can to figure out what

View File

@ -1159,7 +1159,6 @@ class ArgumentAnalyzer
if ($type_param->isEmpty() && isset($param_atomic_type->type_params[$i])) { if ($type_param->isEmpty() && isset($param_atomic_type->type_params[$i])) {
$input_type_changed = true; $input_type_changed = true;
/** @psalm-suppress PropertyTypeCoercion */
$input_atomic_type->type_params[$i] = clone $param_atomic_type->type_params[$i]; $input_atomic_type->type_params[$i] = clone $param_atomic_type->type_params[$i];
} }
} }

View File

@ -532,7 +532,6 @@ class ArrayFunctionArgumentsAnalyzer
} }
$array_type->addType($array_atomic_type); $array_type->addType($array_atomic_type);
$context->removeDescendents($var_id, $array_type);
} elseif ($array_atomic_type instanceof TNonEmptyList) { } elseif ($array_atomic_type instanceof TNonEmptyList) {
if (!$context->inside_loop && $array_atomic_type->count !== null) { if (!$context->inside_loop && $array_atomic_type->count !== null) {
if ($array_atomic_type->count === 0) { if ($array_atomic_type->count === 0) {
@ -550,10 +549,10 @@ class ArrayFunctionArgumentsAnalyzer
} }
$array_type->addType($array_atomic_type); $array_type->addType($array_atomic_type);
$context->removeDescendents($var_id, $array_type);
} }
} }
$context->removeDescendents($var_id, $array_type);
$context->vars_in_scope[$var_id] = $array_type; $context->vars_in_scope[$var_id] = $array_type;
} }
} }

View File

@ -18,6 +18,7 @@ use Psalm\Type\Atomic\TTemplateParamClass;
use Psalm\Type\Atomic\TDependentGetClass; use Psalm\Type\Atomic\TDependentGetClass;
use Psalm\Type\Atomic\TDependentGetDebugType; use Psalm\Type\Atomic\TDependentGetDebugType;
use Psalm\Type\Atomic\TDependentGetType; use Psalm\Type\Atomic\TDependentGetType;
use Psalm\Type\Atomic\TDependentListKey;
use Psalm\Type\Atomic\THtmlEscapedString; use Psalm\Type\Atomic\THtmlEscapedString;
use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TLiteralClassString;
@ -245,11 +246,11 @@ class ScalarTypeComparator
return true; return true;
} }
if (get_class($container_type_part) === TInt::class && $input_type_part instanceof TLiteralInt) { if ((get_class($container_type_part) === TInt::class
return true; || get_class($container_type_part) === TDependentListKey::class)
} && ($input_type_part instanceof TLiteralInt
|| $input_type_part instanceof TPositiveInt)
if (get_class($container_type_part) === TInt::class && $input_type_part instanceof TPositiveInt) { ) {
return true; return true;
} }
@ -272,7 +273,8 @@ class ScalarTypeComparator
return true; return true;
} }
if (get_class($container_type_part) === TInt::class if ((get_class($container_type_part) === TInt::class
|| get_class($container_type_part) === TDependentListKey::class)
&& $input_type_part instanceof TInt && $input_type_part instanceof TInt
) { ) {
return true; return true;

View File

@ -0,0 +1,48 @@
<?php
namespace Psalm\Type\Atomic;
/**
* Represents a list key created from foreach ($list as $key => $value)
*/
class TDependentListKey extends TInt implements DependentType
{
/**
* Used to hold information as to what list variable this refers to
*
* @var string
*/
public $var_id;
/**
* @param string $var_id the variable id
*/
public function __construct(string $var_id)
{
$this->var_id = $var_id;
}
public function getId(bool $nested = false): string
{
return 'list-key<' . $this->var_id . '>';
}
public function getVarId() : string
{
return $this->var_id;
}
public function getAssertionString(): string
{
return 'int';
}
public function getReplacement() : \Psalm\Type\Atomic
{
return new TInt();
}
public function canBeFullyExpressedInPhp(int $php_major_version, int $php_minor_version): bool
{
return false;
}
}

View File

@ -1509,6 +1509,20 @@ class ArrayAssignmentTest extends TestCase
]; ];
', ',
], ],
'assignToListWithForeachKey' => [
'<?php
/**
* @param list<string> $list
* @return list<string>
*/
function getList(array $list): array {
foreach ($list as $key => $value) {
$list[$key] = $value . "!";
}
return $list;
}'
],
]; ];
} }
@ -1825,6 +1839,40 @@ class ArrayAssignmentTest extends TestCase
}', }',
'error_message' => 'RedundantCast', 'error_message' => 'RedundantCast',
], ],
'assignToListWithUpdatedForeachKey' => [
'<?php
/**
* @param list<string> $list
* @return list<string>
*/
function getList(array $list): array {
foreach ($list as $key => $value) {
$list[$key + 1] = $value . "!";
}
return $list;
}',
'error_message' => 'LessSpecificReturnStatement',
],
'assignToListWithAlteredForeachKeyVar' => [
'<?php
/**
* @param list<string> $list
* @return list<string>
*/
function getList(array $list): array {
foreach ($list as $key => $value) {
if (rand(0, 1)) {
array_pop($list);
}
$list[$key] = $value . "!";
}
return $list;
}',
'error_message' => 'LessSpecificReturnStatement',
],
]; ];
} }
} }