Skip to content

Commit ae6ebcd

Browse files
Improve ReplaceFunctionsDynamicReturnTypeExtension
1 parent 339a29d commit ae6ebcd

File tree

3 files changed

+128
-54
lines changed

3 files changed

+128
-54
lines changed

src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php

Lines changed: 81 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,23 @@
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Reflection\FunctionReflection;
88
use PHPStan\Reflection\ParametersAcceptorSelector;
9+
use PHPStan\Type\Accessory\AccessoryArrayListType;
910
use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
1011
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
1112
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
1213
use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
14+
use PHPStan\Type\Accessory\NonEmptyArrayType;
15+
use PHPStan\Type\ArrayType;
1316
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
1417
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
1518
use PHPStan\Type\IntersectionType;
1619
use PHPStan\Type\MixedType;
20+
use PHPStan\Type\NeverType;
1721
use PHPStan\Type\StringType;
1822
use PHPStan\Type\Type;
1923
use PHPStan\Type\TypeCombinator;
2024
use PHPStan\Type\TypeUtils;
25+
use PHPStan\Type\UnionType;
2126
use function array_key_exists;
2227
use function count;
2328
use function in_array;
@@ -84,6 +89,12 @@ private function getPreliminarilyResolvedTypeFromFunctionCall(
8489
return TypeUtils::toBenevolentUnion($defaultReturnType);
8590
}
8691

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

@@ -92,68 +103,89 @@ private function getPreliminarilyResolvedTypeFromFunctionCall(
92103
if ($replaceArgumentType->isArray()->yes()) {
93104
$replaceArgumentType = $replaceArgumentType->getIterableValueType();
94105
}
106+
}
107+
}
95108

96-
$accessories = [];
97-
if ($subjectArgumentType->isNonFalsyString()->yes() && $replaceArgumentType->isNonFalsyString()->yes()) {
98-
$accessories[] = new AccessoryNonFalsyStringType();
99-
} elseif ($subjectArgumentType->isNonEmptyString()->yes() && $replaceArgumentType->isNonEmptyString()->yes()) {
100-
$accessories[] = new AccessoryNonEmptyStringType();
101-
}
109+
$result = [];
102110

103-
if ($subjectArgumentType->isLowercaseString()->yes() && $replaceArgumentType->isLowercaseString()->yes()) {
104-
$accessories[] = new AccessoryLowercaseStringType();
105-
}
111+
$stringArgumentType = TypeCombinator::intersect(new StringType(), $subjectArgumentType);
112+
if (!$stringArgumentType instanceof NeverType) {
113+
$result[] = $this->getReplaceType($stringArgumentType, $replaceArgumentType);
106114

107-
if ($subjectArgumentType->isUppercaseString()->yes() && $replaceArgumentType->isUppercaseString()->yes()) {
108-
$accessories[] = new AccessoryUppercaseStringType();
109-
}
115+
$subjectArgumentType = $subjectArgumentType->tryRemove($stringArgumentType);
116+
}
110117

111-
if (count($accessories) > 0) {
112-
$accessories[] = new StringType();
113-
return new IntersectionType($accessories);
118+
if ($subjectArgumentType->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 = $subjectArgumentType->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();
114139
}
140+
} else {
141+
$newArrayType = new ArrayType(
142+
$subjectArgumentType->getIterableKeyType(),
143+
$this->getReplaceType($subjectArgumentType->getIterableValueType(), $replaceArgumentType),
144+
);
145+
if ($subjectArgumentType->isList()->yes()) {
146+
$newArrayType = TypeCombinator::intersect($newArrayType, new AccessoryArrayListType());
147+
}
148+
if ($subjectArgumentType->isIterableAtLeastOnce()->yes()) {
149+
$newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType());
150+
}
151+
152+
$result[] = $newArrayType;
115153
}
116154
}
117155

118-
$isStringSuperType = $subjectArgumentType->isString();
119-
$isArraySuperType = $subjectArgumentType->isArray();
120-
$compareSuperTypes = $isStringSuperType->compareTo($isArraySuperType);
121-
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) {
122165
return new StringType();
123-
} elseif ($compareSuperTypes === $isArraySuperType) {
124-
$subjectArrays = $subjectArgumentType->getArrays();
125-
if (count($subjectArrays) > 0) {
126-
$result = [];
127-
foreach ($subjectArrays as $arrayType) {
128-
$constantArrays = $arrayType->getConstantArrays();
129-
130-
if (
131-
$constantArrays !== []
132-
&& in_array($functionReflection->getName(), ['preg_replace', 'preg_replace_callback', 'preg_replace_callback_array'], true)
133-
) {
134-
foreach ($constantArrays as $constantArray) {
135-
$generalizedArray = $constantArray->generalizeValues();
136-
137-
$builder = ConstantArrayTypeBuilder::createEmpty();
138-
// turn all keys optional
139-
foreach ($constantArray->getKeyTypes() as $keyType) {
140-
$builder->setOffsetValueType($keyType, $generalizedArray->getOffsetValueType($keyType), true);
141-
}
142-
$result[] = $builder->getArray();
143-
}
144-
145-
continue;
146-
}
166+
}
147167

148-
$result[] = $arrayType->generalizeValues();
149-
}
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+
}
150174

151-
return TypeCombinator::union(...$result);
152-
}
153-
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);
154186
}
155187

156-
return $defaultReturnType;
188+
return new StringType();
157189
}
158190

159191
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)