Skip to content

Commit 8fe28fa

Browse files
authored
Bleeding edge - Precise array shape for preg_replace_callback() $matches
1 parent 164691d commit 8fe28fa

File tree

4 files changed

+141
-0
lines changed

4 files changed

+141
-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\PregReplaceCallbackClosureTypeExtension:
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\PregReplaceCallbackClosureTypeExtension
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 PregReplaceCallbackClosureTypeExtension 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: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace PregReplaceCallbackMatchShapes72;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function (string $s): void {
8+
preg_replace_callback(
9+
$s,
10+
function ($matches) {
11+
assertType('array<int|string, string>', $matches);
12+
return '';
13+
},
14+
$s
15+
);
16+
};
17+
18+
function (string $s): void {
19+
preg_replace_callback(
20+
'|<p>(\s*)\w|',
21+
function ($matches) {
22+
assertType('array{string, string}', $matches);
23+
return '';
24+
},
25+
$s
26+
);
27+
};
28+
29+
// The flags parameter was added in PHP 7.4
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php // lint >= 7.4
2+
3+
namespace PregReplaceCallbackMatchShapes;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function (string $s): void {
8+
preg_replace_callback(
9+
'/(foo)?(bar)?(baz)?/',
10+
function ($matches) {
11+
assertType('array{string, non-empty-string|null, non-empty-string|null, non-empty-string|null}', $matches);
12+
return '';
13+
},
14+
$s,
15+
-1,
16+
$count,
17+
PREG_UNMATCHED_AS_NULL
18+
);
19+
};
20+
21+
function (string $s): void {
22+
preg_replace_callback(
23+
'/(foo)?(bar)?(baz)?/',
24+
function ($matches) {
25+
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);
26+
return '';
27+
},
28+
$s,
29+
-1,
30+
$count,
31+
PREG_OFFSET_CAPTURE
32+
);
33+
};
34+
35+
function (string $s): void {
36+
preg_replace_callback(
37+
'/(foo)?(bar)?(baz)?/',
38+
function ($matches) {
39+
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);
40+
return '';
41+
},
42+
$s,
43+
-1,
44+
$count,
45+
PREG_OFFSET_CAPTURE|PREG_UNMATCHED_AS_NULL
46+
);
47+
};

0 commit comments

Comments
 (0)