Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 95 additions & 1 deletion src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@
use UnhandledMatchError;
use function array_fill_keys;
use function array_filter;
use function array_flip;
use function array_key_exists;
use function array_key_last;
use function array_keys;
Expand Down Expand Up @@ -3049,7 +3050,25 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto

if (
$functionReflection !== null
&& in_array($functionReflection->getName(), ['natcasesort', 'natsort', 'arsort', 'asort', 'ksort', 'krsort', 'uasort', 'uksort'], true)
&& in_array($functionReflection->getName(), ['ksort', 'krsort'], true)
&& count($normalizedExpr->getArgs()) >= 1
) {
$arrayArg = $normalizedExpr->getArgs()[0]->value;
$reverse = $functionReflection->getName() === 'krsort';

$scope = $this->processVirtualAssign(
$scope,
$storage,
$stmt,
$arrayArg,
new NativeTypeExpr($this->getArrayKsortFunctionType($scope->getType($arrayArg), $reverse), $this->getArrayKsortFunctionType($scope->getNativeType($arrayArg), $reverse)),
$nodeCallback,
)->getScope();
}

if (
$functionReflection !== null
&& in_array($functionReflection->getName(), ['natcasesort', 'natsort', 'arsort', 'asort', 'uasort', 'uksort'], true)
&& count($normalizedExpr->getArgs()) >= 1
) {
$arrayArg = $normalizedExpr->getArgs()[0]->value;
Expand Down Expand Up @@ -4734,6 +4753,81 @@ private function getArraySortDoNotPreserveListFunctionType(Type $type): Type
});
}

private function getArrayKsortFunctionType(Type $type, bool $reverse): Type
{
$isIterableAtLeastOnce = $type->isIterableAtLeastOnce();
if ($isIterableAtLeastOnce->no()) {
return $type;
}

return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($isIterableAtLeastOnce, $reverse): Type {
if ($type instanceof UnionType) {
return $traverse($type);
}

$constantArrays = $type->getConstantArrays();
if (count($constantArrays) > 0) {
$types = [];
foreach ($constantArrays as $constantArray) {
$keyTypes = $constantArray->getKeyTypes();
$valueTypes = $constantArray->getValueTypes();
$optionalKeys = $constantArray->getOptionalKeys();

$indices = array_keys($keyTypes);
usort($indices, static function (int $a, int $b) use ($keyTypes, $reverse): int {
$keyA = $keyTypes[$a]->getValue();
$keyB = $keyTypes[$b]->getValue();
$result = $keyA <=> $keyB;
return $reverse ? -$result : $result;
});

$sortedKeyTypes = [];
$sortedValueTypes = [];
$newOptionalKeys = [];
$optionalKeysSet = array_flip($optionalKeys);
foreach ($indices as $newIndex => $oldIndex) {
$sortedKeyTypes[] = $keyTypes[$oldIndex];
$sortedValueTypes[] = $valueTypes[$oldIndex];
if (!isset($optionalKeysSet[$oldIndex])) {
continue;
}

$newOptionalKeys[] = $newIndex;
}

$isList = TrinaryLogic::createNo();
$isSequential = true;
foreach ($sortedKeyTypes as $i => $keyType) {
if (!$keyType instanceof ConstantIntegerType || $keyType->getValue() !== $i) {
$isSequential = false;
break;
}
}
if ($isSequential) {
$isList = $constantArray->isList();
}

$types[] = new ConstantArrayType(
$sortedKeyTypes,
$sortedValueTypes,
$constantArray->getNextAutoIndexes(),
$newOptionalKeys,
$isList,
);
}

return TypeCombinator::union(...$types);
}

$newArrayType = new ArrayType($type->getIterableKeyType(), $type->getIterableValueType());
if ($isIterableAtLeastOnce->yes()) {
$newArrayType = new IntersectionType([$newArrayType, new NonEmptyArrayType()]);
}

return $newArrayType;
});
}

private function getFunctionThrowPoint(
FunctionReflection $functionReflection,
?ParametersAcceptor $parametersAcceptor,
Expand Down
8 changes: 4 additions & 4 deletions tests/PHPStan/Analyser/nsrt/bug-10627.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public function sayHello5(): void
$list = ['A', 'C', 'B'];
ksort($list);
assertType("array{'A', 'C', 'B'}", $list);
assertType('bool', array_is_list($list));
assertType('true', array_is_list($list));
}

public function sayHello6(): void
Expand All @@ -71,8 +71,8 @@ public function sayHello8(): void
{
$list = ['A', 'C', 'B'];
krsort($list);
assertType("array{'A', 'C', 'B'}", $list);
assertType('bool', array_is_list($list));
assertType("array{2: 'B', 1: 'C', 0: 'A'}", $list);
assertType('false', array_is_list($list));
}

/**
Expand All @@ -89,7 +89,7 @@ public function sayHello10(): void
{
$list = ['a' => 'A', 'c' => 'C', 'b' => 'B'];
krsort($list);
assertType("array{a: 'A', c: 'C', b: 'B'}", $list);
assertType("array{c: 'C', b: 'B', a: 'A'}", $list);
assertType('false', array_is_list($list));
}
}
74 changes: 74 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-11569.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php declare(strict_types = 1);

namespace Bug11569;

use function PHPStan\Testing\assertType;

class HelloWorld
{
/** @return array{name: string, age: int} */
public function getFoo(): array
{
return ['name' => 'John Doe', 'age' => 30];
}

public function testKsort(): void
{
$value = ['name' => 'John Doe', 'age' => 30];
ksort($value);
assertType("array{age: 30, name: 'John Doe'}", $value);
}

public function testKrsort(): void
{
$value = ['name' => 'John Doe', 'age' => 30];
krsort($value);
assertType("array{name: 'John Doe', age: 30}", $value);
}

public function testKsortWithArrayValues(): void
{
$data = $this->getFoo();
ksort($data);
assertType('array{age: int, name: string}', $data);
$values = array_values($data);
assertType('array{int, string}', $values);
}

public function testKsortWithArrayCombine(): void
{
$data = $this->getFoo();
ksort($data);
$values = array_values($data);
$result = array_combine(['age', 'name'], $values);
assertType('array{age: int, name: string}', $result);
}

public function testKsortIntegerKeys(): void
{
$list = ['A', 'C', 'B'];
ksort($list);
assertType("array{'A', 'C', 'B'}", $list);
}

public function testKrsortIntegerKeys(): void
{
$list = ['A', 'C', 'B'];
krsort($list);
assertType("array{2: 'B', 1: 'C', 0: 'A'}", $list);
}

public function testKsortStringKeys(): void
{
$value = ['c' => 3, 'a' => 1, 'b' => 2];
ksort($value);
assertType('array{a: 1, b: 2, c: 3}', $value);
}

public function testKrsortStringKeys(): void
{
$value = ['c' => 3, 'a' => 1, 'b' => 2];
krsort($value);
assertType('array{c: 3, b: 2, a: 1}', $value);
}
}
Loading