Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions src/Type/Php/RegexArrayShapeMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -347,10 +347,10 @@ private function isSubjectOptional(TrinaryLogic $wasMatched, bool $matchesAll):
*/
private function createSubjectValueType(Type $baseType, int $flags, bool $matchesAll, TrinaryLogic $wasMatched): Type
{
$subjectValueType = TypeCombinator::removeNull($this->getValueType($baseType, $flags, $matchesAll));
$subjectValueType = TypeCombinator::removeNull($this->getValueType($baseType, $flags, $matchesAll, false));

if ($matchesAll) {
$subjectValueType = TypeCombinator::removeNull($this->getValueType(new StringType(), $flags, $matchesAll));
$subjectValueType = TypeCombinator::removeNull($this->getValueType(new StringType(), $flags, $matchesAll, false));

if ($this->containsPatternOrder($flags)) {
$accessoryTypes = [
Expand Down Expand Up @@ -395,6 +395,8 @@ private function isGroupOptional(RegexCapturingGroup $captureGroup, TrinaryLogic

private function createGroupValueType(RegexCapturingGroup $captureGroup, TrinaryLogic $wasMatched, int $flags, bool $isTrailingOptional, bool $isLastGroup, bool $matchesAll): Type
{
$canBeUnmatched = $captureGroup->isOptional() || $captureGroup->isForcedUnmatched();

if ($matchesAll) {
if (
(
Expand All @@ -414,10 +416,11 @@ private function createGroupValueType(RegexCapturingGroup $captureGroup, Trinary
TypeCombinator::union($captureGroup->getType(), new ConstantStringType('')),
$flags,
$matchesAll,
$canBeUnmatched,
);
$groupValueType = TypeCombinator::removeNull($groupValueType);
} else {
$groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll);
$groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll, $canBeUnmatched);
}

if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll) && !$captureGroup->isOptional()) {
Expand All @@ -441,9 +444,10 @@ private function createGroupValueType(RegexCapturingGroup $captureGroup, Trinary
TypeCombinator::union($captureGroup->getType(), new ConstantStringType('')),
$flags,
$matchesAll,
$canBeUnmatched,
);
} else {
$groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll);
$groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll, $canBeUnmatched);
}

if ($wasMatched->yes()) {
Expand Down Expand Up @@ -491,12 +495,11 @@ private function getKeyType(int|string $key): Type
return new ConstantIntegerType($key);
}

private function getValueType(Type $baseType, int $flags, bool $matchesAll): Type
private function getValueType(Type $baseType, int $flags, bool $matchesAll, bool $canBeUnmatched): Type
{
$valueType = $baseType;

// unmatched groups return -1 as offset
$offsetType = IntegerRangeType::fromInterval(-1, null);
$offsetType = IntegerRangeType::fromInterval($canBeUnmatched ? -1 : 0, null);
if ($this->containsUnmatchedAsNull($flags, $matchesAll)) {
$valueType = TypeCombinator::addNull($valueType);
}
Expand Down
9 changes: 7 additions & 2 deletions src/Type/Regex/RegexCapturingGroup.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ public function forceType(Type $type): self
$this->alternation,
$this->inOptionalQuantification,
$this->parent,
$type,
$this->type,
$this->forceNonOptional,
$this->forceType,
$type,
);
}

Expand Down Expand Up @@ -106,6 +106,11 @@ public function isOptional(): bool
|| $this->parent !== null && $this->parent->isOptional();
}

public function isForcedUnmatched(): bool
{
return $this->forceType !== null;
}

public function inOptionalQuantification(): bool
{
return $this->inOptionalQuantification;
Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-11311.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ function (string $s): void {

function (string $s): void {
if (preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_UNMATCHED_AS_NULL|PREG_OFFSET_CAPTURE) === 1) {
assertType("array{array{non-empty-string|null, int<-1, max>}, array{numeric-string|null, int<-1, max>}, array{non-empty-string|null, int<-1, max>}}", $matches);
assertType("array{array{non-empty-string|null, int<0, max>}, array{numeric-string|null, int<-1, max>}, array{non-empty-string|null, int<-1, max>}}", $matches);
}
};

Expand Down
11 changes: 11 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14959.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Bug14959;

use function PHPStan\Testing\assertType;

if (1 === preg_match('/^<(\w+)([^>]+?)?/', '<div>hello world</div>', $matches, PREG_OFFSET_CAPTURE)) {
assertType('int<0, max>', $matches[0][1]);
assertType('int<0, max>', $matches[1][1]);
assertType('int<-1, max>|null', $matches[2][1] ?? null);
}
14 changes: 7 additions & 7 deletions tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,25 +112,25 @@ function (string $size): void {

function (string $size): void {
if (preg_match_all('/ab(?P<num>\d+)(?P<suffix>ab)?/', $size, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) {
assertType("non-empty-list<array{0: array{string, int<-1, max>}, 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("non-empty-list<array{0: array{string, int<0, max>}, num: array{numeric-string, int<0, max>}, 1: array{numeric-string, int<0, max>}, suffix?: array{'ab', int<-1, max>}, 2?: array{'ab', int<-1, max>}}>", $matches);
}
};

function (string $size): void {
if (preg_match_all('/ab(?P<num>\d+)(?P<suffix>ab)?/', $size, $matches, PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE)) {
assertType("array{0: non-empty-list<array{string, int<-1, max>}>, num: non-empty-list<array{numeric-string, int<-1, max>}>, 1: non-empty-list<array{numeric-string, int<-1, max>}>, suffix: non-empty-list<array{''|'ab', int<-1, max>}>, 2: non-empty-list<array{''|'ab', int<-1, max>}>}", $matches);
assertType("array{0: non-empty-list<array{string, int<0, max>}>, num: non-empty-list<array{numeric-string, int<0, max>}>, 1: non-empty-list<array{numeric-string, int<0, max>}>, suffix: non-empty-list<array{''|'ab', int<-1, max>}>, 2: non-empty-list<array{''|'ab', int<-1, max>}>}", $matches);
}
};

function (string $size): void {
if (preg_match_all('/ab(?P<num>\d+)(?P<suffix>ab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) {
assertType("non-empty-list<array{0: array{string|null, int<-1, max>}, 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("non-empty-list<array{0: array{string|null, int<0, max>}, num: array{numeric-string|null, int<0, max>}, 1: array{numeric-string|null, int<0, max>}, suffix: array{'ab'|null, int<-1, max>}, 2: array{'ab'|null, int<-1, max>}}>", $matches);
}
};

function (string $size): void {
if (preg_match_all('/ab(?P<num>\d+)(?P<suffix>ab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE)) {
assertType("array{0: non-empty-list<array{string|null, int<-1, max>}>, num: non-empty-list<array{numeric-string|null, int<-1, max>}>, 1: non-empty-list<array{numeric-string|null, int<-1, max>}>, suffix: non-empty-list<array{'ab'|null, int<-1, max>}>, 2: non-empty-list<array{'ab'|null, int<-1, max>}>}", $matches);
assertType("array{0: non-empty-list<array{string|null, int<0, max>}>, num: non-empty-list<array{numeric-string|null, int<0, max>}>, 1: non-empty-list<array{numeric-string|null, int<0, max>}>, suffix: non-empty-list<array{'ab'|null, int<-1, max>}>, 2: non-empty-list<array{'ab'|null, int<-1, max>}>}", $matches);
}
};

Expand All @@ -142,7 +142,7 @@ public function sayHello(string $content): void
return;
}

assertType('array{list<array{string, int<-1, max>}>}', $matches);
assertType('array{list<array{string, int<0, max>}>}', $matches);
}

public function sayFoo(string $content): void
Expand All @@ -165,13 +165,13 @@ public function sayBar(string $content): void

function doFoobar(string $s): void {
if (preg_match_all('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_OFFSET_CAPTURE)) {
assertType("array{non-empty-list<array{string, int<-1, max>}>, non-empty-list<array{''|'foo', int<-1, max>}>, non-empty-list<array{''|'bar', int<-1, max>}>, non-empty-list<array{''|'baz', int<-1, max>}>}", $matches);
assertType("array{non-empty-list<array{string, int<0, max>}>, non-empty-list<array{''|'foo', int<-1, max>}>, non-empty-list<array{''|'bar', int<-1, max>}>, non-empty-list<array{''|'baz', int<-1, max>}>}", $matches);
}
}

function doFoobarNull(string $s): void {
if (preg_match_all('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_OFFSET_CAPTURE|PREG_UNMATCHED_AS_NULL)) {
assertType("array{non-empty-list<array{string|null, int<-1, max>}>, non-empty-list<array{'foo'|null, int<-1, max>}>, non-empty-list<array{'bar'|null, int<-1, max>}>, non-empty-list<array{'baz'|null, int<-1, max>}>}", $matches);
assertType("array{non-empty-list<array{string|null, int<0, max>}>, non-empty-list<array{'foo'|null, int<-1, max>}>, non-empty-list<array{'bar'|null, int<-1, max>}>, non-empty-list<array{'baz'|null, int<-1, max>}>}", $matches);
}
}
}
Expand Down
10 changes: 5 additions & 5 deletions tests/PHPStan/Analyser/nsrt/preg_match_shapes.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,9 @@ function doNamedSubpattern(string $s): void {

function doOffsetCapture(string $s): void {
if (preg_match('/(foo)(bar)(baz)/', $s, $matches, PREG_OFFSET_CAPTURE)) {
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{non-falsy-string, int<0, max>}, array{'foo', int<0, max>}, array{'bar', int<0, max>}, array{'baz', int<0, 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);
assertType("array{}|array{array{non-falsy-string, int<0, max>}, array{'foo', int<0, max>}, array{'bar', int<0, max>}, array{'baz', int<0, max>}}", $matches);
}

function doUnknownFlags(string $s, int $flags): void {
Expand Down Expand Up @@ -679,13 +679,13 @@ function (string $s): void {
function (string $value): void
{
if (preg_match('/^(x)*$/', $value, $matches, PREG_OFFSET_CAPTURE)) {
assertType("array{0: array{string, int<-1, max>}, 1?: array{non-empty-string, int<-1, max>}}", $matches);
assertType("array{0: array{string, int<0, max>}, 1?: array{non-empty-string, int<0, max>}}", $matches);
}
};

function (string $value): void {
if (preg_match('/^(?:(x)|(y))*$/', $value, $matches, PREG_OFFSET_CAPTURE)) {
assertType("array{0: array{string, int<-1, max>}, 1?: array{non-empty-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{string, int<0, max>}, 1?: array{non-empty-string, int<0, max>}}|array{array{string, int<0, max>}, array{'', int<-1, max>}, array{non-empty-string, int<0, max>}}", $matches);
}
};

Expand Down Expand Up @@ -724,7 +724,7 @@ function (string $s): void {

function (string $s): void {
if (preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_OFFSET_CAPTURE) === 1) {
assertType("array{0: array{non-empty-string, int<-1, max>}, 1?: array{numeric-string, int<-1, max>}}|array{array{non-empty-string, int<-1, max>}, array{'', int<-1, max>}, array{non-empty-string, int<-1, max>}}", $matches);
assertType("array{0: array{non-empty-string, int<0, max>}, 1?: array{numeric-string, int<0, max>}}|array{array{non-empty-string, int<0, max>}, array{'', int<-1, max>}, array{non-empty-string, int<0, max>}}", $matches);
}
};

Expand Down
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
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{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{non-falsy-string|null, int<0, max>}, array{'foo'|null, int<0, max>}, array{'bar'|null, int<0, max>}, array{'baz'|null, int<0, 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);
assertType("array{}|array{array{non-falsy-string|null, int<0, max>}, array{'foo'|null, int<0, max>}, array{'bar'|null, int<0, max>}, array{'baz'|null, int<0, max>}}", $matches);
}

function doNonAutoCapturingModifier(string $s): void {
Expand Down
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function (string $s): void {
preg_replace_callback(
'/(foo)?(bar)?(baz)?/',
function ($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);
assertType("list{0: array{string, int<0, max>}, 1?: array{''|'foo', int<-1, max>}, 2?: array{''|'bar', int<-1, max>}, 3?: array{'baz', int<-1, max>}}", $matches);
return '';
},
$s,
Expand All @@ -37,7 +37,7 @@ function (string $s): void {
preg_replace_callback(
'/(foo)?(bar)?(baz)?/',
function ($matches) {
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{string|null, int<0, max>}, array{'foo'|null, int<-1, max>}, array{'bar'|null, int<-1, max>}, array{'baz'|null, int<-1, max>}}", $matches);
return '';
},
$s,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2934,11 +2934,11 @@ public function testConstantParameterCheck(): void
110,
],
[
'Parameter #2 $callback of function preg_replace_callback expects callable(array<string>): string, Closure(mixed): array{non-falsy-string, int<-1, max>} given.',
'Parameter #2 $callback of function preg_replace_callback expects callable(array<string>): string, Closure(mixed): array{non-falsy-string, int<0, max>} given.',
113,
],
[
'Parameter #2 $callback of function preg_replace_callback expects callable(array<string>): string, Closure(mixed): array{non-falsy-string|null, int<-1, max>} given.',
'Parameter #2 $callback of function preg_replace_callback expects callable(array<string>): string, Closure(mixed): array{non-falsy-string|null, int<0, max>} given.',
116,
],
[
Expand Down