|
19 | 19 | use PHPStan\Type\ExpressionTypeResolverExtension; |
20 | 20 | use PHPStan\Type\Type; |
21 | 21 | 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; |
23 | 23 |
|
24 | 24 |
|
25 | 25 | /** |
@@ -90,9 +90,10 @@ private function resolveFuncCall(FuncCall $expr, Scope $scope): ?Type |
90 | 90 | } |
91 | 91 |
|
92 | 92 | // 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 |
93 | 94 | if (str_starts_with($functionName, 'preg_')) { |
94 | 95 | $args = $expr->getArgs(); |
95 | | - if ($args === [] || $scope->getType($args[0]->value)->getConstantStrings() === []) { |
| 96 | + if ($args === [] || self::isUtf8ValidationPattern($scope->getType($args[0]->value))) { |
96 | 97 | return null; |
97 | 98 | } |
98 | 99 | } |
@@ -259,6 +260,32 @@ private function isMethodSupportedByType(Type $callerType, string $methodName): |
259 | 260 | } |
260 | 261 |
|
261 | 262 |
|
| 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 | + |
262 | 289 | private static function removeFalse(Type $type): Type |
263 | 290 | { |
264 | 291 | return TypeCombinator::remove($type, new ConstantBooleanType(false)); |
|
0 commit comments