Skip to content

Commit 8794315

Browse files
kamil-zacekondrejmirtes
authored andcommitted
Support nette 4 for Strings::replace + support regexp array shapes for Strings::replace callback
1 parent f41257b commit 8794315

File tree

4 files changed

+211
-0
lines changed

4 files changed

+211
-0
lines changed

Diff for: extension.neon

+5
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ conditionalTags:
5757
phpstan.broker.dynamicStaticMethodReturnTypeExtension: %featureToggles.narrowPregMatches%
5858
PHPStan\Type\Nette\StringsMatchAllDynamicReturnTypeExtension:
5959
phpstan.broker.dynamicStaticMethodReturnTypeExtension: %featureToggles.narrowPregMatches%
60+
PHPStan\Type\Nette\StringsReplaceCallbackClosureTypeExtension:
61+
phpstan.staticMethodParameterClosureTypeExtension: %featureToggles.narrowPregMatches%
6062

6163
services:
6264
-
@@ -126,3 +128,6 @@ services:
126128

127129
-
128130
class: PHPStan\Type\Nette\StringsMatchDynamicReturnTypeExtension
131+
132+
-
133+
class: PHPStan\Type\Nette\StringsReplaceCallbackClosureTypeExtension
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Nette;
4+
5+
use Nette\Utils\Strings;
6+
use PhpParser\Node\Arg;
7+
use PhpParser\Node\Expr\StaticCall;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Reflection\MethodReflection;
10+
use PHPStan\Reflection\ParameterReflection;
11+
use PHPStan\Reflection\PassedByReference;
12+
use PHPStan\TrinaryLogic;
13+
use PHPStan\Type\ClosureType;
14+
use PHPStan\Type\Constant\ConstantBooleanType;
15+
use PHPStan\Type\Constant\ConstantIntegerType;
16+
use PHPStan\Type\Php\RegexArrayShapeMatcher;
17+
use PHPStan\Type\StaticMethodParameterClosureTypeExtension;
18+
use PHPStan\Type\StringType;
19+
use PHPStan\Type\Type;
20+
use function array_key_exists;
21+
use const PREG_OFFSET_CAPTURE;
22+
use const PREG_UNMATCHED_AS_NULL;
23+
24+
final class StringsReplaceCallbackClosureTypeExtension implements StaticMethodParameterClosureTypeExtension
25+
{
26+
27+
/** @var RegexArrayShapeMatcher */
28+
private $regexArrayShapeMatcher;
29+
30+
public function __construct(RegexArrayShapeMatcher $regexArrayShapeMatcher)
31+
{
32+
$this->regexArrayShapeMatcher = $regexArrayShapeMatcher;
33+
}
34+
35+
public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool
36+
{
37+
return $methodReflection->getDeclaringClass()->getName() === Strings::class
38+
&& $methodReflection->getName() === 'replace'
39+
&& $parameter->getName() === 'replacement';
40+
}
41+
42+
public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type
43+
{
44+
$args = $methodCall->getArgs();
45+
$patternArg = $args[1] ?? null;
46+
$replacementArg = $args[2] ?? null;
47+
48+
if ($patternArg === null || $replacementArg === null) {
49+
return null;
50+
}
51+
52+
$replacementType = $scope->getType($replacementArg->value);
53+
54+
if (!$replacementType->isCallable()->yes()) {
55+
return null;
56+
}
57+
58+
$matchesType = $this->regexArrayShapeMatcher->matchExpr(
59+
$patternArg->value,
60+
$this->resolveFlagsType($args, $scope),
61+
TrinaryLogic::createYes(),
62+
$scope
63+
);
64+
65+
if ($matchesType === null) {
66+
return null;
67+
}
68+
69+
return new ClosureType(
70+
[
71+
$this->createParameterReflectionClass($parameter, $matchesType),
72+
],
73+
new StringType()
74+
);
75+
}
76+
77+
/**
78+
* @param array<Arg> $args
79+
*/
80+
private function resolveFlagsType(array $args, Scope $scope): ConstantIntegerType
81+
{
82+
$captureOffsetType = array_key_exists(4, $args) ? $scope->getType($args[4]->value) : new ConstantBooleanType(false);
83+
$unmatchedAsNullType = array_key_exists(5, $args) ? $scope->getType($args[5]->value) : new ConstantBooleanType(false);
84+
85+
$captureOffset = $captureOffsetType->isTrue()->yes();
86+
$unmatchedAsNull = $unmatchedAsNullType->isTrue()->yes();
87+
88+
return new ConstantIntegerType(($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0));
89+
}
90+
91+
private function createParameterReflectionClass(ParameterReflection $parameter, Type $matchesType): ParameterReflection
92+
{
93+
return new class($parameter, $matchesType) implements ParameterReflection {
94+
95+
/** @var ParameterReflection */
96+
private $parameter;
97+
98+
/** @var Type */
99+
private $matchesType;
100+
101+
public function __construct(
102+
ParameterReflection $parameter,
103+
Type $matchesType
104+
)
105+
{
106+
$this->parameter = $parameter;
107+
$this->matchesType = $matchesType;
108+
}
109+
110+
public function getName(): string
111+
{
112+
return $this->parameter->getName();
113+
}
114+
115+
public function isOptional(): bool
116+
{
117+
return $this->parameter->isOptional();
118+
}
119+
120+
public function getType(): Type
121+
{
122+
return $this->matchesType;
123+
}
124+
125+
public function passedByReference(): PassedByReference
126+
{
127+
return $this->parameter->passedByReference();
128+
}
129+
130+
public function isVariadic(): bool
131+
{
132+
return $this->parameter->isVariadic();
133+
}
134+
135+
public function getDefaultValue(): ?Type
136+
{
137+
return $this->parameter->getDefaultValue();
138+
}
139+
140+
};
141+
}
142+
143+
}

Diff for: tests/Type/Nette/StringsMatchDynamicReturnTypeExtensionTest.php

+7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Type\Nette;
44

55
use PHPStan\Testing\TypeInferenceTestCase;
6+
use const PHP_VERSION_ID;
67

78
class StringsMatchDynamicReturnTypeExtensionTest extends TypeInferenceTestCase
89
{
@@ -13,6 +14,12 @@ class StringsMatchDynamicReturnTypeExtensionTest extends TypeInferenceTestCase
1314
public function dataFileAsserts(): iterable
1415
{
1516
yield from self::gatherAssertTypes(__DIR__ . '/data/strings-match.php');
17+
18+
if (PHP_VERSION_ID < 70400) {
19+
return;
20+
}
21+
22+
yield from self::gatherAssertTypes(__DIR__ . '/data/strings-match-74.php');
1623
}
1724

1825
/**

Diff for: tests/Type/Nette/data/strings-match-74.php

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
namespace StringsMatch;
4+
5+
use Nette\Utils\Strings;
6+
use function PHPStan\Testing\assertType;
7+
8+
function (string $s): void {
9+
Strings::replace(
10+
$s,
11+
'/(foo)?(bar)?(baz)?/',
12+
function ($matches) {
13+
assertType('array{string, \'foo\'|null, \'bar\'|null, \'baz\'|null}', $matches);
14+
return '';
15+
},
16+
-1,
17+
false,
18+
true
19+
);
20+
};
21+
22+
function (string $s): void {
23+
Strings::replace(
24+
$s,
25+
'/(foo)?(bar)?(baz)?/',
26+
function ($matches) {
27+
assertType('array{0: array{string, int<-1, max>}, 1?: array{\'\'|\'foo\', int<-1, max>}, 2?: array{\'\'|\'bar\', int<-1, max>}, 3?: array{\'baz\', int<-1, max>}}', $matches);
28+
return '';
29+
},
30+
-1,
31+
true
32+
);
33+
};
34+
35+
function (string $s): void {
36+
Strings::replace(
37+
$s,
38+
'/(foo)?(bar)?(baz)?/',
39+
function ($matches) {
40+
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);
41+
return '';
42+
},
43+
-1,
44+
true,
45+
true
46+
);
47+
};
48+
49+
function (string $s): void {
50+
$result = Strings::replace(
51+
$s,
52+
'/(foo)?(bar)?(baz)?/',
53+
'bee'
54+
);
55+
assertType('string', $result);
56+
};

0 commit comments

Comments
 (0)