Skip to content

Commit 88bbba9

Browse files
authored
Merge branch refs/heads/1.11.x into 1.12.x
2 parents 2d04c15 + c6c06b1 commit 88bbba9

File tree

8 files changed

+135
-73
lines changed

8 files changed

+135
-73
lines changed

src/Type/Php/RegexGroupParser.php

Lines changed: 52 additions & 14 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;
@@ -89,6 +90,7 @@ public function parseGroups(string $regex): ?array
8990
$groupCombinations,
9091
$markVerbs,
9192
$captureOnlyNamed,
93+
false,
9294
);
9395

9496
return [$capturingGroups, $groupCombinations, $markVerbs];
@@ -111,28 +113,39 @@ private function walkRegexAst(
111113
array &$groupCombinations,
112114
array &$markVerbs,
113115
bool $captureOnlyNamed,
116+
bool $repeatedMoreThanOnce,
114117
): void
115118
{
116119
$group = null;
117120
if ($ast->getId() === '#capturing') {
121+
$maybeConstant = !$repeatedMoreThanOnce;
122+
if ($parentGroup !== null && $parentGroup->resetsGroupCounter()) {
123+
$maybeConstant = false;
124+
}
125+
118126
$group = new RegexCapturingGroup(
119127
$captureGroupId++,
120128
null,
121129
$inAlternation ? $alternationId : null,
122130
$inOptionalQuantification,
123131
$parentGroup,
124-
$this->createGroupType($ast),
132+
$this->createGroupType($ast, $maybeConstant),
125133
);
126134
$parentGroup = $group;
127135
} elseif ($ast->getId() === '#namedcapturing') {
136+
$maybeConstant = !$repeatedMoreThanOnce;
137+
if ($parentGroup !== null && $parentGroup->resetsGroupCounter()) {
138+
$maybeConstant = false;
139+
}
140+
128141
$name = $ast->getChild(0)->getValueValue();
129142
$group = new RegexCapturingGroup(
130143
$captureGroupId++,
131144
$name,
132145
$inAlternation ? $alternationId : null,
133146
$inOptionalQuantification,
134147
$parentGroup,
135-
$this->createGroupType($ast),
148+
$this->createGroupType($ast, $maybeConstant),
136149
);
137150
$parentGroup = $group;
138151
} elseif ($ast->getId() === '#noncapturing') {
@@ -155,11 +168,15 @@ private function walkRegexAst(
155168

156169
$inOptionalQuantification = false;
157170
if ($ast->getId() === '#quantification') {
158-
[$min] = $this->getQuantificationRange($ast);
171+
[$min, $max] = $this->getQuantificationRange($ast);
159172

160173
if ($min === 0) {
161174
$inOptionalQuantification = true;
162175
}
176+
177+
if ($max === null || $max > 1) {
178+
$repeatedMoreThanOnce = true;
179+
}
163180
}
164181

165182
if ($ast->getId() === '#alternation') {
@@ -200,6 +217,7 @@ private function walkRegexAst(
200217
$groupCombinations,
201218
$markVerbs,
202219
$captureOnlyNamed,
220+
$repeatedMoreThanOnce,
203221
);
204222

205223
if ($ast->getId() !== '#alternation') {
@@ -259,13 +277,18 @@ private function getQuantificationRange(TreeNode $node): array
259277
return [$min, $max];
260278
}
261279

262-
private function createGroupType(TreeNode $group): Type
280+
private function createGroupType(TreeNode $group, bool $maybeConstant): Type
263281
{
264282
$isNonEmpty = TrinaryLogic::createMaybe();
265283
$isNumeric = TrinaryLogic::createMaybe();
266284
$inOptionalQuantification = false;
285+
$onlyLiterals = [];
286+
287+
$this->walkGroupAst($group, $isNonEmpty, $isNumeric, $inOptionalQuantification, $onlyLiterals);
267288

268-
$this->walkGroupAst($group, $isNonEmpty, $isNumeric, $inOptionalQuantification);
289+
if ($maybeConstant && $onlyLiterals !== null && $onlyLiterals !== []) {
290+
return new ConstantStringType(implode('', $onlyLiterals));
291+
}
269292

270293
if ($isNumeric->yes()) {
271294
$result = new IntersectionType([new StringType(), new AccessoryNumericStringType()]);
@@ -280,7 +303,10 @@ private function createGroupType(TreeNode $group): Type
280303
return new StringType();
281304
}
282305

283-
private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryLogic &$isNumeric, bool &$inOptionalQuantification): void
306+
/**
307+
* @param array<string>|null $onlyLiterals
308+
*/
309+
private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryLogic &$isNumeric, bool &$inOptionalQuantification, ?array &$onlyLiterals): void
284310
{
285311
$children = $ast->getChildren();
286312

@@ -289,9 +315,7 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL
289315
&& count($children) > 0
290316
) {
291317
$isNonEmpty = TrinaryLogic::createYes();
292-
}
293-
294-
if ($ast->getId() === '#quantification') {
318+
} elseif ($ast->getId() === '#quantification') {
295319
[$min] = $this->getQuantificationRange($ast);
296320

297321
if ($min === 0) {
@@ -301,10 +325,10 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL
301325
$isNonEmpty = TrinaryLogic::createYes();
302326
$inOptionalQuantification = false;
303327
}
304-
}
305328

306-
if ($ast->getId() === 'token') {
307-
$literalValue = $this->getLiteralValue($ast);
329+
$onlyLiterals = null;
330+
} elseif ($ast->getId() === 'token') {
331+
$literalValue = $this->getLiteralValue($ast, $onlyLiterals);
308332
if ($literalValue !== null) {
309333
if (Strings::match($literalValue, '/^\d+$/') === null) {
310334
$isNumeric = TrinaryLogic::createNo();
@@ -315,7 +339,11 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL
315339
if (!$inOptionalQuantification) {
316340
$isNonEmpty = TrinaryLogic::createYes();
317341
}
342+
} elseif (!in_array($ast->getValueToken(), ['capturing_name'], true)) {
343+
$onlyLiterals = null;
318344
}
345+
} elseif (!in_array($ast->getId(), ['#capturing', '#namedcapturing'], true)) {
346+
$onlyLiterals = null;
319347
}
320348

321349
// [^0-9] should not parse as numeric-string, and [^list-everything-but-numbers] is technically
@@ -331,11 +359,15 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL
331359
$isNonEmpty,
332360
$isNumeric,
333361
$inOptionalQuantification,
362+
$onlyLiterals,
334363
);
335364
}
336365
}
337366

338-
private function getLiteralValue(TreeNode $node): ?string
367+
/**
368+
* @param array<string>|null $onlyLiterals
369+
*/
370+
private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals): ?string
339371
{
340372
if ($node->getId() !== 'token') {
341373
return null;
@@ -346,8 +378,14 @@ private function getLiteralValue(TreeNode $node): ?string
346378
$value = $node->getValueValue();
347379

348380
if (in_array($token, ['literal', 'escaped_end_class'], true)) {
349-
if (strlen($node->getValueValue()) > 1 && $value[0] === '\\') {
381+
if (strlen($value) > 1 && $value[0] === '\\') {
350382
return substr($value, 1);
383+
} elseif (
384+
$token === 'literal'
385+
&& $onlyLiterals !== null
386+
&& !in_array($value, ['.'], true)
387+
) {
388+
$onlyLiterals[] = $value;
351389
}
352390

353391
return $value;

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@ function doFoo(string $s) {
1414

1515
function doUnmatchedAsNull(string $s): void {
1616
if (preg_match('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_UNMATCHED_AS_NULL)) {
17-
assertType('array{0: string, 1?: non-empty-string, 2?: non-empty-string, 3?: non-empty-string}', $matches);
17+
assertType("array{0: string, 1?: 'foo', 2?: 'bar', 3?: 'baz'}", $matches);
1818
}
19-
assertType('array{}|array{0: string, 1?: non-empty-string, 2?: non-empty-string, 3?: non-empty-string}', $matches);
19+
assertType("array{}|array{0: string, 1?: 'foo', 2?: 'bar', 3?: 'baz'}", $matches);
2020
}
2121

2222
// see https://3v4l.org/VeDob#veol
2323
function unmatchedAsNullWithOptionalGroup(string $s): void {
2424
if (preg_match('/Price: (£|€)?\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) {
25-
assertType('array{0: string, 1?: non-empty-string}', $matches);
25+
assertType("array{0: string, 1?: non-empty-string}", $matches);
2626
} else {
2727
assertType('array{}', $matches);
2828
}
29-
assertType('array{}|array{0: string, 1?: non-empty-string}', $matches);
29+
assertType("array{}|array{0: string, 1?: non-empty-string}", $matches);
3030
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ function doFoo(string $s) {
1414

1515
function doUnmatchedAsNull(string $s): void {
1616
if (preg_match('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_UNMATCHED_AS_NULL)) {
17-
assertType('array{string, non-empty-string|null, non-empty-string|null, non-empty-string|null}', $matches);
17+
assertType("array{string, 'foo'|null, 'bar'|null, 'baz'|null}", $matches);
1818
}
19-
assertType('array{}|array{string, non-empty-string|null, non-empty-string|null, non-empty-string|null}', $matches);
19+
assertType("array{}|array{string, 'foo'|null, 'bar'|null, 'baz'|null}", $matches);
2020
}
2121

2222
// see https://3v4l.org/VeDob
@@ -70,13 +70,13 @@ function bug11331c(string $url):void {
7070
class UnmatchedAsNullWithTopLevelAlternation {
7171
function doFoo(string $s): void {
7272
if (preg_match('/Price: (?:(£)|(€))\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) {
73-
assertType('array{string, non-empty-string|null, non-empty-string|null}', $matches); // could be array{0: string, 1: null, 2: non-empty-string}|array{0: string, 1: non-empty-string, 2: null}
73+
assertType("array{string, '£'|null, '€'|null}", $matches); // could be tagged union
7474
}
7575
}
7676

7777
function doBar(string $s): void {
7878
if (preg_match('/Price: (?:(£)|(€))?\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) {
79-
assertType('array{string, non-empty-string|null, non-empty-string|null}', $matches);
79+
assertType("array{string, '£'|null, '€'|null}", $matches); // could be tagged union
8080
}
8181
}
8282
}

tests/PHPStan/Analyser/nsrt/bug11384.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class HelloWorld
1414
public function sayHello(string $s): void
1515
{
1616
if (preg_match('{(' . Bar::VAL . ')}', $s, $m)) {
17-
assertType('array{string, numeric-string}', $m);
17+
assertType("array{string, '3'}", $m);
1818
}
1919
}
2020
}

tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,65 +71,65 @@ function (string $size): void {
7171

7272
function (string $size): void {
7373
preg_match_all('/a(b)(\d+)?/', $size, $matches, PREG_SET_ORDER);
74-
assertType("list<array{0: string, 1: non-empty-string, 2?: numeric-string}>", $matches);
74+
assertType("list<array{0: string, 1: 'b', 2?: numeric-string}>", $matches);
7575
};
7676

7777
function (string $size): void {
7878
if (preg_match_all('/ab(?P<num>\d+)(?P<suffix>ab)?/', $size, $matches)) {
79-
assertType("array{0: list<string>, num: list<numeric-string>, 1: list<numeric-string>, suffix: list<string>, 2: list<string>}", $matches);
79+
assertType("array{0: list<string>, num: list<numeric-string>, 1: list<numeric-string>, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $matches);
8080
}
8181
};
8282

8383
function (string $size): void {
8484
if (preg_match_all('/ab(?P<num>\d+)(?P<suffix>ab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL)) {
85-
assertType("array{0: list<string>, num: list<numeric-string>, 1: list<numeric-string>, suffix: list<non-empty-string|null>, 2: list<non-empty-string|null>}", $matches);
85+
assertType("array{0: list<string>, num: list<numeric-string>, 1: list<numeric-string>, suffix: list<'ab'|null>, 2: list<'ab'|null>}", $matches);
8686
}
8787
};
8888

8989
function (string $size): void {
9090
if (preg_match_all('/ab(?P<num>\d+)(?P<suffix>ab)?/', $size, $matches, PREG_SET_ORDER)) {
91-
assertType("list<array{0: string, num: numeric-string, 1: numeric-string, suffix?: non-empty-string, 2?: non-empty-string}>", $matches);
91+
assertType("list<array{0: string, num: numeric-string, 1: numeric-string, suffix?: 'ab', 2?: 'ab'}>", $matches);
9292
}
9393
};
9494

9595
function (string $size): void {
9696
if (preg_match_all('/ab(?P<num>\d+)(?P<suffix>ab)?/', $size, $matches, PREG_PATTERN_ORDER)) {
97-
assertType("array{0: list<string>, num: list<numeric-string>, 1: list<numeric-string>, suffix: list<string>, 2: list<string>}", $matches);
97+
assertType("array{0: list<string>, num: list<numeric-string>, 1: list<numeric-string>, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $matches);
9898
}
9999
};
100100

101101
function (string $size): void {
102102
if (preg_match_all('/ab(?P<num>\d+)(?P<suffix>ab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_SET_ORDER)) {
103-
assertType("list<array{0: string, num: numeric-string, 1: numeric-string, suffix: non-empty-string|null, 2: non-empty-string|null}>", $matches);
103+
assertType("list<array{0: string, num: numeric-string, 1: numeric-string, suffix: 'ab'|null, 2: 'ab'|null}>", $matches);
104104
}
105105
};
106106

107107
function (string $size): void {
108108
if (preg_match_all('/ab(?P<num>\d+)(?P<suffix>ab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_PATTERN_ORDER)) {
109-
assertType("array{0: list<string>, num: list<numeric-string>, 1: list<numeric-string>, suffix: list<non-empty-string|null>, 2: list<non-empty-string|null>}", $matches);
109+
assertType("array{0: list<string>, num: list<numeric-string>, 1: list<numeric-string>, suffix: list<'ab'|null>, 2: list<'ab'|null>}", $matches);
110110
}
111111
};
112112

113113
function (string $size): void {
114114
if (preg_match_all('/ab(?P<num>\d+)(?P<suffix>ab)?/', $size, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) {
115-
assertType("list<array{0: array{string, int<0, max>}, num: array{numeric-string, int<0, max>}, 1: array{numeric-string, int<0, max>}, suffix?: array{non-empty-string, int<0, max>}, 2?: array{non-empty-string, int<0, max>}}>", $matches);
115+
assertType("list<array{0: array{string, int<0, max>}, num: array{numeric-string, int<0, max>}, 1: array{numeric-string, int<0, max>}, suffix?: array{'ab', int<0, max>}, 2?: array{'ab', int<0, max>}}>", $matches);
116116
}
117117
};
118118

119119
function (string $size): void {
120120
if (preg_match_all('/ab(?P<num>\d+)(?P<suffix>ab)?/', $size, $matches, PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE)) {
121-
assertType("array{0: list<array{string, int<0, max>}>, num: list<array{numeric-string, int<0, max>}>, 1: list<array{numeric-string, int<0, max>}>, suffix: list<''|array{non-empty-string, int<0, max>}>, 2: list<''|array{non-empty-string, int<0, max>}>}", $matches);
121+
assertType("array{0: list<array{string, int<0, max>}>, num: list<array{numeric-string, int<0, max>}>, 1: list<array{numeric-string, int<0, max>}>, suffix: list<''|array{'ab', int<0, max>}>, 2: list<''|array{'ab', int<0, max>}>}", $matches);
122122
}
123123
};
124124

125125
function (string $size): void {
126126
if (preg_match_all('/ab(?P<num>\d+)(?P<suffix>ab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) {
127-
assertType("list<array{0: array{string|null, int<-1, max>}, num: array{numeric-string|null, int<-1, max>}, 1: array{numeric-string|null, int<-1, max>}, suffix: array{non-empty-string|null, int<-1, max>}, 2: array{non-empty-string|null, int<-1, max>}}>", $matches);
127+
assertType("list<array{0: array{string|null, int<-1, max>}, num: array{numeric-string|null, int<-1, max>}, 1: array{numeric-string|null, int<-1, max>}, suffix: array{'ab'|null, int<-1, max>}, 2: array{'ab'|null, int<-1, max>}}>", $matches);
128128
}
129129
};
130130

131131
function (string $size): void {
132132
if (preg_match_all('/ab(?P<num>\d+)(?P<suffix>ab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE)) {
133-
assertType("array{0: list<array{string|null, int<-1, max>}>, num: list<array{numeric-string|null, int<-1, max>}>, 1: list<array{numeric-string|null, int<-1, max>}>, suffix: list<array{non-empty-string|null, int<-1, max>}>, 2: list<array{non-empty-string|null, int<-1, max>}>}", $matches);
133+
assertType("array{0: list<array{string|null, int<-1, max>}>, num: list<array{numeric-string|null, int<-1, max>}>, 1: list<array{numeric-string|null, int<-1, max>}>, suffix: list<array{'ab'|null, int<-1, max>}>, 2: list<array{'ab'|null, int<-1, max>}>}", $matches);
134134
}
135135
};

0 commit comments

Comments
 (0)