From ab5c826f63a7c0af90d2a982b29115b3d1a49293 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 18 Mar 2025 07:33:31 +0100 Subject: [PATCH 01/17] RegexArrayShapeMatcher - more precise subject types --- src/Type/Php/RegexArrayShapeMatcher.php | 9 ++++ test.php | 53 +++++++++++++++++++ .../Analyser/nsrt/preg_match_shapes.php | 8 +++ 3 files changed, 70 insertions(+) create mode 100644 test.php diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index da6a44e735..fef11f673d 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -114,6 +114,15 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched } [$groupList, $markVerbs] = $parseResult; + if ($groupList === [] && $markVerbs === []) { + $rawRegex = $this->regexExpressionHelper->removeDelimitersAndModifiers($regex); + $type = $this->matchRegex('{('.$rawRegex.')}', $flags, $wasMatched, $matchesAll); + if ($type === null) { + return null; + } + return $type->shiftArray(); + } + $regexGroupList = new RegexGroupList($groupList); $trailingOptionals = $regexGroupList->countTrailingOptionals(); $onlyOptionalTopLevelGroup = $regexGroupList->getOnlyOptionalTopLevelGroup(); diff --git a/test.php b/test.php new file mode 100644 index 0000000000..e65db2c74c --- /dev/null +++ b/test.php @@ -0,0 +1,53 @@ + Date: Sat, 22 Mar 2025 08:37:33 +0100 Subject: [PATCH 02/17] RegexArrayShapeMatcher - more precise subject types --- src/Type/Php/RegexArrayShapeMatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index fef11f673d..3058a0ede3 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -116,7 +116,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched if ($groupList === [] && $markVerbs === []) { $rawRegex = $this->regexExpressionHelper->removeDelimitersAndModifiers($regex); - $type = $this->matchRegex('{('.$rawRegex.')}', $flags, $wasMatched, $matchesAll); + $type = $this->matchRegex('{(' . $rawRegex . ')}', $flags, $wasMatched, $matchesAll); if ($type === null) { return null; } From b1c5d37311efecf11fe6e2076eb1f70beee82353 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 22 Mar 2025 11:24:51 +0100 Subject: [PATCH 03/17] refactor --- src/Type/Php/RegexArrayShapeMatcher.php | 35 ++++++------ src/Type/Regex/RegexAstWalkResult.php | 28 +++++++++- src/Type/Regex/RegexGroupParser.php | 13 +++-- .../Analyser/nsrt/preg_match_all_shapes.php | 32 +++++------ .../Analyser/nsrt/preg_match_shapes.php | 54 ++++++++++++++++--- .../Analyser/nsrt/preg_match_shapes_php82.php | 20 +++---- 6 files changed, 127 insertions(+), 55 deletions(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 3058a0ede3..aba51ca9c3 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -107,20 +107,16 @@ private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLo */ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched, bool $matchesAll): ?Type { - $parseResult = $this->regexGroupParser->parseGroups($regex); - if ($parseResult === null) { + $astWalkResult = $this->regexGroupParser->parseGroups($regex); + if ($astWalkResult === null) { // regex could not be parsed by Hoa/Regex return null; } - [$groupList, $markVerbs] = $parseResult; - - if ($groupList === [] && $markVerbs === []) { - $rawRegex = $this->regexExpressionHelper->removeDelimitersAndModifiers($regex); - $type = $this->matchRegex('{(' . $rawRegex . ')}', $flags, $wasMatched, $matchesAll); - if ($type === null) { - return null; - } - return $type->shiftArray(); + $groupList = $astWalkResult->getCapturingGroups(); + $markVerbs = $astWalkResult->getMarkVerbs(); + $subjectBaseType = new StringType(); + if ($wasMatched->yes()) { + $subjectBaseType = $astWalkResult->getSubjectBaseType(); } $regexGroupList = new RegexGroupList($groupList); @@ -139,6 +135,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched $regexGroupList = $regexGroupList->forceGroupNonOptional($onlyOptionalTopLevelGroup); $combiType = $this->buildArrayType( + $subjectBaseType, $regexGroupList, $wasMatched, $trailingOptionals, @@ -150,7 +147,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched if (!$this->containsUnmatchedAsNull($flags, $matchesAll)) { // positive match has a subject but not any capturing group $combiType = TypeCombinator::union( - new ConstantArrayType([new ConstantIntegerType(0)], [$this->createSubjectValueType($flags, $matchesAll)], [1], [], TrinaryLogic::createYes()), + new ConstantArrayType([new ConstantIntegerType(0)], [$this->createSubjectValueType($subjectBaseType, $flags, $matchesAll)], [1], [], TrinaryLogic::createYes()), $combiType, ); } @@ -189,6 +186,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched } $combiType = $this->buildArrayType( + $subjectBaseType, $comboList, $wasMatched, $trailingOptionals, @@ -208,7 +206,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched ) ) { // positive match has a subject but not any capturing group - $combiTypes[] = new ConstantArrayType([new ConstantIntegerType(0)], [$this->createSubjectValueType($flags, $matchesAll)], [1], [], TrinaryLogic::createYes()); + $combiTypes[] = new ConstantArrayType([new ConstantIntegerType(0)], [$this->createSubjectValueType($subjectBaseType, $flags, $matchesAll)], [1], [], TrinaryLogic::createYes()); } return TypeCombinator::union(...$combiTypes); @@ -217,6 +215,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched // the general case, which should work in all cases but does not yield the most // precise result possible in some cases return $this->buildArrayType( + $subjectBaseType, $regexGroupList, $wasMatched, $trailingOptionals, @@ -230,6 +229,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched * @param list $markVerbs */ private function buildArrayType( + Type $subjectBaseType, RegexGroupList $captureGroups, TrinaryLogic $wasMatched, int $trailingOptionals, @@ -243,7 +243,7 @@ private function buildArrayType( // first item in matches contains the overall match. $builder->setOffsetValueType( $this->getKeyType(0), - $this->createSubjectValueType($flags, $matchesAll), + $this->createSubjectValueType($subjectBaseType, $flags, $matchesAll), $this->isSubjectOptional($wasMatched, $matchesAll), ); @@ -307,9 +307,12 @@ private function isSubjectOptional(TrinaryLogic $wasMatched, bool $matchesAll): return !$wasMatched->yes(); } - private function createSubjectValueType(int $flags, bool $matchesAll): Type + /** + * @param Type $baseType A string type (or string variant) representing the subject of the match + */ + private function createSubjectValueType(Type $baseType, int $flags, bool $matchesAll): Type { - $subjectValueType = TypeCombinator::removeNull($this->getValueType(new StringType(), $flags, $matchesAll)); + $subjectValueType = TypeCombinator::removeNull($this->getValueType($baseType, $flags, $matchesAll)); if ($matchesAll) { if ($this->containsPatternOrder($flags)) { diff --git a/src/Type/Regex/RegexAstWalkResult.php b/src/Type/Regex/RegexAstWalkResult.php index 32e017a254..ba9e7a90e5 100644 --- a/src/Type/Regex/RegexAstWalkResult.php +++ b/src/Type/Regex/RegexAstWalkResult.php @@ -2,6 +2,9 @@ namespace PHPStan\Type\Regex; +use PHPStan\Type\StringType; +use PHPStan\Type\Type; + /** @immutable */ final class RegexAstWalkResult { @@ -15,9 +18,9 @@ public function __construct( private int $captureGroupId, private array $capturingGroups, private array $markVerbs, + private Type $subjectBaseType, ) - { - } + {} public static function createEmpty(): self { @@ -27,6 +30,7 @@ public static function createEmpty(): self 100, [], [], + new StringType() ); } @@ -37,6 +41,7 @@ public function nextAlternationId(): self $this->captureGroupId, $this->capturingGroups, $this->markVerbs, + $this->subjectBaseType, ); } @@ -47,6 +52,7 @@ public function nextCaptureGroupId(): self $this->captureGroupId + 1, $this->capturingGroups, $this->markVerbs, + $this->subjectBaseType, ); } @@ -60,6 +66,7 @@ public function addCapturingGroup(RegexCapturingGroup $group): self $this->captureGroupId, $capturingGroups, $this->markVerbs, + $this->subjectBaseType, ); } @@ -73,6 +80,18 @@ public function markVerb(string $markVerb): self $this->captureGroupId, $this->capturingGroups, $verbs, + $this->subjectBaseType, + ); + } + + public function withSubjectBaseType(Type $subjectBaseType): self + { + return new self( + $this->alternationId, + $this->captureGroupId, + $this->capturingGroups, + $this->markVerbs, + $subjectBaseType, ); } @@ -102,4 +121,9 @@ public function getMarkVerbs(): array return $this->markVerbs; } + public function getSubjectBaseType(): Type + { + return $this->subjectBaseType; + } + } diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 69eb455eaf..3ea0357bec 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -49,10 +49,7 @@ public function __construct( { } - /** - * @return array{array, list}|null - */ - public function parseGroups(string $regex): ?array + public function parseGroups(string $regex): ?RegexAstWalkResult { if (self::$parser === null) { /** @throws void */ @@ -105,7 +102,7 @@ public function parseGroups(string $regex): ?array RegexAstWalkResult::createEmpty(), ); - return [$astWalkResult->getCapturingGroups(), $astWalkResult->getMarkVerbs()]; + return $astWalkResult; } private function createEmptyTokenTreeNode(TreeNode $parentAst): TreeNode @@ -263,6 +260,12 @@ private function walkRegexAst( $astWalkResult, ); + if ($alternation === null && !$inOptionalQuantification) { + $astWalkResult = $astWalkResult->withSubjectBaseType( + TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()) + ); + } + if ($ast->getId() !== '#alternation') { continue; } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php index 7ed783a8e9..ab1d6c2c43 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php @@ -26,16 +26,16 @@ function (string $size): void { function (string $size): void { if (preg_match_all('/ab(?P\d+)?/', $size, $matches)) { - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); } else { assertType("array{}", $matches); } - assertType("array{}|array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{}|array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); }; function (string $size): void { if (preg_match_all('/ab(?P\d+)?/', $size, $matches) > 0) { - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); } else { assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); } @@ -44,7 +44,7 @@ function (string $size): void { function (string $size): void { if (preg_match_all('/ab(?P\d+)?/', $size, $matches) != false) { - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); } else { assertType("array{}", $matches); } @@ -53,7 +53,7 @@ function (string $size): void { function (string $size): void { if (preg_match_all('/ab(?P\d+)?/', $size, $matches) == true) { - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); } else { assertType("array{}", $matches); } @@ -62,7 +62,7 @@ function (string $size): void { function (string $size): void { if (preg_match_all('/ab(?P\d+)?/', $size, $matches) === 1) { - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); } else { assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); } @@ -76,55 +76,55 @@ function (string $size): void { function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches)) { - assertType("array{0: list, num: list, 1: list, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $matches); + assertType("array{0: list, num: list, 1: list, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType("array{0: list, num: list, 1: list, suffix: list<'ab'|null>, 2: list<'ab'|null>}", $matches); + assertType("array{0: list, num: list, 1: list, suffix: list<'ab'|null>, 2: list<'ab'|null>}", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_SET_ORDER)) { - assertType("list", $matches); + assertType("list", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_PATTERN_ORDER)) { - assertType("array{0: list, num: list, 1: list, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $matches); + assertType("array{0: list, num: list, 1: list, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_SET_ORDER)) { - assertType("list", $matches); + assertType("list", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_PATTERN_ORDER)) { - assertType("array{0: list, num: list, 1: list, suffix: list<'ab'|null>, 2: list<'ab'|null>}", $matches); + assertType("array{0: list, num: list, 1: list, suffix: list<'ab'|null>, 2: list<'ab'|null>}", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) { - assertType("list}, num: array{numeric-string, int<-1, max>}, 1: array{numeric-string, int<-1, max>}, suffix?: array{'ab', int<-1, max>}, 2?: array{'ab', int<-1, max>}}>", $matches); + assertType("list}, num: array{numeric-string, int<-1, max>}, 1: array{numeric-string, int<-1, max>}, suffix?: array{'ab', int<-1, max>}, 2?: array{'ab', int<-1, max>}}>", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE)) { - assertType("array{0: list}>, num: list}>, 1: list}>, suffix: list}>, 2: list}>}", $matches); + assertType("array{0: list}>, num: list}>, 1: list}>, suffix: list}>, 2: list}>}", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) { - assertType("list}, num: array{numeric-string|null, int<-1, max>}, 1: array{numeric-string|null, int<-1, max>}, suffix: array{'ab'|null, int<-1, max>}, 2: array{'ab'|null, int<-1, max>}}>", $matches); + assertType("list}, num: array{numeric-string|null, int<-1, max>}, 1: array{numeric-string|null, int<-1, max>}, suffix: array{'ab'|null, int<-1, max>}, 2: array{'ab'|null, int<-1, max>}}>", $matches); } }; @@ -160,7 +160,7 @@ public function sayBar(string $content): void return; } - assertType('array{list}', $matches); + assertType('array{list}', $matches); } function doFoobar(string $s): void { diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 2b4d8584f0..9c9a723371 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -7,9 +7,9 @@ function doMatch(string $s): void { if (preg_match('/Price: /i', $s, $matches)) { - assertType('array{string}', $matches); + assertType('array{non-falsy-string}', $matches); } - assertType('array{}|array{string}', $matches); + assertType('array{}|array{non-falsy-string}', $matches); if (preg_match('/Price: (£|€)\d+/', $s, $matches)) { assertType("array{string, '£'|'€'}", $matches); @@ -157,9 +157,9 @@ function hoaBug31(string $s): void { // https://github.com/phpstan/phpstan/issues/10855#issuecomment-2044323638 function testHoaUnsupportedRegexSyntax(string $s): void { if (preg_match('#\QPHPDoc type array of property App\Log::$fillable is not covariant with PHPDoc type array of overridden property Illuminate\Database\E\\\\\QEloquent\Model::$fillable.\E#', $s, $matches)) { - assertType('array{string}', $matches); + assertType('array{non-falsy-string}', $matches); } - assertType('array{}|array{string}', $matches); + assertType('array{}|array{non-falsy-string}', $matches); } function testPregMatchSimpleCondition(string $value): void { @@ -961,10 +961,52 @@ function bug11744(string $string): void assertType('array{0: string, 1: non-empty-string, 2?: non-falsy-string}', $matches); } -/** @return non-empty-string|null */ function bug12749(string $str): void { if (preg_match('/[A-Z]/', $str, $match)) { - assertType('string', $match); + assertType('array{non-empty-string}', $match); + } +} + +function bug12749a(string $str): void +{ + if (preg_match('/[A-Z]{2,}/', $str, $match)) { + assertType('array{non-falsy-string}', $match); + } +} + +function bug12749b(string $str): void +{ + if (preg_match('/[0-9][A-Z]/', $str, $match)) { + assertType('array{non-falsy-string}', $match); + } +} + +function bug12749c(string $str): void +{ + if (preg_match('/[0-9][A-Z]?/', $str, $match)) { + assertType('array{non-empty-string}', $match); + } +} + +function bug12749d(string $str): void +{ + if (preg_match('/[0-9]?[A-Z]/', $str, $match)) { + assertType('array{non-falsy-string}', $match); + } +} + +function bug12749e(string $str): void +{ + // no ^ $ delims, therefore can be anything which contains a number + if (preg_match('/[0-9]/', $str, $match)) { + assertType('array{non-empty-string}', $match); // could be non-falsy-string + } +} + +function bug12749f(string $str): void +{ + if (preg_match('/^[0-9]$/', $str, $match)) { + assertType('array{non-empty-string}', $match); // could be numeric-string } } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php index dfbcab477e..aea2a7b634 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php @@ -8,39 +8,39 @@ // 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}', $matches); + assertType('array{non-empty-string}', $matches); } - assertType('array{}|array{string}', $matches); + assertType('array{}|array{non-empty-string}', $matches); if (preg_match('/(\d+)(?P\d+)/n', $s, $matches)) { - assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-empty-string, num: numeric-string, 1: numeric-string}', $matches); } - assertType('array{}|array{0: string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{}|array{0: non-empty-string, num: numeric-string, 1: numeric-string}', $matches); if (preg_match('/(\w)-(?P\d+)-(\w)/n', $s, $matches)) { - assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-empty-string, num: numeric-string, 1: numeric-string}', $matches); } - assertType('array{}|array{0: string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{}|array{0: non-empty-string, num: numeric-string, 1: numeric-string}', $matches); } // delimiter variants, see https://www.php.net/manual/en/regexp.reference.delimiters.php function (string $s): void { if (preg_match('{(\d+)(?P\d+)}n', $s, $matches)) { - assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-empty-string, num: numeric-string, 1: numeric-string}', $matches); } }; function (string $s): void { if (preg_match('<(\d+)(?P\d+)>n', $s, $matches)) { - assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-empty-string, num: numeric-string, 1: numeric-string}', $matches); } }; function (string $s): void { if (preg_match('((\d+)(?P\d+))n', $s, $matches)) { - assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-empty-string, num: numeric-string, 1: numeric-string}', $matches); } }; function (string $s): void { if (preg_match('[(\d+)(?P\d+)]n', $s, $matches)) { - assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-empty-string, num: numeric-string, 1: numeric-string}', $matches); } }; From bc88756a8a2e9b212a8f98c67a88d461b4ed9ade Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 22 Mar 2025 11:33:44 +0100 Subject: [PATCH 04/17] Update preg_match_shapes.php --- tests/PHPStan/Analyser/nsrt/preg_match_shapes.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 9c9a723371..7763dc5a96 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -964,21 +964,21 @@ function bug11744(string $string): void function bug12749(string $str): void { if (preg_match('/[A-Z]/', $str, $match)) { - assertType('array{non-empty-string}', $match); + assertType('array{non-empty-string}', $match); // could be non-falsy-string } } function bug12749a(string $str): void { if (preg_match('/[A-Z]{2,}/', $str, $match)) { - assertType('array{non-falsy-string}', $match); + assertType('array{non-empty-string}', $match); // could be non-falsy-string } } function bug12749b(string $str): void { if (preg_match('/[0-9][A-Z]/', $str, $match)) { - assertType('array{non-falsy-string}', $match); + assertType('array{non-empty-string}', $match); // could be non-falsy-string } } @@ -992,7 +992,7 @@ function bug12749c(string $str): void function bug12749d(string $str): void { if (preg_match('/[0-9]?[A-Z]/', $str, $match)) { - assertType('array{non-falsy-string}', $match); + assertType('array{non-empty-string}', $match); // could be non-falsy-string } } From d16250dfd3fc79ce30ddfa0842b909527cf41fca Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 22 Mar 2025 11:50:52 +0100 Subject: [PATCH 05/17] Update RegexGroupParser.php --- src/Type/Regex/RegexGroupParser.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 3ea0357bec..709eb0ce3c 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -102,6 +102,19 @@ public function parseGroups(string $regex): ?RegexAstWalkResult RegexAstWalkResult::createEmpty(), ); + $subjectAsGroupResult = $this->walkGroupAst( + $ast, + false, + false, + $modifiers, + RegexGroupWalkResult::createEmpty(), + ); + if ($subjectAsGroupResult->isNonEmpty()->yes()) { + $astWalkResult = $astWalkResult->withSubjectBaseType( + TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()) + ); + } + return $astWalkResult; } @@ -260,12 +273,6 @@ private function walkRegexAst( $astWalkResult, ); - if ($alternation === null && !$inOptionalQuantification) { - $astWalkResult = $astWalkResult->withSubjectBaseType( - TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()) - ); - } - if ($ast->getId() !== '#alternation') { continue; } From 80508d39e54757ad3b2c30c81f96d55514c29881 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 22 Mar 2025 12:00:57 +0100 Subject: [PATCH 06/17] fix --- src/Type/Php/RegexArrayShapeMatcher.php | 7 +++- .../Analyser/nsrt/preg_match_all_shapes.php | 32 +++++++++---------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index aba51ca9c3..344c7b7bce 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -315,8 +315,13 @@ private function createSubjectValueType(Type $baseType, int $flags, bool $matche $subjectValueType = TypeCombinator::removeNull($this->getValueType($baseType, $flags, $matchesAll)); if ($matchesAll) { + $subjectValueType = TypeCombinator::removeNull($this->getValueType(new StringType(), $flags, $matchesAll)); + if ($this->containsPatternOrder($flags)) { - $subjectValueType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $subjectValueType), new AccessoryArrayListType()); + $subjectValueType = TypeCombinator::intersect( + new ArrayType(new IntegerType(), $subjectValueType), + new AccessoryArrayListType() + ); } } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php index ab1d6c2c43..7ed783a8e9 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php @@ -26,16 +26,16 @@ function (string $size): void { function (string $size): void { if (preg_match_all('/ab(?P\d+)?/', $size, $matches)) { - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); } else { assertType("array{}", $matches); } - assertType("array{}|array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{}|array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); }; function (string $size): void { if (preg_match_all('/ab(?P\d+)?/', $size, $matches) > 0) { - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); } else { assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); } @@ -44,7 +44,7 @@ function (string $size): void { function (string $size): void { if (preg_match_all('/ab(?P\d+)?/', $size, $matches) != false) { - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); } else { assertType("array{}", $matches); } @@ -53,7 +53,7 @@ function (string $size): void { function (string $size): void { if (preg_match_all('/ab(?P\d+)?/', $size, $matches) == true) { - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); } else { assertType("array{}", $matches); } @@ -62,7 +62,7 @@ function (string $size): void { function (string $size): void { if (preg_match_all('/ab(?P\d+)?/', $size, $matches) === 1) { - assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); } else { assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); } @@ -76,55 +76,55 @@ function (string $size): void { function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches)) { - assertType("array{0: list, num: list, 1: list, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $matches); + assertType("array{0: list, num: list, 1: list, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType("array{0: list, num: list, 1: list, suffix: list<'ab'|null>, 2: list<'ab'|null>}", $matches); + assertType("array{0: list, num: list, 1: list, suffix: list<'ab'|null>, 2: list<'ab'|null>}", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_SET_ORDER)) { - assertType("list", $matches); + assertType("list", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_PATTERN_ORDER)) { - assertType("array{0: list, num: list, 1: list, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $matches); + assertType("array{0: list, num: list, 1: list, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_SET_ORDER)) { - assertType("list", $matches); + assertType("list", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_PATTERN_ORDER)) { - assertType("array{0: list, num: list, 1: list, suffix: list<'ab'|null>, 2: list<'ab'|null>}", $matches); + assertType("array{0: list, num: list, 1: list, suffix: list<'ab'|null>, 2: list<'ab'|null>}", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) { - assertType("list}, num: array{numeric-string, int<-1, max>}, 1: array{numeric-string, int<-1, max>}, suffix?: array{'ab', int<-1, max>}, 2?: array{'ab', int<-1, max>}}>", $matches); + assertType("list}, num: array{numeric-string, int<-1, max>}, 1: array{numeric-string, int<-1, max>}, suffix?: array{'ab', int<-1, max>}, 2?: array{'ab', int<-1, max>}}>", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE)) { - assertType("array{0: list}>, num: list}>, 1: list}>, suffix: list}>, 2: list}>}", $matches); + assertType("array{0: list}>, num: list}>, 1: list}>, suffix: list}>, 2: list}>}", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) { - assertType("list}, num: array{numeric-string|null, int<-1, max>}, 1: array{numeric-string|null, int<-1, max>}, suffix: array{'ab'|null, int<-1, max>}, 2: array{'ab'|null, int<-1, max>}}>", $matches); + assertType("list}, num: array{numeric-string|null, int<-1, max>}, 1: array{numeric-string|null, int<-1, max>}, suffix: array{'ab'|null, int<-1, max>}, 2: array{'ab'|null, int<-1, max>}}>", $matches); } }; @@ -160,7 +160,7 @@ public function sayBar(string $content): void return; } - assertType('array{list}', $matches); + assertType('array{list}', $matches); } function doFoobar(string $s): void { From 900f66fd5f50a32f2c8e5598d4532c69197b05d2 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 22 Mar 2025 12:32:31 +0100 Subject: [PATCH 07/17] tests --- src/Type/Regex/RegexGroupParser.php | 6 +- .../Analyser/nsrt/preg_match_shapes.php | 350 +++++++++--------- .../Analyser/nsrt/preg_match_shapes_php82.php | 20 +- .../preg_replace_callback_shapes-php72.php | 2 +- 4 files changed, 191 insertions(+), 187 deletions(-) diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 709eb0ce3c..31bf730af9 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -109,7 +109,11 @@ public function parseGroups(string $regex): ?RegexAstWalkResult $modifiers, RegexGroupWalkResult::createEmpty(), ); - if ($subjectAsGroupResult->isNonEmpty()->yes()) { + if ($subjectAsGroupResult->isNonFalsy()->yes() || $subjectAsGroupResult->isNumeric()->yes()) { + $astWalkResult = $astWalkResult->withSubjectBaseType( + TypeCombinator::intersect(new StringType(), new AccessoryNonFalsyStringType()) + ); + } elseif ($subjectAsGroupResult->isNonEmpty()->yes()) { $astWalkResult = $astWalkResult->withSubjectBaseType( TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()) ); diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 7763dc5a96..04520da85f 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{non-falsy-string}', $matches); if (preg_match('/Price: (£|€)\d+/', $s, $matches)) { - assertType("array{string, '£'|'€'}", $matches); + assertType("array{non-falsy-string, '£'|'€'}", $matches); } else { assertType('array{}', $matches); } - assertType("array{}|array{string, '£'|'€'}", $matches); + assertType("array{}|array{non-falsy-string, '£'|'€'}", $matches); if (preg_match('/Price: (£|€)(\d+)/i', $s, $matches)) { - assertType('array{string, non-empty-string, numeric-string}', $matches); + assertType('array{non-falsy-string, non-empty-string, numeric-string}', $matches); } - assertType('array{}|array{string, non-empty-string, numeric-string}', $matches); + assertType('array{}|array{non-falsy-string, non-empty-string, numeric-string}', $matches); if (preg_match(' /Price: (£|€)\d+/ i u', $s, $matches)) { - assertType('array{string, non-empty-string}', $matches); + assertType('array{non-falsy-string, non-empty-string}', $matches); } - assertType('array{}|array{string, non-empty-string}', $matches); + assertType('array{}|array{non-falsy-string, non-empty-string}', $matches); if (preg_match('(Price: (£|€))i', $s, $matches)) { - assertType('array{string, non-empty-string}', $matches); + assertType('array{non-falsy-string, non-empty-string}', $matches); } - assertType('array{}|array{string, non-empty-string}', $matches); + assertType('array{}|array{non-falsy-string, non-empty-string}', $matches); if (preg_match('_foo(.)\_i_i', $s, $matches)) { - assertType('array{string, non-empty-string}', $matches); + assertType('array{non-falsy-string, non-empty-string}', $matches); } - assertType('array{}|array{string, non-empty-string}', $matches); + assertType('array{}|array{non-falsy-string, non-empty-string}', $matches); if (preg_match('/(a)(b)*(c)(d)*/', $s, $matches)) { - assertType("array{0: string, 1: 'a', 2: string, 3: 'c', 4?: non-empty-string}", $matches); + assertType("array{0: non-falsy-string, 1: 'a', 2: string, 3: 'c', 4?: non-empty-string}", $matches); } - assertType("array{}|array{0: string, 1: 'a', 2: string, 3: 'c', 4?: non-empty-string}", $matches); + assertType("array{}|array{0: non-falsy-string, 1: 'a', 2: string, 3: 'c', 4?: non-empty-string}", $matches); if (preg_match('/(a)(?b)*(c)(d)*/', $s, $matches)) { - assertType("array{0: string, 1: 'a', name: string, 2: string, 3: 'c', 4?: non-empty-string}", $matches); + assertType("array{0: non-falsy-string, 1: 'a', name: string, 2: string, 3: 'c', 4?: non-empty-string}", $matches); } - assertType("array{}|array{0: string, 1: 'a', name: string, 2: string, 3: 'c', 4?: non-empty-string}", $matches); + assertType("array{}|array{0: non-falsy-string, 1: 'a', name: string, 2: string, 3: 'c', 4?: non-empty-string}", $matches); if (preg_match('/(a)(b)*(c)(?d)*/', $s, $matches)) { - assertType("array{0: string, 1: 'a', 2: string, 3: 'c', name?: non-empty-string, 4?: non-empty-string}", $matches); + assertType("array{0: non-falsy-string, 1: 'a', 2: string, 3: 'c', name?: non-empty-string, 4?: non-empty-string}", $matches); } - assertType("array{}|array{0: string, 1: 'a', 2: string, 3: 'c', name?: non-empty-string, 4?: non-empty-string}", $matches); + assertType("array{}|array{0: non-falsy-string, 1: 'a', 2: string, 3: 'c', name?: non-empty-string, 4?: non-empty-string}", $matches); if (preg_match('/(a|b)|(?:c)/', $s, $matches)) { - assertType("array{0: string, 1?: 'a'|'b'}", $matches); + assertType("array{0: non-empty-string, 1?: 'a'|'b'}", $matches); } - assertType("array{}|array{0: string, 1?: 'a'|'b'}", $matches); + assertType("array{}|array{0: non-empty-string, 1?: 'a'|'b'}", $matches); if (preg_match('/(foo)(bar)(baz)+/', $s, $matches)) { - assertType("array{string, 'foo', 'bar', non-falsy-string}", $matches); + assertType("array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); } - assertType("array{}|array{string, 'foo', 'bar', non-falsy-string}", $matches); + assertType("array{}|array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); if (preg_match('/(foo)(bar)(baz)*/', $s, $matches)) { - assertType("array{0: string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches); + assertType("array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches); } - assertType("array{}|array{0: string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches); + assertType("array{}|array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches); if (preg_match('/(foo)(bar)(baz)?/', $s, $matches)) { - assertType("array{0: string, 1: 'foo', 2: 'bar', 3?: 'baz'}", $matches); + assertType("array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: 'baz'}", $matches); } - assertType("array{}|array{0: string, 1: 'foo', 2: 'bar', 3?: 'baz'}", $matches); + assertType("array{}|array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: 'baz'}", $matches); if (preg_match('/(foo)(bar)(baz){0,3}/', $s, $matches)) { - assertType("array{0: string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches); + assertType("array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches); } - assertType("array{}|array{0: string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches); + assertType("array{}|array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches); if (preg_match('/(foo)(bar)(baz){2,3}/', $s, $matches)) { - assertType("array{string, 'foo', 'bar', non-falsy-string}", $matches); + assertType("array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); } - assertType("array{}|array{string, 'foo', 'bar', non-falsy-string}", $matches); + assertType("array{}|array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); if (preg_match('/(foo)(bar)(baz){2}/', $s, $matches)) { - assertType("array{string, 'foo', 'bar', non-falsy-string}", $matches); + assertType("array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); } - assertType("array{}|array{string, 'foo', 'bar', non-falsy-string}", $matches); + assertType("array{}|array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); } function doNonCapturingGroup(string $s): void { if (preg_match('/Price: (?:£|€)(\d+)/', $s, $matches)) { - assertType('array{string, numeric-string}', $matches); + assertType('array{non-falsy-string, numeric-string}', $matches); } - assertType('array{}|array{string, numeric-string}', $matches); + assertType('array{}|array{non-falsy-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: non-empty-string}', $matches); + assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string, 2: non-empty-string}', $matches); } - assertType('array{}|array{0: string, num: numeric-string, 1: numeric-string, 2: non-empty-string}', $matches); + assertType('array{}|array{0: non-falsy-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-falsy-string, 1: non-falsy-string}', $matches); + assertType('array{0: non-falsy-string, name: non-falsy-string, 1: non-falsy-string}', $matches); } - assertType('array{}|array{0: string, name: non-falsy-string, 1: non-falsy-string}', $matches); + assertType('array{}|array{0: non-falsy-string, name: non-falsy-string, 1: non-falsy-string}', $matches); if (preg_match('/^(?\S+::\S+)(?:(? with data set (?:#\d+|"[^"]+"))\s\()?/', $s, $matches)) { - assertType('array{0: string, name: non-falsy-string, 1: non-falsy-string, dataname?: non-falsy-string, 2?: non-falsy-string}', $matches); + assertType('array{0: non-falsy-string, name: non-falsy-string, 1: non-falsy-string, dataname?: non-falsy-string, 2?: non-falsy-string}', $matches); } - assertType('array{}|array{0: string, name: non-falsy-string, 1: non-falsy-string, dataname?: non-falsy-string, 2?: non-falsy-string}', $matches); + assertType('array{}|array{0: non-falsy-string, name: non-falsy-string, 1: non-falsy-string, dataname?: non-falsy-string, 2?: non-falsy-string}', $matches); } function doOffsetCapture(string $s): void { if (preg_match('/(foo)(bar)(baz)/', $s, $matches, PREG_OFFSET_CAPTURE)) { - assertType("array{array{string, int<-1, max>}, array{'foo', int<-1, max>}, array{'bar', int<-1, max>}, array{'baz', int<-1, max>}}", $matches); + assertType("array{array{non-falsy-string, int<-1, max>}, array{'foo', int<-1, max>}, array{'bar', int<-1, max>}, array{'baz', int<-1, max>}}", $matches); } - assertType("array{}|array{array{string, int<-1, max>}, array{'foo', int<-1, max>}, array{'bar', int<-1, max>}, array{'baz', int<-1, max>}}", $matches); + assertType("array{}|array{array{non-falsy-string, int<-1, max>}, array{'foo', int<-1, max>}, array{'bar', int<-1, max>}, array{'baz', int<-1, max>}}", $matches); } function doUnknownFlags(string $s, int $flags): void { @@ -129,29 +129,29 @@ function doUnknownFlags(string $s, int $flags): void { function doMultipleAlternativeCaptureGroupsWithSameNameWithModifier(string $s): void { if (preg_match('/(?J)(?[a-z]+)|(?[0-9]+)/', $s, $matches)) { - assertType("array{0: string, Foo: non-empty-string, 1: non-empty-string}|array{0: string, Foo: numeric-string, 1: '', 2: numeric-string}", $matches); + assertType("array{0: non-empty-string, Foo: non-empty-string, 1: non-empty-string}|array{0: non-empty-string, Foo: numeric-string, 1: '', 2: numeric-string}", $matches); } - assertType("array{}|array{0: string, Foo: non-empty-string, 1: non-empty-string}|array{0: string, Foo: numeric-string, 1: '', 2: numeric-string}", $matches); + assertType("array{}|array{0: non-empty-string, Foo: non-empty-string, 1: non-empty-string}|array{0: non-empty-string, Foo: numeric-string, 1: '', 2: numeric-string}", $matches); } function doMultipleConsecutiveCaptureGroupsWithSameNameWithModifier(string $s): void { if (preg_match('/(?J)(?[a-z]+)|(?[0-9]+)/', $s, $matches)) { - assertType("array{0: string, Foo: non-empty-string, 1: non-empty-string}|array{0: string, Foo: numeric-string, 1: '', 2: numeric-string}", $matches); + assertType("array{0: non-empty-string, Foo: non-empty-string, 1: non-empty-string}|array{0: non-empty-string, Foo: numeric-string, 1: '', 2: numeric-string}", $matches); } - assertType("array{}|array{0: string, Foo: non-empty-string, 1: non-empty-string}|array{0: string, Foo: numeric-string, 1: '', 2: numeric-string}", $matches); + assertType("array{}|array{0: non-empty-string, Foo: non-empty-string, 1: non-empty-string}|array{0: non-empty-string, Foo: numeric-string, 1: '', 2: numeric-string}", $matches); } // https://github.com/hoaproject/Regex/issues/31 function hoaBug31(string $s): void { if (preg_match('/([\w-])/', $s, $matches)) { - assertType('array{string, non-empty-string}', $matches); + assertType('array{non-empty-string, non-empty-string}', $matches); } - assertType('array{}|array{string, non-empty-string}', $matches); + assertType('array{}|array{non-empty-string, non-empty-string}', $matches); if (preg_match('/\w-(\d+)-(\w)/', $s, $matches)) { - assertType('array{string, numeric-string, non-empty-string}', $matches); + assertType('array{non-falsy-string, numeric-string, non-empty-string}', $matches); } - assertType('array{}|array{string, numeric-string, non-empty-string}', $matches); + assertType('array{}|array{non-falsy-string, numeric-string, non-empty-string}', $matches); } // https://github.com/phpstan/phpstan/issues/10855#issuecomment-2044323638 @@ -164,56 +164,56 @@ function testHoaUnsupportedRegexSyntax(string $s): void { function testPregMatchSimpleCondition(string $value): void { if (preg_match('/%env\((.*)\:.*\)%/U', $value, $matches)) { - assertType('array{string, string}', $matches); + assertType('array{non-falsy-string, string}', $matches); } } function testPregMatchIdenticalToOne(string $value): void { if (preg_match('/%env\((.*)\:.*\)%/U', $value, $matches) === 1) { - assertType('array{string, string}', $matches); + assertType('array{non-falsy-string, string}', $matches); } } function testPregMatchIdenticalToOneFalseyContext(string $value): void { if (!(preg_match('/%env\((.*)\:.*\)%/U', $value, $matches) !== 1)) { - assertType('array{string, string}', $matches); + assertType('array{non-falsy-string, string}', $matches); } } function testPregMatchIdenticalToOneInverted(string $value): void { if (1 === preg_match('/%env\((.*)\:.*\)%/U', $value, $matches)) { - assertType('array{string, string}', $matches); + assertType('array{non-falsy-string, string}', $matches); } } function testPregMatchIdenticalToOneFalseyContextInverted(string $value): void { if (!(1 !== preg_match('/%env\((.*)\:.*\)%/U', $value, $matches))) { - assertType('array{string, string}', $matches); + assertType('array{non-falsy-string, string}', $matches); } } function testPregMatchEqualToOne(string $value): void { if (preg_match('/%env\((.*)\:.*\)%/U', $value, $matches) == 1) { - assertType('array{string, string}', $matches); + assertType('array{non-falsy-string, string}', $matches); } } function testPregMatchEqualToOneFalseyContext(string $value): void { if (!(preg_match('/%env\((.*)\:.*\)%/U', $value, $matches) != 1)) { - assertType('array{string, string}', $matches); + assertType('array{non-falsy-string, string}', $matches); } } function testPregMatchEqualToOneInverted(string $value): void { if (1 == preg_match('/%env\((.*)\:.*\)%/U', $value, $matches)) { - assertType('array{string, string}', $matches); + assertType('array{non-falsy-string, string}', $matches); } } function testPregMatchEqualToOneFalseyContextInverted(string $value): void { if (!(1 != preg_match('/%env\((.*)\:.*\)%/U', $value, $matches))) { - assertType('array{string, string}', $matches); + assertType('array{non-falsy-string, string}', $matches); } } @@ -225,18 +225,18 @@ 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{non-falsy-string, numeric-string, numeric-string, numeric-string}|array{non-falsy-string, numeric-string}', $matches); } - assertType('array{}|array{string, numeric-string, numeric-string, numeric-string}|array{string, numeric-string}', $matches); + assertType('array{}|array{non-falsy-string, numeric-string, numeric-string, numeric-string}|array{non-falsy-string, numeric-string}', $matches); } function doFoo(string $row): void { if (preg_match('~^(a(b))$~', $row, $matches) === 1) { - assertType("array{string, 'ab', 'b'}", $matches); + assertType("array{non-falsy-string, 'ab', 'b'}", $matches); } if (preg_match('~^(a(b)?)$~', $row, $matches) === 1) { - assertType("array{0: string, 1: non-falsy-string, 2?: 'b'}", $matches); + assertType("array{0: non-falsy-string, 1: non-falsy-string, 2?: 'b'}", $matches); } if (preg_match('~^(a(b)?)?$~', $row, $matches) === 1) { assertType("array{0: string, 1?: non-falsy-string, 2?: 'b'}", $matches); @@ -249,7 +249,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: non-falsy-string&numeric-string, 4: non-falsy-string&numeric-string}", $matches); + assertType("array{0: non-falsy-string, 1: string, branchCode: ''|numeric-string, 2: ''|numeric-string, accountNumber: numeric-string, 3: numeric-string, bankCode: non-falsy-string&numeric-string, 4: non-falsy-string&numeric-string}", $matches); } function doFoo3(string $row): void @@ -258,56 +258,56 @@ function doFoo3(string $row): void return; } - assertType('array{string, non-falsy-string, non-falsy-string, numeric-string, numeric-string, numeric-string, numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string, non-falsy-string, 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-falsy-string, numeric-string, numeric-string, non-empty-string}|array{string}', $matches); + assertType('array{non-falsy-string, non-falsy-string, numeric-string, numeric-string, non-empty-string}|array{non-falsy-string}', $matches); }; function (string $size): void { if (preg_match('~^a\.b(c(\d+))?d~', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{string, non-falsy-string, numeric-string}|array{string}', $matches); + assertType('array{non-falsy-string, non-falsy-string, numeric-string}|array{non-falsy-string}', $matches); }; function (string $size): void { if (preg_match('~^a\.b(c(\d+)?)d~', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{0: string, 1: non-falsy-string, 2?: numeric-string}', $matches); + assertType('array{0: non-falsy-string, 1: non-falsy-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-falsy-string, 2?: numeric-string}', $matches); + assertType('array{0: non-falsy-string, 1?: non-falsy-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-falsy-string, numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string, numeric-string}', $matches); }; 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?: ''|'b', 2?: 'c'}", $matches); + assertType("array{0: non-falsy-string, 1?: ''|'b', 2?: 'c'}", $matches); }; function (string $size): void { if (preg_match('~^(?:(\\d+)x(\\d+)|(\\d+)|x(\\d+))$~', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType("array{string, '', '', '', numeric-string}|array{string, '', '', numeric-string}|array{string, numeric-string, numeric-string}", $matches); + assertType("array{non-empty-string, '', '', '', numeric-string}|array{non-empty-string, '', '', numeric-string}|array{non-empty-string, numeric-string, numeric-string}", $matches); }; function (string $size): void { @@ -321,16 +321,16 @@ function (string $size): void { if (preg_match('~\{(?:(include)\\s+(?:[$]?\\w+(?£|€)\d+/', $s, $matches)) { - assertType("array{0: string, currency: '£'|'€', 1: '£'|'€'}", $matches); + assertType("array{0: non-falsy-string, currency: '£'|'€', 1: '£'|'€'}", $matches); } else { assertType('array{}', $matches); } - assertType("array{}|array{0: string, currency: '£'|'€', 1: '£'|'€'}", $matches); + assertType("array{}|array{0: non-falsy-string, currency: '£'|'€', 1: '£'|'€'}", $matches); } function bug11323b(string $s): void { if (preg_match('/Price: (?£|€)\d+/', $s, $matches)) { - assertType("array{0: string, currency: '£'|'€', 1: '£'|'€'}", $matches); + assertType("array{0: non-falsy-string, currency: '£'|'€', 1: '£'|'€'}", $matches); } else { assertType('array{}', $matches); } - assertType("array{}|array{0: string, currency: '£'|'€', 1: '£'|'€'}", $matches); + assertType("array{}|array{0: non-falsy-string, currency: '£'|'€', 1: '£'|'€'}", $matches); } function unmatchedAsNullWithMandatoryGroup(string $s): void { if (preg_match('/Price: (?£|€)\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType("array{0: string, currency: '£'|'€', 1: '£'|'€'}", $matches); + assertType("array{0: non-falsy-string, currency: '£'|'€', 1: '£'|'€'}", $matches); } else { assertType('array{}', $matches); } - assertType("array{}|array{0: string, currency: '£'|'€', 1: '£'|'€'}", $matches); + assertType("array{}|array{0: non-falsy-string, currency: '£'|'€', 1: '£'|'€'}", $matches); } function (string $s): void { if (preg_match('{' . preg_quote('xxx') . '(z)}', $s, $matches)) { - assertType("array{string, 'z'}", $matches); + assertType("array{non-falsy-string, 'z'}", $matches); } else { assertType('array{}', $matches); } - assertType("array{}|array{string, 'z'}", $matches); + assertType("array{}|array{non-falsy-string, 'z'}", $matches); }; function (string $s): void { if (preg_match('{' . preg_quote($s) . '(z)}', $s, $matches)) { - assertType("array{string, 'z'}", $matches); + assertType("array{non-falsy-string, 'z'}", $matches); } else { assertType('array{}', $matches); } - assertType("array{}|array{string, 'z'}", $matches); + assertType("array{}|array{non-falsy-string, 'z'}", $matches); }; function (string $s): void { if (preg_match('/' . preg_quote($s, '/') . '(\d)/', $s, $matches)) { - assertType('array{string, numeric-string}', $matches); + assertType('array{non-empty-string, numeric-string}', $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{string, numeric-string}', $matches); + assertType('array{}|array{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: 'z', 2?: 'def'}", $matches); + assertType("array{0: non-falsy-string, 1: 'z', 2?: 'def'}", $matches); } else { assertType('array{}', $matches); } - assertType("array{}|array{0: string, 1: 'z', 2?: 'def'}", $matches); + assertType("array{}|array{0: non-falsy-string, 1: 'z', 2?: 'def'}", $matches); }; function (string $s, $mixed): void { @@ -429,97 +429,97 @@ function (string $s, $mixed): void { function (string $s): void { if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*([sbdeEfFgGhHouxX])$/', $s, $matches) === 1) { - assertType("array{string, string, 'b'|'d'|'E'|'e'|'F'|'f'|'G'|'g'|'H'|'h'|'o'|'s'|'u'|'X'|'x'}", $matches); + assertType("array{non-falsy-string, string, 'b'|'d'|'E'|'e'|'F'|'f'|'G'|'g'|'H'|'h'|'o'|'s'|'u'|'X'|'x'}", $matches); } }; function (string $s): void { if (preg_match('~^((\\d{1,6})-)$~', $s, $matches) === 1) { - assertType("array{string, non-falsy-string, numeric-string}", $matches); + assertType("array{non-falsy-string, non-falsy-string, numeric-string}", $matches); } }; function (string $s): void { if (preg_match('~^((\\d{1,6}).)$~', $s, $matches) === 1) { - assertType("array{string, non-falsy-string, numeric-string}", $matches); + assertType("array{non-falsy-string, non-falsy-string, numeric-string}", $matches); } }; function (string $s): void { if (preg_match('~^([157])$~', $s, $matches) === 1) { - assertType("array{string, '1'|'5'|'7'}", $matches); + assertType("array{non-falsy-string, '1'|'5'|'7'}", $matches); } }; function (string $s): void { if (preg_match('~^([157XY])$~', $s, $matches) === 1) { - assertType("array{string, '1'|'5'|'7'|'X'|'Y'}", $matches); + assertType("array{non-falsy-string, '1'|'5'|'7'|'X'|'Y'}", $matches); } }; function bug11323(string $s): void { if (preg_match('/([*|+?{}()]+)([^*|+[:digit:]?{}()]+)/', $s, $matches)) { - assertType('array{string, non-empty-string, non-empty-string}', $matches); + assertType('array{non-falsy-string, non-empty-string, non-empty-string}', $matches); } if (preg_match('/\p{L}[[\]]+([-*|+?{}(?:)]+)([^*|+[:digit:]?{a-z}(\p{L})\a-]+)/', $s, $matches)) { - assertType('array{string, non-empty-string, non-empty-string}', $matches); + assertType('array{non-falsy-string, non-empty-string, non-empty-string}', $matches); } if (preg_match('{([-\p{L}[\]*|\x03\a\b+?{}(?:)-]+[^[:digit:]?{}a-z0-9#-k]+)(a-z)}', $s, $matches)) { - assertType("array{string, non-falsy-string, 'a-z'}", $matches); + assertType("array{non-falsy-string, non-falsy-string, 'a-z'}", $matches); } if (preg_match('{(\d+)(?i)insensitive((?xs-i)case SENSITIVE here.+and dot matches new lines)}', $s, $matches)) { - assertType('array{string, numeric-string, non-falsy-string}', $matches); + assertType('array{non-falsy-string, numeric-string, non-falsy-string}', $matches); } if (preg_match('{(\d+)(?i)insensitive((?x-i)case SENSITIVE here(?i:insensitive non-capturing group))}', $s, $matches)) { - assertType('array{string, numeric-string, non-falsy-string}', $matches); + assertType('array{non-falsy-string, numeric-string, non-falsy-string}', $matches); } if (preg_match('{([]] [^]])}', $s, $matches)) { - assertType('array{string, non-falsy-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string}', $matches); } if (preg_match('{([[:digit:]])}', $s, $matches)) { - assertType('array{string, numeric-string}', $matches); + assertType('array{non-falsy-string, numeric-string}', $matches); } if (preg_match('{([\d])(\d)}', $s, $matches)) { - assertType('array{string, numeric-string, numeric-string}', $matches); + assertType('array{non-falsy-string, numeric-string, numeric-string}', $matches); } if (preg_match('{([0-9])}', $s, $matches)) { - assertType('array{string, numeric-string}', $matches); + assertType('array{non-falsy-string, numeric-string}', $matches); } if (preg_match('{(\p{L})(\p{P})(\p{Po})}', $s, $matches)) { - assertType('array{string, non-empty-string, non-empty-string, non-empty-string}', $matches); + assertType('array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string}', $matches); } if (preg_match('{(a)??(b)*+(c++)(d)+?}', $s, $matches)) { - assertType("array{string, ''|'a', string, non-empty-string, non-empty-string}", $matches); + assertType("array{non-falsy-string, ''|'a', string, non-empty-string, non-empty-string}", $matches); } if (preg_match('{(.\d)}', $s, $matches)) { - assertType('array{string, non-falsy-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string}', $matches); } if (preg_match('{(\d.)}', $s, $matches)) { - assertType('array{string, non-falsy-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string}', $matches); } if (preg_match('{(\d\d)}', $s, $matches)) { - assertType('array{string, non-falsy-string&numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); } if (preg_match('{(.(\d))}', $s, $matches)) { - assertType('array{string, non-falsy-string, numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string, numeric-string}', $matches); } if (preg_match('{((\d).)}', $s, $matches)) { - assertType('array{string, non-falsy-string, numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string, numeric-string}', $matches); } if (preg_match('{(\d([1-4])\d)}', $s, $matches)) { - assertType('array{string, non-falsy-string&numeric-string, numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string&numeric-string, numeric-string}', $matches); } if (preg_match('{(x?([1-4])\d)}', $s, $matches)) { - assertType('array{string, non-falsy-string, numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string, numeric-string}', $matches); } if (preg_match('{([^1-4])}', $s, $matches)) { - assertType('array{string, non-empty-string}', $matches); + assertType('array{non-empty-string, non-empty-string}', $matches); } if (preg_match("{([\r\n]+)(\n)([\n])}", $s, $matches)) { - assertType('array{string, non-empty-string, "\n", "\n"}', $matches); + assertType('array{non-falsy-string, non-empty-string, "\n", "\n"}', $matches); } if (preg_match('/foo(*:first)|bar(*:second)([x])/', $s, $matches)) { - assertType("array{0: string, 1?: 'x', MARK?: 'first'|'second'}", $matches); + assertType("array{0: non-empty-string, 1?: 'x', MARK?: 'first'|'second'}", $matches); } } @@ -539,7 +539,7 @@ public function test(string $str): void public function test2(string $str): void { if (preg_match('~^(?:(\w+)::)?(\w+)$~', $str, $matches) === 1) { - assertType('array{string, string, non-empty-string}', $matches); + assertType('array{non-empty-string, string, non-empty-string}', $matches); } } } @@ -552,7 +552,7 @@ function (string $s): void { } if (preg_match($p, $s, $matches)) { - assertType("array{string, '£', 'abc'}|array{string, numeric-string, 'b'}", $matches); + assertType("array{non-falsy-string, '£', 'abc'}|array{non-falsy-string, numeric-string, 'b'}", $matches); } }; @@ -564,97 +564,97 @@ function (string $s): void { } if (preg_match($p, $s, $matches)) { - assertType("array{0: string, 1: 'x'|'£'|numeric-string, 2?: ''|numeric-string, 3?: 'x'}", $matches); + assertType("array{0: non-falsy-string, 1: 'x'|'£'|numeric-string, 2?: ''|numeric-string, 3?: 'x'}", $matches); } }; function (string $s): void { if (preg_match('/Price: ([a-z])/i', $s, $matches)) { - assertType("array{string, non-empty-string}", $matches); + assertType("array{non-falsy-string, non-empty-string}", $matches); } }; function (string $s): void { if (preg_match('/Price: ([0-9])/i', $s, $matches)) { - assertType("array{string, numeric-string}", $matches); + assertType("array{non-falsy-string, numeric-string}", $matches); } }; function (string $s): void { if (preg_match('/Price: ([xXa])/i', $s, $matches)) { - assertType("array{string, non-empty-string}", $matches); + assertType("array{non-falsy-string, non-empty-string}", $matches); } }; function (string $s): void { if (preg_match('/Price: ([xXa])/', $s, $matches)) { - assertType("array{string, 'a'|'X'|'x'}", $matches); + assertType("array{non-falsy-string, 'a'|'X'|'x'}", $matches); } }; function (string $s): void { if (preg_match('/Price: (ba[rz])/', $s, $matches)) { - assertType("array{string, 'bar'|'baz'}", $matches); + assertType("array{non-falsy-string, 'bar'|'baz'}", $matches); } }; function (string $s): void { if (preg_match('/Price: (b[ao][mn])/', $s, $matches)) { - assertType("array{string, 'bam'|'ban'|'bom'|'bon'}", $matches); + assertType("array{non-falsy-string, 'bam'|'ban'|'bom'|'bon'}", $matches); } }; function (string $s): void { if (preg_match('/Price: (\s{3}|0)/', $s, $matches)) { - assertType("array{string, non-empty-string}", $matches); + assertType("array{non-falsy-string, non-empty-string}", $matches); } }; function (string $s): void { if (preg_match('/Price: (a|bc?)/', $s, $matches)) { - assertType("array{string, non-falsy-string}", $matches); + assertType("array{non-falsy-string, non-falsy-string}", $matches); } }; function (string $s): void { if (preg_match('/Price: (?a|bc?)/', $s, $matches)) { - assertType("array{0: string, named: non-falsy-string, 1: non-falsy-string}", $matches); + assertType("array{0: non-falsy-string, named: non-falsy-string, 1: non-falsy-string}", $matches); } }; function (string $s): void { if (preg_match('/Price: (a|0c?)/', $s, $matches)) { - assertType("array{string, non-empty-string}", $matches); + assertType("array{non-falsy-string, non-empty-string}", $matches); } }; function (string $s): void { if (preg_match('/Price: (a|\d)/', $s, $matches)) { - assertType("array{string, 'a'|numeric-string}", $matches); + assertType("array{non-falsy-string, 'a'|numeric-string}", $matches); } }; function (string $s): void { if (preg_match('/Price: (?a|\d)/', $s, $matches)) { - assertType("array{0: string, named: 'a'|numeric-string, 1: 'a'|numeric-string}", $matches); + assertType("array{0: non-falsy-string, named: 'a'|numeric-string, 1: 'a'|numeric-string}", $matches); } }; function (string $s): void { if (preg_match('/Price: (a|0)/', $s, $matches)) { - assertType("array{string, '0'|'a'}", $matches); + assertType("array{non-falsy-string, '0'|'a'}", $matches); } }; function (string $s): void { if (preg_match('/Price: (aa|0)/', $s, $matches)) { - assertType("array{string, '0'|'aa'}", $matches); + assertType("array{non-falsy-string, '0'|'aa'}", $matches); } }; function (string $s): void { if (preg_match('/( \d+ )/x', $s, $matches)) { - assertType('array{string, numeric-string}', $matches); + assertType('array{non-falsy-string, numeric-string}', $matches); } }; @@ -672,7 +672,7 @@ function (string $s): void { function (string $s): void { if (preg_match('/( .+ )/x', $s, $matches)) { - assertType('array{string, non-empty-string}', $matches); + assertType('array{non-empty-string, non-empty-string}', $matches); } }; @@ -712,19 +712,19 @@ static public function sayHello(string $source): void function (string $s): void { if (preg_match('~a|(\d)|(\s)~', $s, $matches) === 1) { - assertType("array{0: string, 1?: numeric-string}|array{string, '', non-empty-string}", $matches); + assertType("array{0: non-empty-string, 1?: numeric-string}|array{non-empty-string, '', non-empty-string}", $matches); } }; function (string $s): void { if (preg_match('~a|((u)x)|((v)y)~', $s, $matches) === 1) { - assertType("array{string, '', '', 'vy', 'v'}|array{string, 'ux', 'u'}|array{string}", $matches); + assertType("array{non-empty-string, '', '', 'vy', 'v'}|array{non-empty-string, 'ux', 'u'}|array{non-empty-string}", $matches); } }; function (string $s): void { if (preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_OFFSET_CAPTURE) === 1) { - assertType("array{0: array{string, int<-1, max>}, 1?: array{numeric-string, int<-1, max>}}|array{array{string, int<-1, max>}, array{'', int<-1, max>}, array{non-empty-string, int<-1, max>}}", $matches); + assertType("array{0: array{non-empty-string, int<-1, max>}, 1?: array{numeric-string, int<-1, max>}}|array{array{non-empty-string, int<-1, max>}, array{'', int<-1, max>}, array{non-empty-string, int<-1, max>}}", $matches); } }; @@ -737,7 +737,7 @@ function bug11490 (string $expression): void { $matches = []; if (preg_match('/([-+])?([\d]+)%/', $expression, $matches) === 1) { - assertType("array{string, ''|'+'|'-', numeric-string}", $matches); + assertType("array{non-falsy-string, ''|'+'|'-', numeric-string}", $matches); } } @@ -745,7 +745,7 @@ function bug11490b (string $expression): void { $matches = []; if (preg_match('/([\\[+])?([\d]+)%/', $expression, $matches) === 1) { - assertType("array{string, ''|'+'|'[', numeric-string}", $matches); + assertType("array{non-falsy-string, ''|'+'|'[', numeric-string}", $matches); } } @@ -753,7 +753,7 @@ function bug11622 (string $expression): void { $matches = []; if (preg_match('/^abc(def|$)/', $expression, $matches) === 1) { - assertType("array{string, string}", $matches); + assertType("array{non-falsy-string, string}", $matches); } } @@ -762,23 +762,23 @@ function bug11604 (string $string): void { return; } - assertType("array{0: string, 1?: ''|'XX', 2?: 'YY'}", $matches); + assertType("array{0: non-empty-string, 1?: ''|'XX', 2?: 'YY'}", $matches); // could be array{string, '', 'YY'}|array{string, 'XX'}|array{string} } function bug11604b (string $string): void { if (preg_match('/(XX)|(YY)?(ZZ)/', $string, $matches)) { - assertType("array{0: string, 1?: ''|'XX', 2?: ''|'YY', 3?: 'ZZ'}", $matches); + assertType("array{0: non-empty-string, 1?: ''|'XX', 2?: ''|'YY', 3?: 'ZZ'}", $matches); } } function testLtrimDelimiter (string $string): void { if (preg_match(' /(x)/', $string, $matches)) { - assertType("array{string, 'x'}", $matches); + assertType("array{non-empty-string, 'x'}", $matches); } if (preg_match(' /(x)/', $string, $matches)) { - assertType("array{string, 'x'}", $matches); + assertType("array{non-empty-string, 'x'}", $matches); } } @@ -786,31 +786,31 @@ function testUnescapeBackslash (string $string): void { if (preg_match(<<<'EOD' ~(\[)~ EOD, $string, $matches)) { - assertType("array{string, '['}", $matches); + assertType("array{non-empty-string, '['}", $matches); } if (preg_match(<<<'EOD' ~(\d)~ EOD, $string, $matches)) { - assertType("array{string, numeric-string}", $matches); + assertType("array{non-falsy-string, numeric-string}", $matches); } if (preg_match(<<<'EOD' ~(\\d)~ EOD, $string, $matches)) { - assertType("array{string, '\\\d'}", $matches); + assertType("array{non-falsy-string, '\\\d'}", $matches); } if (preg_match(<<<'EOD' ~(\\\d)~ EOD, $string, $matches)) { - assertType("array{string, non-falsy-string}", $matches); + assertType("array{non-falsy-string, non-falsy-string}", $matches); } if (preg_match(<<<'EOD' ~(\\\\d)~ EOD, $string, $matches)) { - assertType("array{string, '\\\\\\\d'}", $matches); + assertType("array{non-falsy-string, '\\\\\\\d'}", $matches); } } @@ -818,86 +818,86 @@ function testEscapedDelimiter (string $string): void { if (preg_match(<<<'EOD' /(\/)/ EOD, $string, $matches)) { - assertType("array{string, '/'}", $matches); + assertType("array{non-empty-string, '/'}", $matches); } if (preg_match(<<<'EOD' ~(\~)~ EOD, $string, $matches)) { - assertType("array{string, '~'}", $matches); + assertType("array{non-empty-string, '~'}", $matches); } if (preg_match(<<<'EOD' ~(\[2])~ EOD, $string, $matches)) { - assertType("array{string, '[2]'}", $matches); + assertType("array{non-falsy-string, '[2]'}", $matches); } if (preg_match(<<<'EOD' [(\[2\])] EOD, $string, $matches)) { - assertType("array{string, '[2]'}", $matches); + assertType("array{non-falsy-string, '[2]'}", $matches); } if (preg_match(<<<'EOD' ~(\{2})~ EOD, $string, $matches)) { - assertType("array{string, '{2}'}", $matches); + assertType("array{non-falsy-string, '{2}'}", $matches); } if (preg_match(<<<'EOD' {(\{2\})} EOD, $string, $matches)) { - assertType("array{string, '{2}'}", $matches); + assertType("array{non-falsy-string, '{2}'}", $matches); } if (preg_match(<<<'EOD' ~([a\]])~ EOD, $string, $matches)) { - assertType("array{string, ']'|'a'}", $matches); + assertType("array{non-empty-string, ']'|'a'}", $matches); } if (preg_match(<<<'EOD' ~([a[])~ EOD, $string, $matches)) { - assertType("array{string, '['|'a'}", $matches); + assertType("array{non-empty-string, '['|'a'}", $matches); } if (preg_match(<<<'EOD' ~([a\]b])~ EOD, $string, $matches)) { - assertType("array{string, ']'|'a'|'b'}", $matches); + assertType("array{non-empty-string, ']'|'a'|'b'}", $matches); } if (preg_match(<<<'EOD' ~([a[b])~ EOD, $string, $matches)) { - assertType("array{string, '['|'a'|'b'}", $matches); + assertType("array{non-empty-string, '['|'a'|'b'}", $matches); } if (preg_match(<<<'EOD' ~([a\[b])~ EOD, $string, $matches)) { - assertType("array{string, '['|'a'|'b'}", $matches); + assertType("array{non-empty-string, '['|'a'|'b'}", $matches); } if (preg_match(<<<'EOD' [([a\[b])] EOD, $string, $matches)) { - assertType("array{string, '['|'a'|'b'}", $matches); + assertType("array{non-empty-string, '['|'a'|'b'}", $matches); } if (preg_match(<<<'EOD' {(x\\\{)|(y\\\\\})} EOD, $string, $matches)) { - assertType("array{string, '', 'y\\\\\\\}'}|array{string, 'x\\\{'}", $matches); + assertType("array{non-empty-string, '', 'y\\\\\\\}'}|array{non-empty-string, 'x\\\{'}", $matches); } } function bugUnescapedDashAfterRange (string $string): void { if (preg_match('/([0-1-y])/', $string, $matches)) { - assertType("array{string, non-empty-string}", $matches); + assertType("array{non-empty-string, non-empty-string}", $matches); } } @@ -915,31 +915,31 @@ function bugEmptySubexpression (string $string): void { } if (preg_match('~|(a)~', $string, $matches)) { - assertType("array{0: string, 1?: 'a'}", $matches); + assertType("array{0: non-empty-string, 1?: 'a'}", $matches); } if (preg_match('~(a)|~', $string, $matches)) { - assertType("array{0: string, 1?: 'a'}", $matches); + assertType("array{0: non-empty-string, 1?: 'a'}", $matches); } if (preg_match('~(a)||(b)~', $string, $matches)) { - assertType("array{0: string, 1?: 'a'}|array{string, '', 'b'}", $matches); + assertType("array{0: non-empty-string, 1?: 'a'}|array{non-empty-string, '', 'b'}", $matches); } if (preg_match('~(|(a))~', $string, $matches)) { - assertType("array{0: string, 1: ''|'a', 2?: 'a'}", $matches); + assertType("array{0: non-empty-string, 1: ''|'a', 2?: 'a'}", $matches); } if (preg_match('~((a)|)~', $string, $matches)) { - assertType("array{0: string, 1: ''|'a', 2?: 'a'}", $matches); + assertType("array{0: non-empty-string, 1: ''|'a', 2?: 'a'}", $matches); } if (preg_match('~((a)||(b))~', $string, $matches)) { - assertType("array{0: string, 1: ''|'a'|'b', 2?: ''|'a', 3?: 'b'}", $matches); + assertType("array{0: non-empty-string, 1: ''|'a'|'b', 2?: ''|'a', 3?: 'b'}", $matches); } if (preg_match('~((a)|()|(b))~', $string, $matches)) { - assertType("array{0: string, 1: ''|'a'|'b', 2?: ''|'a', 3?: '', 4?: 'b'}", $matches); + assertType("array{0: non-empty-string, 1: ''|'a'|'b', 2?: ''|'a', 3?: '', 4?: 'b'}", $matches); } } @@ -958,7 +958,7 @@ function bug11744(string $string): void if (!preg_match('~^((/[a-z]+)?.+)~', $string, $matches)) { return; } - assertType('array{0: string, 1: non-empty-string, 2?: non-falsy-string}', $matches); + assertType('array{0: non-empty-string, 1: non-empty-string, 2?: non-falsy-string}', $matches); } function bug12749(string $str): void @@ -971,14 +971,14 @@ function bug12749(string $str): void function bug12749a(string $str): void { if (preg_match('/[A-Z]{2,}/', $str, $match)) { - assertType('array{non-empty-string}', $match); // could be non-falsy-string + assertType('array{non-falsy-string}', $match); } } function bug12749b(string $str): void { if (preg_match('/[0-9][A-Z]/', $str, $match)) { - assertType('array{non-empty-string}', $match); // could be non-falsy-string + assertType('array{non-falsy-string}', $match); } } @@ -992,7 +992,7 @@ function bug12749c(string $str): void function bug12749d(string $str): void { if (preg_match('/[0-9]?[A-Z]/', $str, $match)) { - assertType('array{non-empty-string}', $match); // could be non-falsy-string + assertType('array{non-falsy-string}', $match); } } @@ -1000,7 +1000,7 @@ function bug12749e(string $str): void { // no ^ $ delims, therefore can be anything which contains a number if (preg_match('/[0-9]/', $str, $match)) { - assertType('array{non-empty-string}', $match); // could be non-falsy-string + assertType('array{non-falsy-string}', $match); } } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php index aea2a7b634..63cdb2869b 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php @@ -8,39 +8,39 @@ // 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{non-empty-string}', $matches); + assertType('array{non-falsy-string}', $matches); } - assertType('array{}|array{non-empty-string}', $matches); + assertType('array{}|array{non-falsy-string}', $matches); if (preg_match('/(\d+)(?P\d+)/n', $s, $matches)) { - assertType('array{0: non-empty-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); } - assertType('array{}|array{0: non-empty-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{}|array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); if (preg_match('/(\w)-(?P\d+)-(\w)/n', $s, $matches)) { - assertType('array{0: non-empty-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); } - assertType('array{}|array{0: non-empty-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{}|array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); } // delimiter variants, see https://www.php.net/manual/en/regexp.reference.delimiters.php function (string $s): void { if (preg_match('{(\d+)(?P\d+)}n', $s, $matches)) { - assertType('array{0: non-empty-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); } }; function (string $s): void { if (preg_match('<(\d+)(?P\d+)>n', $s, $matches)) { - assertType('array{0: non-empty-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); } }; function (string $s): void { if (preg_match('((\d+)(?P\d+))n', $s, $matches)) { - assertType('array{0: non-empty-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); } }; function (string $s): void { if (preg_match('[(\d+)(?P\d+)]n', $s, $matches)) { - assertType('array{0: non-empty-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); } }; diff --git a/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes-php72.php b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes-php72.php index f7230851e4..5ca06f4d58 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes-php72.php +++ b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes-php72.php @@ -8,7 +8,7 @@ function (string $s): void { preg_replace_callback( $s, function ($matches) { - assertType('array', $matches); + assertType('array{string, string}', $matches); return ''; }, $s From 196727be631d18b894a55b08b83ad186239a7821 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 22 Mar 2025 12:34:55 +0100 Subject: [PATCH 08/17] Delete test.php --- test.php | 53 ----------------------------------------------------- 1 file changed, 53 deletions(-) delete mode 100644 test.php diff --git a/test.php b/test.php deleted file mode 100644 index e65db2c74c..0000000000 --- a/test.php +++ /dev/null @@ -1,53 +0,0 @@ - Date: Sat, 22 Mar 2025 12:35:54 +0100 Subject: [PATCH 09/17] Update preg_replace_callback_shapes-php72.php --- .../Analyser/nsrt/preg_replace_callback_shapes-php72.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes-php72.php b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes-php72.php index 5ca06f4d58..d5e650e708 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes-php72.php +++ b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes-php72.php @@ -8,7 +8,7 @@ function (string $s): void { preg_replace_callback( $s, function ($matches) { - assertType('array{string, string}', $matches); + assertType('array', $matches); return ''; }, $s @@ -19,7 +19,7 @@ function (string $s): void { preg_replace_callback( '|

(\s*)\w|', function ($matches) { - assertType('array{string, string}', $matches); + assertType('array{non-falsy-string, string}', $matches); return ''; }, $s From 4ca31289a4239d1e1ecbabdf6676cded63cc2e05 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 22 Mar 2025 12:38:32 +0100 Subject: [PATCH 10/17] fix --- tests/PHPStan/Analyser/nsrt/bug-11293.php | 12 ++++++------ tests/PHPStan/Analyser/nsrt/bug-11580.php | 6 +++--- tests/PHPStan/Analyser/nsrt/bug-12210.php | 8 ++++---- tests/PHPStan/Analyser/nsrt/bug-12242.php | 2 +- .../Analyser/nsrt/preg_match_shapes_php80.php | 8 ++++---- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-11293.php b/tests/PHPStan/Analyser/nsrt/bug-11293.php index 0c190b23fc..19a9a1eb5c 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11293.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11293.php @@ -9,21 +9,21 @@ class HelloWorld public function sayHello(string $s): void { if (preg_match('/data-(\d{6})\.json$/', $s, $matches) > 0) { - assertType('array{string, non-falsy-string&numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); } } public function sayHello2(string $s): void { if (preg_match('/data-(\d{6})\.json$/', $s, $matches) === 1) { - assertType('array{string, non-falsy-string&numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); } } public function sayHello3(string $s): void { if (preg_match('/data-(\d{6})\.json$/', $s, $matches) >= 1) { - assertType('array{string, non-falsy-string&numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); } } @@ -35,7 +35,7 @@ public function sayHello4(string $s): void return; } - assertType('array{string, non-falsy-string&numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); } public function sayHello5(string $s): void @@ -46,7 +46,7 @@ public function sayHello5(string $s): void return; } - assertType('array{string, non-falsy-string&numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); } public function sayHello6(string $s): void @@ -57,6 +57,6 @@ public function sayHello6(string $s): void return; } - assertType('array{string, non-falsy-string&numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11580.php b/tests/PHPStan/Analyser/nsrt/bug-11580.php index ebb4220372..2081bb0624 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11580.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11580.php @@ -10,7 +10,7 @@ public function bad(string $in): void { $matches = []; if (preg_match('~^/xxx/([\w\-]+)/?([\w\-]+)?/?$~', $in, $matches)) { - assertType('array{0: string, 1: non-empty-string, 2?: non-empty-string}', $matches); + assertType('array{0: non-falsy-string, 1: non-empty-string, 2?: non-empty-string}', $matches); } } @@ -19,7 +19,7 @@ public function bad2(string $in): void $matches = []; $result = preg_match('~^/xxx/([\w\-]+)/?([\w\-]+)?/?$~', $in, $matches); if ($result) { - assertType('array{0: string, 1: non-empty-string, 2?: non-empty-string}', $matches); + assertType('array{0: non-falsy-string, 1: non-empty-string, 2?: non-empty-string}', $matches); } } @@ -28,7 +28,7 @@ public function bad3(string $in): void $result = preg_match('~^/xxx/([\w\-]+)/?([\w\-]+)?/?$~', $in, $matches); assertType('array{0?: string, 1?: non-empty-string, 2?: non-empty-string}', $matches); if ($result) { - assertType('array{0: string, 1: non-empty-string, 2?: non-empty-string}', $matches); + assertType('array{0: non-falsy-string, 1: non-empty-string, 2?: non-empty-string}', $matches); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12210.php b/tests/PHPStan/Analyser/nsrt/bug-12210.php index 165b61b63e..13cf62ed26 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12210.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12210.php @@ -8,20 +8,20 @@ function bug12210a(string $text): void { assert(preg_match('(((sum|min|max)))', $text, $match) === 1); - assertType("array{string, 'max'|'min'|'sum', 'max'|'min'|'sum'}", $match); + assertType("array{non-empty-string, 'max'|'min'|'sum', 'max'|'min'|'sum'}", $match); } function bug12210b(string $text): void { assert(preg_match('(((sum|min|ma.)))', $text, $match) === 1); - assertType("array{string, non-empty-string, non-falsy-string}", $match); + assertType("array{non-empty-string, non-empty-string, non-falsy-string}", $match); } function bug12210c(string $text): void { assert(preg_match('(((su.|min|max)))', $text, $match) === 1); - assertType("array{string, non-empty-string, non-falsy-string}", $match); + assertType("array{non-empty-string, non-empty-string, non-falsy-string}", $match); } function bug12210d(string $text): void { assert(preg_match('(((sum|mi.|max)))', $text, $match) === 1); - assertType("array{string, non-empty-string, non-falsy-string}", $match); + assertType("array{non-empty-string, non-empty-string, non-falsy-string}", $match); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12242.php b/tests/PHPStan/Analyser/nsrt/bug-12242.php index 4d065367a2..d9335610d3 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12242.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12242.php @@ -27,7 +27,7 @@ function bar(string $str): void (\w*) # extra description (UNSIGNED, CHARACTER SET, ...) [3] $/x'; if (preg_match($regexp, $str, $matches)) { - assertType('array{string, non-empty-string, string, string}', $matches); + assertType('array{non-falsy-string, non-empty-string, string, 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 34b1b72756..280c1b62f9 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php @@ -7,15 +7,15 @@ 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{'foo'|null, int<-1, max>}, array{'bar'|null, int<-1, max>}, array{'baz'|null, int<-1, max>}}", $matches); + assertType("array{array{non-falsy-string|null, int<-1, max>}, array{'foo'|null, int<-1, max>}, array{'bar'|null, int<-1, max>}, array{'baz'|null, int<-1, max>}}", $matches); } - assertType("array{}|array{array{string|null, int<-1, max>}, array{'foo'|null, int<-1, max>}, array{'bar'|null, int<-1, max>}, array{'baz'|null, int<-1, max>}}", $matches); + assertType("array{}|array{array{non-falsy-string|null, int<-1, max>}, array{'foo'|null, int<-1, max>}, array{'bar'|null, int<-1, max>}, array{'baz'|null, int<-1, max>}}", $matches); } function doNonAutoCapturingModifier(string $s): void { if (preg_match('/(?n)(\d+)/', $s, $matches)) { // should be assertType('array{string}', $matches); - assertType('array{string, numeric-string}', $matches); + assertType('array{non-falsy-string, numeric-string}', $matches); } - assertType('array{}|array{string, numeric-string}', $matches); + assertType('array{}|array{non-falsy-string, numeric-string}', $matches); } From 4bb0da6a0c2678719ffe1b4f2d3726ebbe8d4c8d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 22 Mar 2025 12:42:39 +0100 Subject: [PATCH 11/17] fix --- tests/PHPStan/Analyser/nsrt/bug-11311.php | 52 +++++++++++------------ tests/PHPStan/Analyser/nsrt/bug-12211.php | 2 +- tests/PHPStan/Analyser/nsrt/bug11384.php | 2 +- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-11311.php b/tests/PHPStan/Analyser/nsrt/bug-11311.php index a30f261fae..ff99e4699c 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11311.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11311.php @@ -8,7 +8,7 @@ function doFoo(string $s) { if (1 === preg_match('/(?\d+)\.(?\d+)(?:\.(?\d+))?/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{0: string, major: numeric-string, 1: numeric-string, minor: numeric-string, 2: numeric-string, patch: numeric-string|null, 3: numeric-string|null}', $matches); + assertType('array{0: non-falsy-string, major: numeric-string, 1: numeric-string, minor: numeric-string, 2: numeric-string, patch: numeric-string|null, 3: numeric-string|null}', $matches); } } @@ -23,11 +23,11 @@ function doUnmatchedAsNull(string $s): void { 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, '£'|'€'|null}", $matches); + assertType("array{non-falsy-string, '£'|'€'|null}", $matches); } else { assertType('array{}', $matches); } - assertType("array{}|array{string, '£'|'€'|null}", $matches); + assertType("array{}|array{non-falsy-string, '£'|'€'|null}", $matches); } function bug11331a(string $url):void { @@ -37,7 +37,7 @@ function bug11331a(string $url):void { (?.+) )? (?.+)}mix', $url, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{0: string, a: non-empty-string|null, 1: non-empty-string|null, b: non-empty-string, 2: non-empty-string}', $matches); + assertType('array{0: non-empty-string, a: non-empty-string|null, 1: non-empty-string|null, b: non-empty-string, 2: non-empty-string}', $matches); } } @@ -63,20 +63,20 @@ function bug11331c(string $url):void { ([^/]+?) (?:\.git|/)? $}x', $url, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{string, non-empty-string|null, non-empty-string|null, non-empty-string, non-empty-string}', $matches); + assertType('array{non-falsy-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, '£'|null, '€'|null}", $matches); // could be tagged union + assertType("array{non-falsy-string, '£'|null, '€'|null}", $matches); // could be tagged union } } function doBar(string $s): void { if (preg_match('/Price: (?:(£)|(€))?\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType("array{string, '£'|null, '€'|null}", $matches); // could be tagged union + assertType("array{non-falsy-string, '£'|null, '€'|null}", $matches); // could be tagged union } } } @@ -85,101 +85,101 @@ 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, numeric-string, numeric-string|null}', $matches); + assertType('array{non-falsy-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-falsy-string, numeric-string, numeric-string, non-empty-string}', $matches); + assertType('array{non-falsy-string, non-falsy-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-falsy-string, numeric-string, non-falsy-string|null}', $matches); + assertType('array{non-falsy-string, non-falsy-string, numeric-string, non-falsy-string|null}', $matches); }; function (string $size): void { if (preg_match('/ab(\d+)e(\d?)/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType("array{string, numeric-string, ''|numeric-string}", $matches); + assertType("array{non-falsy-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: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-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-falsy-string&numeric-string}', $matches); + assertType('array{non-falsy-string, non-falsy-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-falsy-string}', $matches); + assertType('array{non-falsy-string, non-falsy-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); + assertType('array{non-falsy-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); + assertType('array{non-falsy-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); + assertType('array{non-falsy-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{string, non-empty-string|null}', $matches); + assertType('array{non-falsy-string, non-empty-string|null}', $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, numeric-string}', $matches); + assertType('array{non-falsy-string, numeric-string}', $matches); }; function (string $s): void { if (preg_match('/Price: ([2-5])/i', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{string, numeric-string}', $matches); + assertType('array{non-falsy-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); + assertType('array{non-falsy-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-falsy-string|null, 'b'|'d'|'E'|'e'|'F'|'f'|'G'|'g'|'H'|'h'|'o'|'s'|'u'|'X'|'x'}", $matches); + assertType("array{non-falsy-string, non-falsy-string|null, 'b'|'d'|'E'|'e'|'F'|'f'|'G'|'g'|'H'|'h'|'o'|'s'|'u'|'X'|'x'}", $matches); } }; @@ -201,22 +201,22 @@ function (string $s): void { function (string $s): void { if (preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType("array{string, numeric-string|null, non-empty-string|null}", $matches); + assertType("array{non-empty-string, numeric-string|null, non-empty-string|null}", $matches); } else { assertType("array{}", $matches); } - assertType("array{}|array{string, numeric-string|null, non-empty-string|null}", $matches); + assertType("array{}|array{non-empty-string, numeric-string|null, non-empty-string|null}", $matches); }; function (string $s): void { if (preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_UNMATCHED_AS_NULL|PREG_OFFSET_CAPTURE) === 1) { - assertType("array{array{string|null, int<-1, max>}, array{numeric-string|null, int<-1, max>}, array{non-empty-string|null, int<-1, max>}}", $matches); + assertType("array{array{non-empty-string|null, int<-1, max>}, array{numeric-string|null, int<-1, max>}, array{non-empty-string|null, int<-1, max>}}", $matches); } }; function (string $s): void { if (preg_match('~a|((u)x)|((v)y)~', $s, $matches, PREG_UNMATCHED_AS_NULL) === 1) { - assertType("array{string, 'ux'|null, 'u'|null, 'vy'|null, 'v'|null}", $matches); + assertType("array{non-empty-string, 'ux'|null, 'u'|null, 'vy'|null, 'v'|null}", $matches); } }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-12211.php b/tests/PHPStan/Analyser/nsrt/bug-12211.php index 72a268c506..33131edfe8 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12211.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12211.php @@ -10,7 +10,7 @@ function foo(string $text): void { assert(preg_match(REGEX, $text, $match) === 1); - assertType('array{string, non-falsy-string}', $match); + assertType('array{non-falsy-string, non-falsy-string}', $match); } diff --git a/tests/PHPStan/Analyser/nsrt/bug11384.php b/tests/PHPStan/Analyser/nsrt/bug11384.php index 12020de0b9..96284ef387 100644 --- a/tests/PHPStan/Analyser/nsrt/bug11384.php +++ b/tests/PHPStan/Analyser/nsrt/bug11384.php @@ -14,7 +14,7 @@ class HelloWorld public function sayHello(string $s): void { if (preg_match('{(' . Bar::VAL . ')}', $s, $m)) { - assertType("array{string, '3'}", $m); + assertType("array{non-falsy-string, '3'}", $m); } } } From f4cb3edac7b668766e84f881b29eacec1e1e60ba Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 22 Mar 2025 12:42:42 +0100 Subject: [PATCH 12/17] fix --- src/Type/Php/RegexArrayShapeMatcher.php | 2 +- src/Type/Regex/RegexAstWalkResult.php | 5 +++-- src/Type/Regex/RegexGroupParser.php | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 344c7b7bce..64c2f0c496 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -320,7 +320,7 @@ private function createSubjectValueType(Type $baseType, int $flags, bool $matche if ($this->containsPatternOrder($flags)) { $subjectValueType = TypeCombinator::intersect( new ArrayType(new IntegerType(), $subjectValueType), - new AccessoryArrayListType() + new AccessoryArrayListType(), ); } } diff --git a/src/Type/Regex/RegexAstWalkResult.php b/src/Type/Regex/RegexAstWalkResult.php index ba9e7a90e5..ff234b6092 100644 --- a/src/Type/Regex/RegexAstWalkResult.php +++ b/src/Type/Regex/RegexAstWalkResult.php @@ -20,7 +20,8 @@ public function __construct( private array $markVerbs, private Type $subjectBaseType, ) - {} + { + } public static function createEmpty(): self { @@ -30,7 +31,7 @@ public static function createEmpty(): self 100, [], [], - new StringType() + new StringType(), ); } diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 31bf730af9..61ab64e5f6 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -111,11 +111,11 @@ public function parseGroups(string $regex): ?RegexAstWalkResult ); if ($subjectAsGroupResult->isNonFalsy()->yes() || $subjectAsGroupResult->isNumeric()->yes()) { $astWalkResult = $astWalkResult->withSubjectBaseType( - TypeCombinator::intersect(new StringType(), new AccessoryNonFalsyStringType()) + TypeCombinator::intersect(new StringType(), new AccessoryNonFalsyStringType()), ); } elseif ($subjectAsGroupResult->isNonEmpty()->yes()) { $astWalkResult = $astWalkResult->withSubjectBaseType( - TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()) + TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()), ); } From 708198b8f6978244b43ceffd090fcf227ba103cf Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 22 Mar 2025 12:44:41 +0100 Subject: [PATCH 13/17] Update NonexistentOffsetInArrayDimFetchRuleTest.php --- .../Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 9a1f34bf73..ca7b7372a9 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -743,7 +743,7 @@ public function testBug11655(): void { $this->analyse([__DIR__ . '/data/bug-11655.php'], [ [ - "Offset 3 does not exist on array{string, 'x', array{string, 'x'}}.", + "Offset 3 does not exist on array{non-falsy-string, 'x', array{non-falsy-string, 'x'}}.", 15, ], ]); From df6fccf7a10b4bb79d4330cdef2e2d460541ac9b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 22 Mar 2025 15:22:28 +0100 Subject: [PATCH 14/17] fix numeric string handling --- src/Type/Regex/RegexGroupParser.php | 17 ++++++++++++----- tests/PHPStan/Analyser/nsrt/bug11384.php | 2 +- .../PHPStan/Analyser/nsrt/preg_match_shapes.php | 10 +++++----- .../Analyser/nsrt/preg_match_shapes_php80.php | 4 ++-- .../Analyser/nsrt/preg_match_shapes_php82.php | 4 ++-- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 61ab64e5f6..8c74a513d2 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -109,13 +109,20 @@ public function parseGroups(string $regex): ?RegexAstWalkResult $modifiers, RegexGroupWalkResult::createEmpty(), ); - if ($subjectAsGroupResult->isNonFalsy()->yes() || $subjectAsGroupResult->isNumeric()->yes()) { - $astWalkResult = $astWalkResult->withSubjectBaseType( - TypeCombinator::intersect(new StringType(), new AccessoryNonFalsyStringType()), - ); + + // we could handle numeric-string, in case we know the regex is delimited by ^ and $ + $accessories = []; + if ($subjectAsGroupResult->isNonFalsy()->yes()) { + $accessories[] = new AccessoryNonFalsyStringType(); } elseif ($subjectAsGroupResult->isNonEmpty()->yes()) { + $accessories[] = new AccessoryNonEmptyStringType(); + } + + if ($accessories !== []) { + $accessories[] = new StringType(); + $astWalkResult = $astWalkResult->withSubjectBaseType( - TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()), + TypeCombinator::intersect(...$accessories), ); } diff --git a/tests/PHPStan/Analyser/nsrt/bug11384.php b/tests/PHPStan/Analyser/nsrt/bug11384.php index 96284ef387..709f298635 100644 --- a/tests/PHPStan/Analyser/nsrt/bug11384.php +++ b/tests/PHPStan/Analyser/nsrt/bug11384.php @@ -14,7 +14,7 @@ class HelloWorld public function sayHello(string $s): void { if (preg_match('{(' . Bar::VAL . ')}', $s, $m)) { - assertType("array{non-falsy-string, '3'}", $m); + assertType("array{non-empty-string, '3'}", $m); } } } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 04520da85f..980535be7e 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -477,13 +477,13 @@ function bug11323(string $s): void { assertType('array{non-falsy-string, non-falsy-string}', $matches); } if (preg_match('{([[:digit:]])}', $s, $matches)) { - assertType('array{non-falsy-string, numeric-string}', $matches); + assertType('array{non-empty-string, numeric-string}', $matches); } if (preg_match('{([\d])(\d)}', $s, $matches)) { assertType('array{non-falsy-string, numeric-string, numeric-string}', $matches); } if (preg_match('{([0-9])}', $s, $matches)) { - assertType('array{non-falsy-string, numeric-string}', $matches); + assertType('array{non-empty-string, numeric-string}', $matches); } if (preg_match('{(\p{L})(\p{P})(\p{Po})}', $s, $matches)) { assertType('array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string}', $matches); @@ -654,7 +654,7 @@ function (string $s): void { function (string $s): void { if (preg_match('/( \d+ )/x', $s, $matches)) { - assertType('array{non-falsy-string, numeric-string}', $matches); + assertType('array{non-empty-string, numeric-string}', $matches); } }; @@ -792,7 +792,7 @@ function testUnescapeBackslash (string $string): void { if (preg_match(<<<'EOD' ~(\d)~ EOD, $string, $matches)) { - assertType("array{non-falsy-string, numeric-string}", $matches); + assertType("array{non-empty-string, numeric-string}", $matches); } if (preg_match(<<<'EOD' @@ -1000,7 +1000,7 @@ function bug12749e(string $str): void { // no ^ $ delims, therefore can be anything which contains a number if (preg_match('/[0-9]/', $str, $match)) { - assertType('array{non-falsy-string}', $match); + assertType('array{non-empty-string}', $match); } } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php index 280c1b62f9..4620565210 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php @@ -15,7 +15,7 @@ function doOffsetCaptureWithUnmatchedNull(string $s): void { function doNonAutoCapturingModifier(string $s): void { if (preg_match('/(?n)(\d+)/', $s, $matches)) { // should be assertType('array{string}', $matches); - assertType('array{non-falsy-string, numeric-string}', $matches); + assertType('array{non-empty-string, numeric-string}', $matches); } - assertType('array{}|array{non-falsy-string, numeric-string}', $matches); + assertType('array{}|array{non-empty-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 63cdb2869b..1b5dc597b7 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php @@ -8,9 +8,9 @@ // 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{non-falsy-string}', $matches); + assertType('array{non-empty-string}', $matches); } - assertType('array{}|array{non-falsy-string}', $matches); + assertType('array{}|array{non-empty-string}', $matches); if (preg_match('/(\d+)(?P\d+)/n', $s, $matches)) { assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); From 9036121c5fafbb01c89936c9c1b146c58febce7c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 22 Mar 2025 15:30:40 +0100 Subject: [PATCH 15/17] Update RegexGroupParser.php --- src/Type/Regex/RegexGroupParser.php | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 8c74a513d2..18fe8c91a7 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -111,18 +111,13 @@ public function parseGroups(string $regex): ?RegexAstWalkResult ); // we could handle numeric-string, in case we know the regex is delimited by ^ and $ - $accessories = []; if ($subjectAsGroupResult->isNonFalsy()->yes()) { - $accessories[] = new AccessoryNonFalsyStringType(); + $astWalkResult = $astWalkResult->withSubjectBaseType( + TypeCombinator::intersect(new StringType(), new AccessoryNonFalsyStringType()), + ); } elseif ($subjectAsGroupResult->isNonEmpty()->yes()) { - $accessories[] = new AccessoryNonEmptyStringType(); - } - - if ($accessories !== []) { - $accessories[] = new StringType(); - $astWalkResult = $astWalkResult->withSubjectBaseType( - TypeCombinator::intersect(...$accessories), + TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()), ); } From caec1cec8d66b3c5180578472c044b984f6f47fd Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 23 Mar 2025 08:15:56 +0100 Subject: [PATCH 16/17] fix --- src/Type/Regex/RegexGroupParser.php | 20 ++++++++++--------- src/Type/Regex/RegexGroupWalkResult.php | 14 +++++++++++++ .../Analyser/nsrt/preg_match_shapes.php | 14 ++++++------- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 18fe8c91a7..8b0c23564f 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -110,15 +110,17 @@ public function parseGroups(string $regex): ?RegexAstWalkResult RegexGroupWalkResult::createEmpty(), ); - // we could handle numeric-string, in case we know the regex is delimited by ^ and $ - if ($subjectAsGroupResult->isNonFalsy()->yes()) { - $astWalkResult = $astWalkResult->withSubjectBaseType( - TypeCombinator::intersect(new StringType(), new AccessoryNonFalsyStringType()), - ); - } elseif ($subjectAsGroupResult->isNonEmpty()->yes()) { - $astWalkResult = $astWalkResult->withSubjectBaseType( - TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()), - ); + if (!$subjectAsGroupResult->containsEmptyStringLiteral()) { + // we could handle numeric-string, in case we know the regex is delimited by ^ and $ + if ($subjectAsGroupResult->isNonFalsy()->yes()) { + $astWalkResult = $astWalkResult->withSubjectBaseType( + TypeCombinator::intersect(new StringType(), new AccessoryNonFalsyStringType()), + ); + } elseif ($subjectAsGroupResult->isNonEmpty()->yes()) { + $astWalkResult = $astWalkResult->withSubjectBaseType( + TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()), + ); + } } return $astWalkResult; diff --git a/src/Type/Regex/RegexGroupWalkResult.php b/src/Type/Regex/RegexGroupWalkResult.php index 65e7fd1691..6459728a73 100644 --- a/src/Type/Regex/RegexGroupWalkResult.php +++ b/src/Type/Regex/RegexGroupWalkResult.php @@ -103,6 +103,20 @@ public function getOnlyLiterals(): ?array return $this->onlyLiterals; } + public function containsEmptyStringLiteral(): bool + { + if ($this->onlyLiterals === null) { + return false; + } + foreach ($this->onlyLiterals as $onlyLiteral) { + if ($onlyLiteral === '') { + return true; + } + } + + return false; + } + public function isNonEmpty(): TrinaryLogic { return $this->isNonEmpty; diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 980535be7e..88bbe9fad6 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -915,31 +915,31 @@ function bugEmptySubexpression (string $string): void { } if (preg_match('~|(a)~', $string, $matches)) { - assertType("array{0: non-empty-string, 1?: 'a'}", $matches); + assertType("array{0: string, 1?: 'a'}", $matches); } if (preg_match('~(a)|~', $string, $matches)) { - assertType("array{0: non-empty-string, 1?: 'a'}", $matches); + assertType("array{0: string, 1?: 'a'}", $matches); } if (preg_match('~(a)||(b)~', $string, $matches)) { - assertType("array{0: non-empty-string, 1?: 'a'}|array{non-empty-string, '', 'b'}", $matches); + assertType("array{0: string, 1?: 'a'}|array{string, '', 'b'}", $matches); } if (preg_match('~(|(a))~', $string, $matches)) { - assertType("array{0: non-empty-string, 1: ''|'a', 2?: 'a'}", $matches); + assertType("array{0: string, 1: ''|'a', 2?: 'a'}", $matches); } if (preg_match('~((a)|)~', $string, $matches)) { - assertType("array{0: non-empty-string, 1: ''|'a', 2?: 'a'}", $matches); + assertType("array{0: string, 1: ''|'a', 2?: 'a'}", $matches); } if (preg_match('~((a)||(b))~', $string, $matches)) { - assertType("array{0: non-empty-string, 1: ''|'a'|'b', 2?: ''|'a', 3?: 'b'}", $matches); + assertType("array{0: string, 1: ''|'a'|'b', 2?: ''|'a', 3?: 'b'}", $matches); } if (preg_match('~((a)|()|(b))~', $string, $matches)) { - assertType("array{0: non-empty-string, 1: ''|'a'|'b', 2?: ''|'a', 3?: '', 4?: 'b'}", $matches); + assertType("array{0: string, 1: ''|'a'|'b', 2?: ''|'a', 3?: '', 4?: 'b'}", $matches); } } From 9040aaf61d16be2c37114e610f4e92896d4a2cff Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 23 Mar 2025 08:20:28 +0100 Subject: [PATCH 17/17] better method name --- src/Type/Regex/RegexGroupParser.php | 2 +- src/Type/Regex/RegexGroupWalkResult.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 8b0c23564f..0383ea4c5a 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -110,7 +110,7 @@ public function parseGroups(string $regex): ?RegexAstWalkResult RegexGroupWalkResult::createEmpty(), ); - if (!$subjectAsGroupResult->containsEmptyStringLiteral()) { + if (!$subjectAsGroupResult->mightContainEmptyStringLiteral()) { // we could handle numeric-string, in case we know the regex is delimited by ^ and $ if ($subjectAsGroupResult->isNonFalsy()->yes()) { $astWalkResult = $astWalkResult->withSubjectBaseType( diff --git a/src/Type/Regex/RegexGroupWalkResult.php b/src/Type/Regex/RegexGroupWalkResult.php index 6459728a73..9169af89ba 100644 --- a/src/Type/Regex/RegexGroupWalkResult.php +++ b/src/Type/Regex/RegexGroupWalkResult.php @@ -103,7 +103,7 @@ public function getOnlyLiterals(): ?array return $this->onlyLiterals; } - public function containsEmptyStringLiteral(): bool + public function mightContainEmptyStringLiteral(): bool { if ($this->onlyLiterals === null) { return false;