Skip to content

Commit 28022f2

Browse files
authored
Support non-falsy-string in RegexGroupParser
1 parent e8b9695 commit 28022f2

File tree

3 files changed

+91
-44
lines changed

3 files changed

+91
-44
lines changed

src/Type/Php/RegexGroupParser.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\ShouldNotHappenException;
1414
use PHPStan\TrinaryLogic;
1515
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
16+
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
1617
use PHPStan\Type\Accessory\AccessoryNumericStringType;
1718
use PHPStan\Type\Constant\ConstantStringType;
1819
use PHPStan\Type\IntersectionType;
@@ -295,13 +296,16 @@ private function getQuantificationRange(TreeNode $node): array
295296
private function createGroupType(TreeNode $group, bool $maybeConstant): Type
296297
{
297298
$isNonEmpty = TrinaryLogic::createMaybe();
299+
$isNonFalsy = TrinaryLogic::createMaybe();
298300
$isNumeric = TrinaryLogic::createMaybe();
299301
$inOptionalQuantification = false;
300302
$onlyLiterals = [];
301303

302304
$this->walkGroupAst(
303305
$group,
306+
false,
304307
$isNonEmpty,
308+
$isNonFalsy,
305309
$isNumeric,
306310
$inOptionalQuantification,
307311
$onlyLiterals,
@@ -318,11 +322,21 @@ private function createGroupType(TreeNode $group, bool $maybeConstant): Type
318322
}
319323

320324
if ($isNumeric->yes()) {
325+
if ($isNonFalsy->yes()) {
326+
return new IntersectionType([
327+
new StringType(),
328+
new AccessoryNumericStringType(),
329+
new AccessoryNonFalsyStringType(),
330+
]);
331+
}
332+
321333
$result = new IntersectionType([new StringType(), new AccessoryNumericStringType()]);
322334
if (!$isNonEmpty->yes()) {
323335
return TypeCombinator::union(new ConstantStringType(''), $result);
324336
}
325337
return $result;
338+
} elseif ($isNonFalsy->yes()) {
339+
return new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]);
326340
} elseif ($isNonEmpty->yes()) {
327341
return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]);
328342
}
@@ -335,7 +349,9 @@ private function createGroupType(TreeNode $group, bool $maybeConstant): Type
335349
*/
336350
private function walkGroupAst(
337351
TreeNode $ast,
352+
bool $inAlternation,
338353
TrinaryLogic &$isNonEmpty,
354+
TrinaryLogic &$isNonFalsy,
339355
TrinaryLogic &$isNumeric,
340356
bool &$inOptionalQuantification,
341357
?array &$onlyLiterals,
@@ -349,6 +365,9 @@ private function walkGroupAst(
349365
&& count($children) > 0
350366
) {
351367
$isNonEmpty = TrinaryLogic::createYes();
368+
if (!$inAlternation) {
369+
$isNonFalsy = TrinaryLogic::createYes();
370+
}
352371
} elseif ($ast->getId() === '#quantification') {
353372
[$min] = $this->getQuantificationRange($ast);
354373

@@ -359,6 +378,9 @@ private function walkGroupAst(
359378
$isNonEmpty = TrinaryLogic::createYes();
360379
$inOptionalQuantification = false;
361380
}
381+
if ($min >= 2 && !$inAlternation) {
382+
$isNonFalsy = TrinaryLogic::createYes();
383+
}
362384

363385
$onlyLiterals = null;
364386
} elseif ($ast->getId() === '#class' && $onlyLiterals !== null) {
@@ -396,6 +418,10 @@ private function walkGroupAst(
396418
$onlyLiterals = null;
397419
}
398420

421+
if ($ast->getId() === '#alternation') {
422+
$inAlternation = true;
423+
}
424+
399425
// [^0-9] should not parse as numeric-string, and [^list-everything-but-numbers] is technically
400426
// doable but really silly compared to just \d so we can safely assume the string is not numeric
401427
// for negative classes
@@ -406,7 +432,9 @@ private function walkGroupAst(
406432
foreach ($children as $child) {
407433
$this->walkGroupAst(
408434
$child,
435+
$inAlternation,
409436
$isNonEmpty,
437+
$isNonFalsy,
410438
$isNumeric,
411439
$inOptionalQuantification,
412440
$onlyLiterals,

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,14 @@ function (string $size): void {
9292
if (preg_match('/a(\dAB){2}b(\d){2,4}([1-5])([1-5a-z])e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
9393
throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size));
9494
}
95-
assertType('array{string, non-empty-string, numeric-string, numeric-string, non-empty-string}', $matches);
95+
assertType('array{string, non-falsy-string, numeric-string, numeric-string, non-empty-string}', $matches);
9696
};
9797

9898
function (string $size): void {
9999
if (preg_match('/ab(ab(\d)){2,4}xx([0-9][a-c])?e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
100100
throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size));
101101
}
102-
assertType('array{string, non-empty-string, numeric-string, non-empty-string|null}', $matches);
102+
assertType('array{string, non-falsy-string, numeric-string, non-falsy-string|null}', $matches);
103103
};
104104

105105
function (string $size): void {
@@ -120,14 +120,14 @@ function (string $size): void {
120120
if (preg_match('/ab(\d\d)/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
121121
throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size));
122122
}
123-
assertType('array{string, numeric-string}', $matches);
123+
assertType('array{string, non-falsy-string&numeric-string}', $matches);
124124
};
125125

126126
function (string $size): void {
127127
if (preg_match('/ab(\d+\s)e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
128128
throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size));
129129
}
130-
assertType('array{string, non-empty-string}', $matches);
130+
assertType('array{string, non-falsy-string}', $matches);
131131
};
132132

133133
function (string $size): void {
@@ -162,7 +162,7 @@ function (string $size): void {
162162
if (preg_match('/ab(\d+\d?)e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
163163
throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size));
164164
}
165-
assertType('array{string, numeric-string}', $matches);
165+
assertType('array{string, non-falsy-string&numeric-string}', $matches);
166166
};
167167

168168
function (string $s): void {
@@ -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, 'b'|'d'|'E'|'e'|'F'|'f'|'G'|'g'|'H'|'h'|'o'|'s'|'u'|'X'|'x'}", $matches);
182+
assertType("array{string, non-falsy-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: 57 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -59,34 +59,34 @@ function doMatch(string $s): void {
5959
assertType('array{}|array{0: string, 1?: non-empty-string}', $matches);
6060

6161
if (preg_match('/(foo)(bar)(baz)+/', $s, $matches)) {
62-
assertType("array{string, 'foo', 'bar', non-empty-string}", $matches);
62+
assertType("array{string, 'foo', 'bar', non-falsy-string}", $matches);
6363
}
64-
assertType("array{}|array{string, 'foo', 'bar', non-empty-string}", $matches);
64+
assertType("array{}|array{string, 'foo', 'bar', non-falsy-string}", $matches);
6565

6666
if (preg_match('/(foo)(bar)(baz)*/', $s, $matches)) {
67-
assertType("array{0: string, 1: 'foo', 2: 'bar', 3?: non-empty-string}", $matches);
67+
assertType("array{0: string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches);
6868
}
69-
assertType("array{}|array{0: string, 1: 'foo', 2: 'bar', 3?: non-empty-string}", $matches);
69+
assertType("array{}|array{0: string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches);
7070

7171
if (preg_match('/(foo)(bar)(baz)?/', $s, $matches)) {
7272
assertType("array{0: string, 1: 'foo', 2: 'bar', 3?: 'baz'}", $matches);
7373
}
7474
assertType("array{}|array{0: string, 1: 'foo', 2: 'bar', 3?: 'baz'}", $matches);
7575

7676
if (preg_match('/(foo)(bar)(baz){0,3}/', $s, $matches)) {
77-
assertType("array{0: string, 1: 'foo', 2: 'bar', 3?: non-empty-string}", $matches);
77+
assertType("array{0: string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches);
7878
}
79-
assertType("array{}|array{0: string, 1: 'foo', 2: 'bar', 3?: non-empty-string}", $matches);
79+
assertType("array{}|array{0: string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches);
8080

8181
if (preg_match('/(foo)(bar)(baz){2,3}/', $s, $matches)) {
82-
assertType("array{string, 'foo', 'bar', non-empty-string}", $matches);
82+
assertType("array{string, 'foo', 'bar', non-falsy-string}", $matches);
8383
}
84-
assertType("array{}|array{string, 'foo', 'bar', non-empty-string}", $matches);
84+
assertType("array{}|array{string, 'foo', 'bar', non-falsy-string}", $matches);
8585

8686
if (preg_match('/(foo)(bar)(baz){2}/', $s, $matches)) {
87-
assertType("array{string, 'foo', 'bar', non-empty-string}", $matches);
87+
assertType("array{string, 'foo', 'bar', non-falsy-string}", $matches);
8888
}
89-
assertType("array{}|array{string, 'foo', 'bar', non-empty-string}", $matches);
89+
assertType("array{}|array{string, 'foo', 'bar', non-falsy-string}", $matches);
9090
}
9191

9292
function doNonCapturingGroup(string $s): void {
@@ -103,14 +103,14 @@ function doNamedSubpattern(string $s): void {
103103
assertType('array{}|array{0: string, num: numeric-string, 1: numeric-string, 2: non-empty-string}', $matches);
104104

105105
if (preg_match('/^(?<name>\S+::\S+)/', $s, $matches)) {
106-
assertType('array{0: string, name: non-empty-string, 1: non-empty-string}', $matches);
106+
assertType('array{0: string, name: non-falsy-string, 1: non-falsy-string}', $matches);
107107
}
108-
assertType('array{}|array{0: string, name: non-empty-string, 1: non-empty-string}', $matches);
108+
assertType('array{}|array{0: string, name: non-falsy-string, 1: non-falsy-string}', $matches);
109109

110110
if (preg_match('/^(?<name>\S+::\S+)(?:(?<dataname> with data set (?:#\d+|"[^"]+"))\s\()?/', $s, $matches)) {
111-
assertType('array{0: string, name: non-empty-string, 1: non-empty-string, dataname?: non-empty-string, 2?: non-empty-string}', $matches);
111+
assertType('array{0: string, name: non-falsy-string, 1: non-falsy-string, dataname?: non-falsy-string, 2?: non-falsy-string}', $matches);
112112
}
113-
assertType('array{}|array{0: string, name: non-empty-string, 1: non-empty-string, dataname?: non-empty-string, 2?: non-empty-string}', $matches);
113+
assertType('array{}|array{0: string, name: non-falsy-string, 1: non-falsy-string, dataname?: non-falsy-string, 2?: non-falsy-string}', $matches);
114114
}
115115

116116
function doOffsetCapture(string $s): void {
@@ -236,10 +236,10 @@ function doFoo(string $row): void
236236
assertType("array{string, 'ab', 'b'}", $matches);
237237
}
238238
if (preg_match('~^(a(b)?)$~', $row, $matches) === 1) {
239-
assertType("array{0: string, 1: non-empty-string, 2?: 'b'}", $matches);
239+
assertType("array{0: string, 1: non-falsy-string, 2?: 'b'}", $matches);
240240
}
241241
if (preg_match('~^(a(b)?)?$~', $row, $matches) === 1) {
242-
assertType("array{0: string, 1?: non-empty-string, 2?: 'b'}", $matches);
242+
assertType("array{0: string, 1?: non-falsy-string, 2?: 'b'}", $matches);
243243
}
244244
}
245245

@@ -249,7 +249,7 @@ function doFoo2(string $row): void
249249
return;
250250
}
251251

252-
assertType("array{0: string, 1: string, branchCode: ''|numeric-string, 2: ''|numeric-string, accountNumber: numeric-string, 3: numeric-string, bankCode: numeric-string, 4: numeric-string}", $matches);
252+
assertType("array{0: string, 1: string, branchCode: ''|numeric-string, 2: ''|numeric-string, accountNumber: numeric-string, 3: numeric-string, bankCode: non-falsy-string&numeric-string, 4: non-falsy-string&numeric-string}", $matches);
253253
}
254254

255255
function doFoo3(string $row): void
@@ -258,42 +258,42 @@ function doFoo3(string $row): void
258258
return;
259259
}
260260

261-
assertType('array{string, non-empty-string, non-empty-string, numeric-string, numeric-string, numeric-string, numeric-string}', $matches);
261+
assertType('array{string, non-falsy-string, non-falsy-string, numeric-string, numeric-string, numeric-string, numeric-string}', $matches);
262262
}
263263

264264
function (string $size): void {
265265
if (preg_match('~^a\.b(c(\d+)(\d+)(\s+))?d~', $size, $matches) !== 1) {
266266
throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size));
267267
}
268-
assertType('array{string, non-empty-string, numeric-string, numeric-string, non-empty-string}|array{string}', $matches);
268+
assertType('array{string, non-falsy-string, numeric-string, numeric-string, non-empty-string}|array{string}', $matches);
269269
};
270270

271271
function (string $size): void {
272272
if (preg_match('~^a\.b(c(\d+))?d~', $size, $matches) !== 1) {
273273
throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size));
274274
}
275-
assertType('array{string, non-empty-string, numeric-string}|array{string}', $matches);
275+
assertType('array{string, non-falsy-string, numeric-string}|array{string}', $matches);
276276
};
277277

278278
function (string $size): void {
279279
if (preg_match('~^a\.b(c(\d+)?)d~', $size, $matches) !== 1) {
280280
throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size));
281281
}
282-
assertType('array{0: string, 1: non-empty-string, 2?: numeric-string}', $matches);
282+
assertType('array{0: string, 1: non-falsy-string, 2?: numeric-string}', $matches);
283283
};
284284

285285
function (string $size): void {
286286
if (preg_match('~^a\.b(c(\d+)?)?d~', $size, $matches) !== 1) {
287287
throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size));
288288
}
289-
assertType('array{0: string, 1?: non-empty-string, 2?: numeric-string}', $matches);
289+
assertType('array{0: string, 1?: non-falsy-string, 2?: numeric-string}', $matches);
290290
};
291291

292292
function (string $size): void {
293293
if (preg_match('~^a\.b(c(\d+))d~', $size, $matches) !== 1) {
294294
throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size));
295295
}
296-
assertType('array{string, non-empty-string, numeric-string}', $matches);
296+
assertType('array{string, non-falsy-string, numeric-string}', $matches);
297297
};
298298

299299
function (string $size): void {
@@ -321,14 +321,14 @@ function (string $size): void {
321321
if (preg_match('~\{(?:(include)\\s+(?:[$]?\\w+(?<!file))\\s)|(?:(include\\s+file)\\s+(?:[$]?\\w+)\\s)|(?:(include(?:Template|(?:\\s+file)))\\s+(?:\'?.*?\.latte\'?)\\s)~', $size, $matches) !== 1) {
322322
throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size));
323323
}
324-
assertType("array{0: string, 1: 'include', 2?: non-empty-string, 3?: non-empty-string}", $matches);
324+
assertType("array{0: string, 1: 'include', 2?: non-falsy-string, 3?: non-falsy-string}", $matches);
325325
};
326326

327327

328328
function bug11277a(string $value): void
329329
{
330330
if (preg_match('/^\[(.+,?)*\]$/', $value, $matches)) {
331-
assertType('array{0: string, 1?: non-empty-string}', $matches);
331+
assertType('array{0: string, 1?: non-falsy-string}', $matches);
332332
if (count($matches) === 2) {
333333
assertType('array{string, string}', $matches); // could be array{string, non-empty-string}
334334
}
@@ -338,7 +338,7 @@ function bug11277a(string $value): void
338338
function bug11277b(string $value): void
339339
{
340340
if (preg_match('/^(?:(.+,?)|(x))*$/', $value, $matches)) {
341-
assertType('array{0: string, 1?: non-empty-string, 2?: non-empty-string}', $matches);
341+
assertType('array{0: string, 1?: non-falsy-string, 2?: non-empty-string}', $matches);
342342
if (count($matches) === 2) {
343343
assertType('array{string, string}', $matches); // could be array{string, non-empty-string}
344344
}
@@ -441,13 +441,13 @@ function (string $s): void {
441441

442442
function (string $s): void {
443443
if (preg_match('~^((\\d{1,6})-)$~', $s, $matches) === 1) {
444-
assertType("array{string, non-empty-string, numeric-string}", $matches);
444+
assertType("array{string, non-falsy-string, numeric-string}", $matches);
445445
}
446446
};
447447

448448
function (string $s): void {
449449
if (preg_match('~^((\\d{1,6}).)$~', $s, $matches) === 1) {
450-
assertType("array{string, non-empty-string, numeric-string}", $matches);
450+
assertType("array{string, non-falsy-string, numeric-string}", $matches);
451451
}
452452
};
453453

@@ -471,13 +471,13 @@ function bug11323(string $s): void {
471471
assertType('array{string, non-empty-string, non-empty-string}', $matches);
472472
}
473473
if (preg_match('{([-\p{L}[\]*|\x03\a\b+?{}(?:)-]+[^[:digit:]?{}a-z0-9#-k]+)(a-z)}', $s, $matches)) {
474-
assertType("array{string, non-empty-string, 'a-z'}", $matches);
474+
assertType("array{string, non-falsy-string, 'a-z'}", $matches);
475475
}
476476
if (preg_match('{(\d+)(?i)insensitive((?x-i)case SENSITIVE here(?i:insensitive non-capturing group))}', $s, $matches)) {
477-
assertType('array{string, numeric-string, non-empty-string}', $matches);
477+
assertType('array{string, numeric-string, non-falsy-string}', $matches);
478478
}
479479
if (preg_match('{([]] [^]])}', $s, $matches)) {
480-
assertType('array{string, non-empty-string}', $matches);
480+
assertType('array{string, non-falsy-string}', $matches);
481481
}
482482
if (preg_match('{([[:digit:]])}', $s, $matches)) {
483483
assertType('array{string, numeric-string}', $matches);
@@ -495,25 +495,25 @@ function bug11323(string $s): void {
495495
assertType("array{string, ''|'a', string, non-empty-string, non-empty-string}", $matches);
496496
}
497497
if (preg_match('{(.\d)}', $s, $matches)) {
498-
assertType('array{string, non-empty-string}', $matches);
498+
assertType('array{string, non-falsy-string}', $matches);
499499
}
500500
if (preg_match('{(\d.)}', $s, $matches)) {
501-
assertType('array{string, non-empty-string}', $matches);
501+
assertType('array{string, non-falsy-string}', $matches);
502502
}
503503
if (preg_match('{(\d\d)}', $s, $matches)) {
504-
assertType('array{string, numeric-string}', $matches);
504+
assertType('array{string, non-falsy-string&numeric-string}', $matches);
505505
}
506506
if (preg_match('{(.(\d))}', $s, $matches)) {
507-
assertType('array{string, non-empty-string, numeric-string}', $matches);
507+
assertType('array{string, non-falsy-string, numeric-string}', $matches);
508508
}
509509
if (preg_match('{((\d).)}', $s, $matches)) {
510-
assertType('array{string, non-empty-string, numeric-string}', $matches);
510+
assertType('array{string, non-falsy-string, numeric-string}', $matches);
511511
}
512512
if (preg_match('{(\d([1-4])\d)}', $s, $matches)) {
513-
assertType('array{string, numeric-string, numeric-string}', $matches);
513+
assertType('array{string, non-falsy-string&numeric-string, numeric-string}', $matches);
514514
}
515515
if (preg_match('{(x?([1-4])\d)}', $s, $matches)) {
516-
assertType('array{string, non-empty-string, numeric-string}', $matches);
516+
assertType('array{string, non-falsy-string, numeric-string}', $matches);
517517
}
518518
if (preg_match('{([^1-4])}', $s, $matches)) {
519519
assertType('array{string, non-empty-string}', $matches);
@@ -606,3 +606,22 @@ function (string $s): void {
606606
assertType("array{string, 'bam'|'ban'|'bom'|'bon'}", $matches);
607607
}
608608
};
609+
610+
function (string $s): void {
611+
if (preg_match('/Price: (\s{3}|0)/', $s, $matches)) {
612+
assertType("array{string, non-empty-string}", $matches);
613+
}
614+
};
615+
616+
function (string $s): void {
617+
if (preg_match('/Price: (a|0)/', $s, $matches)) {
618+
assertType("array{string, non-empty-string}", $matches);
619+
}
620+
};
621+
622+
function (string $s): void {
623+
if (preg_match('/Price: (aa|0)/', $s, $matches)) {
624+
assertType("array{string, non-empty-string}", $matches);
625+
}
626+
};
627+

0 commit comments

Comments
 (0)