Skip to content

Commit b307034

Browse files
committed
fix
1 parent 30b63e3 commit b307034

File tree

7 files changed

+79
-62
lines changed

7 files changed

+79
-62
lines changed

src/Type/Php/RegexGroupParser.php

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ public function parseGroups(string $regex): ?array
9090
$groupCombinations,
9191
$markVerbs,
9292
$captureOnlyNamed,
93+
false,
9394
);
9495

9596
return [$capturingGroups, $groupCombinations, $markVerbs];
@@ -112,6 +113,7 @@ private function walkRegexAst(
112113
array &$groupCombinations,
113114
array &$markVerbs,
114115
bool $captureOnlyNamed,
116+
bool $repeatedMoreThenOnce,
115117
): void
116118
{
117119
$group = null;
@@ -122,7 +124,7 @@ private function walkRegexAst(
122124
$inAlternation ? $alternationId : null,
123125
$inOptionalQuantification,
124126
$parentGroup,
125-
$this->createGroupType($ast),
127+
$this->createGroupType($ast, $repeatedMoreThenOnce),
126128
);
127129
$parentGroup = $group;
128130
} elseif ($ast->getId() === '#namedcapturing') {
@@ -133,7 +135,7 @@ private function walkRegexAst(
133135
$inAlternation ? $alternationId : null,
134136
$inOptionalQuantification,
135137
$parentGroup,
136-
$this->createGroupType($ast),
138+
$this->createGroupType($ast, $repeatedMoreThenOnce),
137139
);
138140
$parentGroup = $group;
139141
} elseif ($ast->getId() === '#noncapturing') {
@@ -156,11 +158,15 @@ private function walkRegexAst(
156158

157159
$inOptionalQuantification = false;
158160
if ($ast->getId() === '#quantification') {
159-
[$min] = $this->getQuantificationRange($ast);
161+
[$min, $max] = $this->getQuantificationRange($ast);
160162

161163
if ($min === 0) {
162164
$inOptionalQuantification = true;
163165
}
166+
167+
if ($max === null || $max > 1) {
168+
$repeatedMoreThenOnce = true;
169+
}
164170
}
165171

166172
if ($ast->getId() === '#alternation') {
@@ -201,6 +207,7 @@ private function walkRegexAst(
201207
$groupCombinations,
202208
$markVerbs,
203209
$captureOnlyNamed,
210+
$repeatedMoreThenOnce,
204211
);
205212

206213
if ($ast->getId() !== '#alternation') {
@@ -260,7 +267,7 @@ private function getQuantificationRange(TreeNode $node): array
260267
return [$min, $max];
261268
}
262269

263-
private function createGroupType(TreeNode $group): Type
270+
private function createGroupType(TreeNode $group, bool $repeatedMoreThenOnce): Type
264271
{
265272
$isNonEmpty = TrinaryLogic::createMaybe();
266273
$isNumeric = TrinaryLogic::createMaybe();
@@ -269,9 +276,10 @@ private function createGroupType(TreeNode $group): Type
269276

270277
$this->walkGroupAst($group, $isNonEmpty, $isNumeric, $inOptionalQuantification, $onlyLiterals);
271278

272-
if ($onlyLiterals !== null && $onlyLiterals !== []) {
279+
if (!$repeatedMoreThenOnce && $onlyLiterals !== null && $onlyLiterals !== []) {
273280
return new ConstantStringType(implode('', $onlyLiterals));
274281
}
282+
275283
if ($isNumeric->yes()) {
276284
$result = new IntersectionType([new StringType(), new AccessoryNumericStringType()]);
277285
if (!$isNonEmpty->yes()) {
@@ -285,6 +293,9 @@ private function createGroupType(TreeNode $group): Type
285293
return new StringType();
286294
}
287295

296+
/**
297+
* @param array<string>|null $onlyLiterals
298+
*/
288299
private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryLogic &$isNumeric, bool &$inOptionalQuantification, ?array &$onlyLiterals): void
289300
{
290301
$children = $ast->getChildren();
@@ -294,7 +305,6 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL
294305
&& count($children) > 0
295306
) {
296307
$isNonEmpty = TrinaryLogic::createYes();
297-
$onlyLiterals = null;
298308
} elseif ($ast->getId() === '#quantification') {
299309
[$min] = $this->getQuantificationRange($ast);
300310

@@ -319,10 +329,10 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL
319329
if (!$inOptionalQuantification) {
320330
$isNonEmpty = TrinaryLogic::createYes();
321331
}
322-
} else {
332+
} elseif (!in_array($ast->getValueToken(), ['capturing_name'], true)) {
323333
$onlyLiterals = null;
324334
}
325-
} elseif (!in_array($ast->getId(), ['#capturing'], true)) {
335+
} elseif (!in_array($ast->getId(), ['#capturing', '#namedcapturing'], true)) {
326336
$onlyLiterals = null;
327337
} else {
328338
$x = 1;
@@ -346,6 +356,9 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL
346356
}
347357
}
348358

359+
/**
360+
* @param array<string>|null $onlyLiterals
361+
*/
349362
private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals): ?string
350363
{
351364
if ($node->getId() !== 'token') {
@@ -357,9 +370,13 @@ private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals): ?string
357370
$value = $node->getValueValue();
358371

359372
if (in_array($token, ['literal', 'escaped_end_class'], true)) {
360-
if (strlen($node->getValueValue()) > 1 && $value[0] === '\\') {
373+
if (strlen($value) > 1 && $value[0] === '\\') {
361374
return substr($value, 1);
362-
} elseif ($token === 'literal' && $onlyLiterals !== null && !in_array($value, ['.'], true)) {
375+
} elseif (
376+
$token === 'literal'
377+
&& $onlyLiterals !== null
378+
&& !in_array($value, ['.'], true)
379+
) {
363380
$onlyLiterals[] = $value;
364381
}
365382

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)