Skip to content

Commit 716aefa

Browse files
Improve ReplaceFunctionsDynamicReturnTypeExtension
1 parent 339a29d commit 716aefa

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
@@ -6,10 +6,13 @@
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;
@@ -18,6 +21,7 @@
1821
use PHPStan\Type\Type;
1922
use PHPStan\Type\TypeCombinator;
2023
use PHPStan\Type\TypeUtils;
24+
use PHPStan\Type\UnionType;
2125
use function array_key_exists;
2226
use function count;
2327
use function in_array;
@@ -84,6 +88,12 @@ private function getPreliminarilyResolvedTypeFromFunctionCall(
8488
return TypeUtils::toBenevolentUnion($defaultReturnType);
8589
}
8690

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

@@ -92,68 +102,88 @@ private function getPreliminarilyResolvedTypeFromFunctionCall(
92102
if ($replaceArgumentType->isArray()->yes()) {
93103
$replaceArgumentType = $replaceArgumentType->getIterableValueType();
94104
}
105+
}
106+
}
95107

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-
}
108+
$result = [];
102109

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

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

118-
$isStringSuperType = $subjectArgumentType->isString();
119-
$isArraySuperType = $subjectArgumentType->isArray();
120-
$compareSuperTypes = $isStringSuperType->compareTo($isArraySuperType);
121-
if ($compareSuperTypes === $isStringSuperType) {
154+
return TypeCombinator::union(...$result);
155+
}
156+
157+
private function getReplaceType(
158+
Type $subjectArgumentType,
159+
?Type $replaceArgumentType,
160+
): Type
161+
{
162+
if ($replaceArgumentType === null) {
122163
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-
}
164+
}
147165

148-
$result[] = $arrayType->generalizeValues();
149-
}
166+
$accessories = [];
167+
if ($subjectArgumentType->isNonFalsyString()->yes() && $replaceArgumentType->isNonFalsyString()->yes()) {
168+
$accessories[] = new AccessoryNonFalsyStringType();
169+
} elseif ($subjectArgumentType->isNonEmptyString()->yes() && $replaceArgumentType->isNonEmptyString()->yes()) {
170+
$accessories[] = new AccessoryNonEmptyStringType();
171+
}
150172

151-
return TypeCombinator::union(...$result);
152-
}
153-
return $subjectArgumentType;
173+
if ($subjectArgumentType->isLowercaseString()->yes() && $replaceArgumentType->isLowercaseString()->yes()) {
174+
$accessories[] = new AccessoryLowercaseStringType();
175+
}
176+
177+
if ($subjectArgumentType->isUppercaseString()->yes() && $replaceArgumentType->isUppercaseString()->yes()) {
178+
$accessories[] = new AccessoryUppercaseStringType();
179+
}
180+
181+
if (count($accessories) > 0) {
182+
$accessories[] = new StringType();
183+
return new IntersectionType($accessories);
154184
}
155185

156-
return $defaultReturnType;
186+
return new StringType();
157187
}
158188

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