Skip to content

Commit b762936

Browse files
Improve ReplaceFunctionsDynamicReturnTypeExtension
1 parent 339a29d commit b762936

File tree

3 files changed

+129
-54
lines changed

3 files changed

+129
-54
lines changed

src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php

Lines changed: 82 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,90 @@ 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->isString()->yes()) {
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+
$arrayArgumentType = TypeCombinator::intersect(new ArrayType(new MixedType(), new MixedType()), $subjectArgumentType);
119+
if ($arrayArgumentType->isArray()->yes()) {
120+
$keyShouldBeOptional = in_array(
121+
$functionReflection->getName(),
122+
['preg_replace', 'preg_replace_callback', 'preg_replace_callback_array'],
123+
true,
124+
);
125+
126+
$constantArrays = $arrayArgumentType->getConstantArrays();
127+
if ($constantArrays !== []) {
128+
foreach ($constantArrays as $constantArray) {
129+
$valueTypes = $constantArray->getValueTypes();
130+
131+
$builder = ConstantArrayTypeBuilder::createEmpty();
132+
foreach ($constantArray->getKeyTypes() as $index => $keyType) {
133+
$builder->setOffsetValueType(
134+
$keyType,
135+
$this->getReplaceType($valueTypes[$index], $replaceArgumentType),
136+
$keyShouldBeOptional || $constantArray->isOptionalKey($index),
137+
);
138+
}
139+
$result[] = $builder->getArray();
114140
}
141+
} else {
142+
$newArrayType = new ArrayType(
143+
$arrayArgumentType->getIterableKeyType(),
144+
$this->getReplaceType($arrayArgumentType->getIterableValueType(), $replaceArgumentType),
145+
);
146+
if ($arrayArgumentType->isList()->yes()) {
147+
$newArrayType = TypeCombinator::intersect($newArrayType, new AccessoryArrayListType());
148+
}
149+
if ($arrayArgumentType->isIterableAtLeastOnce()->yes()) {
150+
$newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType());
151+
}
152+
153+
$result[] = $newArrayType;
115154
}
116155
}
117156

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

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

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

156-
return $defaultReturnType;
189+
return new StringType();
157190
}
158191

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