Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More array fixes #8943

Merged
merged 22 commits into from Dec 19, 2022
2 changes: 2 additions & 0 deletions UPGRADING.md
Expand Up @@ -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

Expand Down
21 changes: 10 additions & 11 deletions psalm-baseline.xml
@@ -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>
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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">
Expand Down Expand Up @@ -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">
<code>$type_tokens[$i - 1]</code>
<code>$type_tokens[$i - 1]</code>
<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 - 2]</code>
</PossiblyInvalidArrayOffset>
</file>
<file src="src/Psalm/Storage/ClassConstantStorage.php">
Expand Down
7 changes: 4 additions & 3 deletions src/Psalm/Internal/Analyzer/Statements/Block/ForAnalyzer.php
Expand Up @@ -106,14 +106,15 @@ public static function analyze(
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;
Expand Down
Expand Up @@ -27,7 +27,8 @@ public static function analyze(
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;

Expand Down
Expand Up @@ -649,17 +649,16 @@ private static function updateArrayAssignmentChildType(
]);
} 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,
);
}
Expand Down
Expand Up @@ -1533,6 +1533,29 @@ private static function handleArrayAccessOnKeyedArray(
$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(
Expand Down
Expand Up @@ -334,7 +334,16 @@ public static function getPathTo(
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;
}
}
}

Expand Down
Expand Up @@ -89,9 +89,13 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
$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,
Expand Down Expand Up @@ -142,6 +146,9 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
$ok = false;
break;
}
} else {
/** @psalm-suppress StringIncrement Actually always an int in this branch */
++$key;
}

$properties[$key] = $result_element_type->setPossiblyUndefined(
Expand Down
Expand Up @@ -149,7 +149,6 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
if (!isset($generic_properties[$key]) || (
!$type->possibly_undefined
&& !$unpacking_possibly_empty
&& $is_replace
)) {
if ($unpacking_possibly_empty) {
$type = $type->setPossiblyUndefined(true);
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -54,15 +53,21 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
$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;
}

Expand Down
Expand Up @@ -47,8 +47,9 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev

$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_) {
Expand Down
37 changes: 37 additions & 0 deletions src/Psalm/Internal/Type/TypeCombination.php
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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;
}
}
56 changes: 41 additions & 15 deletions src/Psalm/Internal/Type/TypeCombiner.php
Expand Up @@ -649,26 +649,12 @@ private static function scrapeTypeProperties(
$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
Expand All @@ -692,9 +678,35 @@ private static function scrapeTypeProperties(
$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;
}
Expand All @@ -718,6 +730,20 @@ private static function scrapeTypeProperties(
->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) {
Expand Down