mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 04:45:20 +01:00
commit
62db5d4f6a
@ -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
|
||||
|
||||
|
@ -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->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">
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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_) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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' => [
|
||||
|
@ -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 {}
|
||||
|
@ -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',
|
||||
[
|
||||
|
Loading…
Reference in New Issue
Block a user