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

Merge pull request #8943 from Nicelocal/fix_8940

More array fixes
This commit is contained in:
orklah 2022-12-19 22:31:12 +01:00 committed by GitHub
commit 62db5d4f6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 330 additions and 51 deletions

View File

@ -7,6 +7,8 @@
- [BC] The only optional boolean parameter of `TKeyedArray::getGenericArrayType` was removed, and was replaced with a string parameter with a different meaning.
- [BC] The `TDependentListKey` type was removed and replaced with an optional property of the `TIntRange` type.
# Upgrading from Psalm 4 to Psalm 5
## Changed

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="dev-master@6eb37b9dc2321e4eaade9d3d2dca1aff6f2c0a8f">
<files psalm-version="dev-master@d90a9a28a53176b4eb329d4c062d37516d3227f3">
<file src="examples/TemplateChecker.php">
<PossiblyUndefinedIntArrayOffset occurrences="2">
<code>$comment_block-&gt;tags['variablesfrom'][0]</code>
@ -182,6 +182,9 @@
</PossiblyUndefinedIntArrayOffset>
</file>
<file src="src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php">
<PossiblyUndefinedIntArrayOffset occurrences="1">
<code>$properties[0]</code>
</PossiblyUndefinedIntArrayOffset>
<ReferenceConstraintViolation occurrences="3">
<code>$stmt_type</code>
<code>$stmt_type</code>
@ -231,6 +234,11 @@
<code>$check_type_string</code>
</PossiblyUndefinedIntArrayOffset>
</file>
<file src="src/Psalm/Internal/Cli/LanguageServer.php">
<PossiblyInvalidArgument occurrences="1">
<code>$options['tcp'] ?? null</code>
</PossiblyInvalidArgument>
</file>
<file src="src/Psalm/Internal/Cli/Refactor.php">
<PossiblyUndefinedIntArrayOffset occurrences="1">
<code>$identifier_name</code>
@ -390,9 +398,6 @@
<InvalidArgument occurrences="1">
<code>$class_strings ?: null</code>
</InvalidArgument>
<RedundantCondition occurrences="2">
<code>$is_replace</code>
</RedundantCondition>
</file>
<file src="src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php">
<PossiblyUndefinedIntArrayOffset occurrences="1">
@ -461,17 +466,11 @@
</PossiblyUndefinedIntArrayOffset>
</file>
<file src="src/Psalm/Internal/Type/TypeTokenizer.php">
<InvalidArrayOffset occurrences="1">
<code>$chars[$i - 1]</code>
</InvalidArrayOffset>
<PossiblyInvalidArrayOffset occurrences="7">
<PossiblyInvalidArrayOffset occurrences="4">
<code>$type_tokens[$i - 1]</code>
<code>$type_tokens[$i - 1]</code>
<code>$type_tokens[$i - 1]</code>
<code>$type_tokens[$i - 1]</code>
<code>$type_tokens[$i - 1]</code>
<code>$type_tokens[$i - 1]</code>
<code>$type_tokens[$i - 2]</code>
</PossiblyInvalidArrayOffset>
</file>
<file src="src/Psalm/Storage/ClassConstantStorage.php">

View File

@ -106,14 +106,15 @@ class ForAnalyzer
if (count($stmt->init) === 1
&& count($stmt->cond) === 1
&& $cond instanceof PhpParser\Node\Expr\BinaryOp
&& $cond->right instanceof PhpParser\Node\Scalar\LNumber
&& ($cond_value = $statements_analyzer->node_data->getType($cond->right))
&& ($cond_value->isSingleIntLiteral() || $cond_value->isSingleStringLiteral())
&& $cond->left instanceof PhpParser\Node\Expr\Variable
&& is_string($cond->left->name)
&& isset($init_var_types[$cond->left->name])
&& $init_var_types[$cond->left->name]->isSingleIntLiteral()
) {
$init_value = $init_var_types[$cond->left->name]->getSingleIntLiteral()->value;
$cond_value = $cond->right->value;
$init_value = $init_var_types[$cond->left->name]->getSingleLiteral()->value;
$cond_value = $cond_value->getSingleLiteral()->value;
if ($cond instanceof PhpParser\Node\Expr\BinaryOp\Smaller && $init_value < $cond_value) {
$always_enters_loop = true;

View File

@ -27,7 +27,8 @@ class WhileAnalyzer
Context $context
): ?bool {
$while_true = ($stmt->cond instanceof PhpParser\Node\Expr\ConstFetch && $stmt->cond->name->parts === ['true'])
|| ($stmt->cond instanceof PhpParser\Node\Scalar\LNumber && $stmt->cond->value > 0);
|| (($t = $statements_analyzer->node_data->getType($stmt->cond))
&& $t->isAlwaysTruthy());
$pre_context = null;

View File

@ -649,17 +649,16 @@ class ArrayAssignmentAnalyzer
]);
} else {
assert($array_atomic_type_list !== null);
$array_atomic_type = array_fill(
$atomic_root_type_array->getMinCount(),
count($atomic_root_type_array->properties)-1,
$array_atomic_type_list,
);
assert(count($array_atomic_type) > 0);
$array_atomic_type = new TKeyedArray(
array_fill(
0,
count($atomic_root_type_array->properties),
$array_atomic_type_list,
),
$array_atomic_type,
null,
null,
[
Type::getListKey(),
$array_atomic_type_list,
],
true,
);
}

View File

@ -1533,6 +1533,29 @@ class ArrayFetchAnalyzer
$properties[$key_value->value] ?? null,
$replacement_type,
);
if (is_int($key_value->value)
&& !$stmt->dim
&& $type->is_list
&& $type->properties[$key_value->value-1]->possibly_undefined
) {
$first = true;
for ($x = 0; $x < $key_value->value; $x++) {
if (!$properties[$x]->possibly_undefined) {
continue;
}
$properties[$x] = Type::combineUnionTypes(
$properties[$x],
$replacement_type,
);
if ($first) {
$first = false;
$properties[$x] = $properties[$x]->setPossiblyUndefined(false);
}
}
$properties[$key_value->value] = $properties[$key_value->value]->
setPossiblyUndefined(true)
;
}
}
$array_access_type = Type::combineUnionTypes(

View File

@ -334,7 +334,16 @@ class IncludeAnalyzer
if ($stmt->getArgs()[1]->value instanceof PhpParser\Node\Scalar\LNumber) {
$dir_level = $stmt->getArgs()[1]->value->value;
} else {
return null;
if ($statements_analyzer) {
$t = $statements_analyzer->node_data->getType($stmt->getArgs()[1]->value);
if ($t && $t->isSingleIntLiteral()) {
$dir_level = $t->getSingleIntLiteral()->value;
} else {
return null;
}
} else {
return null;
}
}
}

View File

@ -89,9 +89,13 @@ class ArrayColumnReturnTypeProvider implements FunctionReturnTypeProviderInterfa
$properties = [];
$ok = true;
$last_custom_key = -1;
$is_list = $input_array->is_list || $key_column_name !== null;
$is_list = true;
$had_possibly_undefined = false;
foreach ($input_array->properties as $key => $property) {
// This incorrectly assumes that the array is sorted, may be problematic
// Will be fixed when order is enforced
$key = -1;
foreach ($input_array->properties as $property) {
$row_shape = self::getRowShape(
$property,
$statements_source,
@ -142,6 +146,9 @@ class ArrayColumnReturnTypeProvider implements FunctionReturnTypeProviderInterfa
$ok = false;
break;
}
} else {
/** @psalm-suppress StringIncrement Actually always an int in this branch */
++$key;
}
$properties[$key] = $result_element_type->setPossiblyUndefined(

View File

@ -149,7 +149,6 @@ class ArrayMergeReturnTypeProvider implements FunctionReturnTypeProviderInterfac
if (!isset($generic_properties[$key]) || (
!$type->possibly_undefined
&& !$unpacking_possibly_empty
&& $is_replace
)) {
if ($unpacking_possibly_empty) {
$type = $type->setPossiblyUndefined(true);

View File

@ -2,7 +2,6 @@
namespace Psalm\Internal\Provider\ReturnTypeProvider;
use PhpParser;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
@ -54,15 +53,21 @@ class ArrayRandReturnTypeProvider implements FunctionReturnTypeProviderInterface
$key_type = $first_arg_array->getGenericKeyType();
}
if (!$second_arg
|| ($second_arg instanceof PhpParser\Node\Scalar\LNumber && $second_arg->value === 1)
if (!$second_arg) {
return $key_type;
}
$second_arg_type = $statements_source->node_data->getType($second_arg);
if ($second_arg_type
&& $second_arg_type->isSingleIntLiteral()
&& $second_arg_type->getSingleIntLiteral()->value === 1
) {
return $key_type;
}
$arr_type = Type::getList($key_type);
if ($second_arg instanceof PhpParser\Node\Scalar\LNumber) {
if ($second_arg_type && $second_arg_type->isSingleIntLiteral()) {
return $arr_type;
}

View File

@ -47,8 +47,9 @@ class ExplodeReturnTypeProvider implements FunctionReturnTypeProviderInterface
$can_return_empty = isset($call_args[2])
&& (
!$call_args[2]->value instanceof PhpParser\Node\Scalar\LNumber
|| $call_args[2]->value->value < 0
!($third_arg_type = $statements_source->node_data->getType($call_args[2]->value))
|| !$third_arg_type->isSingleIntLiteral()
|| $third_arg_type->getSingleIntLiteral()->value < 0
);
if ($call_args[0]->value instanceof PhpParser\Node\Scalar\String_) {

View File

@ -3,15 +3,22 @@
namespace Psalm\Internal\Type;
use Psalm\Type\Atomic;
use Psalm\Type\Atomic\TArrayKey;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TIntRange;
use Psalm\Type\Atomic\TIterable;
use Psalm\Type\Atomic\TLiteralFloat;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TString;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Union;
use function is_int;
use function is_string;
/**
* @internal
*/
@ -88,4 +95,34 @@ class TypeCombination
/** @var array<string, ?TNamedObject> */
public array $class_string_map_as_types = [];
/**
* @psalm-assert-if-true !null $this->objectlike_key_type
* @psalm-assert-if-true !null $this->objectlike_value_type
* @param array-key $k
*/
public function fallbackKeyContains($k): bool
{
if (!$this->objectlike_key_type) {
return false;
}
foreach ($this->objectlike_key_type->getAtomicTypes() as $t) {
if ($t instanceof TArrayKey) {
return true;
} elseif ($t instanceof TLiteralInt || $t instanceof TLiteralString) {
if ($t->value === $k) {
return true;
}
} elseif ($t instanceof TIntRange) {
if (is_int($k) && $t->contains($k)) {
return true;
}
} elseif ($t instanceof TString && is_string($k)) {
return true;
} elseif ($t instanceof TInt && is_int($k)) {
return true;
}
}
return false;
}
}

View File

@ -649,26 +649,12 @@ class TypeCombiner
$combination->objectlike_sealed = $combination->objectlike_sealed
&& $type->fallback_params === null;
if ($type->fallback_params) {
$combination->objectlike_key_type = Type::combineUnionTypes(
$type->fallback_params[0],
$combination->objectlike_key_type,
$codebase,
$overwrite_empty_array,
);
$combination->objectlike_value_type = Type::combineUnionTypes(
$type->fallback_params[1],
$combination->objectlike_value_type,
$codebase,
$overwrite_empty_array,
);
}
$has_defined_keys = false;
foreach ($type->properties as $candidate_property_name => $candidate_property_type) {
$value_type = $combination->objectlike_entries[$candidate_property_name] ?? null;
if (!$value_type) {
$combination->objectlike_entries[$candidate_property_name] = $candidate_property_type
->setPossiblyUndefined($existing_objectlike_entries
@ -692,9 +678,35 @@ class TypeCombiner
$has_defined_keys = true;
}
if (($candidate_property_type->possibly_undefined || ($value_type->possibly_undefined ?? true))
&& $combination->fallbackKeyContains($candidate_property_name)
) {
$combination->objectlike_entries[$candidate_property_name] = Type::combineUnionTypes(
$combination->objectlike_entries[$candidate_property_name],
$combination->objectlike_value_type,
$codebase,
$overwrite_empty_array,
);
}
unset($missing_entries[$candidate_property_name]);
}
if ($type->fallback_params) {
$combination->objectlike_key_type = Type::combineUnionTypes(
$type->fallback_params[0],
$combination->objectlike_key_type,
$codebase,
$overwrite_empty_array,
);
$combination->objectlike_value_type = Type::combineUnionTypes(
$type->fallback_params[1],
$combination->objectlike_value_type,
$codebase,
$overwrite_empty_array,
);
}
if (!$has_defined_keys) {
$combination->array_always_filled = false;
}
@ -718,6 +730,20 @@ class TypeCombiner
->setPossiblyUndefined(true);
}
if ($combination->objectlike_value_type) {
foreach ($missing_entries as $k => $_) {
if (!$combination->fallbackKeyContains($k)) {
continue;
}
$combination->objectlike_entries[$k] = Type::combineUnionTypes(
$combination->objectlike_entries[$k],
$combination->objectlike_value_type,
$codebase,
$overwrite_empty_array,
);
}
}
if (!$type->is_list) {
$combination->all_arrays_lists = false;
} elseif ($combination->all_arrays_lists !== false) {

View File

@ -25,6 +25,7 @@ use function get_class;
use function implode;
use function is_int;
use function is_string;
use function ksort;
use function preg_match;
use function sort;
use function str_replace;
@ -85,6 +86,21 @@ class TKeyedArray extends Atomic
$this->fallback_params = $fallback_params;
$this->is_list = $is_list;
$this->from_docblock = $from_docblock;
if ($this->is_list) {
$last_k = -1;
$had_possibly_undefined = false;
ksort($this->properties);
foreach ($this->properties as $k => $v) {
if (is_string($k) || $last_k !== ($k-1) || ($had_possibly_undefined && !$v->possibly_undefined)) {
$this->is_list = false;
break;
}
if ($v->possibly_undefined) {
$had_possibly_undefined = true;
}
$last_k = $k;
}
}
}
/**
@ -98,6 +114,21 @@ class TKeyedArray extends Atomic
}
$cloned = clone $this;
$cloned->properties = $properties;
if ($cloned->is_list) {
$last_k = -1;
$had_possibly_undefined = false;
ksort($cloned->properties);
foreach ($cloned->properties as $k => $v) {
if (is_string($k) || $last_k !== ($k-1) || ($had_possibly_undefined && !$v->possibly_undefined)) {
$cloned->is_list = false;
break;
}
if ($v->possibly_undefined) {
$had_possibly_undefined = true;
}
$last_k = $k;
}
}
return $cloned;
}

View File

@ -433,6 +433,59 @@ class ArrayAccessTest extends TestCase
public function providerValidCodeParse(): iterable
{
return [
'testBuildList' => [
'code' => '<?php
$a = [];
if (random_int(0, 1)) {
$a []= 0;
}
if (random_int(0, 1)) {
$a []= 1;
}
$pre = $a;
$a []= 2;
',
'assertions' => [
'$pre===' => 'list{0?: 0|1, 1?: 1}',
'$a===' => 'list{0: 0|1|2, 1?: 1|2, 2?: 2}',
],
],
'testBuildListOther' => [
'code' => '<?php
$list = [];
$entropy = random_int(0, 2);
if ($entropy === 0) {
$list[] = "A";
} elseif ($entropy === 1) {
$list[] = "B";
}
$list[] = "C";
',
'assertions' => [
'$list===' => "list{0: 'A'|'B'|'C', 1?: 'C'}",
],
],
'testBuildList3' => [
'code' => '<?php
$a = [0];
if (random_int(0, 1)) {
$a []= 1;
}
if (random_int(0, 1)) {
$a []= 2;
}
$a []= 3;
',
'assertions' => [
'$a===' => "list{0: 0, 1: 1|2|3, 2?: 2|3, 3?: 3}",
],
],
'instanceOfStringOffset' => [
'code' => '<?php
class A {

View File

@ -211,6 +211,17 @@ class ArrayFunctionCallTest extends TestCase
'ignored_issues' => [],
'php_version' => '8.0',
],
'arrayMergeOverWrite' => [
'code' => '<?php
$a1 = ["a" => "a1"];
$a2 = ["a" => "a2"];
$result = array_merge($a1, $a2);
',
'assertions' => [
'$result===' => "array{a: 'a2'}",
],
],
'arrayMergeListOfShapes' => [
'code' => '<?php
@ -826,7 +837,7 @@ class ArrayFunctionCallTest extends TestCase
'$vars' => 'array{x: string, y: string}',
'$c' => 'string',
'$e' => 'list<string>',
'$f' => 'list<string>|string',
'$f' => 'list<string>',
],
],
'arrayKeysNoEmpty' => [
@ -1600,6 +1611,8 @@ class ArrayFunctionCallTest extends TestCase
/** @var array{a: array{v: "a", k: 0}, b: array{v: "b", k: 1}, c?: array{v: "c", k: 2}} */
$aa = [];
$k = array_column($aa, null, "k");
$l = array_column(["test" => ["v" => "a"], "test2" => ["v" => "b"]], "v");
',
'assertions' => [
'$a===' => "list{'a', 'b', 'c', 'd'}",
@ -1610,9 +1623,10 @@ class ArrayFunctionCallTest extends TestCase
'$f===' => "array{0: 'd', 1: 'c', 2: 'b', 3: 'a'}",
'$g===' => "list{array{k: 0, v: 'a'}, array{k: 1, v: 'b'}, array{k: 2, v: 'c'}, array{k: 3, v: 'd'}}",
'$h===' => "list{array{k: 0}, array{k: 1}, array{k: 2}}",
'$i===' => "array{a: 0, b?: 1}",
'$i===' => "list{0: 0, 1?: 1}",
'$j===' => "array{0: array{k: 0, v: 'a'}, 1?: array{k: 1, v: 'b'}, 2: array{k: 2, v: 'c'}}",
'$k===' => "list{0: array{k: 0, v: 'a'}, 1: array{k: 1, v: 'b'}, 2?: array{k: 2, v: 'c'}}",
'$l===' => "list{'a', 'b'}",
],
],
'splatArrayIntersect' => [

View File

@ -15,6 +15,57 @@ class ReturnTypeTest extends TestCase
public function providerValidCodeParse(): iterable
{
return [
'arrayCombine' => [
'code' => '<?php
class a {}
/**
* @return list{0, 0}|list<a>
*/
function ret() {
return [new a, new a, new a];
}
$result = ret();
',
'assertions' => [
'$result===' => 'list{0?: 0|a, 1?: 0|a, ...<int<0, max>, a>}',
],
],
'arrayCombineInv' => [
'code' => '<?php
class a {}
/**
* @return list<a>|list{0, 0}
*/
function ret() {
return [new a, new a, new a];
}
$result = ret();
',
'assertions' => [
'$result===' => 'list{0?: 0|a, 1?: 0|a, ...<int<0, max>, a>}',
],
],
'arrayCombine2' => [
'code' => '<?php
class a {}
/**
* @return array{test1: 0, test2: 0}|list<a>
*/
function ret() {
return [new a, new a, new a];
}
$result = ret();
',
'assertions' => [
'$result===' => 'array{0?: a, test1?: 0, test2?: 0, ...<int<0, max>, a>}',
],
],
'returnTypeAfterUselessNullCheck' => [
'code' => '<?php
class One {}

View File

@ -7,6 +7,8 @@ use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
use Psalm\Type;
use Psalm\Type\Atomic;
use function array_reverse;
class TypeCombinationTest extends TestCase
{
use ValidCodeAnalysisTestTrait;
@ -30,6 +32,11 @@ class TypeCombinationTest extends TestCase
$expected,
TypeCombiner::combine($converted_types)->getId(),
);
$this->assertSame(
$expected,
TypeCombiner::combine(array_reverse($converted_types))->getId(),
);
}
public function providerValidCodeParse(): iterable
@ -90,6 +97,20 @@ class TypeCombinationTest extends TestCase
public function providerTestValidTypeCombination(): array
{
return [
'complexArrayFallback1' => [
'array{other_references: list<Psalm\Internal\Analyzer\DataFlowNodeData>|null, taint_trace: list<array<array-key, mixed>>|null, ...<string, mixed>}',
[
'array{other_references: list<Psalm\Internal\Analyzer\DataFlowNodeData>|null, taint_trace: null}&array<string, mixed>',
'array{other_references: list<Psalm\Internal\Analyzer\DataFlowNodeData>|null, taint_trace: list<array<array-key, mixed>>}&array<string, mixed>',
],
],
'complexArrayFallback2' => [
'list{0?: 0|a, 1?: 0|a, ...<int<0, max>, a>}',
[
'list<a>',
'list{0, 0}',
],
],
'intOrString' => [
'int|string',
[