From e2c94bb28c00bc0d52a7c905852971121a307e7c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 27 Jul 2024 22:34:22 +0200 Subject: [PATCH] remove delimiters and modifiers before parsing --- src/Type/Php/RegexArrayShapeMatcher.php | 3 +- src/Type/Php/RegexExpressionHelper.php | 46 +++++++++++++------ .../Analyser/nsrt/preg_match_shapes.php | 4 +- .../Analyser/nsrt/preg_match_shapes_php82.php | 5 ++ 4 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 3e65133284..626bc4c35d 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -406,8 +406,9 @@ private function parseGroups(string $regex): ?array return null; } + $rawRegex = $this->regexExpressionHelper->removeDelimitersAndModifiers($regex); try { - $ast = self::$parser->parse($regex); + $ast = self::$parser->parse($rawRegex); } catch (Exception) { return null; } diff --git a/src/Type/Php/RegexExpressionHelper.php b/src/Type/Php/RegexExpressionHelper.php index b06d977e94..532bc4c914 100644 --- a/src/Type/Php/RegexExpressionHelper.php +++ b/src/Type/Php/RegexExpressionHelper.php @@ -72,11 +72,33 @@ public function resolve(Expr $expr): Type public function getPatternModifiers(string $pattern): ?string { - $delimiter = $this->getDelimiterFromString(new ConstantStringType($pattern)); - if ($delimiter === null) { + $endDelimiterPos = $this->getEndDelimiterPos($pattern); + + if ($endDelimiterPos === false) { return null; } + return substr($pattern, $endDelimiterPos + 1); + } + + public function removeDelimitersAndModifiers(string $pattern): string + { + $endDelimiterPos = $this->getEndDelimiterPos($pattern); + + if ($endDelimiterPos === false) { + return $pattern; + } + + return substr($pattern, 1, $endDelimiterPos - 1); + } + + private function getEndDelimiterPos(string $pattern): false|int + { + $startDelimiter = $this->getPatternDelimiter($pattern); + if ($startDelimiter === null) { + return false; + } + // delimiter variants, see https://www.php.net/manual/en/regexp.reference.delimiters.php $bracketStyleDelimiters = [ '{' => '}', @@ -84,18 +106,14 @@ public function getPatternModifiers(string $pattern): ?string '[' => ']', '<' => '>', ]; - if (array_key_exists($delimiter, $bracketStyleDelimiters)) { - $endDelimiterPos = strrpos($pattern, $bracketStyleDelimiters[$delimiter]); + if (array_key_exists($startDelimiter, $bracketStyleDelimiters)) { + $endDelimiterPos = strrpos($pattern, $bracketStyleDelimiters[$startDelimiter]); } else { // same start and end delimiter - $endDelimiterPos = strrpos($pattern, $delimiter); + $endDelimiterPos = strrpos($pattern, $startDelimiter); } - if ($endDelimiterPos === false) { - return null; - } - - return substr($pattern, $endDelimiterPos + 1); + return $endDelimiterPos; } /** @@ -113,7 +131,7 @@ public function getPatternDelimiters(Concat $concat, Scope $scope): array $delimiters = []; foreach ($left->getConstantStrings() as $leftString) { - $delimiter = $this->getDelimiterFromString($leftString); + $delimiter = $this->getPatternDelimiter($leftString->getValue()); if ($delimiter === null) { continue; } @@ -123,13 +141,13 @@ public function getPatternDelimiters(Concat $concat, Scope $scope): array return $delimiters; } - private function getDelimiterFromString(ConstantStringType $string): ?string + private function getPatternDelimiter(string $regex): ?string { - if ($string->getValue() === '') { + if ($regex === '') { return null; } - return substr($string->getValue(), 0, 1); + return substr($regex, 0, 1); } } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index c273a83a47..af57075078 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -29,9 +29,9 @@ function doMatch(string $s): void { assertType('array{}|array{string, non-empty-string}', $matches); if (preg_match('(Price: (£|€))i', $s, $matches)) { - assertType('array{string, non-empty-string, non-empty-string}', $matches); + assertType('array{string, non-empty-string}', $matches); } - assertType('array{}|array{string, non-empty-string, non-empty-string}', $matches); + assertType('array{}|array{string, non-empty-string}', $matches); if (preg_match('_foo(.)\_i_i', $s, $matches)) { assertType('array{string, non-empty-string}', $matches); diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php index e337e4707a..dfbcab477e 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php @@ -39,3 +39,8 @@ function (string $s): void { assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches); } }; +function (string $s): void { + if (preg_match('[(\d+)(?P\d+)]n', $s, $matches)) { + assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches); + } +};