Skip to content

Commit

Permalink
Check generic variance rules in properties
Browse files Browse the repository at this point in the history
  • Loading branch information
jiripudil committed Dec 10, 2022
1 parent fb0faa4 commit 7b9d51a
Show file tree
Hide file tree
Showing 10 changed files with 541 additions and 0 deletions.
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Expand Up @@ -25,3 +25,4 @@ parameters:
unescapeStrings: true
duplicateStubs: true
invarianceComposition: true
checkPropertyVariance: true
4 changes: 4 additions & 0 deletions conf/config.level2.neon
Expand Up @@ -50,6 +50,8 @@ conditionalTags:
phpstan.rules.rule: %featureToggles.illegalConstructorMethodCall%
PHPStan\Rules\Methods\IllegalConstructorStaticCallRule:
phpstan.rules.rule: %featureToggles.illegalConstructorMethodCall%
PHPStan\Rules\Generics\PropertyVarianceRule:
phpstan.rules.rule: %featureToggles.checkPropertyVariance%

services:
-
Expand All @@ -75,3 +77,5 @@ services:
checkMissingVarTagTypehint: %checkMissingVarTagTypehint%
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Generics\PropertyVarianceRule
2 changes: 2 additions & 0 deletions conf/config.neon
Expand Up @@ -55,6 +55,7 @@ parameters:
unescapeStrings: false
duplicateStubs: false
invarianceComposition: false
checkPropertyVariance: false
fileExtensions:
- php
checkAdvancedIsset: false
Expand Down Expand Up @@ -274,6 +275,7 @@ parametersSchema:
unescapeStrings: bool()
duplicateStubs: bool()
invarianceComposition: bool()
checkPropertyVariance: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
Expand Down
50 changes: 50 additions & 0 deletions src/Rules/Generics/PropertyVarianceRule.php
@@ -0,0 +1,50 @@
<?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());

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

}
39 changes: 39 additions & 0 deletions src/Rules/Generics/VarianceCheck.php
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Rules\Generics;

use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\PropertyReflection;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Generic\TemplateType;
Expand Down Expand Up @@ -77,6 +78,44 @@ public function checkParametersAcceptor(
return $errors;
}

/** @return RuleError[] */
public function checkProperty(
PropertyReflection $propertyReflection,
string $message,
bool $isStatic,
bool $isPrivate,
bool $isReadOnly,
): array
{
$readableType = $propertyReflection->getReadableType();
$writableType = $propertyReflection->getWritableType();

if ($readableType->equals($writableType)) {
$variance = $isReadOnly
? TemplateTypeVariance::createCovariant()
: TemplateTypeVariance::createInvariant();

return $this->check($variance, $readableType, $message, $isStatic, $isPrivate);
}

$errors = [];

if ($propertyReflection->isReadable()) {
foreach ($this->check(TemplateTypeVariance::createCovariant(), $readableType, $message, $isStatic, $isPrivate) as $error) {
$errors[] = $error;
}
}

if ($propertyReflection->isWritable()) {
$checkStatic = $isStatic && !$propertyReflection->isReadable();
foreach ($this->check(TemplateTypeVariance::createContravariant(), $writableType, $message, $checkStatic, $isPrivate) as $error) {
$errors[] = $error;
}
}

return $errors;
}

/** @return RuleError[] */
public function check(
TemplateTypeVariance $positionVariance,
Expand Down
147 changes: 147 additions & 0 deletions tests/PHPStan/Rules/Generics/PropertyVarianceRuleTest.php
@@ -0,0 +1,147 @@
<?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,
],
]);

$this->analyse([__DIR__ . '/data/property-variance-static.php'], [
[
'Class template type X cannot be referenced in a static member in property PropertyVariance\Static\A::$a.',
10,
],
[
'Class template type X cannot be referenced in a static member in property PropertyVariance\Static\A::$b.',
13,
],
]);
}

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
@@ -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,
) {}
}

0 comments on commit 7b9d51a

Please sign in to comment.