Skip to content

Commit c4c57a9

Browse files
committed
RegexArrayShapeMatcher - Support 'n' modifier
1 parent e9c57da commit c4c57a9

File tree

4 files changed

+51
-5
lines changed

4 files changed

+51
-5
lines changed

src/Php/PhpVersion.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,12 @@ public function supportsPregUnmatchedAsNull(): bool
316316
return $this->versionId >= 70400;
317317
}
318318

319+
public function supportsPregCaptureOnlyNamedGroups(): bool
320+
{
321+
// https://php.watch/versions/8.2/preg-n-no-capture-modifier
322+
return $this->versionId >= 80200;
323+
}
324+
319325
public function hasDateTimeExceptions(): bool
320326
{
321327
return $this->versionId >= 80300;

src/Type/Php/RegexArrayShapeMatcher.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
use function is_string;
3434
use function rtrim;
3535
use function sscanf;
36+
use function str_contains;
3637
use function str_replace;
3738
use function strlen;
3839
use function substr;
@@ -411,6 +412,12 @@ private function parseGroups(string $regex): ?array
411412
return null;
412413
}
413414

415+
$captureOnlyNamed = false;
416+
if ($this->phpVersion->supportsPregCaptureOnlyNamedGroups()) {
417+
$modifiers = $this->regexExpressionHelper->getPatternModifiers($regex);
418+
$captureOnlyNamed = str_contains($modifiers ?? '', 'n');
419+
}
420+
414421
$capturingGroups = [];
415422
$groupCombinations = [];
416423
$alternationId = -1;
@@ -427,6 +434,7 @@ private function parseGroups(string $regex): ?array
427434
$capturingGroups,
428435
$groupCombinations,
429436
$markVerbs,
437+
$captureOnlyNamed,
430438
);
431439

432440
return [$capturingGroups, $groupCombinations, $markVerbs];
@@ -448,6 +456,7 @@ private function walkRegexAst(
448456
array &$capturingGroups,
449457
array &$groupCombinations,
450458
array &$markVerbs,
459+
bool $captureOnlyNamed,
451460
): void
452461
{
453462
$group = null;
@@ -509,7 +518,10 @@ private function walkRegexAst(
509518
return;
510519
}
511520

512-
if ($group instanceof RegexCapturingGroup) {
521+
if (
522+
$group instanceof RegexCapturingGroup &&
523+
(!$captureOnlyNamed || $group->isNamed())
524+
) {
513525
$capturingGroups[$group->getId()] = $group;
514526

515527
if (!array_key_exists($alternationId, $groupCombinations)) {
@@ -533,6 +545,7 @@ private function walkRegexAst(
533545
$capturingGroups,
534546
$groupCombinations,
535547
$markVerbs,
548+
$captureOnlyNamed,
536549
);
537550

538551
if ($ast->getId() !== '#alternation') {

src/Type/Php/RegexExpressionHelper.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Type\Constant\ConstantStringType;
1111
use PHPStan\Type\Type;
1212
use PHPStan\Type\TypeCombinator;
13+
use function strrpos;
1314
use function substr;
1415

1516
final class RegexExpressionHelper
@@ -68,6 +69,27 @@ public function resolve(Expr $expr): Type
6869
return $this->initializerExprTypeResolver->getConcatType($concat->left, $concat->right, static fn (Expr $expr): Type => $resolver->resolve($expr));
6970
}
7071

72+
public function getPatternModifiers(string $pattern): ?string
73+
{
74+
$delimiter = $this->getDelimiterFromString(new ConstantStringType($pattern));
75+
if ($delimiter === null) {
76+
return null;
77+
}
78+
79+
if ($delimiter === '{') {
80+
$endDelimiterPos = strrpos($pattern, '}');
81+
} else {
82+
// same start and end delimiter
83+
$endDelimiterPos = strrpos($pattern, $delimiter);
84+
}
85+
86+
if ($endDelimiterPos === false) {
87+
return null;
88+
}
89+
90+
return substr($pattern, $endDelimiterPos + 1);
91+
}
92+
7193
/**
7294
* Get delimiters from non-constant patterns, if possible.
7395
*

tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@
88
// https://php.watch/versions/8.2/preg-n-no-capture-modifier
99
function doNonAutoCapturingFlag(string $s): void {
1010
if (preg_match('/(\d+)/n', $s, $matches)) {
11-
assertType('array{string, numeric-string}', $matches); // should be 'array{string}'
11+
assertType('array{string}', $matches);
1212
}
13-
assertType('array{}|array{string, numeric-string}', $matches);
13+
assertType('array{}|array{string}', $matches);
1414

1515
if (preg_match('/(\d+)(?P<num>\d+)/n', $s, $matches)) {
16-
assertType('array{0: string, 1: numeric-string, num: numeric-string, 2: numeric-string}', $matches);
16+
assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches);
1717
}
18-
assertType('array{}|array{0: string, 1: numeric-string, num: numeric-string, 2: numeric-string}', $matches);
18+
assertType('array{}|array{0: string, num: numeric-string, 1: numeric-string}', $matches);
19+
20+
if (preg_match('/(\w)-(?P<num>\d+)-(\w)/n', $s, $matches)) {
21+
assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches);
22+
}
23+
assertType('array{}|array{0: string, num: numeric-string, 1: numeric-string}', $matches);
1924
}

0 commit comments

Comments
 (0)