7
7
use Hoa \Compiler \Llk \TreeNode ;
8
8
use Hoa \Exception \Exception ;
9
9
use Hoa \File \Read ;
10
+ use Nette \Utils \Strings ;
10
11
use PhpParser \Node \Expr ;
11
12
use PhpParser \Node \Name ;
12
13
use PHPStan \Analyser \Scope ;
13
14
use PHPStan \Php \PhpVersion ;
14
15
use PHPStan \ShouldNotHappenException ;
15
16
use PHPStan \TrinaryLogic ;
17
+ use PHPStan \Type \Accessory \AccessoryNonEmptyStringType ;
18
+ use PHPStan \Type \Accessory \AccessoryNumericStringType ;
16
19
use PHPStan \Type \Constant \ConstantArrayType ;
17
20
use PHPStan \Type \Constant \ConstantArrayTypeBuilder ;
18
21
use PHPStan \Type \Constant \ConstantIntegerType ;
19
22
use PHPStan \Type \Constant \ConstantStringType ;
20
23
use PHPStan \Type \IntegerRangeType ;
24
+ use PHPStan \Type \IntersectionType ;
21
25
use PHPStan \Type \StringType ;
22
26
use PHPStan \Type \Type ;
23
27
use PHPStan \Type \TypeCombinator ;
@@ -126,7 +130,6 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
126
130
$ trailingOptionals ++;
127
131
}
128
132
129
- $ valueType = $ this ->getValueType ($ flags ?? 0 );
130
133
$ onlyOptionalTopLevelGroup = $ this ->getOnlyOptionalTopLevelGroup ($ groupList );
131
134
$ onlyTopLevelAlternationId = $ this ->getOnlyTopLevelAlternationId ($ groupList );
132
135
@@ -141,7 +144,6 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
141
144
142
145
$ combiType = $ this ->buildArrayType (
143
146
$ groupList ,
144
- $ valueType ,
145
147
$ wasMatched ,
146
148
$ trailingOptionals ,
147
149
$ flags ?? 0 ,
@@ -179,7 +181,6 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
179
181
180
182
$ combiType = $ this ->buildArrayType (
181
183
$ comboList ,
182
- $ valueType ,
183
184
$ wasMatched ,
184
185
$ trailingOptionals ,
185
186
$ flags ?? 0 ,
@@ -202,7 +203,6 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
202
203
203
204
return $ this ->buildArrayType (
204
205
$ groupList ,
205
- $ valueType ,
206
206
$ wasMatched ,
207
207
$ trailingOptionals ,
208
208
$ flags ?? 0 ,
@@ -264,7 +264,6 @@ private function getOnlyTopLevelAlternationId(array $captureGroups): ?int
264
264
*/
265
265
private function buildArrayType (
266
266
array $ captureGroups ,
267
- Type $ valueType ,
268
267
TrinaryLogic $ wasMatched ,
269
268
int $ trailingOptionals ,
270
269
int $ flags ,
@@ -275,14 +274,14 @@ private function buildArrayType(
275
274
// first item in matches contains the overall match.
276
275
$ builder ->setOffsetValueType (
277
276
$ this ->getKeyType (0 ),
278
- TypeCombinator::removeNull ($ valueType ),
277
+ TypeCombinator::removeNull ($ this -> getValueType ( new StringType (), $ flags ) ),
279
278
!$ wasMatched ->yes (),
280
279
);
281
280
282
281
$ countGroups = count ($ captureGroups );
283
282
$ i = 0 ;
284
283
foreach ($ captureGroups as $ captureGroup ) {
285
- $ groupValueType = $ valueType ;
284
+ $ groupValueType = $ this -> getValueType ( $ captureGroup -> getType (), $ flags ) ;
286
285
287
286
if (!$ wasMatched ->yes ()) {
288
287
$ optional = true ;
@@ -299,6 +298,10 @@ private function buildArrayType(
299
298
}
300
299
}
301
300
301
+ if (!$ optional && $ captureGroup ->isOptional () && !$ this ->containsUnmatchedAsNull ($ flags )) {
302
+ $ groupValueType = TypeCombinator::union ($ groupValueType , new ConstantStringType ('' ));
303
+ }
304
+
302
305
if ($ captureGroup ->isNamed ()) {
303
306
$ builder ->setOffsetValueType (
304
307
$ this ->getKeyType ($ captureGroup ->getName ()),
@@ -333,9 +336,10 @@ private function getKeyType(int|string $key): Type
333
336
return new ConstantIntegerType ($ key );
334
337
}
335
338
336
- private function getValueType (int $ flags ): Type
339
+ private function getValueType (Type $ baseType , int $ flags ): Type
337
340
{
338
- $ valueType = new StringType ();
341
+ $ valueType = $ baseType ;
342
+
339
343
$ offsetType = IntegerRangeType::fromInterval (0 , null );
340
344
if ($ this ->containsUnmatchedAsNull ($ flags )) {
341
345
$ valueType = TypeCombinator::addNull ($ valueType );
@@ -420,6 +424,7 @@ private function walkRegexAst(
420
424
$ inAlternation ? $ alternationId : null ,
421
425
$ inOptionalQuantification ,
422
426
$ parentGroup ,
427
+ $ this ->createGroupType ($ ast ),
423
428
);
424
429
$ parentGroup = $ group ;
425
430
} elseif ($ ast ->getId () === '#namedcapturing ' ) {
@@ -430,6 +435,7 @@ private function walkRegexAst(
430
435
$ inAlternation ? $ alternationId : null ,
431
436
$ inOptionalQuantification ,
432
437
$ parentGroup ,
438
+ $ this ->createGroupType ($ ast ),
433
439
);
434
440
$ parentGroup = $ group ;
435
441
} elseif ($ ast ->getId () === '#noncapturing ' ) {
@@ -534,6 +540,131 @@ private function getQuantificationRange(TreeNode $node): array
534
540
return [$ min , $ max ];
535
541
}
536
542
543
+ private function createGroupType (TreeNode $ group ): Type
544
+ {
545
+ $ isNonEmpty = TrinaryLogic::createMaybe ();
546
+ $ isNumeric = TrinaryLogic::createMaybe ();
547
+ $ inOptionalQuantification = false ;
548
+
549
+ $ this ->walkGroupAst ($ group , $ isNonEmpty , $ isNumeric , $ inOptionalQuantification );
550
+
551
+ $ accessories = [];
552
+ if ($ isNumeric ->yes ()) {
553
+ $ accessories [] = new AccessoryNumericStringType ();
554
+ } elseif ($ isNonEmpty ->yes ()) {
555
+ $ accessories [] = new AccessoryNonEmptyStringType ();
556
+ }
557
+
558
+ if ($ accessories !== []) {
559
+ $ accessories [] = new StringType ();
560
+ return new IntersectionType ($ accessories );
561
+ }
562
+
563
+ return new StringType ();
564
+ }
565
+
566
+ private function walkGroupAst (TreeNode $ ast , TrinaryLogic &$ isNonEmpty , TrinaryLogic &$ isNumeric , bool &$ inOptionalQuantification ): void
567
+ {
568
+ $ children = $ ast ->getChildren ();
569
+
570
+ if (
571
+ $ ast ->getId () === '#concatenation '
572
+ && count ($ children ) > 0
573
+ ) {
574
+ $ isNonEmpty = TrinaryLogic::createYes ();
575
+ }
576
+
577
+ if ($ ast ->getId () === '#quantification ' ) {
578
+ [$ min ] = $ this ->getQuantificationRange ($ ast );
579
+
580
+ if ($ min === 0 ) {
581
+ $ inOptionalQuantification = true ;
582
+ }
583
+ if ($ min >= 1 ) {
584
+ $ isNonEmpty = TrinaryLogic::createYes ();
585
+ $ inOptionalQuantification = false ;
586
+ }
587
+ }
588
+
589
+ if ($ ast ->getId () === 'token ' ) {
590
+ $ literalValue = $ this ->getLiteralValue ($ ast );
591
+ if ($ literalValue !== null ) {
592
+ if (Strings::match ($ literalValue , '/^\d+$/ ' ) === null ) {
593
+ $ isNumeric = TrinaryLogic::createNo ();
594
+ }
595
+
596
+ if (!$ inOptionalQuantification ) {
597
+ $ isNonEmpty = TrinaryLogic::createYes ();
598
+ }
599
+ }
600
+
601
+ if ($ ast ->getValueToken () === 'character_type ' ) {
602
+ if ($ ast ->getValueValue () === '\d ' ) {
603
+ if ($ isNumeric ->maybe ()) {
604
+ $ isNumeric = TrinaryLogic::createYes ();
605
+ }
606
+ } else {
607
+ $ isNumeric = TrinaryLogic::createNo ();
608
+ }
609
+
610
+ if (!$ inOptionalQuantification ) {
611
+ $ isNonEmpty = TrinaryLogic::createYes ();
612
+ }
613
+ }
614
+ }
615
+
616
+ if ($ ast ->getId () === '#range ' || $ ast ->getId () === '#class ' ) {
617
+ if ($ isNumeric ->maybe ()) {
618
+ $ allNumeric = null ;
619
+ foreach ($ children as $ child ) {
620
+ $ literalValue = $ this ->getLiteralValue ($ child );
621
+
622
+ if ($ literalValue === null ) {
623
+ break ;
624
+ }
625
+
626
+ if (Strings::match ($ literalValue , '/^\d+$/ ' ) === null ) {
627
+ $ allNumeric = false ;
628
+ break ;
629
+ }
630
+
631
+ $ allNumeric = true ;
632
+ }
633
+
634
+ if ($ allNumeric === true ) {
635
+ $ isNumeric = TrinaryLogic::createYes ();
636
+ }
637
+ }
638
+
639
+ if (!$ inOptionalQuantification ) {
640
+ $ isNonEmpty = TrinaryLogic::createYes ();
641
+ }
642
+ }
643
+
644
+ foreach ($ children as $ child ) {
645
+ $ this ->walkGroupAst (
646
+ $ child ,
647
+ $ isNonEmpty ,
648
+ $ isNumeric ,
649
+ $ inOptionalQuantification ,
650
+ );
651
+ }
652
+ }
653
+
654
+ private function getLiteralValue (TreeNode $ node ): ?string
655
+ {
656
+ if ($ node ->getId () === 'token ' && $ node ->getValueToken () === 'literal ' ) {
657
+ return $ node ->getValueValue ();
658
+ }
659
+
660
+ // literal "-" outside of a character class like '~^((\\d{1,6})-)$~'
661
+ if ($ node ->getId () === 'token ' && $ node ->getValueToken () === 'range ' ) {
662
+ return $ node ->getValueValue ();
663
+ }
664
+
665
+ return null ;
666
+ }
667
+
537
668
private function getPatternType (Expr $ patternExpr , Scope $ scope ): Type
538
669
{
539
670
if ($ patternExpr instanceof Expr \BinaryOp \Concat) {
0 commit comments