Skip to content

Commit 53a76dc

Browse files
committed
preg_split sometimes returns array of arrays
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."
1 parent 4f152b2 commit 53a76dc

File tree

4 files changed

+96
-0
lines changed

4 files changed

+96
-0
lines changed

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,11 @@ services:
895895
tags:
896896
- phpstan.broker.dynamicFunctionReturnTypeExtension
897897

898+
-
899+
class: PHPStan\Type\Php\PregSplitDynamicReturnTypeExtension
900+
tags:
901+
- phpstan.broker.dynamicFunctionReturnTypeExtension
902+
898903
-
899904
class: PHPStan\Type\Php\ReplaceFunctionsDynamicReturnTypeExtension
900905
tags:
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Arg;
6+
use PhpParser\Node\Expr\FuncCall;
7+
use PhpParser\Node\Name;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Reflection\FunctionReflection;
10+
use PHPStan\Reflection\ParametersAcceptorSelector;
11+
use PHPStan\Reflection\ReflectionProvider;
12+
use PHPStan\Type\ArrayType;
13+
use PHPStan\Type\Constant\ConstantArrayType;
14+
use PHPStan\Type\Constant\ConstantBooleanType;
15+
use PHPStan\Type\Constant\ConstantIntegerType;
16+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
17+
use PHPStan\Type\IntegerType;
18+
use PHPStan\Type\StringType;
19+
use PHPStan\Type\Type;
20+
use PHPStan\Type\TypeCombinator;
21+
22+
class PregSplitDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
23+
{
24+
25+
private ReflectionProvider $reflectionProvider;
26+
27+
public function __construct(ReflectionProvider $reflectionProvider)
28+
{
29+
$this->reflectionProvider = $reflectionProvider;
30+
}
31+
32+
33+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
34+
{
35+
return strtolower($functionReflection->getName()) === 'preg_split';
36+
}
37+
38+
39+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
40+
{
41+
$flagsArg = $functionCall->args[3] ?? null;
42+
43+
if ($this->hasFlag($this->getConstant('PREG_SPLIT_OFFSET_CAPTURE'), $flagsArg, $scope)) {
44+
$type = new ArrayType(
45+
new IntegerType(),
46+
new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new StringType(), new IntegerType()])
47+
);
48+
return TypeCombinator::union($type, new ConstantBooleanType(false));
49+
}
50+
51+
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
52+
}
53+
54+
55+
private function hasFlag(int $flag, ?Arg $expression, Scope $scope): bool
56+
{
57+
if ($expression === null) {
58+
return false;
59+
}
60+
61+
$type = $scope->getType($expression->value);
62+
return $type instanceof ConstantIntegerType && ($type->getValue() & $flag) === $flag;
63+
}
64+
65+
66+
private function getConstant(string $constantName): int
67+
{
68+
$constant = $this->reflectionProvider->getConstant(new Name($constantName), null);
69+
$valueType = $constant->getValueType();
70+
if (!$valueType instanceof ConstantIntegerType) {
71+
throw new \PHPStan\ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName));
72+
}
73+
74+
return $valueType->getValue();
75+
}
76+
77+
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10048,6 +10048,11 @@ public function dataBug2899(): array
1004810048
return $this->gatherAssertTypes(__DIR__ . '/data/bug-2899.php');
1004910049
}
1005010050

10051+
public function dataPregSplitReturnType(): array
10052+
{
10053+
return $this->gatherAssertTypes(__DIR__ . '/data/preg_split.php');
10054+
}
10055+
1005110056
/**
1005210057
* @dataProvider dataBug2574
1005310058
* @dataProvider dataBug2577
@@ -10116,6 +10121,7 @@ public function dataBug2899(): array
1011610121
* @dataProvider dataBug3133
1011710122
* @dataProvider dataBug2550
1011810123
* @dataProvider dataBug2899
10124+
* @dataProvider dataPregSplitReturnType
1011910125
* @param string $assertType
1012010126
* @param string $file
1012110127
* @param mixed ...$args
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
use function PHPStan\Analyser\assertType;
4+
5+
assertType('array<int, string>|false', preg_split('/-/', '1-2-3'));
6+
assertType('array<int, string>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY));
7+
assertType('array<int, array(string, int)>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE));
8+
assertType('array<int, array(string, int)>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE));

0 commit comments

Comments
 (0)