Skip to content

Commit 18aa502

Browse files
authored
Support classes of literal strings in RegexGroupParser
1 parent c6c06b1 commit 18aa502

File tree

3 files changed

+86
-14
lines changed

3 files changed

+86
-14
lines changed

src/Type/Php/RegexGroupParser.php

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
use PHPStan\Type\TypeCombinator;
2222
use function array_key_exists;
2323
use function count;
24-
use function implode;
2524
use function in_array;
2625
use function is_int;
2726
use function rtrim;
@@ -284,10 +283,22 @@ private function createGroupType(TreeNode $group, bool $maybeConstant): Type
284283
$inOptionalQuantification = false;
285284
$onlyLiterals = [];
286285

287-
$this->walkGroupAst($group, $isNonEmpty, $isNumeric, $inOptionalQuantification, $onlyLiterals);
286+
$this->walkGroupAst(
287+
$group,
288+
$isNonEmpty,
289+
$isNumeric,
290+
$inOptionalQuantification,
291+
$onlyLiterals,
292+
false,
293+
);
288294

289295
if ($maybeConstant && $onlyLiterals !== null && $onlyLiterals !== []) {
290-
return new ConstantStringType(implode('', $onlyLiterals));
296+
$result = [];
297+
foreach ($onlyLiterals as $literal) {
298+
$result[] = new ConstantStringType($literal);
299+
300+
}
301+
return TypeCombinator::union(...$result);
291302
}
292303

293304
if ($isNumeric->yes()) {
@@ -306,7 +317,14 @@ private function createGroupType(TreeNode $group, bool $maybeConstant): Type
306317
/**
307318
* @param array<string>|null $onlyLiterals
308319
*/
309-
private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryLogic &$isNumeric, bool &$inOptionalQuantification, ?array &$onlyLiterals): void
320+
private function walkGroupAst(
321+
TreeNode $ast,
322+
TrinaryLogic &$isNonEmpty,
323+
TrinaryLogic &$isNumeric,
324+
bool &$inOptionalQuantification,
325+
?array &$onlyLiterals,
326+
bool $inClass,
327+
): void
310328
{
311329
$children = $ast->getChildren();
312330

@@ -327,8 +345,24 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL
327345
}
328346

329347
$onlyLiterals = null;
348+
} elseif ($ast->getId() === '#class' && $onlyLiterals !== null) {
349+
$inClass = true;
350+
351+
$newLiterals = [];
352+
foreach ($children as $child) {
353+
$oldLiterals = $onlyLiterals;
354+
355+
if ($child->getId() === 'token') {
356+
$this->getLiteralValue($child, $oldLiterals, true);
357+
}
358+
359+
foreach ($oldLiterals ?? [] as $oldLiteral) {
360+
$newLiterals[] = $oldLiteral;
361+
}
362+
}
363+
$onlyLiterals = $newLiterals;
330364
} elseif ($ast->getId() === 'token') {
331-
$literalValue = $this->getLiteralValue($ast, $onlyLiterals);
365+
$literalValue = $this->getLiteralValue($ast, $onlyLiterals, !$inClass);
332366
if ($literalValue !== null) {
333367
if (Strings::match($literalValue, '/^\d+$/') === null) {
334368
$isNumeric = TrinaryLogic::createNo();
@@ -360,14 +394,15 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL
360394
$isNumeric,
361395
$inOptionalQuantification,
362396
$onlyLiterals,
397+
$inClass,
363398
);
364399
}
365400
}
366401

367402
/**
368403
* @param array<string>|null $onlyLiterals
369404
*/
370-
private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals): ?string
405+
private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals, bool $appendLiterals): ?string
371406
{
372407
if ($node->getId() !== 'token') {
373408
return null;
@@ -381,11 +416,18 @@ private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals): ?string
381416
if (strlen($value) > 1 && $value[0] === '\\') {
382417
return substr($value, 1);
383418
} elseif (
384-
$token === 'literal'
419+
$appendLiterals
420+
&& $token === 'literal'
385421
&& $onlyLiterals !== null
386422
&& !in_array($value, ['.'], true)
387423
) {
388-
$onlyLiterals[] = $value;
424+
if ($onlyLiterals === []) {
425+
$onlyLiterals = [$value];
426+
} else {
427+
foreach ($onlyLiterals as &$literal) {
428+
$literal .= $value;
429+
}
430+
}
389431
}
390432

391433
return $value;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ function (string $s): void {
179179

180180
function (string $s): void {
181181
if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*([sbdeEfFgGhHouxX])$/', $s, $matches, PREG_UNMATCHED_AS_NULL) === 1) {
182-
assertType('array{string, non-empty-string|null, non-empty-string}', $matches);
182+
assertType("array{string, non-empty-string|null, 'b'|'d'|'E'|'e'|'F'|'f'|'G'|'g'|'H'|'h'|'o'|'s'|'u'|'X'|'x'}", $matches);
183183
}
184184
};
185185

tests/PHPStan/Analyser/nsrt/preg_match_shapes.php

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ function (string $s, $mixed): void {
435435

436436
function (string $s): void {
437437
if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*([sbdeEfFgGhHouxX])$/', $s, $matches) === 1) {
438-
assertType('array{string, string, non-empty-string}', $matches);
438+
assertType("array{string, string, 'b'|'d'|'E'|'e'|'F'|'f'|'G'|'g'|'H'|'h'|'o'|'s'|'u'|'X'|'x'}", $matches);
439439
}
440440
};
441441

@@ -453,13 +453,13 @@ function (string $s): void {
453453

454454
function (string $s): void {
455455
if (preg_match('~^([157])$~', $s, $matches) === 1) {
456-
assertType("array{string, numeric-string}", $matches);
456+
assertType("array{string, '1'|'5'|'7'}", $matches);
457457
}
458458
};
459459

460460
function (string $s): void {
461461
if (preg_match('~^([157XY])$~', $s, $matches) === 1) {
462-
assertType("array{string, non-empty-string}", $matches);
462+
assertType("array{string, '1'|'5'|'7'|'X'|'Y'}", $matches);
463463
}
464464
};
465465

@@ -519,10 +519,10 @@ function bug11323(string $s): void {
519519
assertType('array{string, non-empty-string}', $matches);
520520
}
521521
if (preg_match("{([\r\n]+)(\n)([\n])}", $s, $matches)) {
522-
assertType('array{string, non-empty-string, "\n", non-empty-string}', $matches);
522+
assertType('array{string, non-empty-string, "\n", "\n"}', $matches);
523523
}
524524
if (preg_match('/foo(*:first)|bar(*:second)([x])/', $s, $matches)) {
525-
assertType("array{0: string, 1?: non-empty-string, MARK?: 'first'|'second'}", $matches);
525+
assertType("array{0: string, 1?: 'x', MARK?: 'first'|'second'}", $matches);
526526
}
527527
}
528528

@@ -570,3 +570,33 @@ function (string $s): void {
570570
assertType("array{0: string, 1: non-empty-string, 2?: numeric-string, 3?: 'x'}", $matches);
571571
}
572572
};
573+
574+
function (string $s): void {
575+
if (preg_match('/Price: ([a-z])/i', $s, $matches)) {
576+
assertType("array{string, non-empty-string}", $matches);
577+
}
578+
};
579+
580+
function (string $s): void {
581+
if (preg_match('/Price: ([0-9])/i', $s, $matches)) {
582+
assertType("array{string, numeric-string}", $matches);
583+
}
584+
};
585+
586+
function (string $s): void {
587+
if (preg_match('/Price: ([xXa])/i', $s, $matches)) {
588+
assertType("array{string, 'a'|'X'|'x'}", $matches);
589+
}
590+
};
591+
592+
function (string $s): void {
593+
if (preg_match('/Price: (ba[rz])/i', $s, $matches)) {
594+
assertType("array{string, 'bar'|'baz'}", $matches);
595+
}
596+
};
597+
598+
function (string $s): void {
599+
if (preg_match('/Price: (b[ao][mn])/i', $s, $matches)) {
600+
assertType("array{string, 'bam'|'ban'|'bom'|'bon'}", $matches);
601+
}
602+
};

0 commit comments

Comments
 (0)