Skip to content

Commit 7b9d51a

Browse files
committed
Check generic variance rules in properties
1 parent fb0faa4 commit 7b9d51a

10 files changed

+541
-0
lines changed

conf/bleedingEdge.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ parameters:
2525
unescapeStrings: true
2626
duplicateStubs: true
2727
invarianceComposition: true
28+
checkPropertyVariance: true

conf/config.level2.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ conditionalTags:
5050
phpstan.rules.rule: %featureToggles.illegalConstructorMethodCall%
5151
PHPStan\Rules\Methods\IllegalConstructorStaticCallRule:
5252
phpstan.rules.rule: %featureToggles.illegalConstructorMethodCall%
53+
PHPStan\Rules\Generics\PropertyVarianceRule:
54+
phpstan.rules.rule: %featureToggles.checkPropertyVariance%
5355

5456
services:
5557
-
@@ -75,3 +77,5 @@ services:
7577
checkMissingVarTagTypehint: %checkMissingVarTagTypehint%
7678
tags:
7779
- phpstan.rules.rule
80+
-
81+
class: PHPStan\Rules\Generics\PropertyVarianceRule

conf/config.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ parameters:
5555
unescapeStrings: false
5656
duplicateStubs: false
5757
invarianceComposition: false
58+
checkPropertyVariance: false
5859
fileExtensions:
5960
- php
6061
checkAdvancedIsset: false
@@ -274,6 +275,7 @@ parametersSchema:
274275
unescapeStrings: bool()
275276
duplicateStubs: bool()
276277
invarianceComposition: bool()
278+
checkPropertyVariance: bool()
277279
])
278280
fileExtensions: listOf(string())
279281
checkAdvancedIsset: bool()
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
return $this->varianceCheck->checkProperty(
42+
$propertyReflection,
43+
sprintf('in property %s::$%s', SprintfHelper::escapeFormatString($classReflection->getDisplayName()), SprintfHelper::escapeFormatString($node->getName())),
44+
$propertyReflection->isStatic(),
45+
$propertyReflection->isPrivate(),
46+
$propertyReflection->isReadOnly(),
47+
);
48+
}
49+
50+
}

src/Rules/Generics/VarianceCheck.php

Lines changed: 39 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\ParametersAcceptor;
6+
use PHPStan\Reflection\PropertyReflection;
67
use PHPStan\Rules\RuleError;
78
use PHPStan\Rules\RuleErrorBuilder;
89
use PHPStan\Type\Generic\TemplateType;
@@ -77,6 +78,44 @@ public function checkParametersAcceptor(
7778
return $errors;
7879
}
7980

81+
/** @return RuleError[] */
82+
public function checkProperty(
83+
PropertyReflection $propertyReflection,
84+
string $message,
85+
bool $isStatic,
86+
bool $isPrivate,
87+
bool $isReadOnly,
88+
): array
89+
{
90+
$readableType = $propertyReflection->getReadableType();
91+
$writableType = $propertyReflection->getWritableType();
92+
93+
if ($readableType->equals($writableType)) {
94+
$variance = $isReadOnly
95+
? TemplateTypeVariance::createCovariant()
96+
: TemplateTypeVariance::createInvariant();
97+
98+
return $this->check($variance, $readableType, $message, $isStatic, $isPrivate);
99+
}
100+
101+
$errors = [];
102+
103+
if ($propertyReflection->isReadable()) {
104+
foreach ($this->check(TemplateTypeVariance::createCovariant(), $readableType, $message, $isStatic, $isPrivate) as $error) {
105+
$errors[] = $error;
106+
}
107+
}
108+
109+
if ($propertyReflection->isWritable()) {
110+
$checkStatic = $isStatic && !$propertyReflection->isReadable();
111+
foreach ($this->check(TemplateTypeVariance::createContravariant(), $writableType, $message, $checkStatic, $isPrivate) as $error) {
112+
$errors[] = $error;
113+
}
114+
}
115+
116+
return $errors;
117+
}
118+
80119
/** @return RuleError[] */
81120
public function check(
82121
TemplateTypeVariance $positionVariance,
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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+
$this->analyse([__DIR__ . '/data/property-variance-static.php'], [
60+
[
61+
'Class template type X cannot be referenced in a static member in property PropertyVariance\Static\A::$a.',
62+
10,
63+
],
64+
[
65+
'Class template type X cannot be referenced in a static member in property PropertyVariance\Static\A::$b.',
66+
13,
67+
],
68+
]);
69+
}
70+
71+
public function testPromoted(): void
72+
{
73+
if (PHP_VERSION_ID < 80000) {
74+
$this->markTestSkipped('Test requires PHP 8.0.');
75+
}
76+
77+
$this->analyse([__DIR__ . '/data/property-variance-promoted.php'], [
78+
[
79+
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$a.',
80+
58,
81+
],
82+
[
83+
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$b.',
84+
59,
85+
],
86+
[
87+
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$c.',
88+
60,
89+
],
90+
[
91+
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$d.',
92+
61,
93+
],
94+
[
95+
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\Promoted\C::$a.',
96+
84,
97+
],
98+
[
99+
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\Promoted\C::$b.',
100+
85,
101+
],
102+
[
103+
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\Promoted\C::$c.',
104+
86,
105+
],
106+
[
107+
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\Promoted\C::$d.',
108+
87,
109+
],
110+
]);
111+
}
112+
113+
public function testReadOnly(): void
114+
{
115+
if (PHP_VERSION_ID < 80100) {
116+
$this->markTestSkipped('Test requires PHP 8.1.');
117+
}
118+
119+
$this->analyse([__DIR__ . '/data/property-variance-readonly.php'], [
120+
[
121+
'Template type X is declared as covariant, but occurs in contravariant position in property PropertyVariance\ReadOnly\B::$b.',
122+
45,
123+
],
124+
[
125+
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\ReadOnly\B::$d.',
126+
51,
127+
],
128+
[
129+
'Template type X is declared as contravariant, but occurs in covariant position in property PropertyVariance\ReadOnly\C::$a.',
130+
62,
131+
],
132+
[
133+
'Template type X is declared as contravariant, but occurs in covariant position in property PropertyVariance\ReadOnly\C::$c.',
134+
68,
135+
],
136+
[
137+
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\ReadOnly\C::$d.',
138+
71,
139+
],
140+
[
141+
'Template type X is declared as contravariant, but occurs in covariant position in property PropertyVariance\ReadOnly\D::$a.',
142+
86,
143+
],
144+
]);
145+
}
146+
147+
}
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)