Skip to content

Commit 3b50917

Browse files
authored
Merge branch refs/heads/1.11.x into 1.12.x
2 parents d193f57 + bfef6da commit 3b50917

File tree

6 files changed

+100
-46
lines changed

6 files changed

+100
-46
lines changed

Diff for: src/Php/PhpVersion.php

+6
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,12 @@ public function supportsPregUnmatchedAsNull(): bool
319319
return $this->versionId >= 70400;
320320
}
321321

322+
public function supportsPregCaptureOnlyNamedGroups(): bool
323+
{
324+
// https://php.watch/versions/8.2/preg-n-no-capture-modifier
325+
return $this->versionId >= 80200;
326+
}
327+
322328
public function hasDateTimeExceptions(): bool
323329
{
324330
return $this->versionId >= 80300;

Diff for: src/Rules/Regexp/RegularExpressionQuotingRule.php

+6-38
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,25 @@
1515
use PHPStan\Rules\Rule;
1616
use PHPStan\Rules\RuleErrorBuilder;
1717
use PHPStan\ShouldNotHappenException;
18-
use PHPStan\Type\Constant\ConstantStringType;
18+
use PHPStan\Type\Php\RegexExpressionHelper;
1919
use function array_filter;
2020
use function array_merge;
2121
use function array_values;
2222
use function count;
2323
use function in_array;
2424
use function sprintf;
2525
use function strlen;
26-
use function substr;
2726

2827
/**
2928
* @implements Rule<Node\Expr\FuncCall>
3029
*/
3130
final class RegularExpressionQuotingRule implements Rule
3231
{
3332

34-
public function __construct(private ReflectionProvider $reflectionProvider)
33+
public function __construct(
34+
private ReflectionProvider $reflectionProvider,
35+
private RegexExpressionHelper $regexExpressionHelper,
36+
)
3537
{
3638
}
3739

@@ -76,7 +78,7 @@ public function processNode(Node $node, Scope $scope): array
7678
return [];
7779
}
7880

79-
$patternDelimiters = $this->getDelimitersFromConcat($normalizedArgs[0]->value, $scope);
81+
$patternDelimiters = $this->regexExpressionHelper->getPatternDelimiters($normalizedArgs[0]->value, $scope);
8082
return $this->validateQuoteDelimiters($normalizedArgs[0]->value, $scope, $patternDelimiters);
8183
}
8284

@@ -193,40 +195,6 @@ private function validatePregQuote(FuncCall $pregQuote, Scope $scope, array $pat
193195
return null;
194196
}
195197

196-
/**
197-
* Get delimiters from non-constant patterns, if possible.
198-
*
199-
* @return string[]
200-
*/
201-
private function getDelimitersFromConcat(Concat $concat, Scope $scope): array
202-
{
203-
if ($concat->left instanceof Concat) {
204-
return $this->getDelimitersFromConcat($concat->left, $scope);
205-
}
206-
207-
$left = $scope->getType($concat->left);
208-
209-
$delimiters = [];
210-
foreach ($left->getConstantStrings() as $leftString) {
211-
$delimiter = $this->getDelimiterFromString($leftString);
212-
if ($delimiter === null) {
213-
continue;
214-
}
215-
216-
$delimiters[] = $delimiter;
217-
}
218-
return $delimiters;
219-
}
220-
221-
private function getDelimiterFromString(ConstantStringType $string): ?string
222-
{
223-
if ($string->getValue() === '') {
224-
return null;
225-
}
226-
227-
return substr($string->getValue(), 0, 1);
228-
}
229-
230198
/**
231199
* @param string[] $delimiters
232200
*

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

+14-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
use function is_string;
3434
use function rtrim;
3535
use function sscanf;
36+
use function str_contains;
3637
use function str_replace;
3738
use function strlen;
3839
use function substr;
@@ -411,6 +412,12 @@ private function parseGroups(string $regex): ?array
411412
return null;
412413
}
413414

415+
$captureOnlyNamed = false;
416+
if ($this->phpVersion->supportsPregCaptureOnlyNamedGroups()) {
417+
$modifiers = $this->regexExpressionHelper->getPatternModifiers($regex);
418+
$captureOnlyNamed = str_contains($modifiers ?? '', 'n');
419+
}
420+
414421
$capturingGroups = [];
415422
$groupCombinations = [];
416423
$alternationId = -1;
@@ -427,6 +434,7 @@ private function parseGroups(string $regex): ?array
427434
$capturingGroups,
428435
$groupCombinations,
429436
$markVerbs,
437+
$captureOnlyNamed,
430438
);
431439

432440
return [$capturingGroups, $groupCombinations, $markVerbs];
@@ -448,6 +456,7 @@ private function walkRegexAst(
448456
array &$capturingGroups,
449457
array &$groupCombinations,
450458
array &$markVerbs,
459+
bool $captureOnlyNamed,
451460
): void
452461
{
453462
$group = null;
@@ -509,7 +518,10 @@ private function walkRegexAst(
509518
return;
510519
}
511520

512-
if ($group instanceof RegexCapturingGroup) {
521+
if (
522+
$group instanceof RegexCapturingGroup &&
523+
(!$captureOnlyNamed || $group->isNamed())
524+
) {
513525
$capturingGroups[$group->getId()] = $group;
514526

515527
if (!array_key_exists($alternationId, $groupCombinations)) {
@@ -533,6 +545,7 @@ private function walkRegexAst(
533545
$capturingGroups,
534546
$groupCombinations,
535547
$markVerbs,
548+
$captureOnlyNamed,
536549
);
537550

538551
if ($ast->getId() !== '#alternation') {

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

+60-2
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
namespace PHPStan\Type\Php;
44

55
use PhpParser\Node\Expr;
6+
use PhpParser\Node\Expr\BinaryOp\Concat;
67
use PhpParser\Node\Name;
78
use PHPStan\Analyser\Scope;
89
use PHPStan\Reflection\InitializerExprTypeResolver;
910
use PHPStan\Type\Constant\ConstantStringType;
1011
use PHPStan\Type\Type;
1112
use PHPStan\Type\TypeCombinator;
13+
use function strrpos;
14+
use function substr;
1215

1316
final class RegexExpressionHelper
1417
{
@@ -26,7 +29,7 @@ public function __construct(
2629
*
2730
* see https://github.com/phpstan/phpstan-src/pull/3233#discussion_r1676938085
2831
*/
29-
public function resolvePatternConcat(Expr\BinaryOp\Concat $concat, Scope $scope): Type
32+
public function resolvePatternConcat(Concat $concat, Scope $scope): Type
3033
{
3134
$resolver = new class($scope) {
3235

@@ -44,7 +47,7 @@ public function resolve(Expr $expr): Type
4447
return new ConstantStringType('');
4548
}
4649

47-
if ($expr instanceof Expr\BinaryOp\Concat) {
50+
if ($expr instanceof Concat) {
4851
$left = $this->resolve($expr->left);
4952
$right = $this->resolve($expr->right);
5053

@@ -66,4 +69,59 @@ public function resolve(Expr $expr): Type
6669
return $this->initializerExprTypeResolver->getConcatType($concat->left, $concat->right, static fn (Expr $expr): Type => $resolver->resolve($expr));
6770
}
6871

72+
public function getPatternModifiers(string $pattern): ?string
73+
{
74+
$delimiter = $this->getDelimiterFromString(new ConstantStringType($pattern));
75+
if ($delimiter === null) {
76+
return null;
77+
}
78+
79+
if ($delimiter === '{') {
80+
$endDelimiterPos = strrpos($pattern, '}');
81+
} else {
82+
// same start and end delimiter
83+
$endDelimiterPos = strrpos($pattern, $delimiter);
84+
}
85+
86+
if ($endDelimiterPos === false) {
87+
return null;
88+
}
89+
90+
return substr($pattern, $endDelimiterPos + 1);
91+
}
92+
93+
/**
94+
* Get delimiters from non-constant patterns, if possible.
95+
*
96+
* @return string[]
97+
*/
98+
public function getPatternDelimiters(Concat $concat, Scope $scope): array
99+
{
100+
if ($concat->left instanceof Concat) {
101+
return $this->getPatternDelimiters($concat->left, $scope);
102+
}
103+
104+
$left = $scope->getType($concat->left);
105+
106+
$delimiters = [];
107+
foreach ($left->getConstantStrings() as $leftString) {
108+
$delimiter = $this->getDelimiterFromString($leftString);
109+
if ($delimiter === null) {
110+
continue;
111+
}
112+
113+
$delimiters[] = $delimiter;
114+
}
115+
return $delimiters;
116+
}
117+
118+
private function getDelimiterFromString(ConstantStringType $string): ?string
119+
{
120+
if ($string->getValue() === '') {
121+
return null;
122+
}
123+
124+
return substr($string->getValue(), 0, 1);
125+
}
126+
69127
}

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

+9-4
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@
88
// https://php.watch/versions/8.2/preg-n-no-capture-modifier
99
function doNonAutoCapturingFlag(string $s): void {
1010
if (preg_match('/(\d+)/n', $s, $matches)) {
11-
assertType('array{string, numeric-string}', $matches); // should be 'array{string}'
11+
assertType('array{string}', $matches);
1212
}
13-
assertType('array{}|array{string, numeric-string}', $matches);
13+
assertType('array{}|array{string}', $matches);
1414

1515
if (preg_match('/(\d+)(?P<num>\d+)/n', $s, $matches)) {
16-
assertType('array{0: string, 1: numeric-string, num: numeric-string, 2: numeric-string}', $matches);
16+
assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches);
1717
}
18-
assertType('array{}|array{0: string, 1: numeric-string, num: numeric-string, 2: numeric-string}', $matches);
18+
assertType('array{}|array{0: string, num: numeric-string, 1: numeric-string}', $matches);
19+
20+
if (preg_match('/(\w)-(?P<num>\d+)-(\w)/n', $s, $matches)) {
21+
assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches);
22+
}
23+
assertType('array{}|array{0: string, num: numeric-string, 1: numeric-string}', $matches);
1924
}

Diff for: tests/PHPStan/Rules/Regexp/RegularExpressionQuotingRuleTest.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHPStan\Rules\Rule;
66
use PHPStan\Testing\RuleTestCase;
7+
use PHPStan\Type\Php\RegexExpressionHelper;
78
use const PHP_VERSION_ID;
89

910
/**
@@ -14,7 +15,10 @@ class RegularExpressionQuotingRuleTest extends RuleTestCase
1415

1516
protected function getRule(): Rule
1617
{
17-
return new RegularExpressionQuotingRule($this->createReflectionProvider());
18+
return new RegularExpressionQuotingRule(
19+
$this->createReflectionProvider(),
20+
self::getContainer()->getByType(RegexExpressionHelper::class),
21+
);
1822
}
1923

2024
public function testRule(): void

0 commit comments

Comments
 (0)