Skip to content

Commit 76eae8b

Browse files
committed
Fix parsing for marker verbs, fixes #8948
1 parent 4afadf6 commit 76eae8b

File tree

3 files changed

+34
-4
lines changed

3 files changed

+34
-4
lines changed

resources/RegexGrammar.pp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@
7878
%token co:_comment \) -> default
7979
%token co:comment .*?(?=(?<!\\)\))
8080

81+
// Marker verbs
82+
%token marker_ \(\*: -> mark
83+
%token mark:name [^)]+
84+
%token mark:_marker \) -> default
85+
8186
// Capturing group.
8287
%token named_capturing_ \(\?P?< -> nc
8388
%token nc:_named_capturing > -> default
@@ -186,7 +191,8 @@
186191
| literal()
187192

188193
#capturing:
189-
::comment_:: <comment>? ::_comment:: #comment
194+
::marker_:: <name> ::_marker:: #mark
195+
| ::comment_:: <comment>? ::_comment:: #comment
190196
| (
191197
::named_capturing_:: <capturing_name> ::_named_capturing:: #namedcapturing
192198
| ::non_capturing_:: #noncapturing

src/Type/Php/RegexArrayShapeMatcher.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
125125
// regex could not be parsed by Hoa/Regex
126126
return null;
127127
}
128-
[$groupList, $groupCombinations] = $parseResult;
128+
[$groupList, $groupCombinations, $hasMark] = $parseResult;
129129

130130
$trailingOptionals = 0;
131131
foreach (array_reverse($groupList) as $captureGroup) {
@@ -152,6 +152,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
152152
$wasMatched,
153153
$trailingOptionals,
154154
$flags ?? 0,
155+
$hasMark,
155156
);
156157

157158
if (!$this->containsUnmatchedAsNull($flags ?? 0)) {
@@ -189,6 +190,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
189190
$wasMatched,
190191
$trailingOptionals,
191192
$flags ?? 0,
193+
$hasMark,
192194
);
193195

194196
$combiTypes[] = $combiType;
@@ -211,6 +213,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
211213
$wasMatched,
212214
$trailingOptionals,
213215
$flags ?? 0,
216+
$hasMark,
214217
);
215218
}
216219

@@ -272,6 +275,7 @@ private function buildArrayType(
272275
TrinaryLogic $wasMatched,
273276
int $trailingOptionals,
274277
int $flags,
278+
bool $hasMark,
275279
): Type
276280
{
277281
$builder = ConstantArrayTypeBuilder::createEmpty();
@@ -325,6 +329,14 @@ private function buildArrayType(
325329
$i++;
326330
}
327331

332+
if ($hasMark) {
333+
$builder->setOffsetValueType(
334+
$this->getKeyType('MARK'),
335+
new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]),
336+
true,
337+
);
338+
}
339+
328340
return $builder->getArray();
329341
}
330342

@@ -372,7 +384,7 @@ private function getValueType(Type $baseType, int $flags): Type
372384
}
373385

374386
/**
375-
* @return array{array<int, RegexCapturingGroup>, array<int, array<int, int[]>>}|null
387+
* @return array{array<int, RegexCapturingGroup>, array<int, array<int, int[]>>, bool}|null
376388
*/
377389
private function parseGroups(string $regex): ?array
378390
{
@@ -398,6 +410,7 @@ private function parseGroups(string $regex): ?array
398410
$groupCombinations = [];
399411
$alternationId = -1;
400412
$captureGroupId = 100;
413+
$hasMark = false;
401414
$this->walkRegexAst(
402415
$ast,
403416
false,
@@ -408,9 +421,10 @@ private function parseGroups(string $regex): ?array
408421
$captureGroupId,
409422
$capturingGroups,
410423
$groupCombinations,
424+
$hasMark,
411425
);
412426

413-
return [$capturingGroups, $groupCombinations];
427+
return [$capturingGroups, $groupCombinations, $hasMark];
414428
}
415429

416430
/**
@@ -427,6 +441,7 @@ private function walkRegexAst(
427441
int &$captureGroupId,
428442
array &$capturingGroups,
429443
array &$groupCombinations,
444+
bool &$hasMark,
430445
): void
431446
{
432447
$group = null;
@@ -483,6 +498,11 @@ private function walkRegexAst(
483498
$inAlternation = true;
484499
}
485500

501+
if ($ast->getId() === '#mark') {
502+
$hasMark = true;
503+
return;
504+
}
505+
486506
if ($group instanceof RegexCapturingGroup) {
487507
$capturingGroups[$group->getId()] = $group;
488508

@@ -506,6 +526,7 @@ private function walkRegexAst(
506526
$captureGroupId,
507527
$capturingGroups,
508528
$groupCombinations,
529+
$hasMark,
509530
);
510531

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

tests/PHPStan/Analyser/nsrt/preg_match_shapes.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,9 @@ function bug11323(string $s): void {
521521
if (preg_match("{([\r\n]+)(\n)([\n])}", $s, $matches)) {
522522
assertType('array{string, non-empty-string, non-empty-string, non-empty-string}', $matches);
523523
}
524+
if (preg_match('/foo(*:first)|bar(*:second)([x])/', $s, $matches)) {
525+
assertType('array{0: string, 1?: non-empty-string, MARK?: non-empty-string}', $matches);
526+
}
524527
}
525528

526529
function (string $s): void {

0 commit comments

Comments
 (0)