Skip to content

Commit

Permalink
preg_split sometimes returns array of arrays
Browse files Browse the repository at this point in the history
When PREG_SPLIT_OFFSET_CAPTURE is given, "this changes the return value in an array where every element is an array consisting of the matched string at offset 0 and its string offset into subject at offset 1."
  • Loading branch information
spaze authored and ondrejmirtes committed Sep 22, 2020
1 parent 2dc11d6 commit 1be28f3
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 0 deletions.
5 changes: 5 additions & 0 deletions conf/config.neon
Expand Up @@ -895,6 +895,11 @@ services:
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

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

-
class: PHPStan\Type\Php\ReplaceFunctionsDynamicReturnTypeExtension
tags:
Expand Down
77 changes: 77 additions & 0 deletions src/Type/Php/PregSplitDynamicReturnTypeExtension.php
@@ -0,0 +1,77 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Arg;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\ArrayType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\IntegerType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;

class PregSplitDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

private ReflectionProvider $reflectionProvider;

public function __construct(ReflectionProvider $reflectionProvider)
{
$this->reflectionProvider = $reflectionProvider;
}


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


public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
{
$flagsArg = $functionCall->args[3] ?? null;

if ($this->hasFlag($this->getConstant('PREG_SPLIT_OFFSET_CAPTURE'), $flagsArg, $scope)) {
$type = new ArrayType(
new IntegerType(),
new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new StringType(), new IntegerType()])
);
return TypeCombinator::union($type, new ConstantBooleanType(false));
}

return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
}


private function hasFlag(int $flag, ?Arg $expression, Scope $scope): bool
{
if ($expression === null) {
return false;
}

$type = $scope->getType($expression->value);
return $type instanceof ConstantIntegerType && ($type->getValue() & $flag) === $flag;
}


private function getConstant(string $constantName): int
{
$constant = $this->reflectionProvider->getConstant(new Name($constantName), null);
$valueType = $constant->getValueType();
if (!$valueType instanceof ConstantIntegerType) {
throw new \PHPStan\ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName));
}

return $valueType->getValue();
}

}
6 changes: 6 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -10048,6 +10048,11 @@ public function dataBug2899(): array
return $this->gatherAssertTypes(__DIR__ . '/data/bug-2899.php');
}

public function dataPregSplitReturnType(): array
{
return $this->gatherAssertTypes(__DIR__ . '/data/preg_split.php');
}

/**
* @dataProvider dataBug2574
* @dataProvider dataBug2577
Expand Down Expand Up @@ -10116,6 +10121,7 @@ public function dataBug2899(): array
* @dataProvider dataBug3133
* @dataProvider dataBug2550
* @dataProvider dataBug2899
* @dataProvider dataPregSplitReturnType
* @param string $assertType
* @param string $file
* @param mixed ...$args
Expand Down
8 changes: 8 additions & 0 deletions tests/PHPStan/Analyser/data/preg_split.php
@@ -0,0 +1,8 @@
<?php

use function PHPStan\Analyser\assertType;

assertType('array<int, string>|false', preg_split('/-/', '1-2-3'));
assertType('array<int, string>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY));
assertType('array<int, array(string, int)>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE));
assertType('array<int, array(string, int)>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE));

0 comments on commit 1be28f3

Please sign in to comment.