Skip to content

Commit 390fe08

Browse files
authored
RegexExpressionHelper - Support all bracket style delimiters
1 parent 5c54586 commit 390fe08

File tree

4 files changed

+66
-17
lines changed

4 files changed

+66
-17
lines changed

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -406,8 +406,9 @@ private function parseGroups(string $regex): ?array
406406
return null;
407407
}
408408

409+
$rawRegex = $this->regexExpressionHelper->removeDelimitersAndModifiers($regex);
409410
try {
410-
$ast = self::$parser->parse($regex);
411+
$ast = self::$parser->parse($rawRegex);
411412
} catch (Exception) {
412413
return null;
413414
}

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

+40-14
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Type\Constant\ConstantStringType;
1111
use PHPStan\Type\Type;
1212
use PHPStan\Type\TypeCombinator;
13+
use function array_key_exists;
1314
use function strrpos;
1415
use function substr;
1516

@@ -71,23 +72,48 @@ public function resolve(Expr $expr): Type
7172

7273
public function getPatternModifiers(string $pattern): ?string
7374
{
74-
$delimiter = $this->getDelimiterFromString(new ConstantStringType($pattern));
75-
if ($delimiter === null) {
75+
$endDelimiterPos = $this->getEndDelimiterPos($pattern);
76+
77+
if ($endDelimiterPos === false) {
7678
return null;
7779
}
7880

79-
if ($delimiter === '{') {
80-
$endDelimiterPos = strrpos($pattern, '}');
81-
} else {
82-
// same start and end delimiter
83-
$endDelimiterPos = strrpos($pattern, $delimiter);
84-
}
81+
return substr($pattern, $endDelimiterPos + 1);
82+
}
83+
84+
public function removeDelimitersAndModifiers(string $pattern): string
85+
{
86+
$endDelimiterPos = $this->getEndDelimiterPos($pattern);
8587

8688
if ($endDelimiterPos === false) {
87-
return null;
89+
return $pattern;
8890
}
8991

90-
return substr($pattern, $endDelimiterPos + 1);
92+
return substr($pattern, 1, $endDelimiterPos - 1);
93+
}
94+
95+
private function getEndDelimiterPos(string $pattern): false|int
96+
{
97+
$startDelimiter = $this->getPatternDelimiter($pattern);
98+
if ($startDelimiter === null) {
99+
return false;
100+
}
101+
102+
// delimiter variants, see https://www.php.net/manual/en/regexp.reference.delimiters.php
103+
$bracketStyleDelimiters = [
104+
'{' => '}',
105+
'(' => ')',
106+
'[' => ']',
107+
'<' => '>',
108+
];
109+
if (array_key_exists($startDelimiter, $bracketStyleDelimiters)) {
110+
$endDelimiterPos = strrpos($pattern, $bracketStyleDelimiters[$startDelimiter]);
111+
} else {
112+
// same start and end delimiter
113+
$endDelimiterPos = strrpos($pattern, $startDelimiter);
114+
}
115+
116+
return $endDelimiterPos;
91117
}
92118

93119
/**
@@ -105,7 +131,7 @@ public function getPatternDelimiters(Concat $concat, Scope $scope): array
105131

106132
$delimiters = [];
107133
foreach ($left->getConstantStrings() as $leftString) {
108-
$delimiter = $this->getDelimiterFromString($leftString);
134+
$delimiter = $this->getPatternDelimiter($leftString->getValue());
109135
if ($delimiter === null) {
110136
continue;
111137
}
@@ -115,13 +141,13 @@ public function getPatternDelimiters(Concat $concat, Scope $scope): array
115141
return $delimiters;
116142
}
117143

118-
private function getDelimiterFromString(ConstantStringType $string): ?string
144+
private function getPatternDelimiter(string $regex): ?string
119145
{
120-
if ($string->getValue() === '') {
146+
if ($regex === '') {
121147
return null;
122148
}
123149

124-
return substr($string->getValue(), 0, 1);
150+
return substr($regex, 0, 1);
125151
}
126152

127153
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ function doMatch(string $s): void {
2929
assertType('array{}|array{string, non-empty-string}', $matches);
3030

3131
if (preg_match('(Price: (£|€))i', $s, $matches)) {
32-
assertType('array{string, non-empty-string, non-empty-string}', $matches);
32+
assertType('array{string, non-empty-string}', $matches);
3333
}
34-
assertType('array{}|array{string, non-empty-string, non-empty-string}', $matches);
34+
assertType('array{}|array{string, non-empty-string}', $matches);
3535

3636
if (preg_match('_foo(.)\_i_i', $s, $matches)) {
3737
assertType('array{string, non-empty-string}', $matches);

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

+22
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,25 @@ function doNonAutoCapturingFlag(string $s): void {
2222
}
2323
assertType('array{}|array{0: string, num: numeric-string, 1: numeric-string}', $matches);
2424
}
25+
26+
// delimiter variants, see https://www.php.net/manual/en/regexp.reference.delimiters.php
27+
function (string $s): void {
28+
if (preg_match('{(\d+)(?P<num>\d+)}n', $s, $matches)) {
29+
assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches);
30+
}
31+
};
32+
function (string $s): void {
33+
if (preg_match('<(\d+)(?P<num>\d+)>n', $s, $matches)) {
34+
assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches);
35+
}
36+
};
37+
function (string $s): void {
38+
if (preg_match('((\d+)(?P<num>\d+))n', $s, $matches)) {
39+
assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches);
40+
}
41+
};
42+
function (string $s): void {
43+
if (preg_match('[(\d+)(?P<num>\d+)]n', $s, $matches)) {
44+
assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches);
45+
}
46+
};

0 commit comments

Comments
 (0)