Skip to content

Commit

Permalink
Merge pull request #8943 from Nicelocal/fix_8940
Browse files Browse the repository at this point in the history
More array fixes
  • Loading branch information
orklah committed Dec 19, 2022
2 parents 96cb44b + 825f46f commit 62db5d4
Show file tree
Hide file tree
Showing 18 changed files with 330 additions and 51 deletions.
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

0 comments on commit 62db5d4

Please sign in to comment.