Skip to content

Commit 6e0c439

Browse files
committed
Allow wider set hook parameter type when assigning properties outside of their hooks
1 parent 86e809a commit 6e0c439

File tree

4 files changed

+84
-2
lines changed

4 files changed

+84
-2
lines changed

src/Reflection/Php/PhpPropertyReflection.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,14 @@ public function getReadableType(): Type
103103

104104
public function getWritableType(): Type
105105
{
106+
if ($this->hasHook('set')) {
107+
$setHookVariant = $this->getHook('set')->getOnlyVariant();
108+
$parameters = $setHookVariant->getParameters();
109+
if (isset($parameters[0])) {
110+
return $parameters[0]->getType();
111+
}
112+
}
113+
106114
return $this->getReadableType();
107115
}
108116

src/Rules/Properties/TypesAssignedToPropertiesRule.php

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@
33
namespace PHPStan\Rules\Properties;
44

55
use PhpParser\Node;
6+
use PhpParser\Node\Expr\PropertyFetch;
7+
use PhpParser\Node\Expr\StaticPropertyFetch;
68
use PHPStan\Analyser\Scope;
79
use PHPStan\Node\PropertyAssignNode;
10+
use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
811
use PHPStan\Reflection\PropertyReflection;
912
use PHPStan\Rules\IdentifierRuleError;
1013
use PHPStan\Rules\Rule;
1114
use PHPStan\Rules\RuleErrorBuilder;
1215
use PHPStan\Rules\RuleLevelHelper;
1316
use PHPStan\Type\VerbosityLevel;
1417
use function array_merge;
18+
use function is_string;
1519
use function sprintf;
1620

1721
/**
@@ -34,12 +38,14 @@ public function getNodeType(): string
3438

3539
public function processNode(Node $node, Scope $scope): array
3640
{
37-
$propertyReflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($node->getPropertyFetch(), $scope);
41+
$propertyFetch = $node->getPropertyFetch();
42+
$propertyReflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($propertyFetch, $scope);
3843

3944
$errors = [];
4045
foreach ($propertyReflections as $propertyReflection) {
4146
$errors = array_merge($errors, $this->processSingleProperty(
4247
$propertyReflection,
48+
$propertyFetch,
4349
$node->getAssignedExpr(),
4450
));
4551
}
@@ -52,15 +58,31 @@ public function processNode(Node $node, Scope $scope): array
5258
*/
5359
private function processSingleProperty(
5460
FoundPropertyReflection $propertyReflection,
61+
PropertyFetch|StaticPropertyFetch $fetch,
5562
Node\Expr $assignedExpr,
5663
): array
5764
{
5865
if (!$propertyReflection->isWritable()) {
5966
return [];
6067
}
6168

62-
$propertyType = $propertyReflection->getWritableType();
6369
$scope = $propertyReflection->getScope();
70+
$inFunction = $scope->getFunction();
71+
if (
72+
$fetch instanceof PropertyFetch
73+
&& $fetch->var instanceof Node\Expr\Variable
74+
&& is_string($fetch->var->name)
75+
&& $fetch->var->name === 'this'
76+
&& $fetch->name instanceof Node\Identifier
77+
&& $inFunction instanceof PhpMethodFromParserNodeReflection
78+
&& $inFunction->isPropertyHook()
79+
&& $inFunction->getHookedPropertyName() === $fetch->name->toString()
80+
) {
81+
$propertyType = $propertyReflection->getReadableType();
82+
} else {
83+
$propertyType = $propertyReflection->getWritableType();
84+
}
85+
6486
$assignedValueType = $scope->getType($assignedExpr);
6587

6688
$accepts = $this->ruleLevelHelper->accepts($propertyType, $assignedValueType, $scope->isDeclareStrictTypes());

tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,4 +722,26 @@ public function testShortBodySetHook(): void
722722
]);
723723
}
724724

725+
public function testPropertyHooks(): void
726+
{
727+
if (PHP_VERSION_ID < 80400) {
728+
$this->markTestSkipped('Test requires PHP 8.4.');
729+
}
730+
731+
$this->analyse([__DIR__ . '/data/assign-hooked-properties.php'], [
732+
[
733+
'Property AssignHookedProperties\Foo::$i (int) does not accept array<string>|int.',
734+
11,
735+
],
736+
[
737+
'Property AssignHookedProperties\Foo::$j (int) does not accept array<string>|int.',
738+
19,
739+
],
740+
[
741+
'Property AssignHookedProperties\Foo::$i (array<string>|int) does not accept array<int, int>.',
742+
27,
743+
],
744+
]);
745+
}
746+
725747
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php // lint >= 8.4
2+
3+
namespace AssignHookedProperties;
4+
5+
class Foo
6+
{
7+
8+
public int $i {
9+
/** @param array<string>|int $val */
10+
set (array|int $val) {
11+
$this->i = $val; // only int allowed
12+
}
13+
}
14+
15+
public int $j {
16+
/** @param array<string>|int $val */
17+
set (array|int $val) {
18+
$this->i = $val; // this is okay - hook called
19+
$this->j = $val; // only int allowed
20+
}
21+
}
22+
23+
public function doFoo(): void
24+
{
25+
$this->i = ['foo']; // okay
26+
$this->i = 1; // okay
27+
$this->i = [1]; // not okay
28+
}
29+
30+
}

0 commit comments

Comments
 (0)