Skip to content

Commit abdddcb

Browse files
committed
Fix handling of str_split / mb_str_split string arg compound types
1 parent 07e341d commit abdddcb

File tree

6 files changed

+87
-17
lines changed

6 files changed

+87
-17
lines changed

src/Type/Php/StrSplitFunctionReturnTypeExtension.php

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616
use PHPStan\Type\Constant\ConstantStringType;
1717
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
1818
use PHPStan\Type\IntegerType;
19+
use PHPStan\Type\IntersectionType;
1920
use PHPStan\Type\StringType;
2021
use PHPStan\Type\Type;
2122
use PHPStan\Type\TypeCombinator;
23+
use PHPStan\Type\TypeTraverser;
2224
use PHPStan\Type\TypeUtils;
25+
use PHPStan\Type\UnionType;
2326
use function array_map;
2427
use function array_unique;
2528
use function count;
@@ -62,6 +65,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
6265
$splitLength = 1;
6366
}
6467

68+
$encoding = null;
6569
if ($functionReflection->getName() === 'mb_str_split') {
6670
if (count($functionCall->getArgs()) >= 3) {
6771
$strings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[2]->value));
@@ -85,22 +89,30 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
8589
}
8690

8791
$stringType = $scope->getType($functionCall->getArgs()[0]->value);
88-
if (!$stringType instanceof ConstantStringType) {
89-
return TypeCombinator::intersect(
90-
new ArrayType(new IntegerType(), new StringType()),
91-
new NonEmptyArrayType(),
92-
);
93-
}
94-
$stringValue = $stringType->getValue();
9592

96-
$items = isset($encoding)
97-
? mb_str_split($stringValue, $splitLength, $encoding)
98-
: str_split($stringValue, $splitLength);
99-
if ($items === false) {
100-
throw new ShouldNotHappenException();
101-
}
93+
return TypeTraverser::map($stringType, static function (Type $type, callable $traverse) use ($encoding, $splitLength, $scope): Type {
94+
if ($type instanceof UnionType || $type instanceof IntersectionType) {
95+
return $traverse($type);
96+
}
97+
98+
if (!$type instanceof ConstantStringType) {
99+
return TypeCombinator::intersect(
100+
new ArrayType(new IntegerType(), new StringType()),
101+
new NonEmptyArrayType(),
102+
);
103+
}
104+
105+
$stringValue = $type->getValue();
106+
107+
$items = $encoding !== null
108+
? mb_str_split($stringValue, $splitLength, $encoding)
109+
: str_split($stringValue, $splitLength);
110+
if ($items === false) {
111+
throw new ShouldNotHappenException();
112+
}
102113

103-
return self::createConstantArrayFrom($items, $scope);
114+
return self::createConstantArrayFrom($items, $scope);
115+
});
104116
}
105117

106118
/**

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5356,7 +5356,7 @@ public function dataFunctions(): array
53565356
'$strSplitConstantStringWithInvalidSplitLengthType',
53575357
],
53585358
[
5359-
'non-empty-array<int, string>',
5359+
'array{\'a\'|\'g\', \'b\'|\'h\', \'c\'|\'i\', \'d\'|\'j\', \'e\'|\'k\', \'f\'|\'l\'}',
53605360
'$strSplitConstantStringWithVariableStringAndConstantSplitLength',
53615361
],
53625362
[
@@ -8744,7 +8744,7 @@ public function dataPhp74Functions(): array
87448744
'$mbStrSplitConstantStringWithInvalidSplitLengthType',
87458745
],
87468746
[
8747-
'non-empty-array<int, string>',
8747+
'array{\'a\'|\'g\', \'b\'|\'h\', \'c\'|\'i\', \'d\'|\'j\', \'e\'|\'k\', \'f\'|\'l\'}',
87488748
'$mbStrSplitConstantStringWithVariableStringAndConstantSplitLength',
87498749
],
87508750
[
@@ -8800,7 +8800,7 @@ public function dataPhp74Functions(): array
88008800
'$mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding',
88018801
],
88028802
[
8803-
'non-empty-array<int, string>',
8803+
'array{\'a\'|\'g\', \'b\'|\'h\', \'c\'|\'i\', \'d\'|\'j\', \'e\'|\'k\', \'f\'|\'l\'}',
88048804
'$mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndValidEncoding',
88058805
],
88068806
[

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,7 @@ public function dataFileAsserts(): iterable
923923
yield from $this->gatherAssertTypes(__DIR__ . '/data/emptyiterator.php');
924924
yield from $this->gatherAssertTypes(__DIR__ . '/data/collected-data.php');
925925
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7550.php');
926+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7580.php');
926927
}
927928

928929
/**
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug7580;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
assertType('array{}', mb_str_split('', 1));
8+
9+
assertType('array{\'x\'}', mb_str_split('x', 1));
10+
11+
$v = (string) (mt_rand() === 0 ? '' : 'x');
12+
assertType('\'\'|\'x\'', $v);
13+
assertType('array{}|array{\'x\'}', mb_str_split($v, 1));
14+
15+
function x(): string { throw new \Exception(); };
16+
$v = x();
17+
assertType('string', $v);
18+
assertType('non-empty-array<int, string>', mb_str_split($v, 1));

tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,23 @@ public function testReportPhpDoc(): void
9292
]);
9393
}
9494

95+
public function testBug7580(): void
96+
{
97+
$this->treatPhpDocTypesAsCertain = false;
98+
$this->analyse([__DIR__ . '/data/bug-7580.php'], [
99+
[
100+
'Ternary operator condition is always false.',
101+
6,
102+
],
103+
[
104+
'Ternary operator condition is always true.',
105+
9,
106+
],
107+
[
108+
'Ternary operator condition is always true.',
109+
20,
110+
],
111+
]);
112+
}
113+
95114
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug7580;
4+
5+
print_r(mb_str_split('', 1));
6+
print_r(mb_str_split('', 1) ?: ['']);
7+
8+
print_r(mb_str_split('x', 1));
9+
print_r(mb_str_split('x', 1) ?: ['']);
10+
11+
$v = (string) (mt_rand() === 0 ? '' : 'x');
12+
\PHPStan\dumpType($v);
13+
print_r(mb_str_split($v, 1));
14+
print_r(mb_str_split($v, 1) ?: ['']); // there must be no phpstan error for this line
15+
16+
function x(): string { throw new \Exception(); };
17+
$v = x();
18+
\PHPStan\dumpType($v);
19+
print_r(mb_str_split($v, 1));
20+
print_r(mb_str_split($v, 1) ?: ['']); // there must be no phpstan error for this line

0 commit comments

Comments
 (0)