diff --git a/src/Type/Regex/RegexExpressionHelper.php b/src/Type/Regex/RegexExpressionHelper.php index dada8e2c5c9..ef14c0f4d7e 100644 --- a/src/Type/Regex/RegexExpressionHelper.php +++ b/src/Type/Regex/RegexExpressionHelper.php @@ -13,6 +13,8 @@ use PHPStan\Type\TypeCombinator; use function array_key_exists; use function ltrim; +use function str_ends_with; +use function str_starts_with; use function strrpos; use function substr; @@ -89,6 +91,13 @@ public function getPatternModifiers(string $pattern): ?string return substr($pattern, $endDelimiterPos + 1); } + public function isAnchoredPattern(string $pattern): bool + { + $cleanedPattern = $this->removeDelimitersAndModifiers($pattern); + + return str_starts_with($cleanedPattern, '^') && str_ends_with($cleanedPattern, '$'); + } + public function removeDelimitersAndModifiers(string $pattern): string { $pattern = ltrim($pattern); diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index e2ab45d31d1..5d8e2d7bfd8 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -13,9 +13,9 @@ use PHPStan\Php\PhpVersion; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; -use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; @@ -125,8 +125,14 @@ public function parseGroups(string $regex): ?RegexAstWalkResult ); if (!$subjectAsGroupResult->mightContainEmptyStringLiteral() && !$this->containsEscapeK($ast)) { - // we could handle numeric-string, in case we know the regex is delimited by ^ and $ - if ($subjectAsGroupResult->isNonFalsy()->yes()) { + if ( + $subjectAsGroupResult->isDecimalInteger()->yes() + && $this->regexExpressionHelper->isAnchoredPattern($regex) + ) { + $astWalkResult = $astWalkResult->withSubjectBaseType( + new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType()]), + ); + } elseif ($subjectAsGroupResult->isNonFalsy()->yes()) { $astWalkResult = $astWalkResult->withSubjectBaseType( new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), ); @@ -420,16 +426,16 @@ private function createGroupType(TreeNode $group, bool $maybeConstant, string $p return TypeCombinator::union(...$result); } - if ($walkResult->isNumeric()->yes()) { + if ($walkResult->isDecimalInteger()->yes()) { if ($walkResult->isNonFalsy()->yes()) { return new IntersectionType([ new StringType(), - new AccessoryNumericStringType(), + new AccessoryDecimalIntegerStringType(), new AccessoryNonFalsyStringType(), ]); } - $result = new IntersectionType([new StringType(), new AccessoryNumericStringType()]); + $result = new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType()]); if (!$walkResult->isNonEmpty()->yes()) { return new UnionType([new ConstantStringType(''), $result]); } @@ -482,12 +488,17 @@ private function walkGroupAst( $meaningfulTokens = 0; foreach ($children as $child) { $nonFalsy = false; - if ($this->isMaybeEmptyNode($child, $patternModifiers, $nonFalsy)) { + $isNonDecimal = false; + if ($this->isMaybeEmptyNode($child, $patternModifiers, $nonFalsy, $isNonDecimal)) { continue; } $meaningfulTokens++; + if ($isNonDecimal) { + $walkResult = $walkResult->decimalInteger(TrinaryLogic::createNo()); + } + if (!$nonFalsy) { continue; } @@ -541,10 +552,19 @@ private function walkGroupAst( $walkResult = $walkResult->onlyLiterals($onlyLiterals); if ($literalValue !== null) { - if (Strings::match($literalValue, '/^\d+$/') === null) { - $walkResult = $walkResult->numeric(TrinaryLogic::createNo()); - } elseif ($walkResult->isNumeric()->maybe()) { - $walkResult = $walkResult->numeric(TrinaryLogic::createYes()); + if (Strings::match($literalValue, '/^\d+$/') !== null) { + if ($walkResult->isDecimalInteger()->maybe()) { + $walkResult = $walkResult->decimalInteger(TrinaryLogic::createYes()); + } + } elseif ( + $literalValue === '-' + && $walkResult->isDecimalInteger()->maybe() + && !$walkResult->hasSeenDecimalIntegerSign() + ) { + // a single leading minus sign keeps the string a decimal integer (e.g. "-1") + $walkResult = $walkResult->seenDecimalIntegerSign(true); + } elseif ($literalValue !== '') { + $walkResult = $walkResult->decimalInteger(TrinaryLogic::createNo()); } if (!$walkResult->isInOptionalQuantification() && $literalValue !== '') { @@ -563,7 +583,7 @@ private function walkGroupAst( $newLiterals = []; $nonEmpty = TrinaryLogic::createYes(); $nonFalsy = TrinaryLogic::createYes(); - $numeric = TrinaryLogic::createYes(); + $decimalInteger = TrinaryLogic::createYes(); foreach ($children as $child) { $childResult = $this->walkGroupAst( $child, @@ -572,12 +592,13 @@ private function walkGroupAst( $walkResult->onlyLiterals([]) ->nonEmpty(TrinaryLogic::createMaybe()) ->nonFalsy(TrinaryLogic::createMaybe()) - ->numeric(TrinaryLogic::createMaybe()), + ->decimalInteger(TrinaryLogic::createMaybe()) + ->seenDecimalIntegerSign(false), ); $nonEmpty = $nonEmpty->and($childResult->isNonEmpty()); $nonFalsy = $nonFalsy->and($childResult->isNonFalsy()); - $numeric = $numeric->and($childResult->isNumeric()); + $decimalInteger = $decimalInteger->and($childResult->isDecimalInteger()); if ($newLiterals === null) { continue; @@ -596,14 +617,14 @@ private function walkGroupAst( ->onlyLiterals($newLiterals) ->nonEmpty($walkResult->isNonEmpty()->or($nonEmpty)) ->nonFalsy($walkResult->isNonFalsy()->or($nonFalsy)) - ->numeric($walkResult->isNumeric()->and($numeric)); + ->decimalInteger($walkResult->isDecimalInteger()->and($decimalInteger)); } - // [^0-9] should not parse as numeric-string, and [^list-everything-but-numbers] is technically - // doable but really silly compared to just \d so we can safely assume the string is not numeric - // for negative classes + // [^0-9] should not parse as decimal-int-string, and [^list-everything-but-numbers] is technically + // doable but really silly compared to just \d so we can safely assume the string is not a decimal + // integer for negative classes if ($ast->getId() === '#negativeclass') { - $walkResult = $walkResult->numeric(TrinaryLogic::createNo()); + $walkResult = $walkResult->decimalInteger(TrinaryLogic::createNo()); } foreach ($children as $child) { @@ -618,7 +639,7 @@ private function walkGroupAst( return $walkResult; } - private function isMaybeEmptyNode(TreeNode $node, string $patternModifiers, bool &$isNonFalsy): bool + private function isMaybeEmptyNode(TreeNode $node, string $patternModifiers, bool &$isNonFalsy, bool &$isNonDecimal): bool { if ($node->getId() === '#quantification') { [$min] = $this->getQuantificationRange($node); @@ -637,11 +658,14 @@ private function isMaybeEmptyNode(TreeNode $node, string $patternModifiers, bool if ($literal !== '' && $literal !== '0') { $isNonFalsy = true; } + if (Strings::match($literal, '/^\d+$/') === null) { + $isNonDecimal = true; + } return $literal === ''; } foreach ($node->getChildren() as $child) { - if (!$this->isMaybeEmptyNode($child, $patternModifiers, $isNonFalsy)) { + if (!$this->isMaybeEmptyNode($child, $patternModifiers, $isNonFalsy, $isNonDecimal)) { return false; } } diff --git a/src/Type/Regex/RegexGroupWalkResult.php b/src/Type/Regex/RegexGroupWalkResult.php index 9169af89ba5..db6c3e1cd67 100644 --- a/src/Type/Regex/RegexGroupWalkResult.php +++ b/src/Type/Regex/RegexGroupWalkResult.php @@ -16,7 +16,8 @@ public function __construct( private ?array $onlyLiterals, private TrinaryLogic $isNonEmpty, private TrinaryLogic $isNonFalsy, - private TrinaryLogic $isNumeric, + private TrinaryLogic $isDecimalInteger, + private bool $seenDecimalIntegerSign, ) { } @@ -29,6 +30,7 @@ public static function createEmpty(): self TrinaryLogic::createMaybe(), TrinaryLogic::createMaybe(), TrinaryLogic::createMaybe(), + false, ); } @@ -39,7 +41,8 @@ public function inOptionalQuantification(bool $inOptionalQuantification): self $this->onlyLiterals, $this->isNonEmpty, $this->isNonFalsy, - $this->isNumeric, + $this->isDecimalInteger, + $this->seenDecimalIntegerSign, ); } @@ -53,7 +56,8 @@ public function onlyLiterals(?array $onlyLiterals): self $onlyLiterals, $this->isNonEmpty, $this->isNonFalsy, - $this->isNumeric, + $this->isDecimalInteger, + $this->seenDecimalIntegerSign, ); } @@ -64,7 +68,8 @@ public function nonEmpty(TrinaryLogic $nonEmpty): self $this->onlyLiterals, $nonEmpty, $this->isNonFalsy, - $this->isNumeric, + $this->isDecimalInteger, + $this->seenDecimalIntegerSign, ); } @@ -75,18 +80,33 @@ public function nonFalsy(TrinaryLogic $nonFalsy): self $this->onlyLiterals, $this->isNonEmpty, $nonFalsy, - $this->isNumeric, + $this->isDecimalInteger, + $this->seenDecimalIntegerSign, ); } - public function numeric(TrinaryLogic $numeric): self + /** A decimal integer string is composed only of digits, optionally preceded by a single leading minus sign. */ + public function decimalInteger(TrinaryLogic $decimalInteger): self { return new self( $this->inOptionalQuantification, $this->onlyLiterals, $this->isNonEmpty, $this->isNonFalsy, - $numeric, + $decimalInteger, + $this->seenDecimalIntegerSign, + ); + } + + public function seenDecimalIntegerSign(bool $seenDecimalIntegerSign): self + { + return new self( + $this->inOptionalQuantification, + $this->onlyLiterals, + $this->isNonEmpty, + $this->isNonFalsy, + $this->isDecimalInteger, + $seenDecimalIntegerSign, ); } @@ -127,9 +147,14 @@ public function isNonFalsy(): TrinaryLogic return $this->isNonFalsy; } - public function isNumeric(): TrinaryLogic + public function isDecimalInteger(): TrinaryLogic + { + return $this->isDecimalInteger; + } + + public function hasSeenDecimalIntegerSign(): bool { - return $this->isNumeric; + return $this->seenDecimalIntegerSign; } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11293.php b/tests/PHPStan/Analyser/nsrt/bug-11293.php index 49f8d008a6e..3014ef61dde 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11293.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11293.php @@ -9,54 +9,54 @@ class HelloWorld public function sayHello(string $s): void { if (preg_match('/data-(\d{6})\.json$/', $s, $matches) > 0) { - assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches); } } public function sayHello2(string $s): void { if (preg_match('/data-(\d{6})\.json$/', $s, $matches) === 1) { - assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches); } } public function sayHello3(string $s): void { if (preg_match('/data-(\d{6})\.json$/', $s, $matches) >= 1) { - assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches); } } public function sayHello4(string $s): void { if (preg_match('/data-(\d{6})\.json$/', $s, $matches) <= 0) { - assertType('array{}|array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{}|array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches); return; } - assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches); } public function sayHello5(string $s): void { if (preg_match('/data-(\d{6})\.json$/', $s, $matches) < 1) { - assertType('array{}|array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{}|array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches); return; } - assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches); } public function sayHello6(string $s): void { if (1 > preg_match('/data-(\d{6})\.json$/', $s, $matches)) { - assertType('array{}|array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{}|array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches); return; } - assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11311.php b/tests/PHPStan/Analyser/nsrt/bug-11311.php index 79fbf3aca44..897375a89d3 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11311.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11311.php @@ -8,7 +8,7 @@ function doFoo(string $s) { if (1 === preg_match('/(?\d+)\.(?\d+)(?:\.(?\d+))?/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{0: non-falsy-string, major: numeric-string, 1: numeric-string, minor: numeric-string, 2: numeric-string, patch: numeric-string|null, 3: numeric-string|null}', $matches); + assertType('array{0: non-falsy-string, major: decimal-int-string, 1: decimal-int-string, minor: decimal-int-string, 2: decimal-int-string, patch: decimal-int-string|null, 3: decimal-int-string|null}', $matches); } } @@ -85,42 +85,42 @@ function (string $size): void { if (preg_match('/ab(\d){2,4}xx([0-9])?e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{non-falsy-string, numeric-string, numeric-string|null}', $matches); + assertType('array{non-falsy-string, decimal-int-string, decimal-int-string|null}', $matches); }; function (string $size): void { if (preg_match('/a(\dAB){2}b(\d){2,4}([1-5])([1-5a-z])e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{non-falsy-string, non-falsy-string, numeric-string, numeric-string, non-empty-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string, decimal-int-string, decimal-int-string, non-empty-string}', $matches); }; function (string $size): void { if (preg_match('/ab(ab(\d)){2,4}xx([0-9][a-c])?e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{non-falsy-string, non-falsy-string, numeric-string, non-falsy-string|null}', $matches); + assertType('array{non-falsy-string, non-falsy-string, decimal-int-string, non-falsy-string|null}', $matches); }; function (string $size): void { if (preg_match('/ab(\d+)e(\d?)/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType("array{non-falsy-string, numeric-string, ''|numeric-string}", $matches); + assertType("array{non-falsy-string, decimal-int-string, ''|decimal-int-string}", $matches); }; function (string $size): void { if (preg_match('/ab(?P\d+)e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-string, num: decimal-int-string, 1: decimal-int-string}', $matches); }; function (string $size): void { if (preg_match('/ab(\d\d)/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches); }; function (string $size): void { @@ -162,12 +162,12 @@ function (string $size): void { if (preg_match('/ab(\d+\d?)e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{non-falsy-string, numeric-string}', $matches); + assertType('array{non-falsy-string, decimal-int-string}', $matches); }; function (string $s): void { if (preg_match('/Price: ([2-5])/i', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{non-falsy-string, numeric-string}', $matches); + assertType('array{non-falsy-string, decimal-int-string}', $matches); } }; @@ -191,26 +191,26 @@ function (string $s): void { function (string $s): void { preg_match('/%a(\d*)/', $s, $matches, PREG_UNMATCHED_AS_NULL); - assertType("array{}|array{non-falsy-string, ''|numeric-string}", $matches); + assertType("array{}|array{non-falsy-string, ''|decimal-int-string}", $matches); }; function (string $s): void { preg_match('/%a(\d*)?/', $s, $matches, PREG_UNMATCHED_AS_NULL); - assertType("array{}|array{non-falsy-string, ''|numeric-string|null}", $matches); + assertType("array{}|array{non-falsy-string, ''|decimal-int-string|null}", $matches); }; function (string $s): void { if (preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType("array{non-empty-string, numeric-string|null, non-empty-string|null}", $matches); + assertType("array{non-empty-string, decimal-int-string|null, non-empty-string|null}", $matches); } else { assertType("array{}", $matches); } - assertType("array{}|array{non-empty-string, numeric-string|null, non-empty-string|null}", $matches); + assertType("array{}|array{non-empty-string, decimal-int-string|null, non-empty-string|null}", $matches); }; function (string $s): void { if (preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_UNMATCHED_AS_NULL|PREG_OFFSET_CAPTURE) === 1) { - assertType("array{array{non-empty-string|null, int<-1, max>}, array{numeric-string|null, int<-1, max>}, array{non-empty-string|null, int<-1, max>}}", $matches); + assertType("array{array{non-empty-string|null, int<-1, max>}, array{decimal-int-string|null, int<-1, max>}, array{non-empty-string|null, int<-1, max>}}", $matches); } }; @@ -222,5 +222,5 @@ function (string $s): void { function (string $s): void { preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_UNMATCHED_AS_NULL); - assertType("array{}|array{non-empty-string, numeric-string|null, non-empty-string|null}", $matches); + assertType("array{}|array{non-empty-string, decimal-int-string|null, non-empty-string|null}", $matches); }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-14177.php b/tests/PHPStan/Analyser/nsrt/bug-14177.php index 26ef94e5577..29a9712032a 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14177.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14177.php @@ -26,7 +26,7 @@ public function placeholderToEditor(string $html): void function (array $matches): string { $id = (int) $matches[1]; - assertType('list{0: non-falsy-string, 1: numeric-string, 2?: string, 3?: string}', $matches); + assertType('list{0: non-falsy-string, 1: decimal-int-string, 2?: string, 3?: string}', $matches); $replacement = sprintf( '', @@ -34,7 +34,7 @@ function (array $matches): string { array_key_exists(3, $matches) ? sprintf(' class="%s"', $matches[3]) : '', ); - assertType('list{0: non-falsy-string, 1: numeric-string, 2?: string, 3?: string}', $matches); + assertType('list{0: non-falsy-string, 1: decimal-int-string, 2?: string, 3?: string}', $matches); return array_key_exists(2, $matches) && $matches[2] !== '' ? sprintf('%s', $matches[2], $replacement) @@ -51,7 +51,7 @@ public function placeholderToEditor2(string $html): void function (array $matches): string { $id = (int) $matches[0]; - assertType('list{0: non-falsy-string, 1?: \'\'|numeric-string, 2?: string, 3?: string}', $matches); + assertType('list{0: non-falsy-string, 1?: \'\'|decimal-int-string, 2?: string, 3?: string}', $matches); $replacement = sprintf( '', @@ -59,7 +59,7 @@ function (array $matches): string { array_key_exists(2, $matches) ? sprintf(' class="%s"', $matches[2]) : '', ); - assertType('list{0: non-falsy-string, 1?: \'\'|numeric-string, 2?: string, 3?: string}', $matches); + assertType('list{0: non-falsy-string, 1?: \'\'|decimal-int-string, 2?: string, 3?: string}', $matches); return array_key_exists(1, $matches) && $matches[1] !== '' ? sprintf('%s', $matches[1], $replacement) diff --git a/tests/PHPStan/Analyser/nsrt/bug-14750.php b/tests/PHPStan/Analyser/nsrt/bug-14750.php new file mode 100644 index 00000000000..432e51862a8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14750.php @@ -0,0 +1,77 @@ +, list}', $matches); + assertType('array{list, list}', $matches); }; function (string $size): void { preg_match_all('/ab(?P\d+)?/', $size, $matches); - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|decimal-int-string>, 1: list<''|decimal-int-string>}", $matches); }; function (string $size): void { preg_match_all('/ab(\d+)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_PATTERN_ORDER); - assertType('array{list, list}', $matches); + assertType('array{list, list}', $matches); }; function (string $size): void { preg_match_all('/ab(?P\d+)?/', $size, $matches, PREG_PATTERN_ORDER); - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|decimal-int-string>, 1: list<''|decimal-int-string>}", $matches); }; function (string $size): void { if (preg_match_all('/ab(?P\d+)?/', $size, $matches)) { - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|decimal-int-string>, 1: list<''|decimal-int-string>}", $matches); } else { assertType("array{}", $matches); } - assertType("array{}|array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{}|array{0: list, num: list<''|decimal-int-string>, 1: list<''|decimal-int-string>}", $matches); }; function (string $size): void { if (preg_match_all('/ab(?P\d+)?/', $size, $matches) > 0) { - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|decimal-int-string>, 1: list<''|decimal-int-string>}", $matches); } else { - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|decimal-int-string>, 1: list<''|decimal-int-string>}", $matches); } - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|decimal-int-string>, 1: list<''|decimal-int-string>}", $matches); }; function (string $size): void { if (preg_match_all('/ab(?P\d+)?/', $size, $matches) != false) { - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|decimal-int-string>, 1: list<''|decimal-int-string>}", $matches); } else { assertType("array{}", $matches); } - assertType("array{}|array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{}|array{0: list, num: list<''|decimal-int-string>, 1: list<''|decimal-int-string>}", $matches); }; function (string $size): void { if (preg_match_all('/ab(?P\d+)?/', $size, $matches) == true) { - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|decimal-int-string>, 1: list<''|decimal-int-string>}", $matches); } else { assertType("array{}", $matches); } - assertType("array{}|array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{}|array{0: list, num: list<''|decimal-int-string>, 1: list<''|decimal-int-string>}", $matches); }; function (string $size): void { if (preg_match_all('/ab(?P\d+)?/', $size, $matches) === 1) { - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|decimal-int-string>, 1: list<''|decimal-int-string>}", $matches); } else { - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|decimal-int-string>, 1: list<''|decimal-int-string>}", $matches); } - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|decimal-int-string>, 1: list<''|decimal-int-string>}", $matches); }; function (string $size): void { preg_match_all('/a(b)(\d+)?/', $size, $matches, PREG_SET_ORDER); - assertType("list", $matches); + assertType("list", $matches); }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches)) { - assertType("array{0: list, num: list, 1: list, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $matches); + assertType("array{0: list, num: list, 1: list, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType("array{0: list, num: list, 1: list, suffix: list<'ab'|null>, 2: list<'ab'|null>}", $matches); + assertType("array{0: list, num: list, 1: list, suffix: list<'ab'|null>, 2: list<'ab'|null>}", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_SET_ORDER)) { - assertType("list", $matches); + assertType("list", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_PATTERN_ORDER)) { - assertType("array{0: list, num: list, 1: list, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $matches); + assertType("array{0: list, num: list, 1: list, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_SET_ORDER)) { - assertType("list", $matches); + assertType("list", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_PATTERN_ORDER)) { - assertType("array{0: list, num: list, 1: list, suffix: list<'ab'|null>, 2: list<'ab'|null>}", $matches); + assertType("array{0: list, num: list, 1: list, suffix: list<'ab'|null>, 2: list<'ab'|null>}", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) { - assertType("list}, num: array{numeric-string, int<-1, max>}, 1: array{numeric-string, int<-1, max>}, suffix?: array{'ab', int<-1, max>}, 2?: array{'ab', int<-1, max>}}>", $matches); + assertType("list}, num: array{decimal-int-string, int<-1, max>}, 1: array{decimal-int-string, int<-1, max>}, suffix?: array{'ab', int<-1, max>}, 2?: array{'ab', int<-1, max>}}>", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE)) { - assertType("array{0: list}>, num: list}>, 1: list}>, suffix: list}>, 2: list}>}", $matches); + assertType("array{0: list}>, num: list}>, 1: list}>, suffix: list}>, 2: list}>}", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) { - assertType("list}, num: array{numeric-string|null, int<-1, max>}, 1: array{numeric-string|null, int<-1, max>}, suffix: array{'ab'|null, int<-1, max>}, 2: array{'ab'|null, int<-1, max>}}>", $matches); + assertType("list}, num: array{decimal-int-string|null, int<-1, max>}, 1: array{decimal-int-string|null, int<-1, max>}, suffix: array{'ab'|null, int<-1, max>}, 2: array{'ab'|null, int<-1, max>}}>", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE)) { - assertType("array{0: list}>, num: list}>, 1: list}>, suffix: list}>, 2: list}>}", $matches); + assertType("array{0: list}>, num: list}>, 1: list}>, suffix: list}>, 2: list}>}", $matches); } }; diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 8dac3aa80ce..fec138d88a0 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -19,9 +19,9 @@ function doMatch(string $s): void { assertType("array{}|array{non-falsy-string, '£'|'€'}", $matches); if (preg_match('/Price: (£|€)(\d+)/i', $s, $matches)) { - assertType('array{non-falsy-string, non-empty-string, numeric-string}', $matches); + assertType('array{non-falsy-string, non-empty-string, decimal-int-string}', $matches); } - assertType('array{}|array{non-falsy-string, non-empty-string, numeric-string}', $matches); + assertType('array{}|array{non-falsy-string, non-empty-string, decimal-int-string}', $matches); if (preg_match(' /Price: (£|€)\d+/ i u', $s, $matches)) { assertType('array{non-falsy-string, non-empty-string}', $matches); @@ -91,16 +91,16 @@ function doMatch(string $s): void { function doNonCapturingGroup(string $s): void { if (preg_match('/Price: (?:£|€)(\d+)/', $s, $matches)) { - assertType('array{non-falsy-string, numeric-string}', $matches); + assertType('array{non-falsy-string, decimal-int-string}', $matches); } - assertType('array{}|array{non-falsy-string, numeric-string}', $matches); + assertType('array{}|array{non-falsy-string, decimal-int-string}', $matches); } function doNamedSubpattern(string $s): void { if (preg_match('/\w-(?P\d+)-(\w)/', $s, $matches)) { - assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string, 2: non-empty-string}', $matches); + assertType('array{0: non-falsy-string, num: decimal-int-string, 1: decimal-int-string, 2: non-empty-string}', $matches); } - assertType('array{}|array{0: non-falsy-string, num: numeric-string, 1: numeric-string, 2: non-empty-string}', $matches); + assertType('array{}|array{0: non-falsy-string, num: decimal-int-string, 1: decimal-int-string, 2: non-empty-string}', $matches); if (preg_match('/^(?\S+::\S+)/', $s, $matches)) { assertType('array{0: non-falsy-string, name: non-falsy-string, 1: non-falsy-string}', $matches); @@ -129,16 +129,16 @@ function doUnknownFlags(string $s, int $flags): void { function doMultipleAlternativeCaptureGroupsWithSameNameWithModifier(string $s): void { if (preg_match('/(?J)(?[a-z]+)|(?[0-9]+)/', $s, $matches)) { - assertType("array{0: non-empty-string, Foo: non-empty-string, 1: non-empty-string}|array{0: non-empty-string, Foo: numeric-string, 1: '', 2: numeric-string}", $matches); + assertType("array{0: non-empty-string, Foo: decimal-int-string, 1: '', 2: decimal-int-string}|array{0: non-empty-string, Foo: non-empty-string, 1: non-empty-string}", $matches); } - assertType("array{}|array{0: non-empty-string, Foo: non-empty-string, 1: non-empty-string}|array{0: non-empty-string, Foo: numeric-string, 1: '', 2: numeric-string}", $matches); + assertType("array{}|array{0: non-empty-string, Foo: decimal-int-string, 1: '', 2: decimal-int-string}|array{0: non-empty-string, Foo: non-empty-string, 1: non-empty-string}", $matches); } function doMultipleConsecutiveCaptureGroupsWithSameNameWithModifier(string $s): void { if (preg_match('/(?J)(?[a-z]+)|(?[0-9]+)/', $s, $matches)) { - assertType("array{0: non-empty-string, Foo: non-empty-string, 1: non-empty-string}|array{0: non-empty-string, Foo: numeric-string, 1: '', 2: numeric-string}", $matches); + assertType("array{0: non-empty-string, Foo: decimal-int-string, 1: '', 2: decimal-int-string}|array{0: non-empty-string, Foo: non-empty-string, 1: non-empty-string}", $matches); } - assertType("array{}|array{0: non-empty-string, Foo: non-empty-string, 1: non-empty-string}|array{0: non-empty-string, Foo: numeric-string, 1: '', 2: numeric-string}", $matches); + assertType("array{}|array{0: non-empty-string, Foo: decimal-int-string, 1: '', 2: decimal-int-string}|array{0: non-empty-string, Foo: non-empty-string, 1: non-empty-string}", $matches); } // https://github.com/hoaproject/Regex/issues/31 @@ -149,9 +149,9 @@ function hoaBug31(string $s): void { assertType('array{}|array{non-empty-string, non-empty-string}', $matches); if (preg_match('/\w-(\d+)-(\w)/', $s, $matches)) { - assertType('array{non-falsy-string, numeric-string, non-empty-string}', $matches); + assertType('array{non-falsy-string, decimal-int-string, non-empty-string}', $matches); } - assertType('array{}|array{non-falsy-string, numeric-string, non-empty-string}', $matches); + assertType('array{}|array{non-falsy-string, decimal-int-string, non-empty-string}', $matches); } // https://github.com/phpstan/phpstan/issues/10855#issuecomment-2044323638 @@ -225,9 +225,9 @@ function testUnionPattern(string $s): void $pattern = '/Price: (\d+)(\d+)(\d+)/'; } if (preg_match($pattern, $s, $matches)) { - assertType('array{non-falsy-string, numeric-string, numeric-string, numeric-string}|array{non-falsy-string, numeric-string}', $matches); + assertType('array{non-falsy-string, decimal-int-string, decimal-int-string, decimal-int-string}|array{non-falsy-string, decimal-int-string}', $matches); } - assertType('array{}|array{non-falsy-string, numeric-string, numeric-string, numeric-string}|array{non-falsy-string, numeric-string}', $matches); + assertType('array{}|array{non-falsy-string, decimal-int-string, decimal-int-string, decimal-int-string}|array{non-falsy-string, decimal-int-string}', $matches); } function doFoo(string $row): void @@ -249,7 +249,7 @@ function doFoo2(string $row): void return; } - assertType("array{0: non-falsy-string, 1: string, branchCode: ''|numeric-string, 2: ''|numeric-string, accountNumber: numeric-string, 3: numeric-string, bankCode: non-falsy-string&numeric-string, 4: non-falsy-string&numeric-string}", $matches); + assertType("array{0: non-falsy-string, 1: string, branchCode: ''|decimal-int-string, 2: ''|decimal-int-string, accountNumber: decimal-int-string, 3: decimal-int-string, bankCode: decimal-int-string&non-falsy-string, 4: decimal-int-string&non-falsy-string}", $matches); } function doFoo3(string $row): void @@ -258,42 +258,42 @@ function doFoo3(string $row): void return; } - assertType('array{non-falsy-string, non-falsy-string, non-falsy-string, numeric-string, numeric-string, numeric-string, numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string, non-falsy-string, decimal-int-string, decimal-int-string, decimal-int-string, decimal-int-string}', $matches); } function (string $size): void { if (preg_match('~^a\.b(c(\d+)(\d+)(\s+))?d~', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{non-falsy-string, non-falsy-string, numeric-string, numeric-string, non-empty-string}|array{non-falsy-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string, decimal-int-string, decimal-int-string, non-empty-string}|array{non-falsy-string}', $matches); }; function (string $size): void { if (preg_match('~^a\.b(c(\d+))?d~', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{non-falsy-string, non-falsy-string, numeric-string}|array{non-falsy-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string, decimal-int-string}|array{non-falsy-string}', $matches); }; function (string $size): void { if (preg_match('~^a\.b(c(\d+)?)d~', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{0: non-falsy-string, 1: non-falsy-string, 2?: numeric-string}', $matches); + assertType('array{0: non-falsy-string, 1: non-falsy-string, 2?: decimal-int-string}', $matches); }; function (string $size): void { if (preg_match('~^a\.b(c(\d+)?)?d~', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('list{0: non-falsy-string, 1?: non-falsy-string, 2?: numeric-string}', $matches); + assertType('list{0: non-falsy-string, 1?: non-falsy-string, 2?: decimal-int-string}', $matches); }; function (string $size): void { if (preg_match('~^a\.b(c(\d+))d~', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{non-falsy-string, non-falsy-string, numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string, decimal-int-string}', $matches); }; function (string $size): void { @@ -307,14 +307,14 @@ function (string $size): void { if (preg_match('~^(?:(\\d+)x(\\d+)|(\\d+)|x(\\d+))$~', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType("array{non-empty-string, '', '', '', numeric-string}|array{non-empty-string, '', '', numeric-string}|array{non-empty-string, numeric-string, numeric-string}", $matches); + assertType("array{non-empty-string, '', '', '', decimal-int-string}|array{non-empty-string, '', '', decimal-int-string}|array{non-empty-string, decimal-int-string, decimal-int-string}", $matches); }; function (string $size): void { if (preg_match('~^(?:(\\d+)x(\\d+)|(\\d+)|x(\\d+))?$~', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType("array{string, '', '', '', numeric-string}|array{string, '', '', numeric-string}|array{string, numeric-string, numeric-string}|array{string}", $matches); + assertType("array{string, '', '', '', decimal-int-string}|array{string, '', '', decimal-int-string}|array{string, decimal-int-string, decimal-int-string}|array{string}", $matches); }; function (string $size): void { @@ -402,11 +402,11 @@ function (string $s): void { function (string $s): void { if (preg_match('/' . preg_quote($s, '/') . '(\d)/', $s, $matches)) { - assertType('array{non-empty-string, numeric-string}', $matches); + assertType('array{non-empty-string, decimal-int-string}', $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{non-empty-string, numeric-string}', $matches); + assertType('array{}|array{non-empty-string, decimal-int-string}', $matches); }; function (string $s): void { @@ -435,19 +435,19 @@ function (string $s): void { function (string $s): void { if (preg_match('~^((\\d{1,6})-)$~', $s, $matches) === 1) { - assertType("array{non-falsy-string, non-falsy-string, numeric-string}", $matches); + assertType("array{non-falsy-string, non-falsy-string, decimal-int-string}", $matches); } }; function (string $s): void { if (preg_match('~^((\\d{1,6}).)$~', $s, $matches) === 1) { - assertType("array{non-falsy-string, non-falsy-string, numeric-string}", $matches); + assertType("array{non-falsy-string, non-falsy-string, decimal-int-string}", $matches); } }; function (string $s): void { if (preg_match('~^([157])$~', $s, $matches) === 1) { - assertType("array{non-falsy-string, '1'|'5'|'7'}", $matches); + assertType("array{decimal-int-string, '1'|'5'|'7'}", $matches); } }; @@ -468,22 +468,22 @@ function bug11323(string $s): void { assertType("array{non-falsy-string, non-falsy-string, 'a-z'}", $matches); } if (preg_match('{(\d+)(?i)insensitive((?xs-i)case SENSITIVE here.+and dot matches new lines)}', $s, $matches)) { - assertType('array{non-falsy-string, numeric-string, non-falsy-string}', $matches); + assertType('array{non-falsy-string, decimal-int-string, non-falsy-string}', $matches); } if (preg_match('{(\d+)(?i)insensitive((?x-i)case SENSITIVE here(?i:insensitive non-capturing group))}', $s, $matches)) { - assertType('array{non-falsy-string, numeric-string, non-falsy-string}', $matches); + assertType('array{non-falsy-string, decimal-int-string, non-falsy-string}', $matches); } if (preg_match('{([]] [^]])}', $s, $matches)) { assertType('array{non-falsy-string, non-falsy-string}', $matches); } if (preg_match('{([[:digit:]])}', $s, $matches)) { - assertType('array{non-empty-string, numeric-string}', $matches); + assertType('array{non-empty-string, decimal-int-string}', $matches); } if (preg_match('{([\d])(\d)}', $s, $matches)) { - assertType('array{non-falsy-string, numeric-string, numeric-string}', $matches); + assertType('array{non-falsy-string, decimal-int-string, decimal-int-string}', $matches); } if (preg_match('{([0-9])}', $s, $matches)) { - assertType('array{non-empty-string, numeric-string}', $matches); + assertType('array{non-empty-string, decimal-int-string}', $matches); } if (preg_match('{(\p{L})(\p{P})(\p{Po})}', $s, $matches)) { assertType('array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string}', $matches); @@ -498,19 +498,19 @@ function bug11323(string $s): void { assertType('array{non-falsy-string, non-falsy-string}', $matches); } if (preg_match('{(\d\d)}', $s, $matches)) { - assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches); } if (preg_match('{(.(\d))}', $s, $matches)) { - assertType('array{non-falsy-string, non-falsy-string, numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string, decimal-int-string}', $matches); } if (preg_match('{((\d).)}', $s, $matches)) { - assertType('array{non-falsy-string, non-falsy-string, numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string, decimal-int-string}', $matches); } if (preg_match('{(\d([1-4])\d)}', $s, $matches)) { - assertType('array{non-falsy-string, non-falsy-string&numeric-string, numeric-string}', $matches); + assertType('array{non-falsy-string, decimal-int-string&non-falsy-string, decimal-int-string}', $matches); } if (preg_match('{(x?([1-4])\d)}', $s, $matches)) { - assertType('array{non-falsy-string, non-falsy-string, numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string, decimal-int-string}', $matches); } if (preg_match('{([^1-4])}', $s, $matches)) { assertType('array{non-empty-string, non-empty-string}', $matches); @@ -525,7 +525,7 @@ function bug11323(string $s): void { function (string $s): void { preg_match('/%a(\d*)/', $s, $matches); - assertType("array{}|array{non-falsy-string, ''|numeric-string}", $matches); + assertType("array{}|array{non-falsy-string, ''|decimal-int-string}", $matches); }; class Bug11376 @@ -552,7 +552,7 @@ function (string $s): void { } if (preg_match($p, $s, $matches)) { - assertType("array{non-falsy-string, '£', 'abc'}|array{non-falsy-string, numeric-string, 'b'}", $matches); + assertType("array{non-falsy-string, '£', 'abc'}|array{non-falsy-string, decimal-int-string, 'b'}", $matches); } }; @@ -564,7 +564,7 @@ function (string $s): void { } if (preg_match($p, $s, $matches)) { - assertType("list{0: non-falsy-string, 1: 'x'|'£'|numeric-string, 2?: ''|numeric-string, 3?: 'x'}", $matches); + assertType("list{0: non-falsy-string, 1: 'x'|'£'|decimal-int-string, 2?: ''|decimal-int-string, 3?: 'x'}", $matches); } }; @@ -576,7 +576,7 @@ function (string $s): void { function (string $s): void { if (preg_match('/Price: ([0-9])/i', $s, $matches)) { - assertType("array{non-falsy-string, numeric-string}", $matches); + assertType("array{non-falsy-string, decimal-int-string}", $matches); } }; @@ -630,13 +630,13 @@ function (string $s): void { function (string $s): void { if (preg_match('/Price: (a|\d)/', $s, $matches)) { - assertType("array{non-falsy-string, 'a'|numeric-string}", $matches); + assertType("array{non-falsy-string, 'a'|decimal-int-string}", $matches); } }; function (string $s): void { if (preg_match('/Price: (?a|\d)/', $s, $matches)) { - assertType("array{0: non-falsy-string, named: 'a'|numeric-string, 1: 'a'|numeric-string}", $matches); + assertType("array{0: non-falsy-string, named: 'a'|decimal-int-string, 1: 'a'|decimal-int-string}", $matches); } }; @@ -654,7 +654,7 @@ function (string $s): void { function (string $s): void { if (preg_match('/( \d+ )/x', $s, $matches)) { - assertType('array{non-empty-string, numeric-string}', $matches); + assertType('array{non-empty-string, decimal-int-string}', $matches); } }; @@ -706,13 +706,13 @@ static public function sayHello(string $source): void // 2 => '1', //) - assertType("array{}|array{0: string, dateFrom?: ''|numeric-string, 1?: ''|numeric-string, dateTo?: numeric-string, 2?: numeric-string}", $matches); + assertType("array{}|array{0: string, dateFrom?: ''|decimal-int-string, 1?: ''|decimal-int-string, dateTo?: decimal-int-string, 2?: decimal-int-string}", $matches); } } function (string $s): void { if (preg_match('~a|(\d)|(\s)~', $s, $matches) === 1) { - assertType("array{0: non-empty-string, 1?: numeric-string}|array{non-empty-string, '', non-empty-string}", $matches); + assertType("array{0: non-empty-string, 1?: decimal-int-string}|array{non-empty-string, '', non-empty-string}", $matches); } }; @@ -724,20 +724,20 @@ function (string $s): void { function (string $s): void { if (preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_OFFSET_CAPTURE) === 1) { - assertType("array{0: array{non-empty-string, int<-1, max>}, 1?: array{numeric-string, int<-1, max>}}|array{array{non-empty-string, int<-1, max>}, array{'', int<-1, max>}, array{non-empty-string, int<-1, max>}}", $matches); + assertType("array{0: array{non-empty-string, int<-1, max>}, 1?: array{decimal-int-string, int<-1, max>}}|array{array{non-empty-string, int<-1, max>}, array{'', int<-1, max>}, array{non-empty-string, int<-1, max>}}", $matches); } }; function (string $s): void { preg_match('~a|(\d)|(\s)~', $s, $matches); - assertType("array{}|array{0: non-empty-string, 1?: numeric-string}|array{non-empty-string, '', non-empty-string}", $matches); + assertType("array{}|array{0: non-empty-string, 1?: decimal-int-string}|array{non-empty-string, '', non-empty-string}", $matches); }; function bug11490 (string $expression): void { $matches = []; if (preg_match('/([-+])?([\d]+)%/', $expression, $matches) === 1) { - assertType("array{non-falsy-string, ''|'+'|'-', numeric-string}", $matches); + assertType("array{non-falsy-string, ''|'+'|'-', decimal-int-string}", $matches); } } @@ -745,7 +745,7 @@ function bug11490b (string $expression): void { $matches = []; if (preg_match('/([\\[+])?([\d]+)%/', $expression, $matches) === 1) { - assertType("array{non-falsy-string, ''|'+'|'[', numeric-string}", $matches); + assertType("array{non-falsy-string, ''|'+'|'[', decimal-int-string}", $matches); } } @@ -792,7 +792,7 @@ function testUnescapeBackslash (string $string): void { if (preg_match(<<<'EOD' ~(\d)~ EOD, $string, $matches)) { - assertType("array{non-empty-string, numeric-string}", $matches); + assertType("array{non-empty-string, decimal-int-string}", $matches); } if (preg_match(<<<'EOD' @@ -1007,13 +1007,13 @@ function bug12749e(string $str): void function bug12749f(string $str): void { if (preg_match('/^[0-9]$/', $str, $match)) { - assertType('array{non-empty-string}', $match); // could be numeric-string + assertType('array{decimal-int-string}', $match); } } function bug12397(string $string): void { $m = preg_match('#\b([A-Z]{2,})-(\d+)#', $string, $match); - assertType("array{}|array{non-falsy-string, non-falsy-string, numeric-string}", $match); + assertType("array{}|array{non-falsy-string, non-falsy-string, decimal-int-string}", $match); } function bug12792(string $string): void { @@ -1085,6 +1085,6 @@ function testExtendedSyntaxEscapedHash(string $string): void { (\d+) # this is a comment ([\#ab]+) # hash in character class (escaped) /x', $string, $matches)) { - assertType('array{non-falsy-string, numeric-string, non-empty-string}', $matches); + assertType('array{non-falsy-string, decimal-int-string, non-empty-string}', $matches); } } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php index 4620565210c..f5f2990bc28 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php @@ -15,7 +15,7 @@ function doOffsetCaptureWithUnmatchedNull(string $s): void { function doNonAutoCapturingModifier(string $s): void { if (preg_match('/(?n)(\d+)/', $s, $matches)) { // should be assertType('array{string}', $matches); - assertType('array{non-empty-string, numeric-string}', $matches); + assertType('array{non-empty-string, decimal-int-string}', $matches); } - assertType('array{}|array{non-empty-string, numeric-string}', $matches); + assertType('array{}|array{non-empty-string, decimal-int-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 1b5dc597b7f..4be11d9712b 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php @@ -13,34 +13,34 @@ function doNonAutoCapturingFlag(string $s): void { assertType('array{}|array{non-empty-string}', $matches); if (preg_match('/(\d+)(?P\d+)/n', $s, $matches)) { - assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-string, num: decimal-int-string, 1: decimal-int-string}', $matches); } - assertType('array{}|array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{}|array{0: non-falsy-string, num: decimal-int-string, 1: decimal-int-string}', $matches); if (preg_match('/(\w)-(?P\d+)-(\w)/n', $s, $matches)) { - assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-string, num: decimal-int-string, 1: decimal-int-string}', $matches); } - assertType('array{}|array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{}|array{0: non-falsy-string, num: decimal-int-string, 1: decimal-int-string}', $matches); } // delimiter variants, see https://www.php.net/manual/en/regexp.reference.delimiters.php function (string $s): void { if (preg_match('{(\d+)(?P\d+)}n', $s, $matches)) { - assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-string, num: decimal-int-string, 1: decimal-int-string}', $matches); } }; function (string $s): void { if (preg_match('<(\d+)(?P\d+)>n', $s, $matches)) { - assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-string, num: decimal-int-string, 1: decimal-int-string}', $matches); } }; function (string $s): void { if (preg_match('((\d+)(?P\d+))n', $s, $matches)) { - assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-string, num: decimal-int-string, 1: decimal-int-string}', $matches); } }; function (string $s): void { if (preg_match('[(\d+)(?P\d+)]n', $s, $matches)) { - assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-string, num: decimal-int-string, 1: decimal-int-string}', $matches); } }; diff --git a/tests/PHPStan/Type/Regex/RegexExpressionHelperTest.php b/tests/PHPStan/Type/Regex/RegexExpressionHelperTest.php index 872ba255868..c6d6b6ea263 100644 --- a/tests/PHPStan/Type/Regex/RegexExpressionHelperTest.php +++ b/tests/PHPStan/Type/Regex/RegexExpressionHelperTest.php @@ -57,4 +57,24 @@ public function testRemoveDelimitersAndModifiers(string $inputPattern, string $e ); } + public static function dataIsAnchoredPattern(): iterable + { + yield ['/^\d$/', true]; + yield ['/^\d/', false]; + yield ['/\d$/', false]; + yield ['/\d/', false]; + yield ['\d', false]; + } + + #[DataProvider('dataIsAnchoredPattern')] + public function testIsAnchoredPattern(string $regex, bool $expected): void + { + $regexExpressionHelper = self::getContainer()->getByType(RegexExpressionHelper::class); + + $this->assertSame( + $expected, + $regexExpressionHelper->isAnchoredPattern($regex), + ); + } + }