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
42 changes: 26 additions & 16 deletions src/Type/Regex/RegexGroupParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
use PHPStan\Php\PhpVersion;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType;
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
use PHPStan\Type\Accessory\AccessoryNumericStringType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\StringType;
Expand Down Expand Up @@ -420,16 +420,16 @@ private function createGroupType(TreeNode $group, bool $maybeConstant, string $p
return TypeCombinator::union(...$result);
}

if ($walkResult->isNumeric()->yes()) {
if ($walkResult->isDecimalInteger()->yes()) {
if ($walkResult->isNonFalsy()->yes()) {
return new IntersectionType([
new StringType(),
new AccessoryNumericStringType(),
new AccessoryDecimalIntegerStringType(),
new AccessoryNonFalsyStringType(),
]);
}

$result = new IntersectionType([new StringType(), new AccessoryNumericStringType()]);
$result = new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType()]);
if (!$walkResult->isNonEmpty()->yes()) {
return new UnionType([new ConstantStringType(''), $result]);
}
Expand Down Expand Up @@ -541,10 +541,19 @@ private function walkGroupAst(
$walkResult = $walkResult->onlyLiterals($onlyLiterals);

if ($literalValue !== null) {
if (Strings::match($literalValue, '/^\d+$/') === null) {
$walkResult = $walkResult->numeric(TrinaryLogic::createNo());
} elseif ($walkResult->isNumeric()->maybe()) {
$walkResult = $walkResult->numeric(TrinaryLogic::createYes());
if (Strings::match($literalValue, '/^\d+$/') !== null) {
if ($walkResult->isDecimalInteger()->maybe()) {
$walkResult = $walkResult->decimalInteger(TrinaryLogic::createYes());
}
} elseif (
$literalValue === '-'
&& $walkResult->isDecimalInteger()->maybe()
&& !$walkResult->hasSeenDecimalIntegerSign()
) {
// a single leading minus sign keeps the string a decimal integer (e.g. "-1")
$walkResult = $walkResult->seenDecimalIntegerSign(true);
} else {
$walkResult = $walkResult->decimalInteger(TrinaryLogic::createNo());
}

if (!$walkResult->isInOptionalQuantification() && $literalValue !== '') {
Expand All @@ -563,7 +572,7 @@ private function walkGroupAst(
$newLiterals = [];
$nonEmpty = TrinaryLogic::createYes();
$nonFalsy = TrinaryLogic::createYes();
$numeric = TrinaryLogic::createYes();
$decimalInteger = TrinaryLogic::createYes();
foreach ($children as $child) {
$childResult = $this->walkGroupAst(
$child,
Expand All @@ -572,12 +581,13 @@ private function walkGroupAst(
$walkResult->onlyLiterals([])
->nonEmpty(TrinaryLogic::createMaybe())
->nonFalsy(TrinaryLogic::createMaybe())
->numeric(TrinaryLogic::createMaybe()),
->decimalInteger(TrinaryLogic::createMaybe())
->seenDecimalIntegerSign(false),
);

$nonEmpty = $nonEmpty->and($childResult->isNonEmpty());
$nonFalsy = $nonFalsy->and($childResult->isNonFalsy());
$numeric = $numeric->and($childResult->isNumeric());
$decimalInteger = $decimalInteger->and($childResult->isDecimalInteger());

if ($newLiterals === null) {
continue;
Expand All @@ -596,14 +606,14 @@ private function walkGroupAst(
->onlyLiterals($newLiterals)
->nonEmpty($walkResult->isNonEmpty()->or($nonEmpty))
->nonFalsy($walkResult->isNonFalsy()->or($nonFalsy))
->numeric($walkResult->isNumeric()->and($numeric));
->decimalInteger($walkResult->isDecimalInteger()->and($decimalInteger));
}

// [^0-9] should not parse as numeric-string, and [^list-everything-but-numbers] is technically
// doable but really silly compared to just \d so we can safely assume the string is not numeric
// for negative classes
// [^0-9] should not parse as decimal-int-string, and [^list-everything-but-numbers] is technically
// doable but really silly compared to just \d so we can safely assume the string is not a decimal
// integer for negative classes
if ($ast->getId() === '#negativeclass') {
$walkResult = $walkResult->numeric(TrinaryLogic::createNo());
$walkResult = $walkResult->decimalInteger(TrinaryLogic::createNo());
}

foreach ($children as $child) {
Expand Down
43 changes: 34 additions & 9 deletions src/Type/Regex/RegexGroupWalkResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ public function __construct(
private ?array $onlyLiterals,
private TrinaryLogic $isNonEmpty,
private TrinaryLogic $isNonFalsy,
private TrinaryLogic $isNumeric,
private TrinaryLogic $isDecimalInteger,
private bool $seenDecimalIntegerSign,
)
{
}
Expand All @@ -29,6 +30,7 @@ public static function createEmpty(): self
TrinaryLogic::createMaybe(),
TrinaryLogic::createMaybe(),
TrinaryLogic::createMaybe(),
false,
);
}

Expand All @@ -39,7 +41,8 @@ public function inOptionalQuantification(bool $inOptionalQuantification): self
$this->onlyLiterals,
$this->isNonEmpty,
$this->isNonFalsy,
$this->isNumeric,
$this->isDecimalInteger,
$this->seenDecimalIntegerSign,
);
}

Expand All @@ -53,7 +56,8 @@ public function onlyLiterals(?array $onlyLiterals): self
$onlyLiterals,
$this->isNonEmpty,
$this->isNonFalsy,
$this->isNumeric,
$this->isDecimalInteger,
$this->seenDecimalIntegerSign,
);
}

Expand All @@ -64,7 +68,8 @@ public function nonEmpty(TrinaryLogic $nonEmpty): self
$this->onlyLiterals,
$nonEmpty,
$this->isNonFalsy,
$this->isNumeric,
$this->isDecimalInteger,
$this->seenDecimalIntegerSign,
);
}

Expand All @@ -75,18 +80,33 @@ public function nonFalsy(TrinaryLogic $nonFalsy): self
$this->onlyLiterals,
$this->isNonEmpty,
$nonFalsy,
$this->isNumeric,
$this->isDecimalInteger,
$this->seenDecimalIntegerSign,
);
}

public function numeric(TrinaryLogic $numeric): self
/** A decimal integer string is composed only of digits, optionally preceded by a single leading minus sign. */
public function decimalInteger(TrinaryLogic $decimalInteger): self
{
return new self(
$this->inOptionalQuantification,
$this->onlyLiterals,
$this->isNonEmpty,
$this->isNonFalsy,
$numeric,
$decimalInteger,
$this->seenDecimalIntegerSign,
);
}

public function seenDecimalIntegerSign(bool $seenDecimalIntegerSign): self
{
return new self(
$this->inOptionalQuantification,
$this->onlyLiterals,
$this->isNonEmpty,
$this->isNonFalsy,
$this->isDecimalInteger,
$seenDecimalIntegerSign,
);
}

Expand Down Expand Up @@ -127,9 +147,14 @@ public function isNonFalsy(): TrinaryLogic
return $this->isNonFalsy;
}

public function isNumeric(): TrinaryLogic
public function isDecimalInteger(): TrinaryLogic
{
return $this->isDecimalInteger;
}

public function hasSeenDecimalIntegerSign(): bool
{
return $this->isNumeric;
return $this->seenDecimalIntegerSign;
}

}
18 changes: 9 additions & 9 deletions tests/PHPStan/Analyser/nsrt/bug-11293.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,54 +9,54 @@ class HelloWorld
public function sayHello(string $s): void
{
if (preg_match('/data-(\d{6})\.json$/', $s, $matches) > 0) {
assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches);
assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches);
}
}

public function sayHello2(string $s): void
{
if (preg_match('/data-(\d{6})\.json$/', $s, $matches) === 1) {
assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches);
assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches);
}
}

public function sayHello3(string $s): void
{
if (preg_match('/data-(\d{6})\.json$/', $s, $matches) >= 1) {
assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches);
assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches);
}
}

public function sayHello4(string $s): void
{
if (preg_match('/data-(\d{6})\.json$/', $s, $matches) <= 0) {
assertType('array{}|array{non-falsy-string, non-falsy-string&numeric-string}', $matches);
assertType('array{}|array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches);

return;
}

assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches);
assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches);
}

public function sayHello5(string $s): void
{
if (preg_match('/data-(\d{6})\.json$/', $s, $matches) < 1) {
assertType('array{}|array{non-falsy-string, non-falsy-string&numeric-string}', $matches);
assertType('array{}|array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches);

return;
}

assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches);
assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches);
}

public function sayHello6(string $s): void
{
if (1 > preg_match('/data-(\d{6})\.json$/', $s, $matches)) {
assertType('array{}|array{non-falsy-string, non-falsy-string&numeric-string}', $matches);
assertType('array{}|array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches);

return;
}

assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches);
assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches);
}
}
30 changes: 15 additions & 15 deletions tests/PHPStan/Analyser/nsrt/bug-11311.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
function doFoo(string $s) {
if (1 === preg_match('/(?<major>\d+)\.(?<minor>\d+)(?:\.(?<patch>\d+))?/', $s, $matches, PREG_UNMATCHED_AS_NULL)) {

assertType('array{0: non-falsy-string, major: numeric-string, 1: numeric-string, minor: numeric-string, 2: numeric-string, patch: numeric-string|null, 3: numeric-string|null}', $matches);
assertType('array{0: non-falsy-string, major: decimal-int-string, 1: decimal-int-string, minor: decimal-int-string, 2: decimal-int-string, patch: decimal-int-string|null, 3: decimal-int-string|null}', $matches);
}
}

Expand Down Expand Up @@ -85,42 +85,42 @@ function (string $size): void {
if (preg_match('/ab(\d){2,4}xx([0-9])?e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size));
}
assertType('array{non-falsy-string, numeric-string, numeric-string|null}', $matches);
assertType('array{non-falsy-string, decimal-int-string, decimal-int-string|null}', $matches);
};

function (string $size): void {
if (preg_match('/a(\dAB){2}b(\d){2,4}([1-5])([1-5a-z])e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size));
}
assertType('array{non-falsy-string, non-falsy-string, numeric-string, numeric-string, non-empty-string}', $matches);
assertType('array{non-falsy-string, non-falsy-string, decimal-int-string, decimal-int-string, non-empty-string}', $matches);
};

function (string $size): void {
if (preg_match('/ab(ab(\d)){2,4}xx([0-9][a-c])?e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size));
}
assertType('array{non-falsy-string, non-falsy-string, numeric-string, non-falsy-string|null}', $matches);
assertType('array{non-falsy-string, non-falsy-string, decimal-int-string, non-falsy-string|null}', $matches);
};

function (string $size): void {
if (preg_match('/ab(\d+)e(\d?)/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size));
}
assertType("array{non-falsy-string, numeric-string, ''|numeric-string}", $matches);
assertType("array{non-falsy-string, decimal-int-string, ''|decimal-int-string}", $matches);
};

function (string $size): void {
if (preg_match('/ab(?P<num>\d+)e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size));
}
assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches);
assertType('array{0: non-falsy-string, num: decimal-int-string, 1: decimal-int-string}', $matches);
};

function (string $size): void {
if (preg_match('/ab(\d\d)/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size));
}
assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches);
assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches);
};

function (string $size): void {
Expand Down Expand Up @@ -162,12 +162,12 @@ function (string $size): void {
if (preg_match('/ab(\d+\d?)e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size));
}
assertType('array{non-falsy-string, numeric-string}', $matches);
assertType('array{non-falsy-string, decimal-int-string}', $matches);
};

function (string $s): void {
if (preg_match('/Price: ([2-5])/i', $s, $matches, PREG_UNMATCHED_AS_NULL)) {
assertType('array{non-falsy-string, numeric-string}', $matches);
assertType('array{non-falsy-string, decimal-int-string}', $matches);
}
};

Expand All @@ -191,26 +191,26 @@ function (string $s): void {

function (string $s): void {
preg_match('/%a(\d*)/', $s, $matches, PREG_UNMATCHED_AS_NULL);
assertType("array{}|array{non-falsy-string, ''|numeric-string}", $matches);
assertType("array{}|array{non-falsy-string, ''|decimal-int-string}", $matches);
};

function (string $s): void {
preg_match('/%a(\d*)?/', $s, $matches, PREG_UNMATCHED_AS_NULL);
assertType("array{}|array{non-falsy-string, ''|numeric-string|null}", $matches);
assertType("array{}|array{non-falsy-string, ''|decimal-int-string|null}", $matches);
};

function (string $s): void {
if (preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_UNMATCHED_AS_NULL)) {
assertType("array{non-empty-string, numeric-string|null, non-empty-string|null}", $matches);
assertType("array{non-empty-string, decimal-int-string|null, non-empty-string|null}", $matches);
} else {
assertType("array{}", $matches);
}
assertType("array{}|array{non-empty-string, numeric-string|null, non-empty-string|null}", $matches);
assertType("array{}|array{non-empty-string, decimal-int-string|null, non-empty-string|null}", $matches);
};

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

Expand All @@ -222,5 +222,5 @@ function (string $s): void {

function (string $s): void {
preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_UNMATCHED_AS_NULL);
assertType("array{}|array{non-empty-string, numeric-string|null, non-empty-string|null}", $matches);
assertType("array{}|array{non-empty-string, decimal-int-string|null, non-empty-string|null}", $matches);
};
Loading
Loading