Skip to content

Commit

Permalink
Check variance of template types in properties
Browse files Browse the repository at this point in the history
  • Loading branch information
jiripudil authored Apr 3, 2023
1 parent 8933feb commit 164241d
Show file tree
Hide file tree
Showing 9 changed files with 495 additions and 0 deletions.
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ parameters:
paramOutVariance: true
allInvalidPhpDocs: true
strictStaticMethodTemplateTypeVariance: true
propertyVariance: true
4 changes: 4 additions & 0 deletions conf/config.level2.neon
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ conditionalTags:
phpstan.rules.rule: %featureToggles.illegalConstructorMethodCall%
PHPStan\Rules\PhpDoc\VarTagChangedExpressionTypeRule:
phpstan.rules.rule: %featureToggles.varTagType%
PHPStan\Rules\Generics\PropertyVarianceRule:
phpstan.rules.rule: %featureToggles.propertyVariance%

services:
-
Expand Down Expand Up @@ -102,3 +104,5 @@ services:
checkTypeAgainstNativeType: %featureToggles.varTagType%
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Generics\PropertyVarianceRule
2 changes: 2 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ parameters:
paramOutVariance: false
allInvalidPhpDocs: false
strictStaticMethodTemplateTypeVariance: false
propertyVariance: false
fileExtensions:
- php
checkAdvancedIsset: false
Expand Down Expand Up @@ -298,6 +299,7 @@ parametersSchema:
paramOutVariance: bool()
allInvalidPhpDocs: bool()
strictStaticMethodTemplateTypeVariance: bool()
propertyVariance: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
Expand Down
52 changes: 52 additions & 0 deletions src/Rules/Generics/PropertyVarianceRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Generics;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Node\ClassPropertyNode;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Rules\Rule;
use function sprintf;

/**
* @implements Rule<ClassPropertyNode>
*/
class PropertyVarianceRule implements Rule
{

public function __construct(private VarianceCheck $varianceCheck)
{
}

public function getNodeType(): string
{
return ClassPropertyNode::class;
}

public function processNode(Node $node, Scope $scope): array
{
$classReflection = $scope->getClassReflection();
if (!$classReflection instanceof ClassReflection) {
return [];
}

if (!$classReflection->hasNativeProperty($node->getName())) {
return [];
}

$propertyReflection = $classReflection->getNativeProperty($node->getName());

if ($propertyReflection->isPrivate()) {
return [];
}

return $this->varianceCheck->checkProperty(
$propertyReflection,
sprintf('in property %s::$%s', SprintfHelper::escapeFormatString($classReflection->getDisplayName()), SprintfHelper::escapeFormatString($node->getName())),
$propertyReflection->isReadOnly(),
);
}

}
16 changes: 16 additions & 0 deletions src/Rules/Generics/VarianceCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Rules\Generics;

use PHPStan\Reflection\ParametersAcceptorWithPhpDocs;
use PHPStan\Reflection\Php\PhpPropertyReflection;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Generic\TemplateType;
Expand Down Expand Up @@ -87,6 +88,21 @@ public function checkParametersAcceptor(
return $errors;
}

/** @return RuleError[] */
public function checkProperty(
PhpPropertyReflection $propertyReflection,
string $message,
bool $isReadOnly,
): array
{
$type = $propertyReflection->getReadableType();
$variance = $isReadOnly
? TemplateTypeVariance::createCovariant()
: TemplateTypeVariance::createInvariant();

return $this->check($variance, $type, $message);
}

/** @return RuleError[] */
public function check(TemplateTypeVariance $positionVariance, Type $type, string $messageContext): array
{
Expand Down
136 changes: 136 additions & 0 deletions tests/PHPStan/Rules/Generics/PropertyVarianceRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Generics;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use const PHP_VERSION_ID;

/**
* @extends RuleTestCase<PropertyVarianceRule>
*/
class PropertyVarianceRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new PropertyVarianceRule(
self::getContainer()->getByType(VarianceCheck::class),
);
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/property-variance.php'], [
[
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\B::$a.',
51,
],
[
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\B::$b.',
54,
],
[
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\B::$c.',
57,
],
[
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\B::$d.',
60,
],
[
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\C::$a.',
80,
],
[
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\C::$b.',
83,
],
[
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\C::$c.',
86,
],
[
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\C::$d.',
89,
],
]);
}

public function testPromoted(): void
{
if (PHP_VERSION_ID < 80000) {
$this->markTestSkipped('Test requires PHP 8.0.');
}

$this->analyse([__DIR__ . '/data/property-variance-promoted.php'], [
[
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$a.',
58,
],
[
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$b.',
59,
],
[
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$c.',
60,
],
[
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$d.',
61,
],
[
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\Promoted\C::$a.',
84,
],
[
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\Promoted\C::$b.',
85,
],
[
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\Promoted\C::$c.',
86,
],
[
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\Promoted\C::$d.',
87,
],
]);
}

public function testReadOnly(): void
{
if (PHP_VERSION_ID < 80100) {
$this->markTestSkipped('Test requires PHP 8.1.');
}

$this->analyse([__DIR__ . '/data/property-variance-readonly.php'], [
[
'Template type X is declared as covariant, but occurs in contravariant position in property PropertyVariance\ReadOnly\B::$b.',
45,
],
[
'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\ReadOnly\B::$d.',
51,
],
[
'Template type X is declared as contravariant, but occurs in covariant position in property PropertyVariance\ReadOnly\C::$a.',
62,
],
[
'Template type X is declared as contravariant, but occurs in covariant position in property PropertyVariance\ReadOnly\C::$c.',
68,
],
[
'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\ReadOnly\C::$d.',
71,
],
[
'Template type X is declared as contravariant, but occurs in covariant position in property PropertyVariance\ReadOnly\D::$a.',
86,
],
]);
}

}
93 changes: 93 additions & 0 deletions tests/PHPStan/Rules/Generics/data/property-variance-promoted.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php // lint >= 8.0

namespace PropertyVariance\Promoted;

/** @template-contravariant T */
interface In {
}

/** @template-covariant T */
interface Out {
}

/** @template T */
interface Invariant {
}

/**
* @template X
*/
class A {
/**
* @param X $a
* @param In<X> $b
* @param Out<X> $c
* @param Invariant<X> $d
* @param X $e
* @param In<X> $f
* @param Out<X> $g
* @param Invariant<X> $h
*/
public function __construct(
public $a,
public $b,
public $c,
public $d,
private $e,
private $f,
private $g,
private $h,
) {}
}

/**
* @template-covariant X
*/
class B {
/**
* @param X $a
* @param In<X> $b
* @param Out<X> $c
* @param Invariant<X> $d
* @param X $e
* @param In<X> $f
* @param Out<X> $g
* @param Invariant<X> $h
*/
public function __construct(
public $a,
public $b,
public $c,
public $d,
private $e,
private $f,
private $g,
private $h,
) {}
}

/**
* @template-contravariant X
*/
class C {
/**
* @param X $a
* @param In<X> $b
* @param Out<X> $c
* @param Invariant<X> $d
* @param X $e
* @param In<X> $f
* @param Out<X> $g
* @param Invariant<X> $h
*/
public function __construct(
public $a,
public $b,
public $c,
public $d,
private $e,
private $f,
private $g,
private $h,
) {}
}
Loading

0 comments on commit 164241d

Please sign in to comment.