Skip to content

Commit 7453f4f

Browse files
committed
Bleeding edge - check too wide private property type
1 parent 9488c63 commit 7453f4f

11 files changed

+342
-1
lines changed

conf/bleedingEdge.neon

+1
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,6 @@ parameters:
5959
preciseMissingReturn: true
6060
validatePregQuote: true
6161
noImplicitWildcard: true
62+
tooWidePropertyType: true
6263
stubFiles:
6364
- ../stubs/bleedingEdge/Rule.stub

conf/config.level4.neon

+5
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ conditionalTags:
5454
phpstan.rules.rule: %featureToggles.pure%
5555
PHPStan\Rules\DeadCode\PossiblyPureStaticCallCollector:
5656
phpstan.collector: %featureToggles.pure%
57+
PHPStan\Rules\TooWideTypehints\TooWidePropertyTypeRule:
58+
phpstan.rules.rule: %featureToggles.tooWidePropertyType%
5759

5860
parameters:
5961
checkAdvancedIsset: true
@@ -313,3 +315,6 @@ services:
313315

314316
-
315317
class: PHPStan\Rules\TooWideTypehints\TooWideMethodParameterOutTypeRule
318+
319+
-
320+
class: PHPStan\Rules\TooWideTypehints\TooWidePropertyTypeRule

conf/config.neon

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ parameters:
9595
validatePregQuote: false
9696
noImplicitWildcard: false
9797
narrowPregMatches: true
98+
tooWidePropertyType: false
9899
fileExtensions:
99100
- php
100101
checkAdvancedIsset: false

conf/parametersSchema.neon

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ parametersSchema:
9090
validatePregQuote: bool()
9191
noImplicitWildcard: bool()
9292
narrowPregMatches: bool()
93+
tooWidePropertyType: bool()
9394
])
9495
fileExtensions: listOf(string())
9596
checkAdvancedIsset: bool()

src/Analyser/NodeScopeResolver.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -870,7 +870,7 @@ private function processStmtNode(
870870
$this->processAttributeGroups($stmt, $stmt->attrGroups, $classScope, $classStatementsGatherer);
871871

872872
$this->processStmtNodes($stmt, $stmt->stmts, $classScope, $classStatementsGatherer, $context);
873-
$nodeCallback(new ClassPropertiesNode($stmt, $this->readWritePropertiesExtensionProvider, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls(), $classStatementsGatherer->getReturnStatementsNodes(), $classReflection), $classScope);
873+
$nodeCallback(new ClassPropertiesNode($stmt, $this->readWritePropertiesExtensionProvider, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls(), $classStatementsGatherer->getReturnStatementsNodes(), $classStatementsGatherer->getPropertyAssigns(), $classReflection), $classScope);
874874
$nodeCallback(new ClassMethodsNode($stmt, $classStatementsGatherer->getMethods(), $classStatementsGatherer->getMethodCalls(), $classReflection), $classScope);
875875
$nodeCallback(new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches(), $classReflection), $classScope);
876876
$classReflection->evictPrivateSymbols();

src/Node/ClassPropertiesNode.php

+11
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\Analyser\Scope;
1414
use PHPStan\Node\Expr\PropertyInitializationExpr;
1515
use PHPStan\Node\Method\MethodCall;
16+
use PHPStan\Node\Property\PropertyAssign;
1617
use PHPStan\Node\Property\PropertyRead;
1718
use PHPStan\Node\Property\PropertyWrite;
1819
use PHPStan\Reflection\ClassReflection;
@@ -40,6 +41,7 @@ class ClassPropertiesNode extends NodeAbstract implements VirtualNode
4041
* @param array<int, PropertyRead|PropertyWrite> $propertyUsages
4142
* @param array<int, MethodCall> $methodCalls
4243
* @param array<string, MethodReturnStatementsNode> $returnStatementNodes
44+
* @param list<PropertyAssign> $propertyAssigns
4345
*/
4446
public function __construct(
4547
private ClassLike $class,
@@ -48,6 +50,7 @@ public function __construct(
4850
private array $propertyUsages,
4951
private array $methodCalls,
5052
private array $returnStatementNodes,
53+
private array $propertyAssigns,
5154
private ClassReflection $classReflection,
5255
)
5356
{
@@ -404,4 +407,12 @@ private function getInitializedProperties(Scope $scope, array $initialInitialize
404407
return $initialInitializedProperties;
405408
}
406409

410+
/**
411+
* @return list<PropertyAssign>
412+
*/
413+
public function getPropertyAssigns(): array
414+
{
415+
return $this->propertyAssigns;
416+
}
417+
407418
}

src/Node/ClassStatementsGatherer.php

+13
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PhpParser\Node\Identifier;
1414
use PHPStan\Analyser\Scope;
1515
use PHPStan\Node\Constant\ClassConstantFetch;
16+
use PHPStan\Node\Property\PropertyAssign;
1617
use PHPStan\Node\Property\PropertyRead;
1718
use PHPStan\Node\Property\PropertyWrite;
1819
use PHPStan\Reflection\ClassReflection;
@@ -55,6 +56,9 @@ final class ClassStatementsGatherer
5556
/** @var array<string, MethodReturnStatementsNode> */
5657
private array $returnStatementNodes = [];
5758

59+
/** @var list<PropertyAssign> */
60+
private array $propertyAssigns = [];
61+
5862
/**
5963
* @param callable(Node $node, Scope $scope): void $nodeCallback
6064
*/
@@ -122,6 +126,14 @@ public function getReturnStatementsNodes(): array
122126
return $this->returnStatementNodes;
123127
}
124128

129+
/**
130+
* @return list<PropertyAssign>
131+
*/
132+
public function getPropertyAssigns(): array
133+
{
134+
return $this->propertyAssigns;
135+
}
136+
125137
public function __invoke(Node $node, Scope $scope): void
126138
{
127139
$nodeCallback = $this->nodeCallback;
@@ -189,6 +201,7 @@ private function gatherNodes(Node $node, Scope $scope): void
189201
}
190202
if ($node instanceof PropertyAssignNode) {
191203
$this->propertyUsages[] = new PropertyWrite($node->getPropertyFetch(), $scope, false);
204+
$this->propertyAssigns[] = new PropertyAssign($node, $scope);
192205
return;
193206
}
194207
if (!$node instanceof Expr) {

src/Node/Property/PropertyAssign.php

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Node\Property;
4+
5+
use PHPStan\Analyser\Scope;
6+
use PHPStan\Node\PropertyAssignNode;
7+
8+
/**
9+
* @api
10+
*/
11+
final class PropertyAssign
12+
{
13+
14+
public function __construct(
15+
private PropertyAssignNode $assign,
16+
private Scope $scope,
17+
)
18+
{
19+
}
20+
21+
public function getAssign(): PropertyAssignNode
22+
{
23+
return $this->assign;
24+
}
25+
26+
public function getScope(): Scope
27+
{
28+
return $this->scope;
29+
}
30+
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\TooWideTypehints;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\ClassPropertiesNode;
8+
use PHPStan\Reflection\PropertyReflection;
9+
use PHPStan\Rules\Properties\PropertyReflectionFinder;
10+
use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleErrorBuilder;
13+
use PHPStan\Type\NullType;
14+
use PHPStan\Type\TypeCombinator;
15+
use PHPStan\Type\UnionType;
16+
use PHPStan\Type\VerbosityLevel;
17+
use function count;
18+
use function sprintf;
19+
20+
/**
21+
* @implements Rule<ClassPropertiesNode>
22+
*/
23+
final class TooWidePropertyTypeRule implements Rule
24+
{
25+
26+
public function __construct(
27+
private ReadWritePropertiesExtensionProvider $extensionProvider,
28+
private PropertyReflectionFinder $propertyReflectionFinder,
29+
)
30+
{
31+
}
32+
33+
public function getNodeType(): string
34+
{
35+
return ClassPropertiesNode::class;
36+
}
37+
38+
public function processNode(Node $node, Scope $scope): array
39+
{
40+
$errors = [];
41+
$classReflection = $node->getClassReflection();
42+
43+
foreach ($node->getProperties() as $property) {
44+
if (!$property->isPrivate()) {
45+
continue;
46+
}
47+
if ($property->isDeclaredInTrait()) {
48+
continue;
49+
}
50+
if ($property->isPromoted()) {
51+
continue;
52+
}
53+
$propertyName = $property->getName();
54+
if (!$classReflection->hasNativeProperty($propertyName)) {
55+
continue;
56+
}
57+
58+
$propertyReflection = $classReflection->getNativeProperty($propertyName);
59+
$propertyType = $propertyReflection->getWritableType();
60+
if (!$propertyType instanceof UnionType) {
61+
continue;
62+
}
63+
foreach ($this->extensionProvider->getExtensions() as $extension) {
64+
if ($extension->isAlwaysWritten($propertyReflection, $propertyName)) {
65+
continue 2;
66+
}
67+
if ($extension->isInitialized($propertyReflection, $propertyName)) {
68+
continue 2;
69+
}
70+
}
71+
72+
$assignedTypes = [];
73+
foreach ($node->getPropertyAssigns() as $assign) {
74+
$assignNode = $assign->getAssign();
75+
$assignPropertyReflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($assignNode->getPropertyFetch(), $assign->getScope());
76+
foreach ($assignPropertyReflections as $assignPropertyReflection) {
77+
if ($propertyName !== $assignPropertyReflection->getName()) {
78+
continue;
79+
}
80+
if ($propertyReflection->getDeclaringClass()->getName() !== $assignPropertyReflection->getDeclaringClass()->getName()) {
81+
continue;
82+
}
83+
84+
$assignedTypes[] = $assignPropertyReflection->getScope()->getType($assignNode->getAssignedExpr());
85+
}
86+
}
87+
88+
if ($property->getDefault() !== null) {
89+
$assignedTypes[] = $scope->getType($property->getDefault());
90+
}
91+
92+
if (count($assignedTypes) === 0) {
93+
continue;
94+
}
95+
96+
$assignedType = TypeCombinator::union(...$assignedTypes);
97+
$propertyDescription = $this->describePropertyByName($propertyReflection, $propertyName);
98+
$verbosityLevel = VerbosityLevel::getRecommendedLevelByType($propertyType, $assignedType);
99+
foreach ($propertyType->getTypes() as $type) {
100+
if (!$type->isSuperTypeOf($assignedType)->no()) {
101+
continue;
102+
}
103+
104+
if ($property->getNativeType() === null && (new NullType())->isSuperTypeOf($type)->yes()) {
105+
continue;
106+
}
107+
108+
$errors[] = RuleErrorBuilder::message(sprintf(
109+
'%s (%s) is never assigned %s so it can be removed from the property type.',
110+
$propertyDescription,
111+
$propertyType->describe($verbosityLevel),
112+
$type->describe($verbosityLevel),
113+
))
114+
->identifier('property.unusedType')
115+
->line($property->getStartLine())
116+
->build();
117+
}
118+
119+
}
120+
return $errors;
121+
}
122+
123+
private function describePropertyByName(PropertyReflection $property, string $propertyName): string
124+
{
125+
if (!$property->isStatic()) {
126+
return sprintf('Property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $propertyName);
127+
}
128+
129+
return sprintf('Static property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $propertyName);
130+
}
131+
132+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\TooWideTypehints;
4+
5+
use PHPStan\Rules\Properties\DirectReadWritePropertiesExtensionProvider;
6+
use PHPStan\Rules\Properties\PropertyReflectionFinder;
7+
use PHPStan\Rules\Rule;
8+
use PHPStan\Testing\RuleTestCase;
9+
use const PHP_VERSION_ID;
10+
11+
/**
12+
* @extends RuleTestCase<TooWidePropertyTypeRule>
13+
*/
14+
class TooWidePropertyTypeRuleTest extends RuleTestCase
15+
{
16+
17+
protected function getRule(): Rule
18+
{
19+
return new TooWidePropertyTypeRule(
20+
new DirectReadWritePropertiesExtensionProvider([]),
21+
new PropertyReflectionFinder(),
22+
);
23+
}
24+
25+
public function testRule(): void
26+
{
27+
if (PHP_VERSION_ID < 80000) {
28+
self::markTestSkipped('Test requires PHP 8.0.');
29+
}
30+
31+
$this->analyse([__DIR__ . '/data/too-wide-property-type.php'], [
32+
[
33+
'Property TooWidePropertyType\Foo::$foo (int|string) is never assigned string so it can be removed from the property type.',
34+
9,
35+
],
36+
/*[
37+
'Property TooWidePropertyType\Foo::$barr (int|null) is never assigned null so it can be removed from the property type.',
38+
15,
39+
],
40+
[
41+
'Property TooWidePropertyType\Foo::$barrr (int|null) is never assigned null so it can be removed from the property type.',
42+
18,
43+
],*/
44+
[
45+
'Property TooWidePropertyType\Foo::$baz (int|null) is never assigned null so it can be removed from the property type.',
46+
20,
47+
],
48+
[
49+
'Property TooWidePropertyType\Bar::$c (int|null) is never assigned int so it can be removed from the property type.',
50+
45,
51+
],
52+
[
53+
'Property TooWidePropertyType\Bar::$d (int|null) is never assigned null so it can be removed from the property type.',
54+
47,
55+
],
56+
]);
57+
}
58+
59+
}

0 commit comments

Comments
 (0)