Skip to content

Commit a9ebd38

Browse files
Merge branch 2.1.x into 2.2.x (#5638)
Co-authored-by: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com>
1 parent 9f3974b commit a9ebd38

10 files changed

Lines changed: 355 additions & 26 deletions

File tree

src/Analyser/MutatingScope.php

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2023,18 +2023,7 @@ public function enterAnonymousFunctionWithoutReflection(
20232023
$isNullable = $this->isParameterValueNullable($parameter);
20242024
$parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
20252025
if ($callableParameters !== null) {
2026-
if (isset($callableParameters[$i])) {
2027-
$parameterType = self::intersectButNotNever($parameterType, $callableParameters[$i]->getType());
2028-
} elseif (count($callableParameters) > 0) {
2029-
$lastParameter = array_last($callableParameters);
2030-
if ($lastParameter->isVariadic()) {
2031-
$parameterType = self::intersectButNotNever($parameterType, $lastParameter->getType());
2032-
} else {
2033-
$parameterType = self::intersectButNotNever($parameterType, new MixedType());
2034-
}
2035-
} else {
2036-
$parameterType = self::intersectButNotNever($parameterType, new MixedType());
2037-
}
2026+
$parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($callableParameters, $i));
20382027
}
20392028
$holder = ExpressionTypeHolder::createYes($parameter->var, $parameterType);
20402029
$expressionTypes[$paramExprString] = $holder;
@@ -2233,20 +2222,8 @@ public function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFun
22332222
foreach ($arrowFunction->params as $i => $parameter) {
22342223
$isNullable = $this->isParameterValueNullable($parameter);
22352224
$parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
2236-
22372225
if ($callableParameters !== null) {
2238-
if (isset($callableParameters[$i])) {
2239-
$parameterType = self::intersectButNotNever($parameterType, $callableParameters[$i]->getType());
2240-
} elseif (count($callableParameters) > 0) {
2241-
$lastParameter = array_last($callableParameters);
2242-
if ($lastParameter->isVariadic()) {
2243-
$parameterType = self::intersectButNotNever($parameterType, $lastParameter->getType());
2244-
} else {
2245-
$parameterType = self::intersectButNotNever($parameterType, new MixedType());
2246-
}
2247-
} else {
2248-
$parameterType = self::intersectButNotNever($parameterType, new MixedType());
2249-
}
2226+
$parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($callableParameters, $i));
22502227
}
22512228

22522229
if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
@@ -2312,6 +2289,27 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type
23122289
return $this->initializerExprTypeResolver->getFunctionType($type, $isNullable, false, InitializerExprContext::fromScope($this));
23132290
}
23142291

2292+
/**
2293+
* @param ParameterReflection[] $callableParameters
2294+
*/
2295+
private function getCallableParameterType(array $callableParameters, int $index): Type
2296+
{
2297+
if (isset($callableParameters[$index])) {
2298+
return $callableParameters[$index]->getType();
2299+
}
2300+
2301+
if (count($callableParameters) === 0) {
2302+
return new MixedType();
2303+
}
2304+
2305+
$lastParameter = array_last($callableParameters);
2306+
if ($lastParameter->isVariadic()) {
2307+
return $lastParameter->getType();
2308+
}
2309+
2310+
return new MixedType();
2311+
}
2312+
23152313
public static function intersectButNotNever(Type $nativeType, Type $inferredType): Type
23162314
{
23172315
if ($nativeType->isSuperTypeOf($inferredType)->no()) {

src/Type/ConditionalType.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,13 @@ public function describe(VerbosityLevel $level): string
113113

114114
public function isResolvable(): bool
115115
{
116-
return !TypeUtils::containsTemplateType($this->subject) && !TypeUtils::containsTemplateType($this->target);
116+
if (!TypeUtils::containsTemplateType($this->subject) && !TypeUtils::containsTemplateType($this->target)) {
117+
return true;
118+
}
119+
120+
$isSuperType = $this->target->isSuperTypeOf($this->subject);
121+
122+
return $isSuperType->yes() || $isSuperType->no();
117123
}
118124

119125
protected function getResult(): Type
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug11894Nsrt;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
/**
10+
* @template T
11+
* @param T $a
12+
* @return (T is string ? string : T)
13+
*/
14+
function conditionalReturn(mixed $a): mixed
15+
{
16+
if (!is_string($a)) {
17+
return $a;
18+
}
19+
return trim($a);
20+
}
21+
22+
/**
23+
* @template T of string|null
24+
* @param T $a
25+
*/
26+
function testNarrowedToString(mixed $a): void
27+
{
28+
if (!is_string($a)) {
29+
return;
30+
}
31+
assertType('string', conditionalReturn($a));
32+
}
33+
34+
/**
35+
* @template T of int|null
36+
* @param T $a
37+
*/
38+
function testNarrowedToNonMatchingType(mixed $a): void
39+
{
40+
if (!is_int($a)) {
41+
return;
42+
}
43+
assertType('T of int (function Bug11894Nsrt\testNarrowedToNonMatchingType(), argument)', conditionalReturn($a));
44+
}
45+
46+
/**
47+
* @template T of string|int
48+
* @param T $a
49+
*/
50+
function testNotFullyNarrowable(mixed $a): void
51+
{
52+
assertType('string|T of int (function Bug11894Nsrt\testNotFullyNarrowable(), argument)', conditionalReturn($a));
53+
}
54+
55+
abstract class ConditionalArrayKeys
56+
{
57+
/**
58+
* @template TKey of array-key
59+
* @template TArray of array<TKey, mixed>
60+
* @param TArray $array
61+
* @return (TArray is non-empty-array ? non-empty-list<TKey> : list<TKey>)
62+
*/
63+
abstract public function arrayKeys(array $array): array;
64+
65+
/** @param non-empty-array<int, int> $nonEmpty */
66+
public function testMaybeStaysUnresolved(array $nonEmpty): void
67+
{
68+
assertType('non-empty-list<int>', $this->arrayKeys($nonEmpty));
69+
}
70+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bug8048Nsrt;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
interface CustomResponseInterface {}
8+
9+
class CustomResponse implements CustomResponseInterface {}
10+
11+
class ApiService
12+
{
13+
/**
14+
* @template T of CustomResponseInterface
15+
*
16+
* @param class-string<T>|null $responseType
17+
*
18+
* @return ($responseType is class-string<T> ? T : null)
19+
*/
20+
public function request(?string $responseType = null): ?CustomResponseInterface
21+
{
22+
if ($responseType === null) {
23+
return null;
24+
}
25+
26+
return new CustomResponse();
27+
}
28+
}
29+
30+
function (): void {
31+
assertType('null', (new ApiService())->request(null));
32+
assertType('Bug8048Nsrt\CustomResponse', (new ApiService())->request(CustomResponse::class));
33+
$x = rand(0, 1) ? CustomResponse::class : null;
34+
assertType('Bug8048Nsrt\CustomResponse|null', (new ApiService())->request($x));
35+
};

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2956,4 +2956,9 @@ public function testBug3842(): void
29562956
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3842.php'], []);
29572957
}
29582958

2959+
public function testBug11894(): void
2960+
{
2961+
$this->analyse([__DIR__ . '/data/bug-11894.php'], []);
2962+
}
2963+
29592964
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug11894;
4+
5+
/**
6+
* @template T of string|null
7+
* @param T $a
8+
*/
9+
function test(mixed $a): mixed
10+
{
11+
if (!is_string($a)) {
12+
return $a;
13+
}
14+
15+
return conditionalReturn($a);
16+
}
17+
18+
/**
19+
* @template T
20+
* @param T $a
21+
* @return (T is string ? string : T)
22+
*/
23+
function conditionalReturn(mixed $a): mixed
24+
{
25+
if (!is_string($a)) {
26+
return $a;
27+
}
28+
29+
return trim($a);
30+
}
31+
32+
/**
33+
* @template T of string|null
34+
* @param T $a
35+
*/
36+
function testNegated(mixed $a): mixed
37+
{
38+
if (!is_string($a)) {
39+
return $a;
40+
}
41+
42+
return conditionalReturnNegated($a);
43+
}
44+
45+
/**
46+
* @template T
47+
* @param T $a
48+
* @return (T is not string ? T : string)
49+
*/
50+
function conditionalReturnNegated(mixed $a): mixed
51+
{
52+
if (!is_string($a)) {
53+
return $a;
54+
}
55+
56+
return trim($a);
57+
}
58+
59+
/**
60+
* @template T of int|null
61+
* @param T $a
62+
*/
63+
function testNoRelation(mixed $a): mixed
64+
{
65+
if (!is_int($a)) {
66+
return $a;
67+
}
68+
69+
return conditionalReturn($a);
70+
}
71+
72+
/**
73+
* @template T of string|int
74+
* @param T $a
75+
*/
76+
function testMaybeRelation(mixed $a): mixed
77+
{
78+
return conditionalReturn($a);
79+
}

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4086,4 +4086,20 @@ public function testBug14549(): void
40864086
]);
40874087
}
40884088

4089+
public function testBug11894(): void
4090+
{
4091+
$this->checkThisOnly = false;
4092+
$this->checkNullables = true;
4093+
$this->checkUnionTypes = true;
4094+
$this->analyse([__DIR__ . '/data/bug-11894.php'], []);
4095+
}
4096+
4097+
public function testBug8048(): void
4098+
{
4099+
$this->checkThisOnly = false;
4100+
$this->checkNullables = true;
4101+
$this->checkUnionTypes = true;
4102+
$this->analyse([__DIR__ . '/data/bug-8048.php'], []);
4103+
}
4104+
40894105
}

tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,4 +1031,10 @@ public function testConstantParameterCheckStatic(): void
10311031
]);
10321032
}
10331033

1034+
public function testBug11894(): void
1035+
{
1036+
$this->checkThisOnly = false;
1037+
$this->analyse([__DIR__ . '/data/bug-11894.php'], []);
1038+
}
1039+
10341040
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug11894Methods;
4+
5+
class Converter
6+
{
7+
/**
8+
* @template T
9+
* @param T $a
10+
* @return (T is string ? string : T)
11+
*/
12+
public function conditionalReturn(mixed $a): mixed
13+
{
14+
if (!is_string($a)) {
15+
return $a;
16+
}
17+
return trim($a);
18+
}
19+
20+
/**
21+
* @template T
22+
* @param T $a
23+
* @return (T is string ? string : T)
24+
*/
25+
public static function conditionalReturnStatic(mixed $a): mixed
26+
{
27+
if (!is_string($a)) {
28+
return $a;
29+
}
30+
return trim($a);
31+
}
32+
}
33+
34+
class Consumer
35+
{
36+
/**
37+
* @template T of string|null
38+
* @param T $a
39+
*/
40+
public function testMethod(mixed $a): mixed
41+
{
42+
if (!is_string($a)) {
43+
return $a;
44+
}
45+
46+
$c = new Converter();
47+
return $c->conditionalReturn($a);
48+
}
49+
50+
/**
51+
* @template T of string|null
52+
* @param T $a
53+
*/
54+
public function testStaticMethod(mixed $a): mixed
55+
{
56+
if (!is_string($a)) {
57+
return $a;
58+
}
59+
60+
return Converter::conditionalReturnStatic($a);
61+
}
62+
63+
/**
64+
* @template T of string|int
65+
* @param T $a
66+
*/
67+
public function testMaybeMethod(mixed $a): mixed
68+
{
69+
$c = new Converter();
70+
return $c->conditionalReturn($a);
71+
}
72+
73+
/**
74+
* @template T of string|int
75+
* @param T $a
76+
*/
77+
public function testMaybeStaticMethod(mixed $a): mixed
78+
{
79+
return Converter::conditionalReturnStatic($a);
80+
}
81+
}

0 commit comments

Comments
 (0)