Skip to content

Commit

Permalink
Fix array_splice bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Sep 22, 2021
1 parent 5f91da6 commit 6754df5
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 0 deletions.
5 changes: 5 additions & 0 deletions conf/config.neon
Expand Up @@ -948,6 +948,11 @@ services:
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\ArraySpliceFunctionReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\ArraySearchFunctionDynamicReturnTypeExtension
tags:
Expand Down
17 changes: 17 additions & 0 deletions src/Analyser/NodeScopeResolver.php
Expand Up @@ -1934,6 +1934,23 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression
$scope = $scope->assignVariable('http_response_header', new ArrayType(new IntegerType(), new StringType()));
}

if (
isset($functionReflection)
&& $functionReflection->getName() === 'array_splice'
&& count($expr->getArgs()) >= 1
) {
$arrayArg = $expr->getArgs()[0]->value;
$arrayArgType = $scope->getType($arrayArg);
$valueType = $arrayArgType->getIterableValueType();
if (count($expr->getArgs()) >= 4) {
$valueType = TypeCombinator::union($valueType, $scope->getType($expr->getArgs()[3]->value)->getIterableValueType());
}
$scope = $scope->invalidateExpression($arrayArg)->specifyExpressionType(
$arrayArg,
new ArrayType($arrayArgType->getIterableKeyType(), $valueType)
);
}

if (isset($functionReflection) && $functionReflection->getName() === 'extract') {
$scope = $scope->afterExtractCall();
}
Expand Down
35 changes: 35 additions & 0 deletions src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php
@@ -0,0 +1,35 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\ArrayType;
use PHPStan\Type\Type;

class ArraySpliceFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension
{

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'array_splice';
}

public function getTypeFromFunctionCall(
FunctionReflection $functionReflection,
FuncCall $functionCall,
Scope $scope
): Type
{
if (!isset($functionCall->getArgs()[0])) {
return ParametersAcceptorSelector::selectFromArgs($scope, $functionCall->getArgs(), $functionReflection->getVariants())->getReturnType();
}

$arrayArg = $scope->getType($functionCall->getArgs()[0]->value);

return new ArrayType($arrayArg->getIterableKeyType(), $arrayArg->getIterableValueType());
}

}
2 changes: 2 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -511,6 +511,8 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/eval-implicit-throw.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5628.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5501.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4743.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5017.php');
}

/**
Expand Down
34 changes: 34 additions & 0 deletions tests/PHPStan/Analyser/data/bug-4743.php
@@ -0,0 +1,34 @@
<?php

namespace Bug4743;

use function PHPStan\Testing\assertType;

class Node {}

/**
* @template T of Node
*/
class NodeList {

/**
* @phpstan-var array<T>
*/
private $nodes;

/**
* @phpstan-param array<T> $nodes
*/
public function __construct(array $nodes)
{
$this->nodes = $nodes;
}

public function splice(int $offset, int $length): void
{
$newNodes = array_splice($this->nodes, $offset, $length);

assertType('array<T of Bug4743\\Node (class Bug4743\\NodeList, argument)>', $this->nodes);
assertType('array<T of Bug4743\\Node (class Bug4743\\NodeList, argument)>', $newNodes);
}
}
56 changes: 56 additions & 0 deletions tests/PHPStan/Analyser/data/bug-5017.php
@@ -0,0 +1,56 @@
<?php

namespace Bug5017;

use function PHPStan\Testing\assertType;

class Foo
{

public function doFoo()
{
$items = [0, 1, 2, 3, 4];

while ($items) {
assertType('array<0|1|2|3|4, 0|1|2|3|4>&nonEmpty', $items);
$batch = array_splice($items, 0, 2);
assertType('array<0|1|2|3|4, 0|1|2|3|4>', $items);
assertType('array<0|1|2|3|4, 0|1|2|3|4>', $batch);
}
}

/**
* @param int[] $items
*/
public function doBar($items)
{
while ($items) {
assertType('array<int>&nonEmpty', $items);
$batch = array_splice($items, 0, 2);
assertType('array<int>', $items);
assertType('array<int>', $batch);
}
}

public function doBar2()
{
$items = [0, 1, 2, 3, 4];
assertType('array(0, 1, 2, 3, 4)', $items);
$batch = array_splice($items, 0, 2);
assertType('array<0|1|2|3|4, 0|1|2|3|4>', $items);
assertType('array<0|1|2|3|4, 0|1|2|3|4>', $batch);
}

/**
* @param int[] $ints
* @param string[] $strings
*/
public function doBar3(array $ints, array $strings)
{
$removed = array_splice($ints, 0, 2, $strings);
assertType('array<int>', $removed);
assertType('array<int|string>', $ints);
assertType('array<string>', $strings);
}

}

0 comments on commit 6754df5

Please sign in to comment.