Skip to content

Commit a6f3493

Browse files
committed
Bleeding edge - Precise array shape for preg_match_callback() $matches
1 parent 164691d commit a6f3493

File tree

3 files changed

+123
-0
lines changed

3 files changed

+123
-0
lines changed

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,8 @@ conditionalTags:
295295
phpstan.typeSpecifier.functionTypeSpecifyingExtension: %featureToggles.narrowPregMatches%
296296
PHPStan\Type\Php\PregMatchParameterOutTypeExtension:
297297
phpstan.functionParameterOutTypeExtension: %featureToggles.narrowPregMatches%
298+
PHPStan\Type\Php\PregMatchCallbackClosureTypeExtension:
299+
phpstan.functionParameterClosureTypeExtension: %featureToggles.narrowPregMatches%
298300

299301
services:
300302
-
@@ -1497,6 +1499,9 @@ services:
14971499
-
14981500
class: PHPStan\Type\Php\PregMatchParameterOutTypeExtension
14991501

1502+
-
1503+
class: PHPStan\Type\Php\PregMatchCallbackClosureTypeExtension
1504+
15001505
-
15011506
class: PHPStan\Type\Php\RegexArrayShapeMatcher
15021507

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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\Native\NativeParameterReflection;
9+
use PHPStan\Reflection\ParameterReflection;
10+
use PHPStan\TrinaryLogic;
11+
use PHPStan\Type\ClosureType;
12+
use PHPStan\Type\FunctionParameterClosureTypeExtension;
13+
use PHPStan\Type\StringType;
14+
use PHPStan\Type\Type;
15+
16+
final class PregMatchCallbackClosureTypeExtension implements FunctionParameterClosureTypeExtension
17+
{
18+
19+
public function __construct(
20+
private RegexArrayShapeMatcher $regexShapeMatcher,
21+
)
22+
{
23+
}
24+
25+
public function isFunctionSupported(FunctionReflection $functionReflection, ParameterReflection $parameter): bool
26+
{
27+
return $functionReflection->getName() === 'preg_replace_callback' && $parameter->getName() === 'callback';
28+
}
29+
30+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, ParameterReflection $parameter, Scope $scope): ?Type
31+
{
32+
$args = $functionCall->getArgs();
33+
$patternArg = $args[0] ?? null;
34+
$flagsArg = $args[5] ?? null;
35+
36+
if (
37+
$patternArg === null
38+
) {
39+
return null;
40+
}
41+
42+
$flagsType = null;
43+
if ($flagsArg !== null) {
44+
$flagsType = $scope->getType($flagsArg->value);
45+
}
46+
47+
$matchesType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope);
48+
if ($matchesType === null) {
49+
return null;
50+
}
51+
52+
return new ClosureType(
53+
[
54+
new NativeParameterReflection($parameter->getName(), $parameter->isOptional(), $matchesType, $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue()),
55+
],
56+
new StringType(),
57+
);
58+
}
59+
60+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
namespace PregReplaceCallbackMatchShapes;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function (string $s): void {
8+
preg_replace_callback(
9+
'|<p>(\s*)\w|',
10+
function ($matches) {
11+
assertType('array{string, string}', $matches);
12+
return '';
13+
},
14+
$s
15+
);
16+
};
17+
18+
function (string $s): void {
19+
preg_replace_callback(
20+
'/(foo)?(bar)?(baz)?/',
21+
function ($matches) {
22+
assertType('array{string, non-empty-string|null, non-empty-string|null, non-empty-string|null}', $matches);
23+
return '';
24+
},
25+
$s,
26+
-1,
27+
$count,
28+
PREG_UNMATCHED_AS_NULL
29+
);
30+
};
31+
32+
function (string $s): void {
33+
preg_replace_callback(
34+
'/(foo)?(bar)?(baz)?/',
35+
function ($matches) {
36+
assertType('array{0: array{string, int<0, max>}, 1?: array{non-empty-string, int<0, max>}, 2?: array{non-empty-string, int<0, max>}, 3?: array{non-empty-string, int<0, max>}}', $matches);
37+
return '';
38+
},
39+
$s,
40+
-1,
41+
$count,
42+
PREG_OFFSET_CAPTURE
43+
);
44+
};
45+
46+
function (string $s): void {
47+
preg_replace_callback(
48+
'/(foo)?(bar)?(baz)?/',
49+
function ($matches) {
50+
assertType('array{array{string|null, int<-1, max>}, array{non-empty-string|null, int<-1, max>}, array{non-empty-string|null, int<-1, max>}, array{non-empty-string|null, int<-1, max>}}', $matches);
51+
return '';
52+
},
53+
$s,
54+
-1,
55+
$count,
56+
PREG_OFFSET_CAPTURE|PREG_UNMATCHED_AS_NULL
57+
);
58+
};

0 commit comments

Comments
 (0)