Skip to content

Commit 676fac4

Browse files
Improve ReplaceFunctionsDynamicReturnTypeExtension
1 parent 1f150cc commit 676fac4

File tree

3 files changed

+126
-54
lines changed

3 files changed

+126
-54
lines changed

src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php

Lines changed: 79 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
use PHPStan\DependencyInjection\AutowiredService;
88
use PHPStan\Reflection\FunctionReflection;
99
use PHPStan\Reflection\ParametersAcceptorSelector;
10+
use PHPStan\Type\Accessory\AccessoryArrayListType;
1011
use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
1112
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
1213
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
1314
use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
15+
use PHPStan\Type\Accessory\NonEmptyArrayType;
16+
use PHPStan\Type\ArrayType;
1417
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
1518
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
1619
use PHPStan\Type\IntersectionType;
@@ -19,6 +22,7 @@
1922
use PHPStan\Type\Type;
2023
use PHPStan\Type\TypeCombinator;
2124
use PHPStan\Type\TypeUtils;
25+
use PHPStan\Type\UnionType;
2226
use function array_key_exists;
2327
use function count;
2428
use function in_array;
@@ -86,6 +90,12 @@ private function getPreliminarilyResolvedTypeFromFunctionCall(
8690
return TypeUtils::toBenevolentUnion($defaultReturnType);
8791
}
8892

93+
$stringOrArray = new UnionType([new StringType(), new ArrayType(new MixedType(), new MixedType())]);
94+
if (!$stringOrArray->isSuperTypeOf($subjectArgumentType)->yes()) {
95+
return $defaultReturnType;
96+
}
97+
98+
$replaceArgumentType = null;
8999
if (array_key_exists($functionReflection->getName(), self::FUNCTIONS_REPLACE_POSITION)) {
90100
$replaceArgumentPosition = self::FUNCTIONS_REPLACE_POSITION[$functionReflection->getName()];
91101

@@ -94,68 +104,88 @@ private function getPreliminarilyResolvedTypeFromFunctionCall(
94104
if ($replaceArgumentType->isArray()->yes()) {
95105
$replaceArgumentType = $replaceArgumentType->getIterableValueType();
96106
}
107+
}
108+
}
97109

98-
$accessories = [];
99-
if ($subjectArgumentType->isNonFalsyString()->yes() && $replaceArgumentType->isNonFalsyString()->yes()) {
100-
$accessories[] = new AccessoryNonFalsyStringType();
101-
} elseif ($subjectArgumentType->isNonEmptyString()->yes() && $replaceArgumentType->isNonEmptyString()->yes()) {
102-
$accessories[] = new AccessoryNonEmptyStringType();
103-
}
110+
$result = [];
104111

105-
if ($subjectArgumentType->isLowercaseString()->yes() && $replaceArgumentType->isLowercaseString()->yes()) {
106-
$accessories[] = new AccessoryLowercaseStringType();
107-
}
112+
$stringArgumentType = TypeCombinator::intersect(new StringType(), $subjectArgumentType);
113+
if ($stringArgumentType->isString()->yes()) {
114+
$result[] = $this->getReplaceType($stringArgumentType, $replaceArgumentType);
115+
}
108116

109-
if ($subjectArgumentType->isUppercaseString()->yes() && $replaceArgumentType->isUppercaseString()->yes()) {
110-
$accessories[] = new AccessoryUppercaseStringType();
117+
$arrayArgumentType = TypeCombinator::intersect(new ArrayType(new MixedType(), new MixedType()), $subjectArgumentType);
118+
if ($arrayArgumentType->isArray()->yes()) {
119+
$keyShouldBeOptional = in_array(
120+
$functionReflection->getName(),
121+
['preg_replace', 'preg_replace_callback', 'preg_replace_callback_array'],
122+
true,
123+
);
124+
125+
$constantArrays = $arrayArgumentType->getConstantArrays();
126+
if ($constantArrays !== []) {
127+
foreach ($constantArrays as $constantArray) {
128+
$valueTypes = $constantArray->getValueTypes();
129+
130+
$builder = ConstantArrayTypeBuilder::createEmpty();
131+
foreach ($constantArray->getKeyTypes() as $index => $keyType) {
132+
$builder->setOffsetValueType(
133+
$keyType,
134+
$this->getReplaceType($valueTypes[$index], $replaceArgumentType),
135+
$keyShouldBeOptional || $constantArray->isOptionalKey($index),
136+
);
137+
}
138+
$result[] = $builder->getArray();
111139
}
112-
113-
if (count($accessories) > 0) {
114-
$accessories[] = new StringType();
115-
return new IntersectionType($accessories);
140+
} else {
141+
$newArrayType = new ArrayType(
142+
$arrayArgumentType->getIterableKeyType(),
143+
$this->getReplaceType($arrayArgumentType->getIterableValueType(), $replaceArgumentType),
144+
);
145+
if ($arrayArgumentType->isList()->yes()) {
146+
$newArrayType = TypeCombinator::intersect($newArrayType, new AccessoryArrayListType());
116147
}
148+
if ($arrayArgumentType->isIterableAtLeastOnce()->yes()) {
149+
$newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType());
150+
}
151+
152+
$result[] = $newArrayType;
117153
}
118154
}
119155

120-
$isStringSuperType = $subjectArgumentType->isString();
121-
$isArraySuperType = $subjectArgumentType->isArray();
122-
$compareSuperTypes = $isStringSuperType->compareTo($isArraySuperType);
123-
if ($compareSuperTypes === $isStringSuperType) {
156+
return TypeCombinator::union(...$result);
157+
}
158+
159+
private function getReplaceType(
160+
Type $subjectArgumentType,
161+
?Type $replaceArgumentType,
162+
): Type
163+
{
164+
if ($replaceArgumentType === null) {
124165
return new StringType();
125-
} elseif ($compareSuperTypes === $isArraySuperType) {
126-
$subjectArrays = $subjectArgumentType->getArrays();
127-
if (count($subjectArrays) > 0) {
128-
$result = [];
129-
foreach ($subjectArrays as $arrayType) {
130-
$constantArrays = $arrayType->getConstantArrays();
131-
132-
if (
133-
$constantArrays !== []
134-
&& in_array($functionReflection->getName(), ['preg_replace', 'preg_replace_callback', 'preg_replace_callback_array'], true)
135-
) {
136-
foreach ($constantArrays as $constantArray) {
137-
$generalizedArray = $constantArray->generalizeValues();
138-
139-
$builder = ConstantArrayTypeBuilder::createEmpty();
140-
// turn all keys optional
141-
foreach ($constantArray->getKeyTypes() as $keyType) {
142-
$builder->setOffsetValueType($keyType, $generalizedArray->getOffsetValueType($keyType), true);
143-
}
144-
$result[] = $builder->getArray();
145-
}
146-
147-
continue;
148-
}
166+
}
149167

150-
$result[] = $arrayType->generalizeValues();
151-
}
168+
$accessories = [];
169+
if ($subjectArgumentType->isNonFalsyString()->yes() && $replaceArgumentType->isNonFalsyString()->yes()) {
170+
$accessories[] = new AccessoryNonFalsyStringType();
171+
} elseif ($subjectArgumentType->isNonEmptyString()->yes() && $replaceArgumentType->isNonEmptyString()->yes()) {
172+
$accessories[] = new AccessoryNonEmptyStringType();
173+
}
152174

153-
return TypeCombinator::union(...$result);
154-
}
155-
return $subjectArgumentType;
175+
if ($subjectArgumentType->isLowercaseString()->yes() && $replaceArgumentType->isLowercaseString()->yes()) {
176+
$accessories[] = new AccessoryLowercaseStringType();
177+
}
178+
179+
if ($subjectArgumentType->isUppercaseString()->yes() && $replaceArgumentType->isUppercaseString()->yes()) {
180+
$accessories[] = new AccessoryUppercaseStringType();
181+
}
182+
183+
if (count($accessories) > 0) {
184+
$accessories[] = new StringType();
185+
return new IntersectionType($accessories);
156186
}
157187

158-
return $defaultReturnType;
188+
return new StringType();
159189
}
160190

161191
private function getSubjectType(

stubs/core.stub

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ function preg_match($pattern, $subject, &$matches = [], int $flags = 0, int $off
227227
* @param string|array<string|int|float> $subject
228228
* @param int $count
229229
* @param-out 0|positive-int $count
230-
* @return ($subject is array ? list<string>|null : string|null)
230+
* @return ($subject is array ? array<string>|null : string|null)
231231
*/
232232
function preg_replace_callback($pattern, $callback, $subject, int $limit = -1, &$count = null, int $flags = 0) {}
233233

@@ -237,7 +237,7 @@ function preg_replace_callback($pattern, $callback, $subject, int $limit = -1, &
237237
* @param string|array<string|int|float> $subject
238238
* @param int $count
239239
* @param-out 0|positive-int $count
240-
* @return ($subject is array ? list<string>|null : string|null)
240+
* @return ($subject is array ? array<string>|null : string|null)
241241
*/
242242
function preg_replace($pattern, $replacement, $subject, int $limit = -1, &$count = null) {}
243243

@@ -256,7 +256,7 @@ function preg_filter($pattern, $replacement, $subject, int $limit = -1, &$count
256256
* @param array<string>|string $replace
257257
* @param array<string>|string $subject
258258
* @param-out int $count
259-
* @return list<string>|string
259+
* @return array<string>|string
260260
*/
261261
function str_replace($search, $replace, $subject, ?int &$count = null) {}
262262

@@ -265,7 +265,7 @@ function str_replace($search, $replace, $subject, ?int &$count = null) {}
265265
* @param array<string>|string $replace
266266
* @param array<string>|string $subject
267267
* @param-out int $count
268-
* @return list<string>|string
268+
* @return array<string>|string
269269
*/
270270
function str_ireplace($search, $replace, $subject, ?int &$count = null) {}
271271

@@ -326,4 +326,3 @@ function get_defined_constants(bool $categorize = false): array {}
326326
* @return __benevolent<array<string,string>|array<string,false>|array<string,list<mixed>>|false>
327327
*/
328328
function getopt(string $short_options, array $long_options = [], &$rest_index = null) {}
329-
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug9870;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class HelloWorld
8+
{
9+
/**
10+
* @param non-empty-string|list<non-empty-string> $date
11+
*/
12+
public function sayHello($date): void
13+
{
14+
if (is_string($date)) {
15+
assertType('non-empty-string', str_replace('-', '/', $date));
16+
} else {
17+
assertType('list<non-empty-string>', str_replace('-', '/', $date));
18+
}
19+
assertType('list<non-empty-string>|non-empty-string', str_replace('-', '/', $date));
20+
}
21+
22+
/**
23+
* @param string|array<string> $stringOrArray
24+
* @param non-empty-string|array<string> $nonEmptyStringOrArray
25+
* @param string|array<non-empty-string> $stringOrArrayNonEmptyString
26+
* @param string|non-empty-array<string> $stringOrNonEmptyArray
27+
* @param string|array<string>|bool|int $wrongParam
28+
*/
29+
public function moreCheck(
30+
$stringOrArray,
31+
$nonEmptyStringOrArray,
32+
$stringOrArrayNonEmptyString,
33+
$stringOrNonEmptyArray,
34+
$wrongParam,
35+
): void {
36+
assertType('array<string>|string', str_replace('-', '/', $stringOrArray));
37+
assertType('array<string>|non-empty-string', str_replace('-', '/', $nonEmptyStringOrArray));
38+
assertType('array<non-empty-string>|string', str_replace('-', '/', $stringOrArrayNonEmptyString));
39+
assertType('non-empty-array<string>|string', str_replace('-', '/', $stringOrNonEmptyArray));
40+
assertType('array<string>|string', str_replace('-', '/', $wrongParam));
41+
assertType('array<string>|string', str_replace('-', '/'));
42+
}
43+
}

0 commit comments

Comments
 (0)