diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 03bbcb6cce..b4000d6ada 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -445,14 +445,14 @@ private function walkGroupAst( foreach ($children as $child) { $oldLiterals = $onlyLiterals; - $this->getLiteralValue($child, $oldLiterals, true, $patternModifiers); + $this->getLiteralValue($child, $oldLiterals, true, $patternModifiers, true); foreach ($oldLiterals ?? [] as $oldLiteral) { $newLiterals[] = $oldLiteral; } } $onlyLiterals = $newLiterals; } elseif ($ast->getId() === 'token') { - $literalValue = $this->getLiteralValue($ast, $onlyLiterals, !$inClass, $patternModifiers); + $literalValue = $this->getLiteralValue($ast, $onlyLiterals, !$inClass, $patternModifiers, false); if ($literalValue !== null) { if (Strings::match($literalValue, '/^\d+$/') === null) { $isNumeric = TrinaryLogic::createNo(); @@ -508,7 +508,7 @@ private function isMaybeEmptyNode(TreeNode $node, string $patternModifiers, bool } } - $literal = $this->getLiteralValue($node, $onlyLiterals, false, $patternModifiers); + $literal = $this->getLiteralValue($node, $onlyLiterals, false, $patternModifiers, false); if ($literal !== null) { if ($literal !== '' && $literal !== '0') { $isNonFalsy = true; @@ -528,7 +528,7 @@ private function isMaybeEmptyNode(TreeNode $node, string $patternModifiers, bool /** * @param array|null $onlyLiterals */ - private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals, bool $appendLiterals, string $patternModifiers): ?string + private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals, bool $appendLiterals, string $patternModifiers, bool $inCharacterClass): ?string { if ($node->getId() !== 'token') { return null; @@ -551,15 +551,17 @@ private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals, bool $ap return null; } + $isEscaped = false; if (strlen($value) > 1 && $value[0] === '\\') { $value = substr($value, 1) ?: ''; + $isEscaped = true; } if ( $appendLiterals && in_array($token, ['literal', 'range', 'class_', '_class_literal'], true) && $onlyLiterals !== null - && !in_array($value, ['.'], true) + && (!in_array($value, ['.'], true) || $isEscaped || $inCharacterClass) ) { if ($onlyLiterals === []) { $onlyLiterals = [$value]; diff --git a/tests/PHPStan/Analyser/nsrt/bug-11699.php b/tests/PHPStan/Analyser/nsrt/bug-11699.php new file mode 100644 index 0000000000..b79e076e41 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11699.php @@ -0,0 +1,42 @@ +[\~,\?\.])~', $string, $match); + if ($result === 1) { + assertType("','|'.'|'?'|'~'", $match['AB']); + } +} + +function doFoo1():void { + $string = 'Foo.bar'; + $match = []; + $result = preg_match('~(?[\~,\?.])~', $string, $match); // dot in character class does not need to be escaped + if ($result === 1) { + assertType("','|'.'|'?'|'~'", $match['AB']); + } +} + +function doFoo2():void { + $string = 'Foo.bar'; + $match = []; + $result = preg_match('~(?.)~', $string, $match); + if ($result === 1) { + assertType("non-empty-string", $match['AB']); + } +} + + +function doFoo3():void { + $string = 'Foo.bar'; + $match = []; + $result = preg_match('~(?\.)~', $string, $match); + if ($result === 1) { + assertType("'.'", $match['AB']); + } +}