Skip to content

Commit b1c5d37

Browse files
committed
refactor
1 parent a0388d5 commit b1c5d37

File tree

6 files changed

+127
-55
lines changed

6 files changed

+127
-55
lines changed

src/Type/Php/RegexArrayShapeMatcher.php

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -107,20 +107,16 @@ private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLo
107107
*/
108108
private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched, bool $matchesAll): ?Type
109109
{
110-
$parseResult = $this->regexGroupParser->parseGroups($regex);
111-
if ($parseResult === null) {
110+
$astWalkResult = $this->regexGroupParser->parseGroups($regex);
111+
if ($astWalkResult === null) {
112112
// regex could not be parsed by Hoa/Regex
113113
return null;
114114
}
115-
[$groupList, $markVerbs] = $parseResult;
116-
117-
if ($groupList === [] && $markVerbs === []) {
118-
$rawRegex = $this->regexExpressionHelper->removeDelimitersAndModifiers($regex);
119-
$type = $this->matchRegex('{(' . $rawRegex . ')}', $flags, $wasMatched, $matchesAll);
120-
if ($type === null) {
121-
return null;
122-
}
123-
return $type->shiftArray();
115+
$groupList = $astWalkResult->getCapturingGroups();
116+
$markVerbs = $astWalkResult->getMarkVerbs();
117+
$subjectBaseType = new StringType();
118+
if ($wasMatched->yes()) {
119+
$subjectBaseType = $astWalkResult->getSubjectBaseType();
124120
}
125121

126122
$regexGroupList = new RegexGroupList($groupList);
@@ -139,6 +135,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
139135
$regexGroupList = $regexGroupList->forceGroupNonOptional($onlyOptionalTopLevelGroup);
140136

141137
$combiType = $this->buildArrayType(
138+
$subjectBaseType,
142139
$regexGroupList,
143140
$wasMatched,
144141
$trailingOptionals,
@@ -150,7 +147,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
150147
if (!$this->containsUnmatchedAsNull($flags, $matchesAll)) {
151148
// positive match has a subject but not any capturing group
152149
$combiType = TypeCombinator::union(
153-
new ConstantArrayType([new ConstantIntegerType(0)], [$this->createSubjectValueType($flags, $matchesAll)], [1], [], TrinaryLogic::createYes()),
150+
new ConstantArrayType([new ConstantIntegerType(0)], [$this->createSubjectValueType($subjectBaseType, $flags, $matchesAll)], [1], [], TrinaryLogic::createYes()),
154151
$combiType,
155152
);
156153
}
@@ -189,6 +186,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
189186
}
190187

191188
$combiType = $this->buildArrayType(
189+
$subjectBaseType,
192190
$comboList,
193191
$wasMatched,
194192
$trailingOptionals,
@@ -208,7 +206,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
208206
)
209207
) {
210208
// positive match has a subject but not any capturing group
211-
$combiTypes[] = new ConstantArrayType([new ConstantIntegerType(0)], [$this->createSubjectValueType($flags, $matchesAll)], [1], [], TrinaryLogic::createYes());
209+
$combiTypes[] = new ConstantArrayType([new ConstantIntegerType(0)], [$this->createSubjectValueType($subjectBaseType, $flags, $matchesAll)], [1], [], TrinaryLogic::createYes());
212210
}
213211

214212
return TypeCombinator::union(...$combiTypes);
@@ -217,6 +215,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
217215
// the general case, which should work in all cases but does not yield the most
218216
// precise result possible in some cases
219217
return $this->buildArrayType(
218+
$subjectBaseType,
220219
$regexGroupList,
221220
$wasMatched,
222221
$trailingOptionals,
@@ -230,6 +229,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
230229
* @param list<string> $markVerbs
231230
*/
232231
private function buildArrayType(
232+
Type $subjectBaseType,
233233
RegexGroupList $captureGroups,
234234
TrinaryLogic $wasMatched,
235235
int $trailingOptionals,
@@ -243,7 +243,7 @@ private function buildArrayType(
243243
// first item in matches contains the overall match.
244244
$builder->setOffsetValueType(
245245
$this->getKeyType(0),
246-
$this->createSubjectValueType($flags, $matchesAll),
246+
$this->createSubjectValueType($subjectBaseType, $flags, $matchesAll),
247247
$this->isSubjectOptional($wasMatched, $matchesAll),
248248
);
249249

@@ -307,9 +307,12 @@ private function isSubjectOptional(TrinaryLogic $wasMatched, bool $matchesAll):
307307
return !$wasMatched->yes();
308308
}
309309

310-
private function createSubjectValueType(int $flags, bool $matchesAll): Type
310+
/**
311+
* @param Type $baseType A string type (or string variant) representing the subject of the match
312+
*/
313+
private function createSubjectValueType(Type $baseType, int $flags, bool $matchesAll): Type
311314
{
312-
$subjectValueType = TypeCombinator::removeNull($this->getValueType(new StringType(), $flags, $matchesAll));
315+
$subjectValueType = TypeCombinator::removeNull($this->getValueType($baseType, $flags, $matchesAll));
313316

314317
if ($matchesAll) {
315318
if ($this->containsPatternOrder($flags)) {

src/Type/Regex/RegexAstWalkResult.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
namespace PHPStan\Type\Regex;
44

5+
use PHPStan\Type\StringType;
6+
use PHPStan\Type\Type;
7+
58
/** @immutable */
69
final class RegexAstWalkResult
710
{
@@ -15,9 +18,9 @@ public function __construct(
1518
private int $captureGroupId,
1619
private array $capturingGroups,
1720
private array $markVerbs,
21+
private Type $subjectBaseType,
1822
)
19-
{
20-
}
23+
{}
2124

2225
public static function createEmpty(): self
2326
{
@@ -27,6 +30,7 @@ public static function createEmpty(): self
2730
100,
2831
[],
2932
[],
33+
new StringType()
3034
);
3135
}
3236

@@ -37,6 +41,7 @@ public function nextAlternationId(): self
3741
$this->captureGroupId,
3842
$this->capturingGroups,
3943
$this->markVerbs,
44+
$this->subjectBaseType,
4045
);
4146
}
4247

@@ -47,6 +52,7 @@ public function nextCaptureGroupId(): self
4752
$this->captureGroupId + 1,
4853
$this->capturingGroups,
4954
$this->markVerbs,
55+
$this->subjectBaseType,
5056
);
5157
}
5258

@@ -60,6 +66,7 @@ public function addCapturingGroup(RegexCapturingGroup $group): self
6066
$this->captureGroupId,
6167
$capturingGroups,
6268
$this->markVerbs,
69+
$this->subjectBaseType,
6370
);
6471
}
6572

@@ -73,6 +80,18 @@ public function markVerb(string $markVerb): self
7380
$this->captureGroupId,
7481
$this->capturingGroups,
7582
$verbs,
83+
$this->subjectBaseType,
84+
);
85+
}
86+
87+
public function withSubjectBaseType(Type $subjectBaseType): self
88+
{
89+
return new self(
90+
$this->alternationId,
91+
$this->captureGroupId,
92+
$this->capturingGroups,
93+
$this->markVerbs,
94+
$subjectBaseType,
7695
);
7796
}
7897

@@ -102,4 +121,9 @@ public function getMarkVerbs(): array
102121
return $this->markVerbs;
103122
}
104123

124+
public function getSubjectBaseType(): Type
125+
{
126+
return $this->subjectBaseType;
127+
}
128+
105129
}

src/Type/Regex/RegexGroupParser.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,7 @@ public function __construct(
4949
{
5050
}
5151

52-
/**
53-
* @return array{array<int, RegexCapturingGroup>, list<string>}|null
54-
*/
55-
public function parseGroups(string $regex): ?array
52+
public function parseGroups(string $regex): ?RegexAstWalkResult
5653
{
5754
if (self::$parser === null) {
5855
/** @throws void */
@@ -105,7 +102,7 @@ public function parseGroups(string $regex): ?array
105102
RegexAstWalkResult::createEmpty(),
106103
);
107104

108-
return [$astWalkResult->getCapturingGroups(), $astWalkResult->getMarkVerbs()];
105+
return $astWalkResult;
109106
}
110107

111108
private function createEmptyTokenTreeNode(TreeNode $parentAst): TreeNode
@@ -263,6 +260,12 @@ private function walkRegexAst(
263260
$astWalkResult,
264261
);
265262

263+
if ($alternation === null && !$inOptionalQuantification) {
264+
$astWalkResult = $astWalkResult->withSubjectBaseType(
265+
TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType())
266+
);
267+
}
268+
266269
if ($ast->getId() !== '#alternation') {
267270
continue;
268271
}

tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,16 @@ function (string $size): void {
2626

2727
function (string $size): void {
2828
if (preg_match_all('/ab(?P<num>\d+)?/', $size, $matches)) {
29-
assertType("array{0: list<string>, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches);
29+
assertType("array{0: list<non-empty-string>, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches);
3030
} else {
3131
assertType("array{}", $matches);
3232
}
33-
assertType("array{}|array{0: list<string>, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches);
33+
assertType("array{}|array{0: list<non-empty-string>, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches);
3434
};
3535

3636
function (string $size): void {
3737
if (preg_match_all('/ab(?P<num>\d+)?/', $size, $matches) > 0) {
38-
assertType("array{0: list<string>, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches);
38+
assertType("array{0: list<non-empty-string>, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches);
3939
} else {
4040
assertType("array{0: list<string>, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches);
4141
}
@@ -44,7 +44,7 @@ function (string $size): void {
4444

4545
function (string $size): void {
4646
if (preg_match_all('/ab(?P<num>\d+)?/', $size, $matches) != false) {
47-
assertType("array{0: list<string>, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches);
47+
assertType("array{0: list<non-empty-string>, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches);
4848
} else {
4949
assertType("array{}", $matches);
5050
}
@@ -53,7 +53,7 @@ function (string $size): void {
5353

5454
function (string $size): void {
5555
if (preg_match_all('/ab(?P<num>\d+)?/', $size, $matches) == true) {
56-
assertType("array{0: list<string>, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches);
56+
assertType("array{0: list<non-empty-string>, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches);
5757
} else {
5858
assertType("array{}", $matches);
5959
}
@@ -62,7 +62,7 @@ function (string $size): void {
6262

6363
function (string $size): void {
6464
if (preg_match_all('/ab(?P<num>\d+)?/', $size, $matches) === 1) {
65-
assertType("array{0: list<string>, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches);
65+
assertType("array{0: list<non-empty-string>, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches);
6666
} else {
6767
assertType("array{0: list<string>, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches);
6868
}
@@ -76,55 +76,55 @@ function (string $size): void {
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<''|'ab'>, 2: list<''|'ab'>}", $matches);
79+
assertType("array{0: list<non-empty-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<'ab'|null>, 2: list<'ab'|null>}", $matches);
85+
assertType("array{0: list<non-empty-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?: 'ab', 2?: 'ab'}>", $matches);
91+
assertType("list<array{0: non-empty-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<''|'ab'>, 2: list<''|'ab'>}", $matches);
97+
assertType("array{0: list<non-empty-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: 'ab'|null, 2: 'ab'|null}>", $matches);
103+
assertType("list<array{0: non-empty-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<'ab'|null>, 2: list<'ab'|null>}", $matches);
109+
assertType("array{0: list<non-empty-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<-1, max>}, num: array{numeric-string, int<-1, max>}, 1: array{numeric-string, int<-1, max>}, suffix?: array{'ab', int<-1, max>}, 2?: array{'ab', int<-1, max>}}>", $matches);
115+
assertType("list<array{0: array{non-empty-string, int<-1, max>}, num: array{numeric-string, int<-1, max>}, 1: array{numeric-string, int<-1, max>}, suffix?: array{'ab', int<-1, max>}, 2?: array{'ab', int<-1, 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<-1, max>}>, num: list<array{numeric-string, int<-1, max>}>, 1: list<array{numeric-string, int<-1, max>}>, suffix: list<array{''|'ab', int<-1, max>}>, 2: list<array{''|'ab', int<-1, max>}>}", $matches);
121+
assertType("array{0: list<array{non-empty-string, int<-1, max>}>, num: list<array{numeric-string, int<-1, max>}>, 1: list<array{numeric-string, int<-1, max>}>, suffix: list<array{''|'ab', int<-1, max>}>, 2: list<array{''|'ab', int<-1, 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{'ab'|null, int<-1, max>}, 2: array{'ab'|null, int<-1, max>}}>", $matches);
127+
assertType("list<array{0: array{non-empty-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

@@ -160,7 +160,7 @@ public function sayBar(string $content): void
160160
return;
161161
}
162162

163-
assertType('array{list<string>}', $matches);
163+
assertType('array{list<non-empty-string>}', $matches);
164164
}
165165

166166
function doFoobar(string $s): void {

0 commit comments

Comments
 (0)