Skip to content

Commit 40513a0

Browse files
authored
RegexArrayShapeMatcher - infer constant string types in alternations
1 parent 760f86f commit 40513a0

File tree

5 files changed

+51
-17
lines changed

5 files changed

+51
-17
lines changed

src/Type/Regex/RegexExpressionHelper.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\Type\Type;
1212
use PHPStan\Type\TypeCombinator;
1313
use function array_key_exists;
14+
use function ltrim;
1415
use function strrpos;
1516
use function substr;
1617

@@ -147,6 +148,8 @@ public function getPatternDelimiters(Concat $concat, Scope $scope): array
147148

148149
private function getPatternDelimiter(string $regex): ?string
149150
{
151+
$regex = ltrim($regex);
152+
150153
if ($regex === '') {
151154
return null;
152155
}

src/Type/Regex/RegexGroupParser.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use PHPStan\Type\StringType;
2121
use PHPStan\Type\Type;
2222
use PHPStan\Type\TypeCombinator;
23+
use function array_merge;
2324
use function count;
2425
use function in_array;
2526
use function is_int;
@@ -432,7 +433,7 @@ private function walkGroupAst(
432433
$isNonEmpty = TrinaryLogic::createYes();
433434
}
434435
}
435-
} elseif (!in_array($ast->getId(), ['#capturing', '#namedcapturing'], true)) {
436+
} elseif (!in_array($ast->getId(), ['#capturing', '#namedcapturing', '#alternation'], true)) {
436437
$onlyLiterals = null;
437438
}
438439

@@ -447,6 +448,7 @@ private function walkGroupAst(
447448
$isNumeric = TrinaryLogic::createNo();
448449
}
449450

451+
$alternativeLiterals = [];
450452
foreach ($children as $child) {
451453
$this->walkGroupAst(
452454
$child,
@@ -459,7 +461,24 @@ private function walkGroupAst(
459461
$inClass,
460462
$patternModifiers,
461463
);
464+
465+
if ($ast->getId() !== '#alternation') {
466+
continue;
467+
}
468+
469+
if ($onlyLiterals !== null && $alternativeLiterals !== null) {
470+
$alternativeLiterals = array_merge($alternativeLiterals, $onlyLiterals);
471+
$onlyLiterals = [];
472+
} else {
473+
$alternativeLiterals = null;
474+
}
462475
}
476+
477+
if ($alternativeLiterals === null || $alternativeLiterals === []) {
478+
return;
479+
}
480+
481+
$onlyLiterals = $alternativeLiterals;
463482
}
464483

465484
private function isMaybeEmptyNode(TreeNode $node, string $patternModifiers, bool &$isNonFalsy): bool

tests/PHPStan/Analyser/nsrt/bug-11311-php72.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ function doUnmatchedAsNull(string $s): void {
2222
// see https://3v4l.org/VeDob#veol
2323
function unmatchedAsNullWithOptionalGroup(string $s): void {
2424
if (preg_match('/Price: (£|€)?\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) {
25-
assertType("array{0: string, 1?: non-empty-string}", $matches);
25+
assertType("array{0: string, 1?: '£'|'€'}", $matches);
2626
} else {
2727
assertType('array{}', $matches);
2828
}
29-
assertType("array{}|array{0: string, 1?: non-empty-string}", $matches);
29+
assertType("array{}|array{0: string, 1?: '£'|'€'}", $matches);
3030
}

tests/PHPStan/Analyser/nsrt/bug-11311.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ function doUnmatchedAsNull(string $s): void {
2323
function unmatchedAsNullWithOptionalGroup(string $s): void {
2424
if (preg_match('/Price: (£|€)?\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) {
2525
// with PREG_UNMATCHED_AS_NULL the offset 1 will always exist. It is correct that it's nullable because it's optional though
26-
assertType('array{string, non-empty-string|null}', $matches);
26+
assertType("array{string, '£'|'€'|null}", $matches);
2727
} else {
2828
assertType('array{}', $matches);
2929
}
30-
assertType('array{}|array{string, non-empty-string|null}', $matches);
30+
assertType("array{}|array{string, '£'|'€'|null}", $matches);
3131
}
3232

3333
function bug11331a(string $url):void {

tests/PHPStan/Analyser/nsrt/preg_match_shapes.php

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ function doMatch(string $s): void {
1212
assertType('array{}|array{string}', $matches);
1313

1414
if (preg_match('/Price: (£|€)\d+/', $s, $matches)) {
15-
assertType('array{string, non-empty-string}', $matches);
15+
assertType("array{string, '£'|'€'}", $matches);
1616
} else {
1717
assertType('array{}', $matches);
1818
}
19-
assertType('array{}|array{string, non-empty-string}', $matches);
19+
assertType("array{}|array{string, '£'|'€'}", $matches);
2020

2121
if (preg_match('/Price: (£|€)(\d+)/i', $s, $matches)) {
2222
assertType('array{string, non-empty-string, numeric-string}', $matches);
@@ -54,9 +54,9 @@ function doMatch(string $s): void {
5454
assertType("array{}|array{0: string, 1: 'a', 2: string, 3: 'c', name?: non-empty-string, 4?: non-empty-string}", $matches);
5555

5656
if (preg_match('/(a|b)|(?:c)/', $s, $matches)) {
57-
assertType('array{0: string, 1?: non-empty-string}', $matches);
57+
assertType("array{0: string, 1?: 'a'|'b'}", $matches);
5858
}
59-
assertType('array{}|array{0: string, 1?: non-empty-string}', $matches);
59+
assertType("array{}|array{0: string, 1?: 'a'|'b'}", $matches);
6060

6161
if (preg_match('/(foo)(bar)(baz)+/', $s, $matches)) {
6262
assertType("array{string, 'foo', 'bar', non-falsy-string}", $matches);
@@ -356,30 +356,30 @@ function bug11291(string $s): void {
356356
function bug11323a(string $s): void
357357
{
358358
if (preg_match('/Price: (?P<currency>£|€)\d+/', $s, $matches)) {
359-
assertType('array{0: string, currency: non-empty-string, 1: non-empty-string}', $matches);
359+
assertType("array{0: string, currency: '£'|'€', 1: '£'|'€'}", $matches);
360360
} else {
361361
assertType('array{}', $matches);
362362
}
363-
assertType('array{}|array{0: string, currency: non-empty-string, 1: non-empty-string}', $matches);
363+
assertType("array{}|array{0: string, currency: '£'|'€', 1: '£'|'€'}", $matches);
364364
}
365365

366366
function bug11323b(string $s): void
367367
{
368368
if (preg_match('/Price: (?<currency>£|€)\d+/', $s, $matches)) {
369-
assertType('array{0: string, currency: non-empty-string, 1: non-empty-string}', $matches);
369+
assertType("array{0: string, currency: '£'|'€', 1: '£'|'€'}", $matches);
370370
} else {
371371
assertType('array{}', $matches);
372372
}
373-
assertType('array{}|array{0: string, currency: non-empty-string, 1: non-empty-string}', $matches);
373+
assertType("array{}|array{0: string, currency: '£'|'€', 1: '£'|'€'}", $matches);
374374
}
375375

376376
function unmatchedAsNullWithMandatoryGroup(string $s): void {
377377
if (preg_match('/Price: (?<currency>£|€)\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) {
378-
assertType('array{0: string, currency: non-empty-string, 1: non-empty-string}', $matches);
378+
assertType("array{0: string, currency: '£'|'€', 1: '£'|'€'}", $matches);
379379
} else {
380380
assertType('array{}', $matches);
381381
}
382-
assertType('array{}|array{0: string, currency: non-empty-string, 1: non-empty-string}', $matches);
382+
assertType("array{}|array{0: string, currency: '£'|'€', 1: '£'|'€'}", $matches);
383383
}
384384

385385
function (string $s): void {
@@ -608,17 +608,29 @@ function (string $s): void {
608608
};
609609

610610
function (string $s): void {
611-
if (preg_match('/Price: (a|0)/', $s, $matches)) {
611+
if (preg_match('/Price: (a|bc?)/', $s, $matches)) {
612612
assertType("array{string, non-empty-string}", $matches);
613613
}
614614
};
615615

616616
function (string $s): void {
617-
if (preg_match('/Price: (aa|0)/', $s, $matches)) {
617+
if (preg_match('/Price: (a|\d)/', $s, $matches)) {
618618
assertType("array{string, non-empty-string}", $matches);
619619
}
620620
};
621621

622+
function (string $s): void {
623+
if (preg_match('/Price: (a|0)/', $s, $matches)) {
624+
assertType("array{string, '0'|'a'}", $matches);
625+
}
626+
};
627+
628+
function (string $s): void {
629+
if (preg_match('/Price: (aa|0)/', $s, $matches)) {
630+
assertType("array{string, '0'|'aa'}", $matches);
631+
}
632+
};
633+
622634
function (string $s): void {
623635
if (preg_match('/( \d+ )/x', $s, $matches)) {
624636
assertType('array{string, numeric-string}', $matches);

0 commit comments

Comments
 (0)