diff --git a/src/Type/Php/RegexGroupParser.php b/src/Type/Php/RegexGroupParser.php index f63959e78d..83dbf58b7d 100644 --- a/src/Type/Php/RegexGroupParser.php +++ b/src/Type/Php/RegexGroupParser.php @@ -21,7 +21,6 @@ use PHPStan\Type\TypeCombinator; use function array_key_exists; use function count; -use function implode; use function in_array; use function is_int; use function rtrim; @@ -284,10 +283,22 @@ private function createGroupType(TreeNode $group, bool $maybeConstant): Type $inOptionalQuantification = false; $onlyLiterals = []; - $this->walkGroupAst($group, $isNonEmpty, $isNumeric, $inOptionalQuantification, $onlyLiterals); + $this->walkGroupAst( + $group, + $isNonEmpty, + $isNumeric, + $inOptionalQuantification, + $onlyLiterals, + false, + ); if ($maybeConstant && $onlyLiterals !== null && $onlyLiterals !== []) { - return new ConstantStringType(implode('', $onlyLiterals)); + $result = []; + foreach ($onlyLiterals as $literal) { + $result[] = new ConstantStringType($literal); + + } + return TypeCombinator::union(...$result); } if ($isNumeric->yes()) { @@ -306,7 +317,14 @@ private function createGroupType(TreeNode $group, bool $maybeConstant): Type /** * @param array|null $onlyLiterals */ - private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryLogic &$isNumeric, bool &$inOptionalQuantification, ?array &$onlyLiterals): void + private function walkGroupAst( + TreeNode $ast, + TrinaryLogic &$isNonEmpty, + TrinaryLogic &$isNumeric, + bool &$inOptionalQuantification, + ?array &$onlyLiterals, + bool $inClass, + ): void { $children = $ast->getChildren(); @@ -327,8 +345,24 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL } $onlyLiterals = null; + } elseif ($ast->getId() === '#class' && $onlyLiterals !== null) { + $inClass = true; + + $newLiterals = []; + foreach ($children as $child) { + $oldLiterals = $onlyLiterals; + + if ($child->getId() === 'token') { + $this->getLiteralValue($child, $oldLiterals, true); + } + + foreach ($oldLiterals ?? [] as $oldLiteral) { + $newLiterals[] = $oldLiteral; + } + } + $onlyLiterals = $newLiterals; } elseif ($ast->getId() === 'token') { - $literalValue = $this->getLiteralValue($ast, $onlyLiterals); + $literalValue = $this->getLiteralValue($ast, $onlyLiterals, !$inClass); if ($literalValue !== null) { if (Strings::match($literalValue, '/^\d+$/') === null) { $isNumeric = TrinaryLogic::createNo(); @@ -360,6 +394,7 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL $isNumeric, $inOptionalQuantification, $onlyLiterals, + $inClass, ); } } @@ -367,7 +402,7 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL /** * @param array|null $onlyLiterals */ - private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals): ?string + private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals, bool $appendLiterals): ?string { if ($node->getId() !== 'token') { return null; @@ -381,11 +416,18 @@ private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals): ?string if (strlen($value) > 1 && $value[0] === '\\') { return substr($value, 1); } elseif ( - $token === 'literal' + $appendLiterals + && $token === 'literal' && $onlyLiterals !== null && !in_array($value, ['.'], true) ) { - $onlyLiterals[] = $value; + if ($onlyLiterals === []) { + $onlyLiterals = [$value]; + } else { + foreach ($onlyLiterals as &$literal) { + $literal .= $value; + } + } } return $value; diff --git a/tests/PHPStan/Analyser/nsrt/bug-11311.php b/tests/PHPStan/Analyser/nsrt/bug-11311.php index 0dfb7cc4b4..51cc0f094f 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11311.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11311.php @@ -179,7 +179,7 @@ function (string $s): void { function (string $s): void { if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*([sbdeEfFgGhHouxX])$/', $s, $matches, PREG_UNMATCHED_AS_NULL) === 1) { - assertType('array{string, non-empty-string|null, non-empty-string}', $matches); + assertType("array{string, non-empty-string|null, 'b'|'d'|'E'|'e'|'F'|'f'|'G'|'g'|'H'|'h'|'o'|'s'|'u'|'X'|'x'}", $matches); } }; diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index a940e62ff4..fde7690f7c 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -435,7 +435,7 @@ function (string $s, $mixed): void { function (string $s): void { if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*([sbdeEfFgGhHouxX])$/', $s, $matches) === 1) { - assertType('array{string, string, non-empty-string}', $matches); + assertType("array{string, string, 'b'|'d'|'E'|'e'|'F'|'f'|'G'|'g'|'H'|'h'|'o'|'s'|'u'|'X'|'x'}", $matches); } }; @@ -453,13 +453,13 @@ function (string $s): void { function (string $s): void { if (preg_match('~^([157])$~', $s, $matches) === 1) { - assertType("array{string, numeric-string}", $matches); + assertType("array{string, '1'|'5'|'7'}", $matches); } }; function (string $s): void { if (preg_match('~^([157XY])$~', $s, $matches) === 1) { - assertType("array{string, non-empty-string}", $matches); + assertType("array{string, '1'|'5'|'7'|'X'|'Y'}", $matches); } }; @@ -519,10 +519,10 @@ function bug11323(string $s): void { assertType('array{string, non-empty-string}', $matches); } if (preg_match("{([\r\n]+)(\n)([\n])}", $s, $matches)) { - assertType('array{string, non-empty-string, "\n", non-empty-string}', $matches); + assertType('array{string, non-empty-string, "\n", "\n"}', $matches); } if (preg_match('/foo(*:first)|bar(*:second)([x])/', $s, $matches)) { - assertType("array{0: string, 1?: non-empty-string, MARK?: 'first'|'second'}", $matches); + assertType("array{0: string, 1?: 'x', MARK?: 'first'|'second'}", $matches); } } @@ -570,3 +570,33 @@ function (string $s): void { assertType("array{0: string, 1: non-empty-string, 2?: numeric-string, 3?: 'x'}", $matches); } }; + +function (string $s): void { + if (preg_match('/Price: ([a-z])/i', $s, $matches)) { + assertType("array{string, non-empty-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: ([0-9])/i', $s, $matches)) { + assertType("array{string, numeric-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: ([xXa])/i', $s, $matches)) { + assertType("array{string, 'a'|'X'|'x'}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: (ba[rz])/i', $s, $matches)) { + assertType("array{string, 'bar'|'baz'}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: (b[ao][mn])/i', $s, $matches)) { + assertType("array{string, 'bam'|'ban'|'bom'|'bon'}", $matches); + } +};