Skip to content

Commit 5cc3c12

Browse files
committed
Merge branch '2.x'
2 parents ea4ab6f + 06d0e49 commit 5cc3c12

9 files changed

+252
-15
lines changed

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@
2121
},
2222
"require-dev": {
2323
"phpunit/phpunit": "^8 || ^9",
24-
"phpstan/phpstan": "^1.11.8",
24+
"phpstan/phpstan": "^1.11.10",
2525
"phpstan/phpstan-strict-rules": "^1.1"
2626
},
2727
"conflict": {
28-
"phpstan/phpstan": "<1.11.8"
28+
"phpstan/phpstan": "<1.11.10"
2929
},
3030
"autoload": {
3131
"psr-4": {

extension.neon

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@
33
# These can be reused by third party packages by including 'vendor/composer/pcre/extension.neon'
44
# in your phpstan config
55

6-
conditionalTags:
7-
Composer\Pcre\PHPStan\PregMatchParameterOutTypeExtension:
8-
phpstan.staticMethodParameterOutTypeExtension: %featureToggles.narrowPregMatches%
9-
Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension:
10-
phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension: %featureToggles.narrowPregMatches%
11-
Composer\Pcre\PHPStan\UnsafeStrictGroupsCallRule:
12-
phpstan.rules.rule: %featureToggles.narrowPregMatches%
13-
146
services:
157
-
168
class: Composer\Pcre\PHPStan\PregMatchParameterOutTypeExtension
9+
tags:
10+
- phpstan.staticMethodParameterOutTypeExtension
1711
-
1812
class: Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension
13+
tags:
14+
- phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension
15+
-
16+
class: Composer\Pcre\PHPStan\PregReplaceCallbackClosureTypeExtension
17+
tags:
18+
- phpstan.staticMethodParameterClosureTypeExtension
1919

2020
rules:
2121
- Composer\Pcre\PHPStan\UnsafeStrictGroupsCallRule

phpstan-baseline.neon

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
parameters:
22
ignoreErrors:
3+
-
4+
message: "#^Creating new PHPStan\\\\Reflection\\\\Native\\\\NativeParameterReflection is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#"
5+
count: 1
6+
path: src/PHPStan/PregReplaceCallbackClosureTypeExtension.php
7+
38
-
49
message: "#^Parameter \\#2 \\$callback of function preg_replace_callback expects callable\\(array\\<int\\|string, string\\>\\)\\: string, \\(callable\\(array\\<int\\|string, array\\{string\\|null, int\\<\\-1, max\\>\\}\\>\\)\\: string\\)\\|\\(callable\\(array\\<int\\|string, string\\|null\\>\\)\\: string\\) given\\.$#"
510
count: 1
@@ -50,6 +55,11 @@ parameters:
5055
count: 2
5156
path: tests/PregTests/ReplaceCallbackArrayTest.php
5257

58+
-
59+
message: "#^Parameter \\#1 \\$string of function strtoupper expects string, string\\|null given\\.$#"
60+
count: 2
61+
path: tests/PregTests/ReplaceCallbackTest.php
62+
5363
-
5464
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
5565
count: 2

src/PHPStan/PregMatchFlags.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ final class PregMatchFlags
1414
static public function getType(?Arg $flagsArg, Scope $scope): ?Type
1515
{
1616
if ($flagsArg === null) {
17-
return new ConstantIntegerType(PREG_UNMATCHED_AS_NULL | RegexArrayShapeMatcher::PREG_UNMATCHED_AS_NULL_ON_72_73);
17+
return new ConstantIntegerType(PREG_UNMATCHED_AS_NULL);
1818
}
1919

2020
$flagsType = $scope->getType($flagsArg->value);
@@ -30,7 +30,7 @@ static public function getType(?Arg $flagsArg, Scope $scope): ?Type
3030
return null;
3131
}
3232

33-
$internalFlagsTypes[] = new ConstantIntegerType($constantScalarValue | PREG_UNMATCHED_AS_NULL | RegexArrayShapeMatcher::PREG_UNMATCHED_AS_NULL_ON_72_73);
33+
$internalFlagsTypes[] = new ConstantIntegerType($constantScalarValue | PREG_UNMATCHED_AS_NULL);
3434
}
3535
return TypeCombinator::union(...$internalFlagsTypes);
3636
}

src/PHPStan/PregMatchParameterOutTypeExtension.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ public function isStaticMethodSupported(MethodReflection $methodReflection, Para
3030
{
3131
return
3232
$methodReflection->getDeclaringClass()->getName() === Preg::class
33-
&& in_array($methodReflection->getName(), ['match', 'isMatch', 'matchStrictGroups', 'isMatchStrictGroups'], true)
33+
&& in_array($methodReflection->getName(), [
34+
'match', 'isMatch', 'matchStrictGroups', 'isMatchStrictGroups',
35+
'matchAll', 'isMatchAll', 'matchAllStrictGroups', 'isMatchAllStrictGroups'
36+
], true)
3437
&& $parameter->getName() === 'matches';
3538
}
3639

@@ -52,6 +55,10 @@ public function getParameterOutTypeFromStaticMethodCall(MethodReflection $method
5255
return null;
5356
}
5457

58+
if (stripos($methodReflection->getName(), 'matchAll') !== false) {
59+
return $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope);
60+
}
61+
5562
return $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope);
5663
}
5764

src/PHPStan/PregMatchTypeSpecifyingExtension.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ public function getClass(): string
4646

4747
public function isStaticMethodSupported(MethodReflection $methodReflection, StaticCall $node, TypeSpecifierContext $context): bool
4848
{
49-
return in_array($methodReflection->getName(), ['match', 'isMatch', 'matchStrictGroups', 'isMatchStrictGroups'], true) && !$context->null();
49+
return in_array($methodReflection->getName(), [
50+
'match', 'isMatch', 'matchStrictGroups', 'isMatchStrictGroups',
51+
'matchAll', 'isMatchAll', 'matchAllStrictGroups', 'isMatchAllStrictGroups'
52+
], true)
53+
&& !$context->null();
5054
}
5155

5256
public function specifyTypes(MethodReflection $methodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
@@ -67,7 +71,12 @@ public function specifyTypes(MethodReflection $methodReflection, StaticCall $nod
6771
return new SpecifiedTypes();
6872
}
6973

70-
$matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope);
74+
if (stripos($methodReflection->getName(), 'matchAll') !== false) {
75+
$matchedType = $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope);
76+
} else {
77+
$matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope);
78+
}
79+
7180
if ($matchedType === null) {
7281
return new SpecifiedTypes();
7382
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Composer\Pcre\PHPStan;
4+
5+
use Composer\Pcre\Preg;
6+
use Composer\Pcre\Regex;
7+
use PhpParser\Node\Expr\StaticCall;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Reflection\MethodReflection;
10+
use PHPStan\Reflection\Native\NativeParameterReflection;
11+
use PHPStan\Reflection\ParameterReflection;
12+
use PHPStan\TrinaryLogic;
13+
use PHPStan\Type\ClosureType;
14+
use PHPStan\Type\Constant\ConstantArrayType;
15+
use PHPStan\Type\Php\RegexArrayShapeMatcher;
16+
use PHPStan\Type\StaticMethodParameterClosureTypeExtension;
17+
use PHPStan\Type\StringType;
18+
use PHPStan\Type\TypeCombinator;
19+
use PHPStan\Type\Type;
20+
21+
final class PregReplaceCallbackClosureTypeExtension implements StaticMethodParameterClosureTypeExtension
22+
{
23+
/**
24+
* @var RegexArrayShapeMatcher
25+
*/
26+
private $regexShapeMatcher;
27+
28+
public function __construct(RegexArrayShapeMatcher $regexShapeMatcher)
29+
{
30+
$this->regexShapeMatcher = $regexShapeMatcher;
31+
}
32+
33+
public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool
34+
{
35+
return in_array($methodReflection->getDeclaringClass()->getName(), [Preg::class, Regex::class], true)
36+
&& in_array($methodReflection->getName(), ['replaceCallback', 'replaceCallbackStrictGroups'], true)
37+
&& $parameter->getName() === 'replacement';
38+
}
39+
40+
public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type
41+
{
42+
$args = $methodCall->getArgs();
43+
$patternArg = $args[0] ?? null;
44+
$flagsArg = $args[5] ?? null;
45+
46+
if (
47+
$patternArg === null
48+
) {
49+
return null;
50+
}
51+
52+
$flagsType = PregMatchFlags::getType($flagsArg, $scope);
53+
54+
$matchesType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope);
55+
if ($matchesType === null) {
56+
return null;
57+
}
58+
59+
if ($methodReflection->getName() === 'replaceCallbackStrictGroups' && count($matchesType->getConstantArrays()) === 1) {
60+
$matchesType = $matchesType->getConstantArrays()[0];
61+
$matchesType = new ConstantArrayType(
62+
$matchesType->getKeyTypes(),
63+
array_map(static function (Type $valueType): Type {
64+
return TypeCombinator::removeNull($valueType);
65+
}, $matchesType->getValueTypes()),
66+
$matchesType->getNextAutoIndexes(),
67+
[],
68+
$matchesType->isList()
69+
);
70+
}
71+
72+
return new ClosureType(
73+
[
74+
new NativeParameterReflection($parameter->getName(), $parameter->isOptional(), $matchesType, $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue()),
75+
],
76+
new StringType()
77+
);
78+
}
79+
}

tests/PHPStanTests/nsrt/preg-match.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,30 @@ function doMatchStrictGroupsUnsafe(string $s): void
8888
}
8989
}
9090

91+
function doMatchAllStrictGroups(string $s): void
92+
{
93+
if (Preg::matchAllStrictGroups('/Price: /i', $s, $matches)) {
94+
assertType('array{list<string>}', $matches);
95+
} else {
96+
assertType('array{}', $matches);
97+
}
98+
assertType('array{}|array{list<string>}', $matches);
99+
100+
if (Preg::matchAllStrictGroups('/Price: (£|€)\d+/', $s, $matches)) {
101+
assertType('array{list<string>, list<non-empty-string>}', $matches);
102+
} else {
103+
assertType('array{}', $matches);
104+
}
105+
assertType('array{}|array{list<string>, list<non-empty-string>}', $matches);
106+
107+
if (Preg::isMatchAllStrictGroups('/Price: (?<test>£|€)\d+/', $s, $matches)) {
108+
assertType('array{0: list<string>, test: list<non-empty-string>, 1: list<non-empty-string>}', $matches);
109+
} else {
110+
assertType('array{}', $matches);
111+
}
112+
assertType('array{}|array{0: list<string>, test: list<non-empty-string>, 1: list<non-empty-string>}', $matches);
113+
}
114+
91115
// disabled until https://github.com/phpstan/phpstan-src/pull/3185 can be resolved
92116
//
93117
//function identicalMatch(string $s): void
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
namespace PregMatchShapes;
4+
5+
use Composer\Pcre\Preg;
6+
use Composer\Pcre\Regex;
7+
use function PHPStan\Testing\assertType;
8+
9+
function (string $s): void {
10+
Preg::replaceCallback(
11+
$s,
12+
function ($matches) {
13+
assertType('array<int|string, string|null>', $matches);
14+
return '';
15+
},
16+
$s
17+
);
18+
19+
Regex::replaceCallback(
20+
$s,
21+
function ($matches) {
22+
assertType('array<int|string, string|null>', $matches);
23+
return '';
24+
},
25+
$s
26+
);
27+
};
28+
29+
function (string $s): void {
30+
Preg::replaceCallback(
31+
'|<p>(\s*)\w|',
32+
function ($matches) {
33+
assertType('array{string, string}', $matches);
34+
return '';
35+
},
36+
$s
37+
);
38+
};
39+
40+
function (string $s): void {
41+
Preg::replaceCallback(
42+
'/(foo)?(bar)?(baz)?/',
43+
function ($matches) {
44+
assertType("array{string, 'foo'|null, 'bar'|null, 'baz'|null}", $matches);
45+
return '';
46+
},
47+
$s,
48+
-1,
49+
$count,
50+
PREG_UNMATCHED_AS_NULL
51+
);
52+
};
53+
54+
function (string $s): void {
55+
Preg::replaceCallback(
56+
'/(foo)?(bar)?(baz)?/',
57+
function ($matches) {
58+
assertType("array{array{string|null, int<-1, max>}, array{'foo'|null, int<-1, max>}, array{'bar'|null, int<-1, max>}, array{'baz'|null, int<-1, max>}}", $matches);
59+
return '';
60+
},
61+
$s,
62+
-1,
63+
$count,
64+
PREG_OFFSET_CAPTURE
65+
);
66+
};
67+
68+
function (string $s): void {
69+
Preg::replaceCallback(
70+
'/(foo)?(bar)?(baz)?/',
71+
function ($matches) {
72+
assertType("array{array{string|null, int<-1, max>}, array{'foo'|null, int<-1, max>}, array{'bar'|null, int<-1, max>}, array{'baz'|null, int<-1, max>}}", $matches);
73+
return '';
74+
},
75+
$s,
76+
-1,
77+
$count,
78+
PREG_OFFSET_CAPTURE|PREG_UNMATCHED_AS_NULL
79+
);
80+
};
81+
82+
function (string $s): void {
83+
Preg::replaceCallbackStrictGroups(
84+
'/(foo)?(bar)?(baz)?/',
85+
function ($matches) {
86+
assertType("array{string, 'foo', 'bar', 'baz'}", $matches);
87+
return '';
88+
},
89+
$s,
90+
-1,
91+
$count
92+
);
93+
};
94+
95+
function (string $s): void {
96+
Preg::replaceCallbackStrictGroups(
97+
'/(foo)?(bar)?(baz)?/',
98+
function ($matches) {
99+
// should be array{array{string, int<-1, max>}, array{'foo', int<-1, max>}, array{'bar', int<-1, max>}, array{'baz', int<-1, max>}}
100+
assertType("array{array{string|null, int<-1, max>}, array{'foo'|null, int<-1, max>}, array{'bar'|null, int<-1, max>}, array{'baz'|null, int<-1, max>}}", $matches);
101+
return '';
102+
},
103+
$s,
104+
-1,
105+
$count,
106+
PREG_OFFSET_CAPTURE
107+
);
108+
};

0 commit comments

Comments
 (0)