Skip to content

Commit 721a0a6

Browse files
authored
Implement array shapes for preg_match() $matches by-ref parameter
1 parent a8ededf commit 721a0a6

26 files changed

+807
-26
lines changed

Diff for: composer.json

+3
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@
8282
"composer/ca-bundle": [
8383
"patches/cloudflare-ca.patch"
8484
],
85+
"hoa/regex": [
86+
"patches/Grammar.patch"
87+
],
8588
"hoa/iterator": [
8689
"patches/Buffer.patch",
8790
"patches/Lookahead.patch"

Diff for: composer.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: conf/bleedingEdge.neon

+1
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,6 @@ parameters:
5454
paramOutType: true
5555
pure: true
5656
checkParameterCastableToStringFunctions: true
57+
narrowPregMatches: true
5758
stubFiles:
5859
- ../stubs/bleedingEdge/Rule.stub

Diff for: conf/config.neon

+14
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ parameters:
8989
paramOutType: false
9090
pure: false
9191
checkParameterCastableToStringFunctions: false
92+
narrowPregMatches: false
9293
fileExtensions:
9394
- php
9495
checkAdvancedIsset: false
@@ -284,6 +285,10 @@ conditionalTags:
284285
phpstan.parser.richParserNodeVisitor: %featureToggles.curlSetOptTypes%
285286
PHPStan\Parser\TypeTraverserInstanceofVisitor:
286287
phpstan.parser.richParserNodeVisitor: %featureToggles.instanceofType%
288+
PHPStan\Type\Php\PregMatchTypeSpecifyingExtension:
289+
phpstan.typeSpecifier.functionTypeSpecifyingExtension: %featureToggles.narrowPregMatches%
290+
PHPStan\Type\Php\PregMatchParameterOutTypeExtension:
291+
phpstan.functionParameterOutTypeExtension: %featureToggles.narrowPregMatches%
287292

288293
services:
289294
-
@@ -1465,6 +1470,15 @@ services:
14651470
tags:
14661471
- phpstan.dynamicFunctionThrowTypeExtension
14671472

1473+
-
1474+
class: PHPStan\Type\Php\PregMatchTypeSpecifyingExtension
1475+
1476+
-
1477+
class: PHPStan\Type\Php\PregMatchParameterOutTypeExtension
1478+
1479+
-
1480+
class: PHPStan\Type\Php\RegexArrayShapeMatcher
1481+
14681482
-
14691483
class: PHPStan\Type\Php\ReflectionClassConstructorThrowTypeExtension
14701484
tags:

Diff for: conf/parametersSchema.neon

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ parametersSchema:
8484
paramOutType: bool()
8585
pure: bool()
8686
checkParameterCastableToStringFunctions: bool()
87+
narrowPregMatches: bool()
8788
])
8889
fileExtensions: listOf(string())
8990
checkAdvancedIsset: bool()

Diff for: patches/Grammar.patch

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--- Grammar.pp 2024-05-18 12:15:53
2+
+++ Grammar.pp.fix 2024-05-18 12:15:05
3+
@@ -109,7 +109,7 @@
4+
// Please, see PCRESYNTAX(3), General Category properties, PCRE special category
5+
// properties and script names for \p{} and \P{}.
6+
%token character_type \\([CdDhHNRsSvVwWX]|[pP]{[^}]+})
7+
-%token anchor \\(bBAZzG)|\^|\$
8+
+%token anchor \\([bBAZzG])|\^|\$
9+
%token match_point_reset \\K
10+
%token literal \\.|.
11+
12+
@@ -168,7 +168,7 @@
13+
::negative_class_:: #negativeclass
14+
| ::class_::
15+
)
16+
- ( range() | literal() )+
17+
+ ( <class_> | range() | literal() )+
18+
::_class::
19+
20+
#range:
21+
@@ -178,7 +178,7 @@
22+
capturing()
23+
| literal()
24+
25+
-capturing:
26+
+#capturing:
27+
::comment_:: <comment>? ::_comment:: #comment
28+
| (
29+
::named_capturing_:: <capturing_name> ::_named_capturing:: #namedcapturing
30+
@@ -191,6 +191,7 @@
31+
32+
literal:
33+
<character>
34+
+ | <range>
35+
| <dynamic_character>
36+
| <character_type>
37+
| <anchor>

Diff for: src/Analyser/TypeSpecifier.php

+16
Original file line numberDiff line numberDiff line change
@@ -1989,6 +1989,22 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
19891989
$unwrappedRightExpr = $rightExpr->getExpr();
19901990
}
19911991
$rightType = $scope->getType($rightExpr);
1992+
1993+
if (
1994+
$context->true()
1995+
&& $unwrappedLeftExpr instanceof FuncCall
1996+
&& $unwrappedLeftExpr->name instanceof Name
1997+
&& $unwrappedLeftExpr->name->toLowerString() === 'preg_match'
1998+
&& (new ConstantIntegerType(1))->isSuperTypeOf($rightType)->yes()
1999+
) {
2000+
return $this->specifyTypesInCondition(
2001+
$scope,
2002+
$leftExpr,
2003+
$context,
2004+
$rootExpr,
2005+
);
2006+
}
2007+
19922008
if (
19932009
$context->true()
19942010
&& $unwrappedLeftExpr instanceof FuncCall

Diff for: src/Php/PhpVersion.php

+8
Original file line numberDiff line numberDiff line change
@@ -287,4 +287,12 @@ public function supportsNeverReturnTypeInArrowFunction(): bool
287287
return $this->versionId >= 80200;
288288
}
289289

290+
// see https://www.php.net/manual/en/migration74.incompatible.php#migration74.incompatible.pcre
291+
public function returnsPregUnmatchedCapturingGroups(): bool
292+
{
293+
// When PREG_UNMATCHED_AS_NULL mode is used, trailing unmatched capturing groups will now also be set to null (or [null, -1] if offset capture is enabled).
294+
// This means that the size of the $matches will always be the same.
295+
return $this->versionId >= 70400;
296+
}
297+
290298
}

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

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\FunctionReflection;
8+
use PHPStan\Reflection\ParameterReflection;
9+
use PHPStan\TrinaryLogic;
10+
use PHPStan\Type\FunctionParameterOutTypeExtension;
11+
use PHPStan\Type\Type;
12+
use function in_array;
13+
use function strtolower;
14+
15+
final class PregMatchParameterOutTypeExtension implements FunctionParameterOutTypeExtension
16+
{
17+
18+
public function __construct(
19+
private RegexArrayShapeMatcher $regexShapeMatcher,
20+
)
21+
{
22+
}
23+
24+
public function isFunctionSupported(FunctionReflection $functionReflection, ParameterReflection $parameter): bool
25+
{
26+
return in_array(strtolower($functionReflection->getName()), ['preg_match'], true) && $parameter->getName() === 'matches';
27+
}
28+
29+
public function getParameterOutTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, ParameterReflection $parameter, Scope $scope): ?Type
30+
{
31+
$args = $funcCall->getArgs();
32+
$patternArg = $args[0] ?? null;
33+
$matchesArg = $args[2] ?? null;
34+
$flagsArg = $args[3] ?? null;
35+
36+
if (
37+
$patternArg === null || $matchesArg === null
38+
) {
39+
return null;
40+
}
41+
42+
$patternType = $scope->getType($patternArg->value);
43+
$flagsType = null;
44+
if ($flagsArg !== null) {
45+
$flagsType = $scope->getType($flagsArg->value);
46+
}
47+
48+
return $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createMaybe());
49+
}
50+
51+
}

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

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Analyser\SpecifiedTypes;
8+
use PHPStan\Analyser\TypeSpecifier;
9+
use PHPStan\Analyser\TypeSpecifierAwareExtension;
10+
use PHPStan\Analyser\TypeSpecifierContext;
11+
use PHPStan\Reflection\FunctionReflection;
12+
use PHPStan\TrinaryLogic;
13+
use PHPStan\Type\FunctionTypeSpecifyingExtension;
14+
use function in_array;
15+
use function strtolower;
16+
17+
final class PregMatchTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
18+
{
19+
20+
private TypeSpecifier $typeSpecifier;
21+
22+
public function __construct(
23+
private RegexArrayShapeMatcher $regexShapeMatcher,
24+
)
25+
{
26+
}
27+
28+
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
29+
{
30+
$this->typeSpecifier = $typeSpecifier;
31+
}
32+
33+
public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool
34+
{
35+
return in_array(strtolower($functionReflection->getName()), ['preg_match'], true) && !$context->null();
36+
}
37+
38+
public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
39+
{
40+
$args = $node->getArgs();
41+
$patternArg = $args[0] ?? null;
42+
$matchesArg = $args[2] ?? null;
43+
$flagsArg = $args[3] ?? null;
44+
45+
if (
46+
$patternArg === null || $matchesArg === null
47+
) {
48+
return new SpecifiedTypes();
49+
}
50+
51+
$patternType = $scope->getType($patternArg->value);
52+
$flagsType = null;
53+
if ($flagsArg !== null) {
54+
$flagsType = $scope->getType($flagsArg->value);
55+
}
56+
57+
$matchedType = $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createFromBoolean($context->true()));
58+
if ($matchedType === null) {
59+
return new SpecifiedTypes();
60+
}
61+
62+
$overwrite = false;
63+
if ($context->false()) {
64+
$overwrite = true;
65+
$context = $context->negate();
66+
}
67+
68+
return $this->typeSpecifier->create(
69+
$matchesArg->value,
70+
$matchedType,
71+
$context,
72+
$overwrite,
73+
$scope,
74+
$node,
75+
);
76+
}
77+
78+
}

0 commit comments

Comments
 (0)