From d250d02149e42aaa3bd465a6f3c5077c10616e5f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 15 Jul 2024 21:25:49 +0200 Subject: [PATCH 01/18] RegexArrayShapeMatcher - Match simple numeric-string groups --- src/Type/Php/RegexArrayShapeMatcher.php | 79 ++++++++++++++++--- src/Type/Php/RegexCapturingGroup.php | 8 ++ .../Analyser/nsrt/preg_match_shapes.php | 25 +++++- 3 files changed, 101 insertions(+), 11 deletions(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 84d9850b39..0e770377ee 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -13,11 +13,13 @@ use PHPStan\Php\PhpVersion; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -126,7 +128,6 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched $trailingOptionals++; } - $valueType = $this->getValueType($flags ?? 0); $onlyOptionalTopLevelGroup = $this->getOnlyOptionalTopLevelGroup($groupList); $onlyTopLevelAlternationId = $this->getOnlyTopLevelAlternationId($groupList); @@ -141,7 +142,6 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched $combiType = $this->buildArrayType( $groupList, - $valueType, $wasMatched, $trailingOptionals, $flags ?? 0, @@ -179,7 +179,6 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched $combiType = $this->buildArrayType( $comboList, - $valueType, $wasMatched, $trailingOptionals, $flags ?? 0, @@ -202,7 +201,6 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched return $this->buildArrayType( $groupList, - $valueType, $wasMatched, $trailingOptionals, $flags ?? 0, @@ -264,7 +262,6 @@ private function getOnlyTopLevelAlternationId(array $captureGroups): ?int */ private function buildArrayType( array $captureGroups, - Type $valueType, TrinaryLogic $wasMatched, int $trailingOptionals, int $flags, @@ -275,14 +272,14 @@ private function buildArrayType( // first item in matches contains the overall match. $builder->setOffsetValueType( $this->getKeyType(0), - TypeCombinator::removeNull($valueType), + TypeCombinator::removeNull($this->getValueType(new StringType(), $flags)), !$wasMatched->yes(), ); $countGroups = count($captureGroups); $i = 0; foreach ($captureGroups as $captureGroup) { - $groupValueType = $valueType; + $groupValueType = $this->getValueType($captureGroup->getType(), $flags); if (!$wasMatched->yes()) { $optional = true; @@ -333,9 +330,10 @@ private function getKeyType(int|string $key): Type return new ConstantIntegerType($key); } - private function getValueType(int $flags): Type + private function getValueType(Type $baseType, int $flags): Type { - $valueType = new StringType(); + $valueType = $baseType; + $offsetType = IntegerRangeType::fromInterval(0, null); if ($this->containsUnmatchedAsNull($flags)) { $valueType = TypeCombinator::addNull($valueType); @@ -420,6 +418,7 @@ private function walkRegexAst( $inAlternation ? $alternationId : null, $inOptionalQuantification, $parentGroup, + $this->createGroupType($ast), ); $parentGroup = $group; } elseif ($ast->getId() === '#namedcapturing') { @@ -430,6 +429,7 @@ private function walkRegexAst( $inAlternation ? $alternationId : null, $inOptionalQuantification, $parentGroup, + $this->createGroupType($ast), ); $parentGroup = $group; } elseif ($ast->getId() === '#noncapturing') { @@ -534,6 +534,67 @@ private function getQuantificationRange(TreeNode $node): array return [$min, $max]; } + private function createGroupType(TreeNode $ast): Type { + if ($this->isNumericGroup($ast)) { + return new IntersectionType([ + new StringType(), + new AccessoryNumericStringType() + ]); + } + + return new StringType(); + } + + private function isNumericGroup(TreeNode $group): bool + { + $children = $group->getChildren(); + + if ( + count($children) === 1 + && $children[0]->getId() === 'token' + && $children[0]->getValueToken() === 'character_type' + && $children[0]->getValueValue() === '\d' + ) { + return true; + } + + if ( + count($children) === 1 + && $children[0]->getId() === '#class' + && count($children[0]->getChildren()) === 1 + && $this->isNumericRange($children[0]->getChildren()[0]) + ) + { + return true; + } + + return false; + } + + private function isNumericRange(TreeNode $node) { + if ( + $node->getId() === '#range' + && count($node->getChildren()) === 2 + ) { + $start = $node->getChildren()[0]; + $end = $node->getChildren()[1]; + + return is_numeric($this->getLiteralValue($start)) + && is_numeric($this->getLiteralValue($end)); + } + + return false; + } + + private function getLiteralValue(TreeNode $node): ?string + { + if ($node->getId() === 'token' && $node->getValueToken() === 'literal') { + return $node->getValueValue(); + } + + return null; + } + private function getPatternType(Expr $patternExpr, Scope $scope): Type { if ($patternExpr instanceof Expr\BinaryOp\Concat) { diff --git a/src/Type/Php/RegexCapturingGroup.php b/src/Type/Php/RegexCapturingGroup.php index 3008e66294..e5ddac62e2 100644 --- a/src/Type/Php/RegexCapturingGroup.php +++ b/src/Type/Php/RegexCapturingGroup.php @@ -2,6 +2,8 @@ namespace PHPStan\Type\Php; +use PHPStan\Type\Type; + class RegexCapturingGroup { @@ -13,6 +15,7 @@ public function __construct( private ?int $alternationId, private bool $inOptionalQuantification, private RegexCapturingGroup|RegexNonCapturingGroup|null $parent, + private Type $type, ) { } @@ -92,4 +95,9 @@ public function getName(): ?string return $this->name; } + public function getType(): Type + { + return $this->type; + } + } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 0028c9e655..d1999b1d8e 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -418,11 +418,11 @@ function (string $s): void { function (string $s): void { if (preg_match('/' . preg_quote($s, '/') . '(\d)/', $s, $matches)) { - assertType('array{string, string}', $matches); + assertType('array{string, numeric-string}', $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{string, string}', $matches); + assertType('array{}|array{string, numeric-string}', $matches); }; function (string $s): void { @@ -442,3 +442,24 @@ function (string $s, $mixed): void { } assertType('array', $matches); }; + +function (string $size): void { + if (preg_match('/ab(\d){2,4}xx([0-9])?e?/', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{0: string, 1: numeric-string, 2?: numeric-string}', $matches); +}; + +function (string $size): void { + if (preg_match('/a(\dAB){2}b(\d){2,4}([1-5])([1-5a-z])e?/', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{string, string, numeric-string, numeric-string, string}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(ab(\d)){2,4}xx([0-9][a-c])?e?/', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{0: string, 1: string, 2: numeric-string, 3?: string}', $matches); +}; From a8b643113307912c70a582fa8a841d611b5416c6 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 15 Jul 2024 22:26:54 +0200 Subject: [PATCH 02/18] fix --- src/Type/Php/RegexArrayShapeMatcher.php | 7 ++++ .../Analyser/nsrt/preg_match_shapes.php | 39 +++++++++++-------- .../Analyser/nsrt/preg_match_shapes_php82.php | 8 ++-- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 0e770377ee..7b560f44fd 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -549,6 +549,13 @@ private function isNumericGroup(TreeNode $group): bool { $children = $group->getChildren(); + if (count($children) === 1 && $children[0]->getId() === '#quantification') { + $quantification = $children[0]; + if (count($quantification->getChildren()) === 2) { + $children = [$quantification->getChildren()[0]]; + } + } + if ( count($children) === 1 && $children[0]->getId() === 'token' diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index d1999b1d8e..3191d032d8 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{string, string}', $matches); if (preg_match('/Price: (£|€)(\d+)/i', $s, $matches)) { - assertType('array{string, string, string}', $matches); + assertType('array{string, string, numeric-string}', $matches); } - assertType('array{}|array{string, string, string}', $matches); + assertType('array{}|array{string, string, numeric-string}', $matches); if (preg_match(' /Price: (£|€)\d+/ i u', $s, $matches)) { assertType('array{string, string}', $matches); @@ -91,9 +91,9 @@ function doMatch(string $s): void { function doNonCapturingGroup(string $s): void { if (preg_match('/Price: (?:£|€)(\d+)/', $s, $matches)) { - assertType('array{string, string}', $matches); + assertType('array{string, numeric-string}', $matches); } - assertType('array{}|array{string, string}', $matches); + assertType('array{}|array{string, numeric-string}', $matches); } function doNamedSubpattern(string $s): void { @@ -159,9 +159,9 @@ function hoaBug31(string $s): void { assertType('array{}|array{string, string}', $matches); if (preg_match('/\w-(\d+)-(\w)/', $s, $matches)) { - assertType('array{string, string, string}', $matches); + assertType('array{string, numeric-string, string}', $matches); } - assertType('array{}|array{string, string, string}', $matches); + assertType('array{}|array{string, numeric-string, string}', $matches); } // https://github.com/phpstan/phpstan/issues/10855#issuecomment-2044323638 @@ -235,9 +235,9 @@ function testUnionPattern(string $s): void $pattern = '/Price: (\d+)(\d+)(\d+)/'; } if (preg_match($pattern, $s, $matches)) { - assertType('array{string, string, string, string}|array{string, string}', $matches); + assertType('array{string, numeric-string, numeric-string, numeric-string}|array{string, numeric-string}', $matches); } - assertType('array{}|array{string, string, string, string}|array{string, string}', $matches); + assertType('array{}|array{string, numeric-string, numeric-string, numeric-string}|array{string, numeric-string}', $matches); } function doFoo(string $row): void @@ -268,42 +268,42 @@ function doFoo3(string $row): void return; } - assertType('array{string, string, string, string, string, string, string}', $matches); + assertType('array{string, string, string, numeric-string, numeric-string, numeric-string, numeric-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{string, string, string, string, string}|array{string}', $matches); + assertType('array{string, string, numeric-string, numeric-string, string}|array{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{string, string, string}|array{string}', $matches); + assertType('array{string, string, numeric-string}|array{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: string, 1: string, 2?: string}', $matches); + assertType('array{0: string, 1: string, 2?: numeric-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: string, 1?: string, 2?: string}', $matches); + assertType('array{0: string, 1?: string, 2?: numeric-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{string, string, string}', $matches); + assertType('array{string, string, numeric-string}', $matches); }; function (string $size): void { @@ -317,14 +317,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{0: string, 1: string, 2: string, 3?: string, 4?: string}', $matches); + assertType('array{0: string, 1: numeric-string, 2: numeric-string, 3?: numeric-string, 4?: numeric-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{0: string, 1: string, 2: string, 3?: string, 4?: string}|array{string}', $matches); + assertType('array{0: string, 1: numeric-string, 2: numeric-string, 3?: numeric-string, 4?: numeric-string}|array{string}', $matches); }; function (string $size): void { @@ -463,3 +463,10 @@ function (string $size): void { } assertType('array{0: string, 1: string, 2: numeric-string, 3?: string}', $matches); }; + +function (string $size): void { + if (preg_match('/ab(\d+)e(\d?)/', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{string, numeric-string, numeric-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 737f7a8c8d..2117cf49b5 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php @@ -8,12 +8,12 @@ // https://php.watch/versions/8.2/preg-n-no-capture-modifier function doNonAutoCapturingFlag(string $s): void { if (preg_match('/(\d+)/n', $s, $matches)) { - assertType('array{string, string}', $matches); // should be 'array{string}' + assertType('array{string, numeric-string}', $matches); // should be 'array{string}' } - assertType('array{}|array{string, string}', $matches); + assertType('array{}|array{string, numeric-string}', $matches); if (preg_match('/(\d+)(?P\d+)/n', $s, $matches)) { - assertType('array{0: string, 1: string, num: string, 2: string}', $matches); + assertType('array{0: string, 1: numeric-string, num: string, 2: string}', $matches); } - assertType('array{}|array{0: string, 1: string, num: string, 2: string}', $matches); + assertType('array{}|array{0: string, 1: numeric-string, num: string, 2: string}', $matches); } From 4990348935c52a0863621abe0529e0c2f59c04e4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 15 Jul 2024 22:30:28 +0200 Subject: [PATCH 03/18] add test --- tests/PHPStan/Analyser/nsrt/preg_match_shapes.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 3191d032d8..4566f74493 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -470,3 +470,10 @@ function (string $size): void { } assertType('array{string, numeric-string, numeric-string}', $matches); }; + +function (string $size): void { + if (preg_match('/ab(?P\d+)e?/', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches); +}; From 1b0bc296099a371b5bb14e12dfb304984a595a00 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 15 Jul 2024 22:45:47 +0200 Subject: [PATCH 04/18] fix --- src/Type/Php/RegexArrayShapeMatcher.php | 6 ++++++ tests/PHPStan/Analyser/nsrt/bug-11311.php | 2 +- tests/PHPStan/Analyser/nsrt/preg_match_shapes.php | 6 +++--- tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php | 4 ++-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 7b560f44fd..768161671e 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -549,6 +549,12 @@ private function isNumericGroup(TreeNode $group): bool { $children = $group->getChildren(); + // remove capturing group name + if ($group->getId() === '#namedcapturing') { + $children = [$children[1]]; + } + + // #quantification is not relevant for the type of the group if (count($children) === 1 && $children[0]->getId() === '#quantification') { $quantification = $children[0]; if (count($quantification->getChildren()) === 2) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-11311.php b/tests/PHPStan/Analyser/nsrt/bug-11311.php index 20d2a4e1bc..7ed9655d22 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11311.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11311.php @@ -7,7 +7,7 @@ function doFoo(string $s) { if (1 === preg_match('/(?\d+)\.(?\d+)(?:\.(?\d+))?/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{0: string, major: string, 1: string, minor: string, 2: string, patch: string|null, 3: string|null}', $matches); + assertType('array{0: string, major: numeric-string, 1: numeric-string, minor: numeric-string, 2: numeric-string, patch: numeric-string|null, 3: numeric-string|null}', $matches); } } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 4566f74493..bbfe223eaf 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -98,9 +98,9 @@ function doNonCapturingGroup(string $s): void { function doNamedSubpattern(string $s): void { if (preg_match('/\w-(?P\d+)-(\w)/', $s, $matches)) { - assertType('array{0: string, num: string, 1: string, 2: string}', $matches); + assertType('array{0: string, num: numeric-string, 1: numeric-string, 2: string}', $matches); } - assertType('array{}|array{0: string, num: string, 1: string, 2: string}', $matches); + assertType('array{}|array{0: string, num: numeric-string, 1: numeric-string, 2: string}', $matches); if (preg_match('/^(?\S+::\S+)/', $s, $matches)) { assertType('array{0: string, name: string, 1: string}', $matches); @@ -259,7 +259,7 @@ function doFoo2(string $row): void return; } - assertType('array{0: string, 1: string, branchCode: string, 2: string, accountNumber: string, 3: string, bankCode: string, 4: string}', $matches); + assertType('array{0: string, 1: string, branchCode: numeric-string, 2: numeric-string, accountNumber: numeric-string, 3: numeric-string, bankCode: numeric-string, 4: numeric-string}', $matches); } function doFoo3(string $row): void diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php index 2117cf49b5..af50a43064 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php @@ -13,7 +13,7 @@ function doNonAutoCapturingFlag(string $s): void { assertType('array{}|array{string, numeric-string}', $matches); if (preg_match('/(\d+)(?P\d+)/n', $s, $matches)) { - assertType('array{0: string, 1: numeric-string, num: string, 2: string}', $matches); + assertType('array{0: string, 1: numeric-string, num: numeric-string, 2: numeric-string}', $matches); } - assertType('array{}|array{0: string, 1: numeric-string, num: string, 2: string}', $matches); + assertType('array{}|array{0: string, 1: numeric-string, num: numeric-string, 2: numeric-string}', $matches); } From 479e5fa1f7def244dc34831dad04165c598e5e46 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jul 2024 16:39:21 +0200 Subject: [PATCH 05/18] fix --- src/Type/Php/RegexArrayShapeMatcher.php | 127 ++++++---- .../Analyser/nsrt/preg_match_shapes.php | 219 +++++++++++------- .../Analyser/nsrt/preg_match_shapes_php80.php | 4 +- .../Analyser/nsrt/preg_match_shapes_php82.php | 8 +- 4 files changed, 231 insertions(+), 127 deletions(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 768161671e..e0b3f37fb7 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -7,12 +7,14 @@ use Hoa\Compiler\Llk\TreeNode; use Hoa\Exception\Exception; use Hoa\File\Read; +use Nette\Utils\Strings; use PhpParser\Node\Expr; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; @@ -534,69 +536,110 @@ private function getQuantificationRange(TreeNode $node): array return [$min, $max]; } - private function createGroupType(TreeNode $ast): Type { - if ($this->isNumericGroup($ast)) { + private function createGroupType(TreeNode $group): Type + { + $isNonEmpty = TrinaryLogic::createMaybe(); + $isNumeric = TrinaryLogic::createMaybe(); + $inOptionalQuantification = false; + + $this->walkGroupAst($group, $isNonEmpty, $isNumeric, $inOptionalQuantification); + + $accessories = []; + if ($isNumeric->yes()) { + $accessories[] = new AccessoryNumericStringType(); + } + + if ($isNonEmpty->yes()) { + $accessories[] = new AccessoryNonEmptyStringType(); + } + + if ($accessories !== []) { return new IntersectionType([ new StringType(), - new AccessoryNumericStringType() + ...$accessories ]); } return new StringType(); } - private function isNumericGroup(TreeNode $group): bool - { - $children = $group->getChildren(); + private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryLogic &$isNumeric, bool &$inOptionalQuantification): void { + $children = $ast->getChildren(); - // remove capturing group name - if ($group->getId() === '#namedcapturing') { - $children = [$children[1]]; + if ( + $ast->getId() === '#concatenation' + && count($children) > 0 + ) { + $isNonEmpty = TrinaryLogic::createYes(); } - // #quantification is not relevant for the type of the group - if (count($children) === 1 && $children[0]->getId() === '#quantification') { - $quantification = $children[0]; - if (count($quantification->getChildren()) === 2) { - $children = [$quantification->getChildren()[0]]; + if ($ast->getId() === '#quantification') { + [$min] = $this->getQuantificationRange($ast); + + if ($min === 0) { + $inOptionalQuantification = true; + } + if ($min >= 1) { + $isNonEmpty = TrinaryLogic::createYes(); + $inOptionalQuantification = false; } } - if ( - count($children) === 1 - && $children[0]->getId() === 'token' - && $children[0]->getValueToken() === 'character_type' - && $children[0]->getValueValue() === '\d' - ) { - return true; - } + if ($ast->getId() === 'token') { + $literalValue = $this->getLiteralValue($ast); + if ($literalValue !== null && !Strings::match($literalValue, '/^\d+$/')) { + $isNumeric = TrinaryLogic::createNo(); + } - if ( - count($children) === 1 - && $children[0]->getId() === '#class' - && count($children[0]->getChildren()) === 1 - && $this->isNumericRange($children[0]->getChildren()[0]) - ) - { - return true; + if ($ast->getValueToken() === 'character_type') { + if ('\d' === $ast->getValueValue()) { + if ($isNumeric->maybe()) { + $isNumeric = TrinaryLogic::createYes(); + } + } else { + $isNumeric = TrinaryLogic::createNo(); + } + } + + if (!$inOptionalQuantification) { + $isNonEmpty = TrinaryLogic::createYes(); + } } - return false; - } + if ($ast->getId() === '#range') { + if ($isNumeric->maybe()) { + $allNumeric = true; + foreach($children as $child) { + $literalValue = $this->getLiteralValue($child); - private function isNumericRange(TreeNode $node) { - if ( - $node->getId() === '#range' - && count($node->getChildren()) === 2 - ) { - $start = $node->getChildren()[0]; - $end = $node->getChildren()[1]; + if ($literalValue === null) { + break; + } - return is_numeric($this->getLiteralValue($start)) - && is_numeric($this->getLiteralValue($end)); + if (!Strings::match($literalValue, '/^\d+$/')) { + $allNumeric = false; + break; + } + } + + if ($allNumeric) { + $isNumeric = TrinaryLogic::createYes(); + } + } + + if (!$inOptionalQuantification) { + $isNonEmpty = TrinaryLogic::createYes(); + } } - return false; + foreach($children as $child) { + $this->walkGroupAst( + $child, + $isNonEmpty, + $isNumeric, + $inOptionalQuantification + ); + } } private function getLiteralValue(TreeNode $node): ?string diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index bbfe223eaf..e324f8e3a2 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -12,112 +12,112 @@ function doMatch(string $s): void { assertType('array{}|array{string}', $matches); if (preg_match('/Price: (£|€)\d+/', $s, $matches)) { - assertType('array{string, string}', $matches); + assertType('array{string, non-empty-string}', $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{string, string}', $matches); + assertType('array{}|array{string, non-empty-string}', $matches); if (preg_match('/Price: (£|€)(\d+)/i', $s, $matches)) { - assertType('array{string, string, numeric-string}', $matches); + assertType('array{string, non-empty-string, non-empty-string&numeric-string}', $matches); } - assertType('array{}|array{string, string, numeric-string}', $matches); + assertType('array{}|array{string, non-empty-string, non-empty-string&numeric-string}', $matches); if (preg_match(' /Price: (£|€)\d+/ i u', $s, $matches)) { - assertType('array{string, string}', $matches); + assertType('array{string, non-empty-string}', $matches); } - assertType('array{}|array{string, string}', $matches); + assertType('array{}|array{string, non-empty-string}', $matches); if (preg_match('(Price: (£|€))i', $s, $matches)) { - assertType('array{string, string, string}', $matches); + assertType('array{string, non-empty-string, non-empty-string}', $matches); } - assertType('array{}|array{string, string, string}', $matches); + assertType('array{}|array{string, non-empty-string, non-empty-string}', $matches); if (preg_match('_foo(.)\_i_i', $s, $matches)) { - assertType('array{string, string}', $matches); + assertType('array{string, non-empty-string}', $matches); } - assertType('array{}|array{string, string}', $matches); + assertType('array{}|array{string, non-empty-string}', $matches); if (preg_match('/(a)(b)*(c)(d)*/', $s, $matches)) { - assertType('array{0: string, 1: string, 2: string, 3: string, 4?: string}', $matches); + assertType('array{0: string, 1: non-empty-string, 2: non-empty-string, 3: non-empty-string, 4?: non-empty-string}', $matches); } - assertType('array{}|array{0: string, 1: string, 2: string, 3: string, 4?: string}', $matches); + assertType('array{}|array{0: string, 1: non-empty-string, 2: non-empty-string, 3: non-empty-string, 4?: non-empty-string}', $matches); if (preg_match('/(a)(?b)*(c)(d)*/', $s, $matches)) { - assertType('array{0: string, 1: string, name: string, 2: string, 3: string, 4?: string}', $matches); + assertType('array{0: string, 1: non-empty-string, name: non-empty-string, 2: non-empty-string, 3: non-empty-string, 4?: non-empty-string}', $matches); } - assertType('array{}|array{0: string, 1: string, name: string, 2: string, 3: string, 4?: string}', $matches); + assertType('array{}|array{0: string, 1: non-empty-string, name: non-empty-string, 2: non-empty-string, 3: non-empty-string, 4?: non-empty-string}', $matches); if (preg_match('/(a)(b)*(c)(?d)*/', $s, $matches)) { - assertType('array{0: string, 1: string, 2: string, 3: string, name?: string, 4?: string}', $matches); + assertType('array{0: string, 1: non-empty-string, 2: non-empty-string, 3: non-empty-string, name?: non-empty-string, 4?: non-empty-string}', $matches); } - assertType('array{}|array{0: string, 1: string, 2: string, 3: string, name?: string, 4?: string}', $matches); + assertType('array{}|array{0: string, 1: non-empty-string, 2: non-empty-string, 3: non-empty-string, name?: non-empty-string, 4?: non-empty-string}', $matches); if (preg_match('/(a|b)|(?:c)/', $s, $matches)) { - assertType('array{0: string, 1?: string}', $matches); + assertType('array{0: string, 1?: non-empty-string}', $matches); } - assertType('array{}|array{0: string, 1?: string}', $matches); + assertType('array{}|array{0: string, 1?: non-empty-string}', $matches); if (preg_match('/(foo)(bar)(baz)+/', $s, $matches)) { - assertType('array{string, string, string, string}', $matches); + assertType('array{string, non-empty-string, non-empty-string, non-empty-string}', $matches); } - assertType('array{}|array{string, string, string, string}', $matches); + assertType('array{}|array{string, non-empty-string, non-empty-string, non-empty-string}', $matches); if (preg_match('/(foo)(bar)(baz)*/', $s, $matches)) { - assertType('array{0: string, 1: string, 2: string, 3?: string}', $matches); + assertType('array{0: string, 1: non-empty-string, 2: non-empty-string, 3?: non-empty-string}', $matches); } - assertType('array{}|array{0: string, 1: string, 2: string, 3?: string}', $matches); + assertType('array{}|array{0: string, 1: non-empty-string, 2: non-empty-string, 3?: non-empty-string}', $matches); if (preg_match('/(foo)(bar)(baz)?/', $s, $matches)) { - assertType('array{0: string, 1: string, 2: string, 3?: string}', $matches); + assertType('array{0: string, 1: non-empty-string, 2: non-empty-string, 3?: non-empty-string}', $matches); } - assertType('array{}|array{0: string, 1: string, 2: string, 3?: string}', $matches); + assertType('array{}|array{0: string, 1: non-empty-string, 2: non-empty-string, 3?: non-empty-string}', $matches); if (preg_match('/(foo)(bar)(baz){0,3}/', $s, $matches)) { - assertType('array{0: string, 1: string, 2: string, 3?: string}', $matches); + assertType('array{0: string, 1: non-empty-string, 2: non-empty-string, 3?: non-empty-string}', $matches); } - assertType('array{}|array{0: string, 1: string, 2: string, 3?: string}', $matches); + assertType('array{}|array{0: string, 1: non-empty-string, 2: non-empty-string, 3?: non-empty-string}', $matches); if (preg_match('/(foo)(bar)(baz){2,3}/', $s, $matches)) { - assertType('array{string, string, string, string}', $matches); + assertType('array{string, non-empty-string, non-empty-string, non-empty-string}', $matches); } - assertType('array{}|array{string, string, string, string}', $matches); + assertType('array{}|array{string, non-empty-string, non-empty-string, non-empty-string}', $matches); if (preg_match('/(foo)(bar)(baz){2}/', $s, $matches)) { - assertType('array{string, string, string, string}', $matches); + assertType('array{string, non-empty-string, non-empty-string, non-empty-string}', $matches); } - assertType('array{}|array{string, string, string, string}', $matches); + assertType('array{}|array{string, non-empty-string, non-empty-string, non-empty-string}', $matches); } function doNonCapturingGroup(string $s): void { if (preg_match('/Price: (?:£|€)(\d+)/', $s, $matches)) { - assertType('array{string, numeric-string}', $matches); + assertType('array{string, non-empty-string&numeric-string}', $matches); } - assertType('array{}|array{string, numeric-string}', $matches); + assertType('array{}|array{string, non-empty-string&numeric-string}', $matches); } function doNamedSubpattern(string $s): void { if (preg_match('/\w-(?P\d+)-(\w)/', $s, $matches)) { - assertType('array{0: string, num: numeric-string, 1: numeric-string, 2: string}', $matches); + assertType('array{0: string, num: non-empty-string&numeric-string, 1: non-empty-string&numeric-string, 2: non-empty-string}', $matches); } - assertType('array{}|array{0: string, num: numeric-string, 1: numeric-string, 2: string}', $matches); + assertType('array{}|array{0: string, num: non-empty-string&numeric-string, 1: non-empty-string&numeric-string, 2: non-empty-string}', $matches); if (preg_match('/^(?\S+::\S+)/', $s, $matches)) { - assertType('array{0: string, name: string, 1: string}', $matches); + assertType('array{0: string, name: non-empty-string, 1: non-empty-string}', $matches); } - assertType('array{}|array{0: string, name: string, 1: string}', $matches); + assertType('array{}|array{0: string, name: non-empty-string, 1: non-empty-string}', $matches); if (preg_match('/^(?\S+::\S+)(?:(? with data set (?:#\d+|"[^"]+"))\s\()?/', $s, $matches)) { - assertType('array{0: string, name: string, 1: string, dataname?: string, 2?: string}', $matches); + assertType('array{0: string, name: non-empty-string, 1: non-empty-string, dataname?: non-empty-string, 2?: non-empty-string}', $matches); } - assertType('array{}|array{0: string, name: string, 1: string, dataname?: string, 2?: string}', $matches); + assertType('array{}|array{0: string, name: non-empty-string, 1: non-empty-string, dataname?: non-empty-string, 2?: non-empty-string}', $matches); } function doOffsetCapture(string $s): void { if (preg_match('/(foo)(bar)(baz)/', $s, $matches, PREG_OFFSET_CAPTURE)) { - assertType('array{array{string, int<0, max>}, array{string, int<0, max>}, array{string, int<0, max>}, array{string, int<0, max>}}', $matches); + assertType('array{array{string, int<0, max>}, array{non-empty-string, int<0, max>}, array{non-empty-string, int<0, max>}, array{non-empty-string, int<0, max>}}', $matches); } - assertType('array{}|array{array{string, int<0, max>}, array{string, int<0, max>}, array{string, int<0, max>}, array{string, int<0, max>}}', $matches); + assertType('array{}|array{array{string, int<0, max>}, array{non-empty-string, int<0, max>}, array{non-empty-string, int<0, max>}, array{non-empty-string, int<0, max>}}', $matches); } function doUnknownFlags(string $s, int $flags): void { @@ -154,14 +154,14 @@ function doMultipleConsecutiveCaptureGroupsWithSameNameWithModifier(string $s): // https://github.com/hoaproject/Regex/issues/31 function hoaBug31(string $s): void { if (preg_match('/([\w-])/', $s, $matches)) { - assertType('array{string, string}', $matches); + assertType('array{string, non-empty-string}', $matches); } - assertType('array{}|array{string, string}', $matches); + assertType('array{}|array{string, non-empty-string}', $matches); if (preg_match('/\w-(\d+)-(\w)/', $s, $matches)) { - assertType('array{string, numeric-string, string}', $matches); + assertType('array{string, non-empty-string&numeric-string, non-empty-string}', $matches); } - assertType('array{}|array{string, numeric-string, string}', $matches); + assertType('array{}|array{string, non-empty-string&numeric-string, non-empty-string}', $matches); } // https://github.com/phpstan/phpstan/issues/10855#issuecomment-2044323638 @@ -235,21 +235,21 @@ function testUnionPattern(string $s): void $pattern = '/Price: (\d+)(\d+)(\d+)/'; } if (preg_match($pattern, $s, $matches)) { - assertType('array{string, numeric-string, numeric-string, numeric-string}|array{string, numeric-string}', $matches); + assertType('array{string, non-empty-string&numeric-string, non-empty-string&numeric-string, non-empty-string&numeric-string}|array{string, non-empty-string&numeric-string}', $matches); } - assertType('array{}|array{string, numeric-string, numeric-string, numeric-string}|array{string, numeric-string}', $matches); + assertType('array{}|array{string, non-empty-string&numeric-string, non-empty-string&numeric-string, non-empty-string&numeric-string}|array{string, non-empty-string&numeric-string}', $matches); } function doFoo(string $row): void { if (preg_match('~^(a(b))$~', $row, $matches) === 1) { - assertType('array{string, string, string}', $matches); + assertType('array{string, non-empty-string, non-empty-string}', $matches); } if (preg_match('~^(a(b)?)$~', $row, $matches) === 1) { - assertType('array{0: string, 1: string, 2?: string}', $matches); + assertType('array{0: string, 1: non-empty-string, 2?: non-empty-string}', $matches); } if (preg_match('~^(a(b)?)?$~', $row, $matches) === 1) { - assertType('array{0: string, 1?: string, 2?: string}', $matches); + assertType('array{0: string, 1?: non-empty-string, 2?: non-empty-string}', $matches); } } @@ -268,21 +268,21 @@ function doFoo3(string $row): void return; } - assertType('array{string, string, string, numeric-string, numeric-string, numeric-string, numeric-string}', $matches); + assertType('array{string, non-empty-string, non-empty-string, non-empty-string&numeric-string, non-empty-string&numeric-string, non-empty-string&numeric-string, non-empty-string&numeric-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{string, string, numeric-string, numeric-string, string}|array{string}', $matches); + assertType('array{string, non-empty-string, non-empty-string&numeric-string, non-empty-string&numeric-string, non-empty-string}|array{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{string, string, numeric-string}|array{string}', $matches); + assertType('array{string, non-empty-string, non-empty-string&numeric-string}|array{string}', $matches); }; function (string $size): void { @@ -296,7 +296,7 @@ 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: string, 1?: string, 2?: numeric-string}', $matches); + assertType('array{0: string, 1?: non-empty-string, 2?: non-empty-string&numeric-string}', $matches); }; function (string $size): void { @@ -310,37 +310,37 @@ function (string $size): void { if (preg_match('~^a\.(b)?(c)?d~', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{0: string, 1?: string, 2?: string}', $matches); + assertType('array{0: string, 1?: non-empty-string, 2?: non-empty-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{0: string, 1: numeric-string, 2: numeric-string, 3?: numeric-string, 4?: numeric-string}', $matches); + assertType('array{0: string, 1: non-empty-string&numeric-string, 2: non-empty-string&numeric-string, 3?: non-empty-string&numeric-string, 4?: non-empty-string&numeric-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{0: string, 1: numeric-string, 2: numeric-string, 3?: numeric-string, 4?: numeric-string}|array{string}', $matches); + assertType('array{0: string, 1: non-empty-string&numeric-string, 2: non-empty-string&numeric-string, 3?: non-empty-string&numeric-string, 4?: non-empty-string&numeric-string}|array{string}', $matches); }; function (string $size): void { if (preg_match('~\{(?:(include)\\s+(?:[$]?\\w+(?£|€)\d+/', $s, $matches)) { - assertType('array{0: string, currency: string, 1: string}', $matches); + assertType('array{0: string, currency: non-empty-string, 1: non-empty-string}', $matches); } else { assertType('array{}', $matches); } @@ -382,56 +382,56 @@ function bug11323a(string $s): void function bug11323b(string $s): void { if (preg_match('/Price: (?£|€)\d+/', $s, $matches)) { - assertType('array{0: string, currency: string, 1: string}', $matches); + assertType('array{0: string, currency: non-empty-string, 1: non-empty-string}', $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{0: string, currency: string, 1: string}', $matches); + assertType('array{}|array{0: string, currency: non-empty-string, 1: non-empty-string}', $matches); } function unmatchedAsNullWithMandatoryGroup(string $s): void { if (preg_match('/Price: (?£|€)\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{0: string, currency: string, 1: string}', $matches); + assertType('array{0: string, currency: non-empty-string, 1: non-empty-string}', $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{0: string, currency: string, 1: string}', $matches); + assertType('array{}|array{0: string, currency: non-empty-string, 1: non-empty-string}', $matches); } function (string $s): void { if (preg_match('{' . preg_quote('xxx') . '(z)}', $s, $matches)) { - assertType('array{string, string}', $matches); + assertType('array{string, non-empty-string}', $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{string, string}', $matches); + assertType('array{}|array{string, non-empty-string}', $matches); }; function (string $s): void { if (preg_match('{' . preg_quote($s) . '(z)}', $s, $matches)) { - assertType('array{string, string}', $matches); + assertType('array{string, non-empty-string}', $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{string, string}', $matches); + assertType('array{}|array{string, non-empty-string}', $matches); }; function (string $s): void { if (preg_match('/' . preg_quote($s, '/') . '(\d)/', $s, $matches)) { - assertType('array{string, numeric-string}', $matches); + assertType('array{string, non-empty-string&numeric-string}', $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{string, numeric-string}', $matches); + assertType('array{}|array{string, non-empty-string&numeric-string}', $matches); }; function (string $s): void { if (preg_match('{' . preg_quote($s) . '(z)' . preg_quote($s) . '(?:abc)(def)?}', $s, $matches)) { - assertType('array{0: string, 1: string, 2?: string}', $matches); + assertType('array{0: string, 1: non-empty-string, 2?: non-empty-string}', $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{0: string, 1: string, 2?: string}', $matches); + assertType('array{}|array{0: string, 1: non-empty-string, 2?: non-empty-string}', $matches); }; function (string $s, $mixed): void { @@ -447,7 +447,7 @@ function (string $size): void { if (preg_match('/ab(\d){2,4}xx([0-9])?e?/', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{0: string, 1: numeric-string, 2?: numeric-string}', $matches); + assertType('array{0: string, 1: non-empty-string&numeric-string, 2?: non-empty-string}', $matches); }; function (string $size): void { @@ -468,7 +468,7 @@ function (string $size): void { if (preg_match('/ab(\d+)e(\d?)/', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{string, numeric-string, numeric-string}', $matches); + assertType('array{string, non-empty-string&numeric-string, numeric-string}', $matches); }; function (string $size): void { @@ -477,3 +477,64 @@ function (string $size): void { } assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches); }; + +function (string $size): void { + if (preg_match('/ab(\d\d)/', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{string, non-empty-string&numeric-string}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(\d+\s)e?/', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{string, non-empty-string}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(\s)e?/', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{string, non-empty-string}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(\S)e?/', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{string, non-empty-string}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(\S?)e?/', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{string, string}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(\S)?e?/', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{0: string, 1?: non-empty-string}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(\d+\d?)e?/', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{string, non-empty-string&numeric-string}', $matches); +}; + +function (string $s): void { + if (preg_match('/Price: ([2-5])/i', $s, $matches)) { + assertType('array{string, non-empty-string&numeric-string}', $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: ([2-5A-Z])/i', $s, $matches)) { + assertType('array{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 d3f0421457..2a7f5cb923 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php @@ -7,7 +7,7 @@ function doOffsetCaptureWithUnmatchedNull(string $s): void { // see https://3v4l.org/07rBO#v8.2.9 if (preg_match('/(foo)(bar)(baz)/', $s, $matches, PREG_OFFSET_CAPTURE|PREG_UNMATCHED_AS_NULL)) { - assertType('array{array{string|null, int<-1, max>}, array{string|null, int<-1, max>}, array{string|null, int<-1, max>}, array{string|null, int<-1, max>}}', $matches); + assertType('array{array{string|null, int<-1, max>}, array{non-empty-string|null, int<-1, max>}, array{non-empty-string|null, int<-1, max>}, array{non-empty-string|null, int<-1, max>}}', $matches); } - assertType('array{}|array{array{string|null, int<-1, max>}, array{string|null, int<-1, max>}, array{string|null, int<-1, max>}, array{string|null, int<-1, max>}}', $matches); + assertType('array{}|array{array{string|null, int<-1, max>}, array{non-empty-string|null, int<-1, max>}, array{non-empty-string|null, int<-1, max>}, array{non-empty-string|null, int<-1, max>}}', $matches); } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php index af50a43064..1b8797a814 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php @@ -8,12 +8,12 @@ // https://php.watch/versions/8.2/preg-n-no-capture-modifier function doNonAutoCapturingFlag(string $s): void { if (preg_match('/(\d+)/n', $s, $matches)) { - assertType('array{string, numeric-string}', $matches); // should be 'array{string}' + assertType('array{string, non-empty-string&numeric-string}', $matches); // should be 'array{string}' } - assertType('array{}|array{string, numeric-string}', $matches); + assertType('array{}|array{string, non-empty-string&numeric-string}', $matches); if (preg_match('/(\d+)(?P\d+)/n', $s, $matches)) { - assertType('array{0: string, 1: numeric-string, num: numeric-string, 2: numeric-string}', $matches); + assertType('array{0: string, 1: non-empty-string&numeric-string, num: non-empty-string&numeric-string, 2: non-empty-string&numeric-string}', $matches); } - assertType('array{}|array{0: string, 1: numeric-string, num: numeric-string, 2: numeric-string}', $matches); + assertType('array{}|array{0: string, 1: non-empty-string&numeric-string, num: non-empty-string&numeric-string, 2: non-empty-string&numeric-string}', $matches); } From b874b8d05a9dbea0e008de8d05f2eae1d63f3270 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jul 2024 16:46:18 +0200 Subject: [PATCH 06/18] fix --- tests/PHPStan/Analyser/nsrt/bug-11311.php | 20 +++++++++---------- .../Analyser/nsrt/preg_match_shapes.php | 16 +++++++-------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-11311.php b/tests/PHPStan/Analyser/nsrt/bug-11311.php index 7ed9655d22..9fbf51e74a 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11311.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11311.php @@ -7,26 +7,26 @@ function doFoo(string $s) { if (1 === preg_match('/(?\d+)\.(?\d+)(?:\.(?\d+))?/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{0: 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: string, major: non-empty-string&numeric-string, 1: non-empty-string&numeric-string, minor: non-empty-string&numeric-string, 2: non-empty-string&numeric-string, patch: (non-empty-string&numeric-string)|null, 3: (non-empty-string&numeric-string)|null}', $matches); } } function doUnmatchedAsNull(string $s): void { if (preg_match('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{string, string|null, string|null, string|null}', $matches); + assertType('array{string, non-empty-string|null, non-empty-string|null, non-empty-string|null}', $matches); } - assertType('array{}|array{string, string|null, string|null, string|null}', $matches); + assertType('array{}|array{string, non-empty-string|null, non-empty-string|null, non-empty-string|null}', $matches); } // see https://3v4l.org/VeDob function unmatchedAsNullWithOptionalGroup(string $s): void { if (preg_match('/Price: (£|€)?\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { // with PREG_UNMATCHED_AS_NULL the offset 1 will always exist. It is correct that it's nullable because it's optional though - assertType('array{string, string|null}', $matches); + assertType('array{string, non-empty-string|null}', $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{string, string|null}', $matches); + assertType('array{}|array{string, non-empty-string|null}', $matches); } function bug11331a(string $url):void { @@ -36,7 +36,7 @@ function bug11331a(string $url):void { (?.+) )? (?.+)}mix', $url, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{0: string, a: string|null, 1: string|null, b: string, 2: string}', $matches); + assertType('array{0: string, a: non-empty-string|null, 1: non-empty-string|null, b: non-empty-string, 2: non-empty-string}', $matches); } } @@ -46,7 +46,7 @@ function bug11331b(string $url):void { (?.+) )? (?.+)?}mix', $url, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{0: string, a: string|null, 1: string|null, b: string|null, 2: string|null}', $matches); + assertType('array{0: string, a: non-empty-string|null, 1: non-empty-string|null, b: non-empty-string|null, 2: non-empty-string|null}', $matches); } } @@ -62,20 +62,20 @@ function bug11331c(string $url):void { ([^/]+?) (?:\.git|/)? $}x', $url, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{string, string|null, string|null, string, string}', $matches); + assertType('array{string, non-empty-string|null, non-empty-string|null, non-empty-string, non-empty-string}', $matches); } } class UnmatchedAsNullWithTopLevelAlternation { function doFoo(string $s): void { if (preg_match('/Price: (?:(£)|(€))\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{string, string|null, string|null}', $matches); // could be array{0: string, 1: null, 2: string}|array{0: string, 1: string, 2: null} + assertType('array{string, non-empty-string|null, non-empty-string|null}', $matches); // could be array{0: string, 1: null, 2: string}|array{0: string, 1: string, 2: null} } } function doBar(string $s): void { if (preg_match('/Price: (?:(£)|(€))?\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{string, string|null, string|null}', $matches); + assertType('array{string, non-empty-string|null, non-empty-string|null}', $matches); } } } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index e324f8e3a2..40903e80ae 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -259,7 +259,7 @@ function doFoo2(string $row): void return; } - assertType('array{0: string, 1: string, branchCode: numeric-string, 2: numeric-string, accountNumber: numeric-string, 3: numeric-string, bankCode: numeric-string, 4: numeric-string}', $matches); + assertType('array{0: string, 1: non-empty-string&numeric-string, branchCode: non-empty-string&numeric-string, 2: non-empty-string&numeric-string, accountNumber: non-empty-string&numeric-string, 3: non-empty-string&numeric-string, bankCode: non-empty-string&numeric-string, 4: non-empty-string&numeric-string}', $matches); } function doFoo3(string $row): void @@ -289,7 +289,7 @@ 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: string, 1: string, 2?: numeric-string}', $matches); + assertType('array{0: string, 1: non-empty-string, 2?: non-empty-string&numeric-string}', $matches); }; function (string $size): void { @@ -303,7 +303,7 @@ 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{string, string, numeric-string}', $matches); + assertType('array{string, non-empty-string, non-empty-string&numeric-string}', $matches); }; function (string $size): void { @@ -376,7 +376,7 @@ function bug11323a(string $s): void } else { assertType('array{}', $matches); } - assertType('array{}|array{0: string, currency: string, 1: string}', $matches); + assertType('array{}|array{0: string, currency: non-empty-string, 1: non-empty-string}', $matches); } function bug11323b(string $s): void @@ -447,21 +447,21 @@ function (string $size): void { if (preg_match('/ab(\d){2,4}xx([0-9])?e?/', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{0: string, 1: non-empty-string&numeric-string, 2?: non-empty-string}', $matches); + assertType('array{0: string, 1: non-empty-string&numeric-string, 2?: non-empty-string&numeric-string}', $matches); }; function (string $size): void { if (preg_match('/a(\dAB){2}b(\d){2,4}([1-5])([1-5a-z])e?/', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{string, string, numeric-string, numeric-string, string}', $matches); + assertType('array{string, non-empty-string, non-empty-string&numeric-string, non-empty-string&numeric-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) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{0: string, 1: string, 2: numeric-string, 3?: string}', $matches); + assertType('array{0: string, 1: non-empty-string, 2: non-empty-string&numeric-string, 3?: non-empty-string}', $matches); }; function (string $size): void { @@ -475,7 +475,7 @@ function (string $size): void { if (preg_match('/ab(?P\d+)e?/', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: string, num: non-empty-string&numeric-string, 1: non-empty-string&numeric-string}', $matches); }; function (string $size): void { From 1111067581d2de47c823869684504238aaf5aba1 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jul 2024 16:54:47 +0200 Subject: [PATCH 07/18] Update preg_match_shapes.php --- tests/PHPStan/Analyser/nsrt/preg_match_shapes.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 40903e80ae..29422fab71 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -340,7 +340,7 @@ function bug11277a(string $value): void if (preg_match('/^\[(.+,?)*\]$/', $value, $matches)) { assertType('array{0: string, 1?: non-empty-string}', $matches); if (count($matches) === 2) { - assertType('array{string, non-empty-string}', $matches); + assertType('array{string, string}', $matches); // could be array{string, non-empty-string} } } } @@ -350,10 +350,10 @@ function bug11277b(string $value): void if (preg_match('/^(?:(.+,?)|(x))*$/', $value, $matches)) { assertType('array{0: string, 1?: non-empty-string, 2?: non-empty-string}', $matches); if (count($matches) === 2) { - assertType('array{string, non-empty-string}', $matches); + assertType('array{string, string}', $matches); // could be array{string, non-empty-string} } if (count($matches) === 3) { - assertType('array{string, non-empty-string, non-empty-string}', $matches); + assertType('array{string, string, string}', $matches); // could be array{string, non-empty-string, non-empty-string} } } } From a09a8f2efec7625db7b085065345f6a64f772c53 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jul 2024 16:56:01 +0200 Subject: [PATCH 08/18] Update RegexArrayShapeMatcher.php --- src/Type/Php/RegexArrayShapeMatcher.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index e0b3f37fb7..dd000361db 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -556,14 +556,15 @@ private function createGroupType(TreeNode $group): Type if ($accessories !== []) { return new IntersectionType([ new StringType(), - ...$accessories + ...$accessories, ]); } return new StringType(); } - private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryLogic &$isNumeric, bool &$inOptionalQuantification): void { + private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryLogic &$isNumeric, bool &$inOptionalQuantification): void + { $children = $ast->getChildren(); if ( @@ -592,7 +593,7 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL } if ($ast->getValueToken() === 'character_type') { - if ('\d' === $ast->getValueValue()) { + if ($ast->getValueValue() === '\d') { if ($isNumeric->maybe()) { $isNumeric = TrinaryLogic::createYes(); } @@ -609,7 +610,7 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL if ($ast->getId() === '#range') { if ($isNumeric->maybe()) { $allNumeric = true; - foreach($children as $child) { + foreach ($children as $child) { $literalValue = $this->getLiteralValue($child); if ($literalValue === null) { @@ -632,12 +633,12 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL } } - foreach($children as $child) { + foreach ($children as $child) { $this->walkGroupAst( $child, $isNonEmpty, $isNumeric, - $inOptionalQuantification + $inOptionalQuantification, ); } } From 76cbb7eab5898389ad0b7077951d2423c2d8349c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jul 2024 17:08:50 +0200 Subject: [PATCH 09/18] Update RegexArrayShapeMatcher.php --- src/Type/Php/RegexArrayShapeMatcher.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index dd000361db..6e3fb2905f 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -588,7 +588,7 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL if ($ast->getId() === 'token') { $literalValue = $this->getLiteralValue($ast); - if ($literalValue !== null && !Strings::match($literalValue, '/^\d+$/')) { + if ($literalValue !== null && Strings::match($literalValue, '/^\d+$/') === null) { $isNumeric = TrinaryLogic::createNo(); } @@ -609,7 +609,7 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL if ($ast->getId() === '#range') { if ($isNumeric->maybe()) { - $allNumeric = true; + $allNumeric = null; foreach ($children as $child) { $literalValue = $this->getLiteralValue($child); @@ -617,13 +617,15 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL break; } - if (!Strings::match($literalValue, '/^\d+$/')) { + if (Strings::match($literalValue, '/^\d+$/') === null) { $allNumeric = false; break; } + + $allNumeric = true; } - if ($allNumeric) { + if ($allNumeric === true) { $isNumeric = TrinaryLogic::createYes(); } } From 37ce85e5feb365bc6d83533bc9c3c076bacae804 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 17 Jul 2024 09:15:34 +0200 Subject: [PATCH 10/18] fix --- src/Type/Php/RegexArrayShapeMatcher.php | 4 + tests/PHPStan/Analyser/nsrt/bug-11311.php | 102 +++++++++++++++++ .../Analyser/nsrt/preg_match_shapes.php | 108 ++---------------- 3 files changed, 115 insertions(+), 99 deletions(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 6e3fb2905f..5e5c138680 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -298,6 +298,10 @@ private function buildArrayType( } } + if (!$optional && $captureGroup->isOptional() && !$this->containsUnmatchedAsNull($flags)) { + $groupValueType = TypeCombinator::union($groupValueType, new ConstantStringType('')); + } + if ($captureGroup->isNamed()) { $builder->setOffsetValueType( $this->getKeyType($captureGroup->getName()), diff --git a/tests/PHPStan/Analyser/nsrt/bug-11311.php b/tests/PHPStan/Analyser/nsrt/bug-11311.php index 9fbf51e74a..b8c28b9beb 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11311.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11311.php @@ -79,3 +79,105 @@ function doBar(string $s): void { } } } + +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{string, non-empty-string&numeric-string, (non-empty-string&numeric-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{string, non-empty-string, non-empty-string&numeric-string, non-empty-string&numeric-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{0: string, 1: non-empty-string, 2: non-empty-string&numeric-string, 3?: non-empty-string}', $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{string, non-empty-string&numeric-string, numeric-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: string, num: non-empty-string&numeric-string, 1: non-empty-string&numeric-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{string, non-empty-string&numeric-string}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(\d+\s)e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{string, non-empty-string}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(\s)e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{string, non-empty-string}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(\S)e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{string, non-empty-string}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(\S?)e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{string, string}', $matches); +}; + +function (string $size): void { + if (preg_match('/ab(\S)?e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType('array{0: string, 1?: non-empty-string}', $matches); +}; + +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{string, non-empty-string&numeric-string}', $matches); +}; + +function (string $s): void { + if (preg_match('/Price: ([2-5])/i', $s, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType('array{string, non-empty-string&numeric-string}', $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: ([2-5A-Z])/i', $s, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType('array{string, non-empty-string}', $matches); + } +}; + +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); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 29422fab71..5593f19c4e 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -39,19 +39,19 @@ function doMatch(string $s): void { assertType('array{}|array{string, non-empty-string}', $matches); if (preg_match('/(a)(b)*(c)(d)*/', $s, $matches)) { - assertType('array{0: string, 1: non-empty-string, 2: non-empty-string, 3: non-empty-string, 4?: non-empty-string}', $matches); + assertType('array{0: string, 1: non-empty-string, 2: string, 3: non-empty-string, 4?: string}', $matches); } - assertType('array{}|array{0: string, 1: non-empty-string, 2: non-empty-string, 3: non-empty-string, 4?: non-empty-string}', $matches); + assertType('array{}|array{0: string, 1: non-empty-string, 2: string, 3: non-empty-string, 4?: string}', $matches); if (preg_match('/(a)(?b)*(c)(d)*/', $s, $matches)) { - assertType('array{0: string, 1: non-empty-string, name: non-empty-string, 2: non-empty-string, 3: non-empty-string, 4?: non-empty-string}', $matches); + assertType('array{0: string, 1: non-empty-string, name: string, 2: string, 3: non-empty-string, 4?: non-empty-string}', $matches); } - assertType('array{}|array{0: string, 1: non-empty-string, name: non-empty-string, 2: non-empty-string, 3: non-empty-string, 4?: non-empty-string}', $matches); + assertType('array{}|array{0: string, 1: non-empty-string, name: string, 2: string, 3: non-empty-string, 4?: non-empty-string}', $matches); if (preg_match('/(a)(b)*(c)(?d)*/', $s, $matches)) { - assertType('array{0: string, 1: non-empty-string, 2: non-empty-string, 3: non-empty-string, name?: non-empty-string, 4?: non-empty-string}', $matches); + assertType('array{0: string, 1: non-empty-string, 2: string, 3: non-empty-string, name?: non-empty-string, 4?: non-empty-string}', $matches); } - assertType('array{}|array{0: string, 1: non-empty-string, 2: non-empty-string, 3: non-empty-string, name?: non-empty-string, 4?: non-empty-string}', $matches); + assertType('array{}|array{0: string, 1: non-empty-string, 2: string, 3: non-empty-string, name?: non-empty-string, 4?: non-empty-string}', $matches); if (preg_match('/(a|b)|(?:c)/', $s, $matches)) { assertType('array{0: string, 1?: non-empty-string}', $matches); @@ -259,7 +259,7 @@ function doFoo2(string $row): void return; } - assertType('array{0: string, 1: non-empty-string&numeric-string, branchCode: non-empty-string&numeric-string, 2: non-empty-string&numeric-string, accountNumber: non-empty-string&numeric-string, 3: non-empty-string&numeric-string, bankCode: non-empty-string&numeric-string, 4: non-empty-string&numeric-string}', $matches); + assertType("array{0: string, 1: ''|(non-empty-string&numeric-string), branchCode: ''|(non-empty-string&numeric-string), 2: ''|(non-empty-string&numeric-string), accountNumber: non-empty-string&numeric-string, 3: non-empty-string&numeric-string, bankCode: non-empty-string&numeric-string, 4: non-empty-string&numeric-string}", $matches); } function doFoo3(string $row): void @@ -443,98 +443,8 @@ function (string $s, $mixed): void { assertType('array', $matches); }; -function (string $size): void { - if (preg_match('/ab(\d){2,4}xx([0-9])?e?/', $size, $matches) !== 1) { - throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); - } - assertType('array{0: string, 1: non-empty-string&numeric-string, 2?: non-empty-string&numeric-string}', $matches); -}; - -function (string $size): void { - if (preg_match('/a(\dAB){2}b(\d){2,4}([1-5])([1-5a-z])e?/', $size, $matches) !== 1) { - throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); - } - assertType('array{string, non-empty-string, non-empty-string&numeric-string, non-empty-string&numeric-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) !== 1) { - throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); - } - assertType('array{0: string, 1: non-empty-string, 2: non-empty-string&numeric-string, 3?: non-empty-string}', $matches); -}; - -function (string $size): void { - if (preg_match('/ab(\d+)e(\d?)/', $size, $matches) !== 1) { - throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); - } - assertType('array{string, non-empty-string&numeric-string, numeric-string}', $matches); -}; - -function (string $size): void { - if (preg_match('/ab(?P\d+)e?/', $size, $matches) !== 1) { - throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); - } - assertType('array{0: string, num: non-empty-string&numeric-string, 1: non-empty-string&numeric-string}', $matches); -}; - -function (string $size): void { - if (preg_match('/ab(\d\d)/', $size, $matches) !== 1) { - throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); - } - assertType('array{string, non-empty-string&numeric-string}', $matches); -}; - -function (string $size): void { - if (preg_match('/ab(\d+\s)e?/', $size, $matches) !== 1) { - throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); - } - assertType('array{string, non-empty-string}', $matches); -}; - -function (string $size): void { - if (preg_match('/ab(\s)e?/', $size, $matches) !== 1) { - throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); - } - assertType('array{string, non-empty-string}', $matches); -}; - -function (string $size): void { - if (preg_match('/ab(\S)e?/', $size, $matches) !== 1) { - throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); - } - assertType('array{string, non-empty-string}', $matches); -}; - -function (string $size): void { - if (preg_match('/ab(\S?)e?/', $size, $matches) !== 1) { - throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); - } - assertType('array{string, string}', $matches); -}; - -function (string $size): void { - if (preg_match('/ab(\S)?e?/', $size, $matches) !== 1) { - throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); - } - assertType('array{0: string, 1?: non-empty-string}', $matches); -}; - -function (string $size): void { - if (preg_match('/ab(\d+\d?)e?/', $size, $matches) !== 1) { - throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); - } - assertType('array{string, non-empty-string&numeric-string}', $matches); -}; - -function (string $s): void { - if (preg_match('/Price: ([2-5])/i', $s, $matches)) { - assertType('array{string, non-empty-string&numeric-string}', $matches); - } -}; - function (string $s): void { - if (preg_match('/Price: ([2-5A-Z])/i', $s, $matches)) { - assertType('array{string, non-empty-string}', $matches); + if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*([sbdeEfFgGhHouxX])$/', $s, $matches) === 1) { + assertType('array{string, string, non-empty-string}', $matches); } }; From f99a741fa15c26c7cd69dbffb7d640e41d427b33 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 17 Jul 2024 09:26:12 +0200 Subject: [PATCH 11/18] fix --- src/Type/Php/RegexArrayShapeMatcher.php | 5 +++++ tests/PHPStan/Analyser/nsrt/preg_match_shapes.php | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 5e5c138680..b6ccfce485 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -655,6 +655,11 @@ private function getLiteralValue(TreeNode $node): ?string return $node->getValueValue(); } + // literal "-" outside of a character class like '~^((\\d{1,6})-)$~' + if ($node->getId() === 'token' && $node->getValueToken() === 'range') { + return $node->getValueValue(); + } + return null; } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 5593f19c4e..6787f7c34e 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -448,3 +448,15 @@ function (string $s): void { assertType('array{string, string, non-empty-string}', $matches); } }; + +function (string $s): void { + if (preg_match('~^((\\d{1,6})-)$~', $s, $matches) === 1) { + assertType("array{string, non-empty-string, non-empty-string&numeric-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('~^((\\d{1,6}).)$~', $s, $matches) === 1) { + assertType("array{string, non-empty-string, non-empty-string&numeric-string}", $matches); + } +}; From 49ad1277d347af520fef7c96efd5f9e56195ad91 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 17 Jul 2024 09:29:02 +0200 Subject: [PATCH 12/18] fix --- tests/PHPStan/Analyser/nsrt/bug-11311.php | 4 ++-- tests/PHPStan/Analyser/nsrt/preg_match_shapes.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-11311.php b/tests/PHPStan/Analyser/nsrt/bug-11311.php index b8c28b9beb..3b1e9ef546 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11311.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11311.php @@ -98,7 +98,7 @@ 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{0: string, 1: non-empty-string, 2: non-empty-string&numeric-string, 3?: non-empty-string}', $matches); + assertType('array{string, non-empty-string, non-empty-string&numeric-string, non-empty-string|null}', $matches); }; function (string $size): void { @@ -154,7 +154,7 @@ function (string $size): void { if (preg_match('/ab(\S)?e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{0: string, 1?: non-empty-string}', $matches); + assertType('array{string, non-empty-string|null}', $matches); }; function (string $size): void { diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 6787f7c34e..739a2311b2 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -39,9 +39,9 @@ function doMatch(string $s): void { assertType('array{}|array{string, non-empty-string}', $matches); if (preg_match('/(a)(b)*(c)(d)*/', $s, $matches)) { - assertType('array{0: string, 1: non-empty-string, 2: string, 3: non-empty-string, 4?: string}', $matches); + assertType('array{0: string, 1: non-empty-string, 2: string, 3: non-empty-string, 4?: non-empty-string}', $matches); } - assertType('array{}|array{0: string, 1: non-empty-string, 2: string, 3: non-empty-string, 4?: string}', $matches); + assertType('array{}|array{0: string, 1: non-empty-string, 2: string, 3: non-empty-string, 4?: non-empty-string}', $matches); if (preg_match('/(a)(?b)*(c)(d)*/', $s, $matches)) { assertType('array{0: string, 1: non-empty-string, name: string, 2: string, 3: non-empty-string, 4?: non-empty-string}', $matches); @@ -259,7 +259,7 @@ function doFoo2(string $row): void return; } - assertType("array{0: string, 1: ''|(non-empty-string&numeric-string), branchCode: ''|(non-empty-string&numeric-string), 2: ''|(non-empty-string&numeric-string), accountNumber: non-empty-string&numeric-string, 3: non-empty-string&numeric-string, bankCode: non-empty-string&numeric-string, 4: non-empty-string&numeric-string}", $matches); + assertType("array{0: string, 1: string, branchCode: ''|(non-empty-string&numeric-string), 2: ''|(non-empty-string&numeric-string), accountNumber: non-empty-string&numeric-string, 3: non-empty-string&numeric-string, bankCode: non-empty-string&numeric-string, 4: non-empty-string&numeric-string}", $matches); } function doFoo3(string $row): void From ac3ead1fa8e31d3f4d212225a5e9ba7a0cbfc832 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 17 Jul 2024 09:32:29 +0200 Subject: [PATCH 13/18] fix --- src/Type/Php/RegexArrayShapeMatcher.php | 2 +- tests/PHPStan/Analyser/nsrt/preg_match_shapes.php | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index b6ccfce485..39e98fffd5 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -611,7 +611,7 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL } } - if ($ast->getId() === '#range') { + if ($ast->getId() === '#range' || $ast->getId() === '#class') { if ($isNumeric->maybe()) { $allNumeric = null; foreach ($children as $child) { diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 739a2311b2..c2cbe7c6ff 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -460,3 +460,15 @@ function (string $s): void { assertType("array{string, non-empty-string, non-empty-string&numeric-string}", $matches); } }; + +function (string $s): void { + if (preg_match('~^([157])$~', $s, $matches) === 1) { + assertType("array{string, non-empty-string&numeric-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('~^([157XY])$~', $s, $matches) === 1) { + assertType("array{string, non-empty-string}", $matches); + } +}; From 2143d55cfb9525b319a386acd657374753ac26e8 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 17 Jul 2024 09:41:01 +0200 Subject: [PATCH 14/18] Update RegexArrayShapeMatcher.php --- src/Type/Php/RegexArrayShapeMatcher.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 39e98fffd5..1f50908486 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -558,10 +558,8 @@ private function createGroupType(TreeNode $group): Type } if ($accessories !== []) { - return new IntersectionType([ - new StringType(), - ...$accessories, - ]); + $accessories[] = new StringType(); + return new IntersectionType($accessories); } return new StringType(); From 670940402f77e4dae5d3e700b9df5ad9c582077a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 17 Jul 2024 09:53:20 +0200 Subject: [PATCH 15/18] a numeric-string is a non-empty-string on its own --- src/Type/Php/RegexArrayShapeMatcher.php | 4 +- tests/PHPStan/Analyser/nsrt/bug-11311.php | 18 +++---- .../Analyser/nsrt/preg_match_shapes.php | 48 +++++++++---------- .../Analyser/nsrt/preg_match_shapes_php82.php | 8 ++-- 4 files changed, 38 insertions(+), 40 deletions(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 1f50908486..674a08f25f 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -551,9 +551,7 @@ private function createGroupType(TreeNode $group): Type $accessories = []; if ($isNumeric->yes()) { $accessories[] = new AccessoryNumericStringType(); - } - - if ($isNonEmpty->yes()) { + } elseif ($isNonEmpty->yes()) { $accessories[] = new AccessoryNonEmptyStringType(); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11311.php b/tests/PHPStan/Analyser/nsrt/bug-11311.php index 3b1e9ef546..7bfc73e37f 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11311.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11311.php @@ -7,7 +7,7 @@ function doFoo(string $s) { if (1 === preg_match('/(?\d+)\.(?\d+)(?:\.(?\d+))?/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{0: string, major: non-empty-string&numeric-string, 1: non-empty-string&numeric-string, minor: non-empty-string&numeric-string, 2: non-empty-string&numeric-string, patch: (non-empty-string&numeric-string)|null, 3: (non-empty-string&numeric-string)|null}', $matches); + assertType('array{0: string, major: numeric-string, 1: numeric-string, minor: numeric-string, 2: numeric-string, patch: numeric-string|null, 3: numeric-string|null}', $matches); } } @@ -84,42 +84,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{string, non-empty-string&numeric-string, (non-empty-string&numeric-string)|null}', $matches); + assertType('array{string, numeric-string, numeric-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{string, non-empty-string, non-empty-string&numeric-string, non-empty-string&numeric-string, non-empty-string}', $matches); + assertType('array{string, non-empty-string, numeric-string, numeric-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{string, non-empty-string, non-empty-string&numeric-string, non-empty-string|null}', $matches); + assertType('array{string, non-empty-string, numeric-string, non-empty-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{string, non-empty-string&numeric-string, numeric-string}', $matches); + assertType('array{string, numeric-string, numeric-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: string, num: non-empty-string&numeric-string, 1: non-empty-string&numeric-string}', $matches); + assertType('array{0: string, num: numeric-string, 1: numeric-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{string, non-empty-string&numeric-string}', $matches); + assertType('array{string, numeric-string}', $matches); }; function (string $size): void { @@ -161,12 +161,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{string, non-empty-string&numeric-string}', $matches); + assertType('array{string, numeric-string}', $matches); }; function (string $s): void { if (preg_match('/Price: ([2-5])/i', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{string, non-empty-string&numeric-string}', $matches); + assertType('array{string, numeric-string}', $matches); } }; diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index c2cbe7c6ff..85831749c5 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{string, non-empty-string}', $matches); if (preg_match('/Price: (£|€)(\d+)/i', $s, $matches)) { - assertType('array{string, non-empty-string, non-empty-string&numeric-string}', $matches); + assertType('array{string, non-empty-string, numeric-string}', $matches); } - assertType('array{}|array{string, non-empty-string, non-empty-string&numeric-string}', $matches); + assertType('array{}|array{string, non-empty-string, numeric-string}', $matches); if (preg_match(' /Price: (£|€)\d+/ i u', $s, $matches)) { assertType('array{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{string, non-empty-string&numeric-string}', $matches); + assertType('array{string, numeric-string}', $matches); } - assertType('array{}|array{string, non-empty-string&numeric-string}', $matches); + assertType('array{}|array{string, numeric-string}', $matches); } function doNamedSubpattern(string $s): void { if (preg_match('/\w-(?P\d+)-(\w)/', $s, $matches)) { - assertType('array{0: string, num: non-empty-string&numeric-string, 1: non-empty-string&numeric-string, 2: non-empty-string}', $matches); + assertType('array{0: string, num: numeric-string, 1: numeric-string, 2: non-empty-string}', $matches); } - assertType('array{}|array{0: string, num: non-empty-string&numeric-string, 1: non-empty-string&numeric-string, 2: non-empty-string}', $matches); + assertType('array{}|array{0: string, num: numeric-string, 1: numeric-string, 2: non-empty-string}', $matches); if (preg_match('/^(?\S+::\S+)/', $s, $matches)) { assertType('array{0: string, name: non-empty-string, 1: non-empty-string}', $matches); @@ -159,9 +159,9 @@ function hoaBug31(string $s): void { assertType('array{}|array{string, non-empty-string}', $matches); if (preg_match('/\w-(\d+)-(\w)/', $s, $matches)) { - assertType('array{string, non-empty-string&numeric-string, non-empty-string}', $matches); + assertType('array{string, numeric-string, non-empty-string}', $matches); } - assertType('array{}|array{string, non-empty-string&numeric-string, non-empty-string}', $matches); + assertType('array{}|array{string, numeric-string, non-empty-string}', $matches); } // https://github.com/phpstan/phpstan/issues/10855#issuecomment-2044323638 @@ -235,9 +235,9 @@ function testUnionPattern(string $s): void $pattern = '/Price: (\d+)(\d+)(\d+)/'; } if (preg_match($pattern, $s, $matches)) { - assertType('array{string, non-empty-string&numeric-string, non-empty-string&numeric-string, non-empty-string&numeric-string}|array{string, non-empty-string&numeric-string}', $matches); + assertType('array{string, numeric-string, numeric-string, numeric-string}|array{string, numeric-string}', $matches); } - assertType('array{}|array{string, non-empty-string&numeric-string, non-empty-string&numeric-string, non-empty-string&numeric-string}|array{string, non-empty-string&numeric-string}', $matches); + assertType('array{}|array{string, numeric-string, numeric-string, numeric-string}|array{string, numeric-string}', $matches); } function doFoo(string $row): void @@ -259,7 +259,7 @@ function doFoo2(string $row): void return; } - assertType("array{0: string, 1: string, branchCode: ''|(non-empty-string&numeric-string), 2: ''|(non-empty-string&numeric-string), accountNumber: non-empty-string&numeric-string, 3: non-empty-string&numeric-string, bankCode: non-empty-string&numeric-string, 4: non-empty-string&numeric-string}", $matches); + assertType("array{0: string, 1: string, branchCode: ''|numeric-string, 2: ''|numeric-string, accountNumber: numeric-string, 3: numeric-string, bankCode: numeric-string, 4: numeric-string}", $matches); } function doFoo3(string $row): void @@ -268,42 +268,42 @@ function doFoo3(string $row): void return; } - assertType('array{string, non-empty-string, non-empty-string, non-empty-string&numeric-string, non-empty-string&numeric-string, non-empty-string&numeric-string, non-empty-string&numeric-string}', $matches); + assertType('array{string, non-empty-string, non-empty-string, numeric-string, numeric-string, numeric-string, numeric-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{string, non-empty-string, non-empty-string&numeric-string, non-empty-string&numeric-string, non-empty-string}|array{string}', $matches); + assertType('array{string, non-empty-string, numeric-string, numeric-string, non-empty-string}|array{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{string, non-empty-string, non-empty-string&numeric-string}|array{string}', $matches); + assertType('array{string, non-empty-string, numeric-string}|array{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: string, 1: non-empty-string, 2?: non-empty-string&numeric-string}', $matches); + assertType('array{0: string, 1: non-empty-string, 2?: numeric-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: string, 1?: non-empty-string, 2?: non-empty-string&numeric-string}', $matches); + assertType('array{0: string, 1?: non-empty-string, 2?: numeric-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{string, non-empty-string, non-empty-string&numeric-string}', $matches); + assertType('array{string, non-empty-string, numeric-string}', $matches); }; function (string $size): void { @@ -317,14 +317,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{0: string, 1: non-empty-string&numeric-string, 2: non-empty-string&numeric-string, 3?: non-empty-string&numeric-string, 4?: non-empty-string&numeric-string}', $matches); + assertType('array{0: string, 1: numeric-string, 2: numeric-string, 3?: numeric-string, 4?: numeric-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{0: string, 1: non-empty-string&numeric-string, 2: non-empty-string&numeric-string, 3?: non-empty-string&numeric-string, 4?: non-empty-string&numeric-string}|array{string}', $matches); + assertType('array{0: string, 1: numeric-string, 2: numeric-string, 3?: numeric-string, 4?: numeric-string}|array{string}', $matches); }; function (string $size): void { @@ -418,11 +418,11 @@ function (string $s): void { function (string $s): void { if (preg_match('/' . preg_quote($s, '/') . '(\d)/', $s, $matches)) { - assertType('array{string, non-empty-string&numeric-string}', $matches); + assertType('array{string, numeric-string}', $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{string, non-empty-string&numeric-string}', $matches); + assertType('array{}|array{string, numeric-string}', $matches); }; function (string $s): void { @@ -451,19 +451,19 @@ function (string $s): void { function (string $s): void { if (preg_match('~^((\\d{1,6})-)$~', $s, $matches) === 1) { - assertType("array{string, non-empty-string, non-empty-string&numeric-string}", $matches); + assertType("array{string, non-empty-string, numeric-string}", $matches); } }; function (string $s): void { if (preg_match('~^((\\d{1,6}).)$~', $s, $matches) === 1) { - assertType("array{string, non-empty-string, non-empty-string&numeric-string}", $matches); + assertType("array{string, non-empty-string, numeric-string}", $matches); } }; function (string $s): void { if (preg_match('~^([157])$~', $s, $matches) === 1) { - assertType("array{string, non-empty-string&numeric-string}", $matches); + assertType("array{string, numeric-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 1b8797a814..af50a43064 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php @@ -8,12 +8,12 @@ // https://php.watch/versions/8.2/preg-n-no-capture-modifier function doNonAutoCapturingFlag(string $s): void { if (preg_match('/(\d+)/n', $s, $matches)) { - assertType('array{string, non-empty-string&numeric-string}', $matches); // should be 'array{string}' + assertType('array{string, numeric-string}', $matches); // should be 'array{string}' } - assertType('array{}|array{string, non-empty-string&numeric-string}', $matches); + assertType('array{}|array{string, numeric-string}', $matches); if (preg_match('/(\d+)(?P\d+)/n', $s, $matches)) { - assertType('array{0: string, 1: non-empty-string&numeric-string, num: non-empty-string&numeric-string, 2: non-empty-string&numeric-string}', $matches); + assertType('array{0: string, 1: numeric-string, num: numeric-string, 2: numeric-string}', $matches); } - assertType('array{}|array{0: string, 1: non-empty-string&numeric-string, num: non-empty-string&numeric-string, 2: non-empty-string&numeric-string}', $matches); + assertType('array{}|array{0: string, 1: numeric-string, num: numeric-string, 2: numeric-string}', $matches); } From 36fcc830573682034a2f6df6cc916f764e6f15a7 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 17 Jul 2024 09:55:48 +0200 Subject: [PATCH 16/18] fix comment --- tests/PHPStan/Analyser/nsrt/bug-11311.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-11311.php b/tests/PHPStan/Analyser/nsrt/bug-11311.php index 7bfc73e37f..94955d9792 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11311.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11311.php @@ -69,7 +69,7 @@ function bug11331c(string $url):void { class UnmatchedAsNullWithTopLevelAlternation { function doFoo(string $s): void { if (preg_match('/Price: (?:(£)|(€))\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{string, non-empty-string|null, non-empty-string|null}', $matches); // could be array{0: string, 1: null, 2: string}|array{0: string, 1: string, 2: null} + assertType('array{string, non-empty-string|null, non-empty-string|null}', $matches); // could be array{0: string, 1: null, 2: non-empty-string}|array{0: string, 1: non-empty-string, 2: null} } } From 40a8751c9058518ccece7c3309516fdcd5df9002 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 17 Jul 2024 10:05:42 +0200 Subject: [PATCH 17/18] fix php 7.3 --- tests/PHPStan/Analyser/nsrt/bug-11311-php72.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-11311-php72.php b/tests/PHPStan/Analyser/nsrt/bug-11311-php72.php index 6e768b5c01..3b60727e84 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11311-php72.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11311-php72.php @@ -8,23 +8,23 @@ function doFoo(string $s) { if (1 === preg_match('/(?\d+)\.(?\d+)(?:\.(?\d+))?/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{0: string, major: string, 1: string, minor: string, 2: string, patch?: string, 3?: string}', $matches); + assertType('array{0: string, major: numeric-string, 1: numeric-string, minor: numeric-string, 2: numeric-string, patch?: numeric-string, 3?: numeric-string}', $matches); } } function doUnmatchedAsNull(string $s): void { if (preg_match('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{0: string, 1?: string, 2?: string, 3?: string}', $matches); + assertType('array{0: string, 1?: non-empty-string, 2?: non-empty-string, 3?: non-empty-string}', $matches); } - assertType('array{}|array{0: string, 1?: string, 2?: string, 3?: string}', $matches); + assertType('array{}|array{0: string, 1?: non-empty-string, 2?: non-empty-string, 3?: non-empty-string}', $matches); } // see https://3v4l.org/VeDob#veol function unmatchedAsNullWithOptionalGroup(string $s): void { if (preg_match('/Price: (£|€)?\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{0: string, 1?: string}', $matches); + assertType('array{0: string, 1?: non-empty-string}', $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{0: string, 1?: string}', $matches); + assertType('array{}|array{0: string, 1?: non-empty-string}', $matches); } From 456107e95ae0e3eb4aa6f284598f4257ccfb435e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 17 Jul 2024 10:32:26 +0200 Subject: [PATCH 18/18] fix --- src/Type/Php/RegexArrayShapeMatcher.php | 16 +++++++++++----- tests/PHPStan/Analyser/nsrt/bug-11311.php | 6 ++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 674a08f25f..c9d8c0bef7 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -588,8 +588,14 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL if ($ast->getId() === 'token') { $literalValue = $this->getLiteralValue($ast); - if ($literalValue !== null && Strings::match($literalValue, '/^\d+$/') === null) { - $isNumeric = TrinaryLogic::createNo(); + if ($literalValue !== null) { + if (Strings::match($literalValue, '/^\d+$/') === null) { + $isNumeric = TrinaryLogic::createNo(); + } + + if (!$inOptionalQuantification) { + $isNonEmpty = TrinaryLogic::createYes(); + } } if ($ast->getValueToken() === 'character_type') { @@ -600,10 +606,10 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL } else { $isNumeric = TrinaryLogic::createNo(); } - } - if (!$inOptionalQuantification) { - $isNonEmpty = TrinaryLogic::createYes(); + if (!$inOptionalQuantification) { + $isNonEmpty = TrinaryLogic::createYes(); + } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11311.php b/tests/PHPStan/Analyser/nsrt/bug-11311.php index 94955d9792..e9cde3e771 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11311.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11311.php @@ -181,3 +181,9 @@ function (string $s): void { assertType('array{string, non-empty-string|null, non-empty-string}', $matches); } }; + +function (string $s): void { + if (preg_match('/(?\s*)(?.*)/', $s, $matches, PREG_UNMATCHED_AS_NULL) === 1) { + assertType('array{0: string, whitespace: string, 1: string, value: string, 2: string}', $matches); + } +};