diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 64c2f0c496..a23e3445d8 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -8,7 +8,6 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; @@ -62,7 +61,7 @@ public function matchExpr(Expr $patternExpr, ?Type $flagsType, TrinaryLogic $was private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLogic $wasMatched, bool $matchesAll): ?Type { if ($wasMatched->no()) { - return new ConstantArrayType([], []); + return ConstantArrayTypeBuilder::createEmpty()->getArray(); } $constantStrings = $patternType->getConstantStrings(); @@ -146,8 +145,11 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched if (!$this->containsUnmatchedAsNull($flags, $matchesAll)) { // positive match has a subject but not any capturing group + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantIntegerType(0), $this->createSubjectValueType($subjectBaseType, $flags, $matchesAll)); + $combiType = TypeCombinator::union( - new ConstantArrayType([new ConstantIntegerType(0)], [$this->createSubjectValueType($subjectBaseType, $flags, $matchesAll)], [1], [], TrinaryLogic::createYes()), + $builder->getArray(), $combiType, ); } @@ -206,7 +208,10 @@ 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($subjectBaseType, $flags, $matchesAll)], [1], [], TrinaryLogic::createYes()); + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantIntegerType(0), $this->createSubjectValueType($subjectBaseType, $flags, $matchesAll)); + + $combiTypes[] = $builder->getArray(); } return TypeCombinator::union(...$combiTypes); @@ -238,6 +243,7 @@ private function buildArrayType( bool $matchesAll, ): Type { + $forceList = count($markVerbs) === 0; $builder = ConstantArrayTypeBuilder::createEmpty(); // first item in matches contains the overall match. @@ -256,6 +262,8 @@ private function buildArrayType( $optional = $this->isGroupOptional($captureGroup, $wasMatched, $flags, $isTrailingOptional, $matchesAll); if ($captureGroup->isNamed()) { + $forceList = false; + $builder->setOffsetValueType( $this->getKeyType($captureGroup->getName()), $groupValueType, @@ -288,13 +296,17 @@ private function buildArrayType( $arrayType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $builder->getArray()), new AccessoryArrayListType()); if (!$wasMatched->yes()) { $arrayType = TypeCombinator::union( - new ConstantArrayType([], []), + ConstantArrayTypeBuilder::createEmpty()->getArray(), $arrayType, ); } return $arrayType; } + if ($forceList) { + return TypeCombinator::intersect($builder->getArray(), new AccessoryArrayListType()); + } + return $builder->getArray(); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11311.php b/tests/PHPStan/Analyser/nsrt/bug-11311.php index ff99e4699c..96b810431d 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11311.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11311.php @@ -191,12 +191,12 @@ function (string $s): void { function (string $s): void { preg_match('/%a(\d*)/', $s, $matches, PREG_UNMATCHED_AS_NULL); - assertType("array{0?: string, 1?: ''|numeric-string|null}", $matches); // could be array{0?: string, 1?: ''|numeric-string} + assertType("list{0?: string, 1?: ''|numeric-string|null}", $matches); // could be array{0?: string, 1?: ''|numeric-string} }; function (string $s): void { preg_match('/%a(\d*)?/', $s, $matches, PREG_UNMATCHED_AS_NULL); - assertType("array{0?: string, 1?: ''|numeric-string|null}", $matches); // could be array{0?: string, 1?: ''|numeric-string} + assertType("list{0?: string, 1?: ''|numeric-string|null}", $matches); // could be array{0?: string, 1?: ''|numeric-string} }; function (string $s): void { @@ -222,5 +222,5 @@ function (string $s): void { function (string $s): void { preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_UNMATCHED_AS_NULL); - assertType("array{0?: string, 1?: numeric-string|null, 2?: non-empty-string|null}", $matches); + assertType("list{0?: string, 1?: numeric-string|null, 2?: non-empty-string|null}", $matches); }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-11580.php b/tests/PHPStan/Analyser/nsrt/bug-11580.php index 2081bb0624..039a1895f5 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11580.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11580.php @@ -26,7 +26,7 @@ public function bad2(string $in): void 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); + assertType('list{0?: string, 1?: non-empty-string, 2?: non-empty-string}', $matches); if ($result) { assertType('array{0: non-falsy-string, 1: non-empty-string, 2?: non-empty-string}', $matches); } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 88bbe9fad6..5e87970c8e 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -300,7 +300,7 @@ 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: non-falsy-string, 1?: ''|'b', 2?: 'c'}", $matches); + assertType("list{0: non-falsy-string, 1?: ''|'b', 2?: 'c'}", $matches); }; function (string $size): void { @@ -525,7 +525,7 @@ function bug11323(string $s): void { function (string $s): void { preg_match('/%a(\d*)/', $s, $matches); - assertType("array{0?: string, 1?: ''|numeric-string}", $matches); + assertType("list{0?: string, 1?: ''|numeric-string}", $matches); }; class Bug11376 @@ -533,7 +533,7 @@ class Bug11376 public function test(string $str): void { preg_match('~^(?:(\w+)::)?(\w+)$~', $str, $matches); - assertType('array{0?: string, 1?: string, 2?: non-empty-string}', $matches); + assertType('list{0?: string, 1?: string, 2?: non-empty-string}', $matches); } public function test2(string $str): void @@ -564,7 +564,7 @@ function (string $s): void { } if (preg_match($p, $s, $matches)) { - assertType("array{0: non-falsy-string, 1: 'x'|'£'|numeric-string, 2?: ''|numeric-string, 3?: 'x'}", $matches); + assertType("list{0: non-falsy-string, 1: 'x'|'£'|numeric-string, 2?: ''|numeric-string, 3?: 'x'}", $matches); } }; @@ -730,7 +730,7 @@ function (string $s): void { function (string $s): void { preg_match('~a|(\d)|(\s)~', $s, $matches); - assertType("array{0?: string, 1?: '', 2?: non-empty-string}|array{0?: string, 1?: numeric-string}", $matches); + assertType("list{0?: string, 1?: '', 2?: non-empty-string}|list{0?: string, 1?: numeric-string}", $matches); }; function bug11490 (string $expression): void { @@ -762,13 +762,13 @@ function bug11604 (string $string): void { return; } - assertType("array{0: non-empty-string, 1?: ''|'XX', 2?: 'YY'}", $matches); + assertType("list{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: non-empty-string, 1?: ''|'XX', 2?: ''|'YY', 3?: 'ZZ'}", $matches); + assertType("list{0: non-empty-string, 1?: ''|'XX', 2?: ''|'YY', 3?: 'ZZ'}", $matches); } } @@ -935,11 +935,11 @@ function bugEmptySubexpression (string $string): void { } if (preg_match('~((a)||(b))~', $string, $matches)) { - assertType("array{0: string, 1: ''|'a'|'b', 2?: ''|'a', 3?: 'b'}", $matches); + assertType("list{0: 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("list{0: string, 1: ''|'a'|'b', 2?: ''|'a', 3?: '', 4?: 'b'}", $matches); } } @@ -1010,3 +1010,8 @@ function bug12749f(string $str): void assertType('array{non-empty-string}', $match); // could be numeric-string } } + +function bug12397(string $string) : array { + $m = preg_match('#\b([A-Z]{2,})-(\d+)#', $string, $match); + assertType('list{0?: string, 1?: non-falsy-string, 2?: numeric-string}', $match); +} diff --git a/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php index 57c486638e..c6ba4824c2 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php @@ -22,7 +22,7 @@ function (string $s): void { preg_replace_callback( '/(foo)?(bar)?(baz)?/', function ($matches) { - assertType("array{0: array{string, int<-1, max>}, 1?: array{''|'foo', int<-1, max>}, 2?: array{''|'bar', int<-1, max>}, 3?: array{'baz', int<-1, max>}}", $matches); + assertType("list{0: array{string, int<-1, max>}, 1?: array{''|'foo', int<-1, max>}, 2?: array{''|'bar', int<-1, max>}, 3?: array{'baz', int<-1, max>}}", $matches); return ''; }, $s, diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index ca7b7372a9..58806979a6 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -900,4 +900,11 @@ public function testNarrowSuperglobals(): void $this->analyse([__DIR__ . '/data/narrow-superglobal.php'], []); } + public function testBug11602(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-11602.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-11602.php b/tests/PHPStan/Rules/Arrays/data/bug-11602.php new file mode 100644 index 0000000000..4e1252e5b4 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-11602.php @@ -0,0 +1,23 @@ +