14
14
use PHPStan \Php \PhpVersion ;
15
15
use PHPStan \ShouldNotHappenException ;
16
16
use PHPStan \TrinaryLogic ;
17
+ use PHPStan \Type \Accessory \AccessoryArrayListType ;
17
18
use PHPStan \Type \Accessory \AccessoryNonEmptyStringType ;
18
19
use PHPStan \Type \Accessory \AccessoryNumericStringType ;
20
+ use PHPStan \Type \ArrayType ;
19
21
use PHPStan \Type \Constant \ConstantArrayType ;
20
22
use PHPStan \Type \Constant \ConstantArrayTypeBuilder ;
21
23
use PHPStan \Type \Constant \ConstantIntegerType ;
22
24
use PHPStan \Type \Constant \ConstantStringType ;
23
25
use PHPStan \Type \IntegerRangeType ;
26
+ use PHPStan \Type \IntegerType ;
24
27
use PHPStan \Type \IntersectionType ;
25
28
use PHPStan \Type \StringType ;
26
29
use PHPStan \Type \Type ;
38
41
use function strlen ;
39
42
use function substr ;
40
43
use const PREG_OFFSET_CAPTURE ;
44
+ use const PREG_PATTERN_ORDER ;
45
+ use const PREG_SET_ORDER ;
41
46
use const PREG_UNMATCHED_AS_NULL ;
42
47
43
48
/**
@@ -60,20 +65,25 @@ public function __construct(
60
65
{
61
66
}
62
67
68
+ public function matchAllExpr (Expr $ patternExpr , ?Type $ flagsType , TrinaryLogic $ wasMatched , Scope $ scope ): ?Type
69
+ {
70
+ return $ this ->matchPatternType ($ this ->getPatternType ($ patternExpr , $ scope ), $ flagsType , $ wasMatched , true );
71
+ }
72
+
63
73
public function matchExpr (Expr $ patternExpr , ?Type $ flagsType , TrinaryLogic $ wasMatched , Scope $ scope ): ?Type
64
74
{
65
- return $ this ->matchPatternType ($ this ->getPatternType ($ patternExpr , $ scope ), $ flagsType , $ wasMatched );
75
+ return $ this ->matchPatternType ($ this ->getPatternType ($ patternExpr , $ scope ), $ flagsType , $ wasMatched, false );
66
76
}
67
77
68
78
/**
69
79
* @deprecated use matchExpr() instead for a more precise result
70
80
*/
71
81
public function matchType (Type $ patternType , ?Type $ flagsType , TrinaryLogic $ wasMatched ): ?Type
72
82
{
73
- return $ this ->matchPatternType ($ patternType , $ flagsType , $ wasMatched );
83
+ return $ this ->matchPatternType ($ patternType , $ flagsType , $ wasMatched, false );
74
84
}
75
85
76
- private function matchPatternType (Type $ patternType , ?Type $ flagsType , TrinaryLogic $ wasMatched ): ?Type
86
+ private function matchPatternType (Type $ patternType , ?Type $ flagsType , TrinaryLogic $ wasMatched, bool $ matchesAll ): ?Type
77
87
{
78
88
if ($ wasMatched ->no ()) {
79
89
return new ConstantArrayType ([], []);
@@ -90,8 +100,8 @@ private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLo
90
100
return null ;
91
101
}
92
102
93
- /** @var int-mask<PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL | self::PREG_UNMATCHED_AS_NULL_ON_72_73> $flags */
94
- $ flags = $ flagsType ->getValue () & (PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL | self ::PREG_UNMATCHED_AS_NULL_ON_72_73 );
103
+ /** @var int-mask<PREG_OFFSET_CAPTURE | PREG_PATTERN_ORDER | PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL | self::PREG_UNMATCHED_AS_NULL_ON_72_73> $flags */
104
+ $ flags = $ flagsType ->getValue () & (PREG_OFFSET_CAPTURE | PREG_PATTERN_ORDER | PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL | self ::PREG_UNMATCHED_AS_NULL_ON_72_73 );
95
105
96
106
// some other unsupported/unexpected flag was passed in
97
107
if ($ flags !== $ flagsType ->getValue ()) {
@@ -101,7 +111,7 @@ private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLo
101
111
102
112
$ matchedTypes = [];
103
113
foreach ($ constantStrings as $ constantString ) {
104
- $ matched = $ this ->matchRegex ($ constantString ->getValue (), $ flags , $ wasMatched );
114
+ $ matched = $ this ->matchRegex ($ constantString ->getValue (), $ flags , $ wasMatched, $ matchesAll );
105
115
if ($ matched === null ) {
106
116
return null ;
107
117
}
@@ -117,9 +127,9 @@ private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLo
117
127
}
118
128
119
129
/**
120
- * @param int-mask<PREG_OFFSET_CAPTURE|PREG_UNMATCHED_AS_NULL|self::PREG_UNMATCHED_AS_NULL_ON_72_73>|null $flags
130
+ * @param int-mask<PREG_OFFSET_CAPTURE|PREG_PATTERN_ORDER|PREG_SET_ORDER| PREG_UNMATCHED_AS_NULL|self::PREG_UNMATCHED_AS_NULL_ON_72_73>|null $flags
121
131
*/
122
- private function matchRegex (string $ regex , ?int $ flags , TrinaryLogic $ wasMatched ): ?Type
132
+ private function matchRegex (string $ regex , ?int $ flags , TrinaryLogic $ wasMatched, bool $ matchesAll ): ?Type
123
133
{
124
134
$ parseResult = $ this ->parseGroups ($ regex );
125
135
if ($ parseResult === null ) {
@@ -140,7 +150,8 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
140
150
$ onlyTopLevelAlternationId = $ this ->getOnlyTopLevelAlternationId ($ groupList );
141
151
142
152
if (
143
- $ wasMatched ->yes ()
153
+ !$ matchesAll
154
+ && $ wasMatched ->yes ()
144
155
&& $ onlyOptionalTopLevelGroup !== null
145
156
) {
146
157
// if only one top level capturing optional group exists
@@ -154,17 +165,20 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
154
165
$ trailingOptionals ,
155
166
$ flags ?? 0 ,
156
167
$ markVerbs ,
168
+ $ matchesAll ,
157
169
);
158
170
159
- if (!$ this ->containsUnmatchedAsNull ($ flags ?? 0 )) {
171
+ if (!$ this ->containsUnmatchedAsNull ($ flags ?? 0 , $ matchesAll )) {
160
172
$ combiType = TypeCombinator::union (
161
173
new ConstantArrayType ([new ConstantIntegerType (0 )], [new StringType ()], [0 ], [], true ),
162
174
$ combiType ,
163
175
);
164
176
}
177
+
165
178
return $ combiType ;
166
179
} elseif (
167
- $ wasMatched ->yes ()
180
+ !$ matchesAll
181
+ && $ wasMatched ->yes ()
168
182
&& $ onlyTopLevelAlternationId !== null
169
183
&& array_key_exists ($ onlyTopLevelAlternationId , $ groupCombinations )
170
184
) {
@@ -181,7 +195,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
181
195
$ beforeCurrentCombo = false ;
182
196
} elseif ($ beforeCurrentCombo && !$ group ->resetsGroupCounter ()) {
183
197
$ group ->forceNonOptional ();
184
- } elseif ($ group ->getAlternationId () === $ onlyTopLevelAlternationId && !$ this ->containsUnmatchedAsNull ($ flags ?? 0 )) {
198
+ } elseif ($ group ->getAlternationId () === $ onlyTopLevelAlternationId && !$ this ->containsUnmatchedAsNull ($ flags ?? 0 , $ matchesAll )) {
185
199
unset($ comboList [$ groupId ]);
186
200
}
187
201
}
@@ -192,6 +206,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
192
206
$ trailingOptionals ,
193
207
$ flags ?? 0 ,
194
208
$ markVerbs ,
209
+ $ matchesAll ,
195
210
);
196
211
197
212
$ combiTypes [] = $ combiType ;
@@ -202,7 +217,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
202
217
}
203
218
}
204
219
205
- if ($ isOptionalAlternation && !$ this ->containsUnmatchedAsNull ($ flags ?? 0 )) {
220
+ if ($ isOptionalAlternation && !$ this ->containsUnmatchedAsNull ($ flags ?? 0 , $ matchesAll )) {
206
221
$ combiTypes [] = new ConstantArrayType ([new ConstantIntegerType (0 )], [new StringType ()], [0 ], [], true );
207
222
}
208
223
@@ -215,6 +230,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
215
230
$ trailingOptionals ,
216
231
$ flags ?? 0 ,
217
232
$ markVerbs ,
233
+ $ matchesAll ,
218
234
);
219
235
}
220
236
@@ -278,23 +294,24 @@ private function buildArrayType(
278
294
int $ trailingOptionals ,
279
295
int $ flags ,
280
296
array $ markVerbs ,
297
+ bool $ matchesAll ,
281
298
): Type
282
299
{
283
300
$ builder = ConstantArrayTypeBuilder::createEmpty ();
284
301
285
302
// first item in matches contains the overall match.
286
303
$ builder ->setOffsetValueType (
287
304
$ this ->getKeyType (0 ),
288
- TypeCombinator:: removeNull ( $ this ->getValueType ( new StringType () , $ flags) ),
289
- ! $ wasMatched -> yes ( ),
305
+ $ this ->createSubjectValueType ( $ wasMatched , $ flags, $ matchesAll ),
306
+ $ this -> isSubjectOptional ( $ wasMatched , $ matchesAll ),
290
307
);
291
308
292
309
$ countGroups = count ($ captureGroups );
293
310
$ i = 0 ;
294
311
foreach ($ captureGroups as $ captureGroup ) {
295
312
$ isTrailingOptional = $ i >= $ countGroups - $ trailingOptionals ;
296
- $ groupValueType = $ this ->createGroupValueType ($ captureGroup , $ wasMatched , $ flags , $ isTrailingOptional );
297
- $ optional = $ this ->isGroupOptional ($ captureGroup , $ wasMatched , $ flags , $ isTrailingOptional );
313
+ $ groupValueType = $ this ->createGroupValueType ($ captureGroup , $ wasMatched , $ flags , $ isTrailingOptional, $ matchesAll );
314
+ $ optional = $ this ->isGroupOptional ($ captureGroup , $ wasMatched , $ flags , $ isTrailingOptional, $ matchesAll );
298
315
299
316
if ($ captureGroup ->isNamed ()) {
300
317
$ builder ->setOffsetValueType (
@@ -325,17 +342,61 @@ private function buildArrayType(
325
342
);
326
343
}
327
344
345
+ if ($ matchesAll && $ this ->containsSetOrder ($ flags )) {
346
+ $ arrayType = AccessoryArrayListType::intersectWith (new ArrayType (new IntegerType (), $ builder ->getArray ()));
347
+ if (!$ wasMatched ->yes ()) {
348
+ $ arrayType = TypeCombinator::union (
349
+ new ConstantArrayType ([], []),
350
+ $ arrayType ,
351
+ );
352
+ }
353
+ return $ arrayType ;
354
+ }
355
+
328
356
return $ builder ->getArray ();
329
357
}
330
358
331
- private function isGroupOptional (RegexCapturingGroup $ captureGroup , TrinaryLogic $ wasMatched , int $ flags , bool $ isTrailingOptional ): bool
359
+ private function isSubjectOptional (TrinaryLogic $ wasMatched , bool $ matchesAll ): bool
360
+ {
361
+ if ($ matchesAll ) {
362
+ return false ;
363
+ }
364
+
365
+ return !$ wasMatched ->yes ();
366
+ }
367
+
368
+ private function createSubjectValueType (TrinaryLogic $ wasMatched , int $ flags , bool $ matchesAll ): Type
332
369
{
370
+ $ subjectValueType = TypeCombinator::removeNull ($ this ->getValueType (new StringType (), $ flags , $ matchesAll ));
371
+
372
+ if ($ matchesAll ) {
373
+ if (!$ wasMatched ->yes ()) {
374
+ $ subjectValueType = TypeCombinator::union ($ subjectValueType , new ConstantStringType ('' ));
375
+ }
376
+ if ($ this ->containsPatternOrder ($ flags )) {
377
+ $ subjectValueType = AccessoryArrayListType::intersectWith (new ArrayType (new IntegerType (), $ subjectValueType ));
378
+ }
379
+ }
380
+
381
+ return $ subjectValueType ;
382
+ }
383
+
384
+ private function isGroupOptional (RegexCapturingGroup $ captureGroup , TrinaryLogic $ wasMatched , int $ flags , bool $ isTrailingOptional , bool $ matchesAll ): bool
385
+ {
386
+ if ($ matchesAll ) {
387
+ if ($ isTrailingOptional && !$ this ->containsUnmatchedAsNull ($ flags , $ matchesAll ) && $ this ->containsSetOrder ($ flags )) {
388
+ return true ;
389
+ }
390
+
391
+ return false ;
392
+ }
393
+
333
394
if (!$ wasMatched ->yes ()) {
334
395
$ optional = true ;
335
396
} else {
336
397
if (!$ isTrailingOptional ) {
337
398
$ optional = false ;
338
- } elseif ($ this ->containsUnmatchedAsNull ($ flags )) {
399
+ } elseif ($ this ->containsUnmatchedAsNull ($ flags, $ matchesAll )) {
339
400
$ optional = false ;
340
401
} else {
341
402
$ optional = $ captureGroup ->isOptional ();
@@ -345,25 +406,59 @@ private function isGroupOptional(RegexCapturingGroup $captureGroup, TrinaryLogic
345
406
return $ optional ;
346
407
}
347
408
348
- private function createGroupValueType (RegexCapturingGroup $ captureGroup , TrinaryLogic $ wasMatched , int $ flags , bool $ isTrailingOptional ): Type
409
+ private function createGroupValueType (RegexCapturingGroup $ captureGroup , TrinaryLogic $ wasMatched , int $ flags , bool $ isTrailingOptional, bool $ matchesAll ): Type
349
410
{
350
- $ groupValueType = $ this ->getValueType ($ captureGroup ->getType (), $ flags );
411
+ $ groupValueType = $ this ->getValueType ($ captureGroup ->getType (), $ flags , $ matchesAll );
412
+
413
+ if ($ matchesAll ) {
414
+ if (!$ isTrailingOptional && $ this ->containsUnmatchedAsNull ($ flags , $ matchesAll ) && !$ captureGroup ->isOptional ()) {
415
+ $ groupValueType = TypeCombinator::removeNull ($ groupValueType );
416
+ }
417
+
418
+ if (!$ this ->containsSetOrder ($ flags ) && !$ this ->containsUnmatchedAsNull ($ flags , $ matchesAll ) && $ captureGroup ->isOptional ()) {
419
+ $ groupValueType = TypeCombinator::removeNull ($ groupValueType );
420
+ $ groupValueType = TypeCombinator::union ($ groupValueType , new ConstantStringType ('' ));
421
+ }
422
+
423
+ if ($ this ->containsPatternOrder ($ flags )) {
424
+ $ groupValueType = AccessoryArrayListType::intersectWith (new ArrayType (new IntegerType (), $ groupValueType ));
425
+ }
426
+
427
+ return $ groupValueType ;
428
+ }
351
429
352
430
if ($ wasMatched ->yes ()) {
353
- if (!$ isTrailingOptional && $ this ->containsUnmatchedAsNull ($ flags ) && !$ captureGroup ->isOptional ()) {
431
+ if (!$ isTrailingOptional && $ this ->containsUnmatchedAsNull ($ flags, $ matchesAll ) && !$ captureGroup ->isOptional ()) {
354
432
$ groupValueType = TypeCombinator::removeNull ($ groupValueType );
355
433
}
356
434
}
357
435
358
- if (!$ isTrailingOptional && !$ this ->containsUnmatchedAsNull ($ flags ) && $ captureGroup ->isOptional ()) {
436
+ if (!$ isTrailingOptional && !$ this ->containsUnmatchedAsNull ($ flags, $ matchesAll ) && $ captureGroup ->isOptional ()) {
359
437
$ groupValueType = TypeCombinator::union ($ groupValueType , new ConstantStringType ('' ));
360
438
}
361
439
362
440
return $ groupValueType ;
363
441
}
364
442
365
- private function containsUnmatchedAsNull (int $ flags ): bool
443
+ private function containsPatternOrder (int $ flags ): bool
444
+ {
445
+ // If no order flag is given, PREG_PATTERN_ORDER is assumed.
446
+ return !$ this ->containsSetOrder ($ flags );
447
+ }
448
+
449
+ private function containsSetOrder (int $ flags ): bool
366
450
{
451
+ return ($ flags & PREG_SET_ORDER ) !== 0 ;
452
+ }
453
+
454
+ private function containsUnmatchedAsNull (int $ flags , bool $ matchesAll ): bool
455
+ {
456
+ if ($ matchesAll ) {
457
+ // preg_match_all() with PREG_UNMATCHED_AS_NULL works consistently across php-versions
458
+ // https://3v4l.org/tKmPn
459
+ return ($ flags & PREG_UNMATCHED_AS_NULL ) !== 0 ;
460
+ }
461
+
367
462
return ($ flags & PREG_UNMATCHED_AS_NULL ) !== 0 && (($ flags & self ::PREG_UNMATCHED_AS_NULL_ON_72_73 ) !== 0 || $ this ->phpVersion ->supportsPregUnmatchedAsNull ());
368
463
}
369
464
@@ -376,12 +471,12 @@ private function getKeyType(int|string $key): Type
376
471
return new ConstantIntegerType ($ key );
377
472
}
378
473
379
- private function getValueType (Type $ baseType , int $ flags ): Type
474
+ private function getValueType (Type $ baseType , int $ flags, bool $ matchesAll ): Type
380
475
{
381
476
$ valueType = $ baseType ;
382
477
383
478
$ offsetType = IntegerRangeType::fromInterval (0 , null );
384
- if ($ this ->containsUnmatchedAsNull ($ flags )) {
479
+ if ($ this ->containsUnmatchedAsNull ($ flags, $ matchesAll )) {
385
480
$ valueType = TypeCombinator::addNull ($ valueType );
386
481
// unmatched groups return -1 as offset
387
482
$ offsetType = IntegerRangeType::fromInterval (-1 , null );
0 commit comments