Skip to content

Commit f3810d9

Browse files
authored
Merge branch refs/heads/1.11.x into 1.12.x
2 parents ef59974 + 2acc115 commit f3810d9

File tree

3 files changed

+103
-14
lines changed

3 files changed

+103
-14
lines changed

Diff for: src/Type/Php/RegexGroupParser.php

+77-11
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
use function str_replace;
3131
use function strlen;
3232
use function substr;
33+
use function trim;
3334

3435
final class RegexGroupParser
3536
{
@@ -126,7 +127,11 @@ private function walkRegexAst(
126127
$inAlternation ? $alternationId : null,
127128
$inOptionalQuantification,
128129
$parentGroup,
129-
$this->createGroupType($ast, $this->allowConstantTypes($patternModifiers, $repeatedMoreThanOnce, $parentGroup)),
130+
$this->createGroupType(
131+
$ast,
132+
$this->allowConstantTypes($patternModifiers, $repeatedMoreThanOnce, $parentGroup),
133+
$patternModifiers,
134+
),
130135
);
131136
$parentGroup = $group;
132137
} elseif ($ast->getId() === '#namedcapturing') {
@@ -137,7 +142,11 @@ private function walkRegexAst(
137142
$inAlternation ? $alternationId : null,
138143
$inOptionalQuantification,
139144
$parentGroup,
140-
$this->createGroupType($ast, $this->allowConstantTypes($patternModifiers, $repeatedMoreThanOnce, $parentGroup)),
145+
$this->createGroupType(
146+
$ast,
147+
$this->allowConstantTypes($patternModifiers, $repeatedMoreThanOnce, $parentGroup),
148+
$patternModifiers,
149+
),
141150
);
142151
$parentGroup = $group;
143152
} elseif ($ast->getId() === '#noncapturing') {
@@ -293,7 +302,7 @@ private function getQuantificationRange(TreeNode $node): array
293302
return [$min, $max];
294303
}
295304

296-
private function createGroupType(TreeNode $group, bool $maybeConstant): Type
305+
private function createGroupType(TreeNode $group, bool $maybeConstant, string $patternModifiers): Type
297306
{
298307
$isNonEmpty = TrinaryLogic::createMaybe();
299308
$isNonFalsy = TrinaryLogic::createMaybe();
@@ -310,6 +319,7 @@ private function createGroupType(TreeNode $group, bool $maybeConstant): Type
310319
$inOptionalQuantification,
311320
$onlyLiterals,
312321
false,
322+
$patternModifiers,
313323
);
314324

315325
if ($maybeConstant && $onlyLiterals !== null && $onlyLiterals !== []) {
@@ -356,6 +366,7 @@ private function walkGroupAst(
356366
bool &$inOptionalQuantification,
357367
?array &$onlyLiterals,
358368
bool $inClass,
369+
string $patternModifiers,
359370
): void
360371
{
361372
$children = $ast->getChildren();
@@ -364,9 +375,31 @@ private function walkGroupAst(
364375
$ast->getId() === '#concatenation'
365376
&& count($children) > 0
366377
) {
367-
$isNonEmpty = TrinaryLogic::createYes();
368-
if (!$inAlternation) {
378+
$meaningfulTokens = 0;
379+
foreach ($children as $child) {
380+
$nonFalsy = false;
381+
if ($this->isMaybeEmptyNode($child, $patternModifiers, $nonFalsy)) {
382+
continue;
383+
}
384+
385+
$meaningfulTokens++;
386+
387+
if (!$nonFalsy || $inAlternation) {
388+
continue;
389+
}
390+
391+
// a single token non-falsy on its own
369392
$isNonFalsy = TrinaryLogic::createYes();
393+
break;
394+
}
395+
396+
if ($meaningfulTokens > 0) {
397+
$isNonEmpty = TrinaryLogic::createYes();
398+
399+
// two non-empty tokens concatenated results in a non-falsy string
400+
if ($meaningfulTokens > 1 && !$inAlternation) {
401+
$isNonFalsy = TrinaryLogic::createYes();
402+
}
370403
}
371404
} elseif ($ast->getId() === '#quantification') {
372405
[$min] = $this->getQuantificationRange($ast);
@@ -390,17 +423,14 @@ private function walkGroupAst(
390423
foreach ($children as $child) {
391424
$oldLiterals = $onlyLiterals;
392425

393-
if ($child->getId() === 'token') {
394-
$this->getLiteralValue($child, $oldLiterals, true);
395-
}
396-
426+
$this->getLiteralValue($child, $oldLiterals, true, $patternModifiers);
397427
foreach ($oldLiterals ?? [] as $oldLiteral) {
398428
$newLiterals[] = $oldLiteral;
399429
}
400430
}
401431
$onlyLiterals = $newLiterals;
402432
} elseif ($ast->getId() === 'token') {
403-
$literalValue = $this->getLiteralValue($ast, $onlyLiterals, !$inClass);
433+
$literalValue = $this->getLiteralValue($ast, $onlyLiterals, !$inClass, $patternModifiers);
404434
if ($literalValue !== null) {
405435
if (Strings::match($literalValue, '/^\d+$/') === null) {
406436
$isNumeric = TrinaryLogic::createNo();
@@ -439,14 +469,46 @@ private function walkGroupAst(
439469
$inOptionalQuantification,
440470
$onlyLiterals,
441471
$inClass,
472+
$patternModifiers,
442473
);
443474
}
444475
}
445476

477+
private function isMaybeEmptyNode(TreeNode $node, string $patternModifiers, bool &$isNonFalsy): bool
478+
{
479+
if ($node->getId() === '#quantification') {
480+
[$min] = $this->getQuantificationRange($node);
481+
482+
if ($min > 0) {
483+
return false;
484+
}
485+
486+
if ($min === 0) {
487+
return true;
488+
}
489+
}
490+
491+
$literal = $this->getLiteralValue($node, $onlyLiterals, false, $patternModifiers);
492+
if ($literal !== null) {
493+
if ($literal !== '' && $literal !== '0') {
494+
$isNonFalsy = true;
495+
}
496+
return false;
497+
}
498+
499+
foreach ($node->getChildren() as $child) {
500+
if (!$this->isMaybeEmptyNode($child, $patternModifiers, $isNonFalsy)) {
501+
return false;
502+
}
503+
}
504+
505+
return true;
506+
}
507+
446508
/**
447509
* @param array<string>|null $onlyLiterals
448510
*/
449-
private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals, bool $appendLiterals): ?string
511+
private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals, bool $appendLiterals, string $patternModifiers): ?string
450512
{
451513
if ($node->getId() !== 'token') {
452514
return null;
@@ -457,6 +519,10 @@ private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals, bool $ap
457519
$value = $node->getValueValue();
458520

459521
if (in_array($token, ['literal', 'escaped_end_class'], true)) {
522+
if (str_contains($patternModifiers, 'x') && trim($value) === '') {
523+
return null;
524+
}
525+
460526
if (strlen($value) > 1 && $value[0] === '\\') {
461527
return substr($value, 1);
462528
} elseif (

Diff for: tests/PHPStan/Analyser/nsrt/bug-11311.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -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, non-falsy-string&numeric-string}', $matches);
165+
assertType('array{string, numeric-string}', $matches);
166166
};
167167

168168
function (string $s): void {

Diff for: tests/PHPStan/Analyser/nsrt/preg_match_shapes.php

+25-2
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ function (string $size): void {
328328
function bug11277a(string $value): void
329329
{
330330
if (preg_match('/^\[(.+,?)*\]$/', $value, $matches)) {
331-
assertType('array{0: string, 1?: non-falsy-string}', $matches);
331+
assertType('array{0: string, 1?: non-empty-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-falsy-string, 2?: non-empty-string}', $matches);
341+
assertType('array{0: string, 1?: non-empty-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
}
@@ -625,3 +625,26 @@ function (string $s): void {
625625
}
626626
};
627627

628+
function (string $s): void {
629+
if (preg_match('/( \d+ )/x', $s, $matches)) {
630+
assertType('array{string, numeric-string}', $matches);
631+
}
632+
};
633+
634+
function (string $s): void {
635+
if (preg_match('/( .? )/x', $s, $matches)) {
636+
assertType('array{string, string}', $matches);
637+
}
638+
};
639+
640+
function (string $s): void {
641+
if (preg_match('/( .* )/x', $s, $matches)) {
642+
assertType('array{string, string}', $matches);
643+
}
644+
};
645+
646+
function (string $s): void {
647+
if (preg_match('/( .+ )/x', $s, $matches)) {
648+
assertType('array{string, non-empty-string}', $matches);
649+
}
650+
};

0 commit comments

Comments
 (0)