Skip to content

Commit 30b63e3

Browse files
committed
Support literal strings in RegexGroupParser
1 parent 76d822d commit 30b63e3

File tree

2 files changed

+42
-17
lines changed

2 files changed

+42
-17
lines changed

src/Type/Php/RegexGroupParser.php

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use PHPStan\Type\TypeCombinator;
2222
use function array_key_exists;
2323
use function count;
24+
use function implode;
2425
use function in_array;
2526
use function is_int;
2627
use function rtrim;
@@ -264,9 +265,13 @@ private function createGroupType(TreeNode $group): Type
264265
$isNonEmpty = TrinaryLogic::createMaybe();
265266
$isNumeric = TrinaryLogic::createMaybe();
266267
$inOptionalQuantification = false;
268+
$onlyLiterals = [];
267269

268-
$this->walkGroupAst($group, $isNonEmpty, $isNumeric, $inOptionalQuantification);
270+
$this->walkGroupAst($group, $isNonEmpty, $isNumeric, $inOptionalQuantification, $onlyLiterals);
269271

272+
if ($onlyLiterals !== null && $onlyLiterals !== []) {
273+
return new ConstantStringType(implode('', $onlyLiterals));
274+
}
270275
if ($isNumeric->yes()) {
271276
$result = new IntersectionType([new StringType(), new AccessoryNumericStringType()]);
272277
if (!$isNonEmpty->yes()) {
@@ -280,7 +285,7 @@ private function createGroupType(TreeNode $group): Type
280285
return new StringType();
281286
}
282287

283-
private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryLogic &$isNumeric, bool &$inOptionalQuantification): void
288+
private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryLogic &$isNumeric, bool &$inOptionalQuantification, ?array &$onlyLiterals): void
284289
{
285290
$children = $ast->getChildren();
286291

@@ -289,9 +294,8 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL
289294
&& count($children) > 0
290295
) {
291296
$isNonEmpty = TrinaryLogic::createYes();
292-
}
293-
294-
if ($ast->getId() === '#quantification') {
297+
$onlyLiterals = null;
298+
} elseif ($ast->getId() === '#quantification') {
295299
[$min] = $this->getQuantificationRange($ast);
296300

297301
if ($min === 0) {
@@ -301,10 +305,10 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL
301305
$isNonEmpty = TrinaryLogic::createYes();
302306
$inOptionalQuantification = false;
303307
}
304-
}
305308

306-
if ($ast->getId() === 'token') {
307-
$literalValue = $this->getLiteralValue($ast);
309+
$onlyLiterals = null;
310+
} elseif ($ast->getId() === 'token') {
311+
$literalValue = $this->getLiteralValue($ast, $onlyLiterals);
308312
if ($literalValue !== null) {
309313
if (Strings::match($literalValue, '/^\d+$/') === null) {
310314
$isNumeric = TrinaryLogic::createNo();
@@ -315,7 +319,13 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL
315319
if (!$inOptionalQuantification) {
316320
$isNonEmpty = TrinaryLogic::createYes();
317321
}
322+
} else {
323+
$onlyLiterals = null;
318324
}
325+
} elseif (!in_array($ast->getId(), ['#capturing'], true)) {
326+
$onlyLiterals = null;
327+
} else {
328+
$x = 1;
319329
}
320330

321331
// [^0-9] should not parse as numeric-string, and [^list-everything-but-numbers] is technically
@@ -331,11 +341,12 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL
331341
$isNonEmpty,
332342
$isNumeric,
333343
$inOptionalQuantification,
344+
$onlyLiterals,
334345
);
335346
}
336347
}
337348

338-
private function getLiteralValue(TreeNode $node): ?string
349+
private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals): ?string
339350
{
340351
if ($node->getId() !== 'token') {
341352
return null;
@@ -348,6 +359,8 @@ private function getLiteralValue(TreeNode $node): ?string
348359
if (in_array($token, ['literal', 'escaped_end_class'], true)) {
349360
if (strlen($node->getValueValue()) > 1 && $value[0] === '\\') {
350361
return substr($value, 1);
362+
} elseif ($token === 'literal' && $onlyLiterals !== null && !in_array($value, ['.'], true)) {
363+
$onlyLiterals[] = $value;
351364
}
352365

353366
return $value;

tests/PHPStan/Analyser/nsrt/preg_match_shapes.php

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ function doMatch(string $s): void {
4444
assertType('array{}|array{0: string, 1: non-empty-string, 2: string, 3: non-empty-string, 4?: non-empty-string}', $matches);
4545

4646
if (preg_match('/(a)(?<name>b)*(c)(d)*/', $s, $matches)) {
47-
assertType('array{0: string, 1: non-empty-string, name: string, 2: string, 3: non-empty-string, 4?: non-empty-string}', $matches);
47+
assertType("array{0: string, 1: 'a', name: string, 2: string, 3: 'c', 4?: 'd'}", $matches);
4848
}
49-
assertType('array{}|array{0: string, 1: non-empty-string, name: string, 2: string, 3: non-empty-string, 4?: non-empty-string}', $matches);
49+
assertType("array{}|array{0: string, 1: 'a', name: string, 2: string, 3: 'c', 4?: 'd'}", $matches);
5050

5151
if (preg_match('/(a)(b)*(c)(?<name>d)*/', $s, $matches)) {
5252
assertType('array{0: string, 1: non-empty-string, 2: string, 3: non-empty-string, name?: non-empty-string, 4?: non-empty-string}', $matches);
@@ -233,13 +233,13 @@ function testUnionPattern(string $s): void
233233
function doFoo(string $row): void
234234
{
235235
if (preg_match('~^(a(b))$~', $row, $matches) === 1) {
236-
assertType('array{string, non-empty-string, non-empty-string}', $matches);
236+
assertType("array{string, non-empty-string, 'b'}", $matches);
237237
}
238238
if (preg_match('~^(a(b)?)$~', $row, $matches) === 1) {
239239
assertType('array{0: string, 1: non-empty-string, 2?: non-empty-string}', $matches);
240240
}
241241
if (preg_match('~^(a(b)?)?$~', $row, $matches) === 1) {
242-
assertType('array{0: string, 1?: non-empty-string, 2?: non-empty-string}', $matches);
242+
assertType("array{0: string, 1?: non-empty-string, 2?: 'b'}", $matches);
243243
}
244244
}
245245

@@ -390,11 +390,11 @@ function unmatchedAsNullWithMandatoryGroup(string $s): void {
390390

391391
function (string $s): void {
392392
if (preg_match('{' . preg_quote('xxx') . '(z)}', $s, $matches)) {
393-
assertType('array{string, non-empty-string}', $matches);
393+
assertType("array{string, 'z'}", $matches);
394394
} else {
395395
assertType('array{}', $matches);
396396
}
397-
assertType('array{}|array{string, non-empty-string}', $matches);
397+
assertType("array{}|array{string, 'z'}", $matches);
398398
};
399399

400400
function (string $s): void {
@@ -417,11 +417,11 @@ function (string $s): void {
417417

418418
function (string $s): void {
419419
if (preg_match('{' . preg_quote($s) . '(z)' . preg_quote($s) . '(?:abc)(def)?}', $s, $matches)) {
420-
assertType('array{0: string, 1: non-empty-string, 2?: non-empty-string}', $matches);
420+
assertType("array{0: string, 1: 'z', 2?: non-empty-string", $matches);
421421
} else {
422422
assertType('array{}', $matches);
423423
}
424-
assertType('array{}|array{0: string, 1: non-empty-string, 2?: non-empty-string}', $matches);
424+
assertType("array{}|array{0: string, 1: 'z', 2?: non-empty-string}", $matches);
425425
};
426426

427427
function (string $s, $mixed): void {
@@ -546,3 +546,15 @@ public function test2(string $str): void
546546
}
547547
}
548548
}
549+
550+
function (string $s): void {
551+
if (rand(0,1)) {
552+
$p = '/Price: (£)(abc)/i';
553+
} else {
554+
$p = '/Price: (\d)(b)/i';
555+
}
556+
557+
if (preg_match($p, $s, $matches)) {
558+
assertType("array{string, '£', 'abc'}|array{string, numeric-string, 'b'}", $matches);
559+
}
560+
};

0 commit comments

Comments
 (0)