Skip to content

Commit c33c5a1

Browse files
Add support for template bound null type
Co-authored-by: Ondřej Mirtes <[email protected]>
1 parent b9161b3 commit c33c5a1

13 files changed

+161
-31
lines changed

phpstan-baseline.neon

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,6 +1251,12 @@ parameters:
12511251
count: 2
12521252
path: src/Type/Generic/TemplateMixedType.php
12531253

1254+
-
1255+
message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#'
1256+
identifier: phpstanApi.instanceofType
1257+
count: 3
1258+
path: src/Type/Generic/TemplateNullType.php
1259+
12541260
-
12551261
message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#'
12561262
identifier: phpstanApi.instanceofType
@@ -1335,6 +1341,12 @@ parameters:
13351341
count: 1
13361342
path: src/Type/Generic/TemplateTypeFactory.php
13371343

1344+
-
1345+
message: '#^Doing instanceof PHPStan\\Type\\NullType is error\-prone and deprecated\. Use Type\:\:isNull\(\) instead\.$#'
1346+
identifier: phpstanApi.instanceofType
1347+
count: 1
1348+
path: src/Type/Generic/TemplateTypeFactory.php
1349+
13381350
-
13391351
message: '#^Doing instanceof PHPStan\\Type\\ObjectShapeType is error\-prone and deprecated\. Use Type\:\:isObject\(\) and Type\:\:hasProperty\(\) instead\.$#'
13401352
identifier: phpstanApi.instanceofType

src/Rules/Generics/TemplateTypeCheck.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use PHPStan\Type\IterableType;
2929
use PHPStan\Type\KeyOfType;
3030
use PHPStan\Type\MixedType;
31+
use PHPStan\Type\NullType;
3132
use PHPStan\Type\ObjectShapeType;
3233
use PHPStan\Type\ObjectType;
3334
use PHPStan\Type\ObjectWithoutClassType;
@@ -132,6 +133,7 @@ public function check(
132133
&& $boundTypeClass !== GenericObjectType::class
133134
&& $boundTypeClass !== KeyOfType::class
134135
&& $boundTypeClass !== IterableType::class
136+
&& $boundTypeClass !== NullType::class
135137
&& !$boundType instanceof UnionType
136138
&& !$boundType instanceof IntersectionType
137139
&& !$boundType instanceof TemplateType

src/Type/Generic/TemplateNullType.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Generic;
4+
5+
use PHPStan\Type\NullType;
6+
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
7+
use PHPStan\Type\Type;
8+
9+
final class TemplateNullType extends NullType implements TemplateType
10+
{
11+
12+
/** @use TemplateTypeTrait<NullType> */
13+
use TemplateTypeTrait;
14+
use UndecidedComparisonCompoundTypeTrait;
15+
16+
/**
17+
* @param non-empty-string $name
18+
*/
19+
public function __construct(
20+
TemplateTypeScope $scope,
21+
TemplateTypeStrategy $templateTypeStrategy,
22+
TemplateTypeVariance $templateTypeVariance,
23+
string $name,
24+
NullType $bound,
25+
?Type $default,
26+
)
27+
{
28+
parent::__construct();
29+
$this->scope = $scope;
30+
$this->strategy = $templateTypeStrategy;
31+
$this->variance = $templateTypeVariance;
32+
$this->name = $name;
33+
$this->bound = $bound;
34+
$this->default = $default;
35+
}
36+
37+
}

src/Type/Generic/TemplateTypeFactory.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use PHPStan\Type\IterableType;
1616
use PHPStan\Type\KeyOfType;
1717
use PHPStan\Type\MixedType;
18+
use PHPStan\Type\NullType;
1819
use PHPStan\Type\ObjectShapeType;
1920
use PHPStan\Type\ObjectType;
2021
use PHPStan\Type\ObjectWithoutClassType;
@@ -112,6 +113,10 @@ public static function create(TemplateTypeScope $scope, string $name, ?Type $bou
112113
return new TemplateIterableType($scope, $strategy, $variance, $name, $bound, $default);
113114
}
114115

116+
if ($bound instanceof NullType && ($boundClass === NullType::class || $bound instanceof TemplateType)) {
117+
return new TemplateNullType($scope, $strategy, $variance, $name, $bound, $default);
118+
}
119+
115120
return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true), $default);
116121
}
117122

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug12894;
6+
7+
use Closure;
8+
9+
/**
10+
* @template TValue of object|null
11+
*/
12+
interface Dependency
13+
{
14+
15+
/**
16+
* @return TValue
17+
*/
18+
public function __invoke(): object|null;
19+
20+
}
21+
22+
interface DependencyResolver
23+
{
24+
25+
/**
26+
* @template V of object|null
27+
* @template D of Dependency<V>
28+
*
29+
* @param D $dependency
30+
*
31+
* @return V
32+
*/
33+
public function resolve(Dependency $dependency): object|null;
34+
35+
}
36+
37+
class Resolver implements DependencyResolver
38+
{
39+
/**
40+
* @var Closure(object|null): void
41+
*/
42+
protected Closure $run;
43+
44+
public function resolve(Dependency $dependency): object|null {
45+
$resolved = $dependency();
46+
\PHPStan\Testing\assertType('V of object|null (method Bug12894\DependencyResolver::resolve(), argument)', $resolved);
47+
$result = is_object($resolved) ? 1 : 2;
48+
\PHPStan\Testing\assertType('V of object (method Bug12894\DependencyResolver::resolve(), argument)|V of null (method Bug12894\DependencyResolver::resolve(), argument)', $resolved);
49+
($this->run)($resolved);
50+
return $resolved;
51+
}
52+
53+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug12989;
4+
5+
/**
6+
* @template T of int|null
7+
* @phpstan-param T $b
8+
* @phpstan-return int|T
9+
*/
10+
function a(?int $b): ?int
11+
{
12+
if ($b === null) {
13+
\PHPStan\Testing\assertType('T of null (function Bug12989\a(), argument)', $b);
14+
return $b;
15+
}
16+
return $b;
17+
}

tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,4 +435,11 @@ public function testPropertyHooks(): void
435435
]);
436436
}
437437

438+
439+
#[RequiresPhp('>= 8.1')]
440+
public function testBug13048(): void
441+
{
442+
$this->analyse([__DIR__ . '/data/bug-13048.php'], []);
443+
}
444+
438445
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php // lint >= 8.1
2+
3+
namespace Bug13048;
4+
5+
enum IndexBy {
6+
case A;
7+
case B;
8+
}
9+
10+
/**
11+
* @template T of IndexBy|null
12+
* @param T $indexBy
13+
*/
14+
function run(?IndexBy $indexBy = null): ?string
15+
{
16+
return match ($indexBy) {
17+
IndexBy::A => 'by A',
18+
IndexBy::B => 'by B',
19+
null => null,
20+
};
21+
}

tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,6 @@ public function testRule(): void
5656
'PHPDoc tag @template T for function FunctionTemplateType\resourceBound() with bound type resource is not supported.',
5757
50,
5858
],
59-
[
60-
'PHPDoc tag @template T for function FunctionTemplateType\nullNotSupported() with bound type null is not supported.',
61-
68,
62-
],
6359
[
6460
'Call-site variance of covariant int in generic type FunctionTemplateType\GenericCovariant<covariant int> in PHPDoc tag @template U is redundant, template type T of class FunctionTemplateType\GenericCovariant has the same variance.',
6561
94,

tests/PHPStan/Rules/Generics/data/function-template.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ function nakano()
6565
}
6666

6767
/** @template T of null */
68-
function nullNotSupported()
68+
function nullSupported()
6969
{
7070

7171
}

0 commit comments

Comments
 (0)