Skip to content

Commit 164241d

Browse files
authored
Check variance of template types in properties
1 parent 8933feb commit 164241d

File tree

9 files changed

+495
-0
lines changed

9 files changed

+495
-0
lines changed

conf/bleedingEdge.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ parameters:
3434
paramOutVariance: true
3535
allInvalidPhpDocs: true
3636
strictStaticMethodTemplateTypeVariance: true
37+
propertyVariance: true

conf/config.level2.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ conditionalTags:
5353
phpstan.rules.rule: %featureToggles.illegalConstructorMethodCall%
5454
PHPStan\Rules\PhpDoc\VarTagChangedExpressionTypeRule:
5555
phpstan.rules.rule: %featureToggles.varTagType%
56+
PHPStan\Rules\Generics\PropertyVarianceRule:
57+
phpstan.rules.rule: %featureToggles.propertyVariance%
5658

5759
services:
5860
-
@@ -102,3 +104,5 @@ services:
102104
checkTypeAgainstNativeType: %featureToggles.varTagType%
103105
tags:
104106
- phpstan.rules.rule
107+
-
108+
class: PHPStan\Rules\Generics\PropertyVarianceRule

conf/config.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ parameters:
6464
paramOutVariance: false
6565
allInvalidPhpDocs: false
6666
strictStaticMethodTemplateTypeVariance: false
67+
propertyVariance: false
6768
fileExtensions:
6869
- php
6970
checkAdvancedIsset: false
@@ -298,6 +299,7 @@ parametersSchema:
298299
paramOutVariance: bool()
299300
allInvalidPhpDocs: bool()
300301
strictStaticMethodTemplateTypeVariance: bool()
302+
propertyVariance: bool()
301303
])
302304
fileExtensions: listOf(string())
303305
checkAdvancedIsset: bool()
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Generics;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Internal\SprintfHelper;
8+
use PHPStan\Node\ClassPropertyNode;
9+
use PHPStan\Reflection\ClassReflection;
10+
use PHPStan\Rules\Rule;
11+
use function sprintf;
12+
13+
/**
14+
* @implements Rule<ClassPropertyNode>
15+
*/
16+
class PropertyVarianceRule implements Rule
17+
{
18+
19+
public function __construct(private VarianceCheck $varianceCheck)
20+
{
21+
}
22+
23+
public function getNodeType(): string
24+
{
25+
return ClassPropertyNode::class;
26+
}
27+
28+
public function processNode(Node $node, Scope $scope): array
29+
{
30+
$classReflection = $scope->getClassReflection();
31+
if (!$classReflection instanceof ClassReflection) {
32+
return [];
33+
}
34+
35+
if (!$classReflection->hasNativeProperty($node->getName())) {
36+
return [];
37+
}
38+
39+
$propertyReflection = $classReflection->getNativeProperty($node->getName());
40+
41+
if ($propertyReflection->isPrivate()) {
42+
return [];
43+
}
44+
45+
return $this->varianceCheck->checkProperty(
46+
$propertyReflection,
47+
sprintf('in property %s::$%s', SprintfHelper::escapeFormatString($classReflection->getDisplayName()), SprintfHelper::escapeFormatString($node->getName())),
48+
$propertyReflection->isReadOnly(),
49+
);
50+
}
51+
52+
}

src/Rules/Generics/VarianceCheck.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Rules\Generics;
44

55
use PHPStan\Reflection\ParametersAcceptorWithPhpDocs;
6+
use PHPStan\Reflection\Php\PhpPropertyReflection;
67
use PHPStan\Rules\RuleError;
78
use PHPStan\Rules\RuleErrorBuilder;
89
use PHPStan\Type\Generic\TemplateType;
@@ -87,6 +88,21 @@ public function checkParametersAcceptor(
8788
return $errors;
8889
}
8990

91+
/** @return RuleError[] */
92+
public function checkProperty(
93+
PhpPropertyReflection $propertyReflection,
94+
string $message,
95+
bool $isReadOnly,
96+
): array
97+
{
98+
$type = $propertyReflection->getReadableType();
99+
$variance = $isReadOnly
100+
? TemplateTypeVariance::createCovariant()
101+
: TemplateTypeVariance::createInvariant();
102+
103+
return $this->check($variance, $type, $message);
104+
}
105+
90106
/** @return RuleError[] */
91107
public function check(TemplateTypeVariance $positionVariance, Type $type, string $messageContext): array
92108
{
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Generics;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use const PHP_VERSION_ID;
8+
9+
/**
10+
* @extends RuleTestCase<PropertyVarianceRule>
11+
*/
12+
class PropertyVarianceRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new PropertyVarianceRule(
18+
self::getContainer()->getByType(VarianceCheck::class),
19+
);
20+
}
21+
22+
public function testRule(): void
23+
{
24+
$this->analyse([__DIR__ . '/data/property-variance.php'], [
25+
[
26+
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\B::$a.',
27+
51,
28+
],
29+
[
30+
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\B::$b.',
31+
54,
32+
],
33+
[
34+
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\B::$c.',
35+
57,
36+
],
37+
[
38+
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\B::$d.',
39+
60,
40+
],
41+
[
42+
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\C::$a.',
43+
80,
44+
],
45+
[
46+
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\C::$b.',
47+
83,
48+
],
49+
[
50+
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\C::$c.',
51+
86,
52+
],
53+
[
54+
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\C::$d.',
55+
89,
56+
],
57+
]);
58+
}
59+
60+
public function testPromoted(): void
61+
{
62+
if (PHP_VERSION_ID < 80000) {
63+
$this->markTestSkipped('Test requires PHP 8.0.');
64+
}
65+
66+
$this->analyse([__DIR__ . '/data/property-variance-promoted.php'], [
67+
[
68+
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$a.',
69+
58,
70+
],
71+
[
72+
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$b.',
73+
59,
74+
],
75+
[
76+
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$c.',
77+
60,
78+
],
79+
[
80+
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$d.',
81+
61,
82+
],
83+
[
84+
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\Promoted\C::$a.',
85+
84,
86+
],
87+
[
88+
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\Promoted\C::$b.',
89+
85,
90+
],
91+
[
92+
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\Promoted\C::$c.',
93+
86,
94+
],
95+
[
96+
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\Promoted\C::$d.',
97+
87,
98+
],
99+
]);
100+
}
101+
102+
public function testReadOnly(): void
103+
{
104+
if (PHP_VERSION_ID < 80100) {
105+
$this->markTestSkipped('Test requires PHP 8.1.');
106+
}
107+
108+
$this->analyse([__DIR__ . '/data/property-variance-readonly.php'], [
109+
[
110+
'Template type X is declared as covariant, but occurs in contravariant position in property PropertyVariance\ReadOnly\B::$b.',
111+
45,
112+
],
113+
[
114+
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\ReadOnly\B::$d.',
115+
51,
116+
],
117+
[
118+
'Template type X is declared as contravariant, but occurs in covariant position in property PropertyVariance\ReadOnly\C::$a.',
119+
62,
120+
],
121+
[
122+
'Template type X is declared as contravariant, but occurs in covariant position in property PropertyVariance\ReadOnly\C::$c.',
123+
68,
124+
],
125+
[
126+
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\ReadOnly\C::$d.',
127+
71,
128+
],
129+
[
130+
'Template type X is declared as contravariant, but occurs in covariant position in property PropertyVariance\ReadOnly\D::$a.',
131+
86,
132+
],
133+
]);
134+
}
135+
136+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php // lint >= 8.0
2+
3+
namespace PropertyVariance\Promoted;
4+
5+
/** @template-contravariant T */
6+
interface In {
7+
}
8+
9+
/** @template-covariant T */
10+
interface Out {
11+
}
12+
13+
/** @template T */
14+
interface Invariant {
15+
}
16+
17+
/**
18+
* @template X
19+
*/
20+
class A {
21+
/**
22+
* @param X $a
23+
* @param In<X> $b
24+
* @param Out<X> $c
25+
* @param Invariant<X> $d
26+
* @param X $e
27+
* @param In<X> $f
28+
* @param Out<X> $g
29+
* @param Invariant<X> $h
30+
*/
31+
public function __construct(
32+
public $a,
33+
public $b,
34+
public $c,
35+
public $d,
36+
private $e,
37+
private $f,
38+
private $g,
39+
private $h,
40+
) {}
41+
}
42+
43+
/**
44+
* @template-covariant X
45+
*/
46+
class B {
47+
/**
48+
* @param X $a
49+
* @param In<X> $b
50+
* @param Out<X> $c
51+
* @param Invariant<X> $d
52+
* @param X $e
53+
* @param In<X> $f
54+
* @param Out<X> $g
55+
* @param Invariant<X> $h
56+
*/
57+
public function __construct(
58+
public $a,
59+
public $b,
60+
public $c,
61+
public $d,
62+
private $e,
63+
private $f,
64+
private $g,
65+
private $h,
66+
) {}
67+
}
68+
69+
/**
70+
* @template-contravariant X
71+
*/
72+
class C {
73+
/**
74+
* @param X $a
75+
* @param In<X> $b
76+
* @param Out<X> $c
77+
* @param Invariant<X> $d
78+
* @param X $e
79+
* @param In<X> $f
80+
* @param Out<X> $g
81+
* @param Invariant<X> $h
82+
*/
83+
public function __construct(
84+
public $a,
85+
public $b,
86+
public $c,
87+
public $d,
88+
private $e,
89+
private $f,
90+
private $g,
91+
private $h,
92+
) {}
93+
}

0 commit comments

Comments
 (0)