Skip to content

Commit 9bb7c8c

Browse files
committed
RemoveFalseReturnTypeExtension: preserved |false for UTF-8 validation patterns like //u
1 parent 9441f21 commit 9bb7c8c

File tree

2 files changed

+41
-3
lines changed

2 files changed

+41
-3
lines changed

src/Php/RemoveFalseReturnTypeExtension.php

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
use PHPStan\Type\ExpressionTypeResolverExtension;
2020
use PHPStan\Type\Type;
2121
use PHPStan\Type\TypeCombinator;
22-
use function array_merge, explode, str_contains, str_starts_with, strtolower;
22+
use function array_merge, explode, str_contains, str_starts_with, strlen, strrpos, strtolower, substr;
2323

2424

2525
/**
@@ -90,9 +90,10 @@ private function resolveFuncCall(FuncCall $expr, Scope $scope): ?Type
9090
}
9191

9292
// preg_* functions return false only for invalid patterns, so skip narrowing for non-constant patterns
93+
// Also preserve |false for UTF-8 validation patterns like //u where false means invalid UTF-8
9394
if (str_starts_with($functionName, 'preg_')) {
9495
$args = $expr->getArgs();
95-
if ($args === [] || $scope->getType($args[0]->value)->getConstantStrings() === []) {
96+
if ($args === [] || self::isUtf8ValidationPattern($scope->getType($args[0]->value))) {
9697
return null;
9798
}
9899
}
@@ -259,6 +260,32 @@ private function isMethodSupportedByType(Type $callerType, string $methodName):
259260
}
260261

261262

263+
/**
264+
* Returns true for non-constant patterns or UTF-8 validation patterns (empty body + u modifier).
265+
*/
266+
private static function isUtf8ValidationPattern(Type $patternType): bool
267+
{
268+
$constants = $patternType->getConstantStrings();
269+
if ($constants === []) {
270+
return true; // non-constant → preserve |false
271+
}
272+
273+
foreach ($constants as $constant) {
274+
$pattern = $constant->getValue();
275+
if (
276+
strlen($pattern) >= 2
277+
&& ($lastPos = strrpos($pattern, $pattern[0], 1)) !== false
278+
&& $lastPos === 1 // empty body → delimiter at pos 0 and 1
279+
&& str_contains(substr($pattern, 2), 'u')
280+
) {
281+
return true;
282+
}
283+
}
284+
285+
return false;
286+
}
287+
288+
262289
private static function removeFalse(Type $type): Type
263290
{
264291
return TypeCombinator::remove($type, new ConstantBooleanType(false));

tests/data/narrow-return-type.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
<?php declare(strict_types=1);
1+
<?php
2+
3+
declare(strict_types=1);
24

35
use function PHPStan\Testing\assertType;
46

@@ -36,6 +38,15 @@ function testRegexConstant(string $s): void
3638
}
3739

3840

41+
// Regex (UTF-8 validation pattern //u — |false preserved)
42+
function testRegexUtf8Validation(string $s): void
43+
{
44+
assertType('0|1|false', preg_match('//u', $s));
45+
assertType('list<string>|false', preg_split('//u', $s));
46+
assertType('array|false', preg_grep('//u', [$s]));
47+
}
48+
49+
3950
// Regex (non-constant pattern — |false preserved)
4051
function testRegexDynamic(string $pattern, string $s): void
4152
{

0 commit comments

Comments
 (0)