diff --git a/src/Type/Php/RegexGroupParser.php b/src/Type/Php/RegexGroupParser.php index 83dbf58b7d..263774dba5 100644 --- a/src/Type/Php/RegexGroupParser.php +++ b/src/Type/Php/RegexGroupParser.php @@ -67,9 +67,9 @@ public function parseGroups(string $regex): ?array } $captureOnlyNamed = false; + $modifiers = $this->regexExpressionHelper->getPatternModifiers($regex) ?? ''; if ($this->phpVersion->supportsPregCaptureOnlyNamedGroups()) { - $modifiers = $this->regexExpressionHelper->getPatternModifiers($regex); - $captureOnlyNamed = str_contains($modifiers ?? '', 'n'); + $captureOnlyNamed = str_contains($modifiers, 'n'); } $capturingGroups = []; @@ -90,6 +90,7 @@ public function parseGroups(string $regex): ?array $markVerbs, $captureOnlyNamed, false, + $modifiers, ); return [$capturingGroups, $groupCombinations, $markVerbs]; @@ -113,30 +114,21 @@ private function walkRegexAst( array &$markVerbs, bool $captureOnlyNamed, bool $repeatedMoreThanOnce, + string $patternModifiers, ): void { $group = null; if ($ast->getId() === '#capturing') { - $maybeConstant = !$repeatedMoreThanOnce; - if ($parentGroup !== null && $parentGroup->resetsGroupCounter()) { - $maybeConstant = false; - } - $group = new RegexCapturingGroup( $captureGroupId++, null, $inAlternation ? $alternationId : null, $inOptionalQuantification, $parentGroup, - $this->createGroupType($ast, $maybeConstant), + $this->createGroupType($ast, $this->allowConstantTypes($patternModifiers, $repeatedMoreThanOnce, $parentGroup)), ); $parentGroup = $group; } elseif ($ast->getId() === '#namedcapturing') { - $maybeConstant = !$repeatedMoreThanOnce; - if ($parentGroup !== null && $parentGroup->resetsGroupCounter()) { - $maybeConstant = false; - } - $name = $ast->getChild(0)->getValueValue(); $group = new RegexCapturingGroup( $captureGroupId++, @@ -144,7 +136,7 @@ private function walkRegexAst( $inAlternation ? $alternationId : null, $inOptionalQuantification, $parentGroup, - $this->createGroupType($ast, $maybeConstant), + $this->createGroupType($ast, $this->allowConstantTypes($patternModifiers, $repeatedMoreThanOnce, $parentGroup)), ); $parentGroup = $group; } elseif ($ast->getId() === '#noncapturing') { @@ -217,6 +209,7 @@ private function walkRegexAst( $markVerbs, $captureOnlyNamed, $repeatedMoreThanOnce, + $patternModifiers, ); if ($ast->getId() !== '#alternation') { @@ -227,6 +220,29 @@ private function walkRegexAst( } } + private function allowConstantTypes( + string $patternModifiers, + bool $repeatedMoreThanOnce, + RegexCapturingGroup|RegexNonCapturingGroup|null $parentGroup, + ): bool + { + if (str_contains($patternModifiers, 'i')) { + // if caseless, we don't use constant types + // because it likely yields too many combinations + return false; + } + + if ($repeatedMoreThanOnce) { + return false; + } + + if ($parentGroup !== null && $parentGroup->resetsGroupCounter()) { + return false; + } + + return true; + } + /** @return array{?int, ?int} */ private function getQuantificationRange(TreeNode $node): array { diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index fde7690f7c..ce48099cf1 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -549,9 +549,9 @@ public function test2(string $str): void function (string $s): void { if (rand(0,1)) { - $p = '/Price: (£)(abc)/i'; + $p = '/Price: (£)(abc)/'; } else { - $p = '/Price: (\d)(b)/i'; + $p = '/Price: (\d)(b)/'; } if (preg_match($p, $s, $matches)) { @@ -561,9 +561,9 @@ function (string $s): void { function (string $s): void { if (rand(0,1)) { - $p = '/Price: (£)/i'; + $p = '/Price: (£)/'; } else { - $p = '/Price: (£|(\d)|(x))/i'; + $p = '/Price: (£|(\d)|(x))/'; } if (preg_match($p, $s, $matches)) { @@ -585,18 +585,24 @@ function (string $s): void { function (string $s): void { if (preg_match('/Price: ([xXa])/i', $s, $matches)) { + assertType("array{string, non-empty-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: ([xXa])/', $s, $matches)) { assertType("array{string, 'a'|'X'|'x'}", $matches); } }; function (string $s): void { - if (preg_match('/Price: (ba[rz])/i', $s, $matches)) { + if (preg_match('/Price: (ba[rz])/', $s, $matches)) { assertType("array{string, 'bar'|'baz'}", $matches); } }; function (string $s): void { - if (preg_match('/Price: (b[ao][mn])/i', $s, $matches)) { + if (preg_match('/Price: (b[ao][mn])/', $s, $matches)) { assertType("array{string, 'bam'|'ban'|'bom'|'bon'}", $matches); } };