Skip to content

Commit acd559e

Browse files
committed
SetPropertyHookParameterRule - level 0 and 3
1 parent 22c80b1 commit acd559e

File tree

5 files changed

+245
-0
lines changed

5 files changed

+245
-0
lines changed

Makefile

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ lint:
9494
--exclude tests/PHPStan/Parser/data/cleaning-property-hooks-before.php \
9595
--exclude tests/PHPStan/Parser/data/cleaning-property-hooks-after.php \
9696
--exclude tests/PHPStan/Rules/Properties/data/existing-classes-property-hooks.php \
97+
--exclude tests/PHPStan/Rules/Properties/data/set-property-hook-parameter.php \
9798
src tests
9899

99100
cs:

conf/config.level0.neon

+7
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,13 @@ services:
212212
tags:
213213
- phpstan.rules.rule
214214

215+
-
216+
class: PHPStan\Rules\Properties\SetPropertyHookParameterRule
217+
arguments:
218+
checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures%
219+
tags:
220+
- phpstan.rules.rule
221+
215222
-
216223
class: PHPStan\Rules\Properties\UninitializedPropertyRule
217224

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Properties;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\InPropertyHookNode;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Rules\RuleErrorBuilder;
10+
use PHPStan\ShouldNotHappenException;
11+
use PHPStan\Type\VerbosityLevel;
12+
use function count;
13+
use function sprintf;
14+
15+
/**
16+
* @implements Rule<InPropertyHookNode>
17+
*/
18+
final class SetPropertyHookParameterRule implements Rule
19+
{
20+
21+
public function __construct(private bool $checkPhpDocMethodSignatures)
22+
{
23+
}
24+
25+
public function getNodeType(): string
26+
{
27+
return InPropertyHookNode::class;
28+
}
29+
30+
public function processNode(Node $node, Scope $scope): array
31+
{
32+
$hookReflection = $node->getHookReflection();
33+
if (!$hookReflection->isPropertyHook()) {
34+
return [];
35+
}
36+
37+
if ($hookReflection->getPropertyHookName() !== 'set') {
38+
return [];
39+
}
40+
41+
$propertyReflection = $node->getPropertyReflection();
42+
$parameters = $hookReflection->getParameters();
43+
if (!isset($parameters[0])) {
44+
throw new ShouldNotHappenException();
45+
}
46+
47+
$classReflection = $node->getClassReflection();
48+
49+
$errors = [];
50+
$parameter = $parameters[0];
51+
if (!$propertyReflection->hasNativeType()) {
52+
if ($parameter->hasNativeType()) {
53+
$errors[] = RuleErrorBuilder::message(sprintf(
54+
'Parameter $%s of set hook has a native type but the property %s::$%s does not.',
55+
$parameter->getName(),
56+
$classReflection->getDisplayName(),
57+
$hookReflection->getHookedPropertyName(),
58+
))->identifier('propertySetHook.nativeParameterType')
59+
->nonIgnorable()
60+
->build();
61+
}
62+
} elseif (!$parameter->hasNativeType()) {
63+
$errors[] = RuleErrorBuilder::message(sprintf(
64+
'Parameter $%s of set hook does not have a native type but the property %s::$%s does.',
65+
$parameter->getName(),
66+
$classReflection->getDisplayName(),
67+
$hookReflection->getHookedPropertyName(),
68+
))->identifier('propertySetHook.nativeParameterType')
69+
->nonIgnorable()
70+
->build();
71+
} else {
72+
if (!$parameter->getNativeType()->isSuperTypeOf($propertyReflection->getNativeType())->yes()) {
73+
$errors[] = RuleErrorBuilder::message(sprintf(
74+
'Native type %s of set hook parameter $%s is not contravariant with native type %s of property %s::$%s.',
75+
$parameter->getNativeType()->describe(VerbosityLevel::typeOnly()),
76+
$parameter->getName(),
77+
$propertyReflection->getNativeType()->describe(VerbosityLevel::typeOnly()),
78+
$classReflection->getDisplayName(),
79+
$hookReflection->getHookedPropertyName(),
80+
))->identifier('propertySetHook.nativeParameterType')
81+
->nonIgnorable()
82+
->build();
83+
}
84+
}
85+
86+
if (!$this->checkPhpDocMethodSignatures || count($errors) > 0) {
87+
return $errors;
88+
}
89+
90+
if (!$parameter->getType()->isSuperTypeOf($propertyReflection->getReadableType())->yes()) {
91+
$errors[] = RuleErrorBuilder::message(sprintf(
92+
'Type %s of set hook parameter $%s is not contravariant with type %s of property %s::$%s.',
93+
$parameter->getType()->describe(VerbosityLevel::value()),
94+
$parameter->getName(),
95+
$propertyReflection->getReadableType()->describe(VerbosityLevel::value()),
96+
$classReflection->getDisplayName(),
97+
$hookReflection->getHookedPropertyName(),
98+
))->identifier('propertySetHook.parameterType')
99+
->build();
100+
}
101+
102+
return $errors;
103+
}
104+
105+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Properties;
4+
5+
use PHPStan\Rules\Rule as TRule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use const PHP_VERSION_ID;
8+
9+
/**
10+
* @extends RuleTestCase<SetPropertyHookParameterRule>
11+
*/
12+
class SetPropertyHookParameterRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): TRule
16+
{
17+
return new SetPropertyHookParameterRule(true);
18+
}
19+
20+
public function testRule(): void
21+
{
22+
if (PHP_VERSION_ID < 80400) {
23+
$this->markTestSkipped('Test requires PHP 8.4.');
24+
}
25+
26+
$this->analyse([__DIR__ . '/data/set-property-hook-parameter.php'], [
27+
[
28+
'Parameter $v of set hook has a native type but the property SetPropertyHookParameter\Bar::$a does not.',
29+
41,
30+
],
31+
[
32+
'Parameter $v of set hook does not have a native type but the property SetPropertyHookParameter\Bar::$b does.',
33+
47,
34+
],
35+
[
36+
'Native type string of set hook parameter $v is not contravariant with native type int of property SetPropertyHookParameter\Bar::$c.',
37+
53,
38+
],
39+
[
40+
'Native type string of set hook parameter $v is not contravariant with native type int|string of property SetPropertyHookParameter\Bar::$d.',
41+
59,
42+
],
43+
[
44+
'Type int<1, max> of set hook parameter $v is not contravariant with type int of property SetPropertyHookParameter\Bar::$e.',
45+
66,
46+
],
47+
[
48+
'Type array<string>|int<1, max> of set hook parameter $v is not contravariant with type int of property SetPropertyHookParameter\Bar::$f.',
49+
73,
50+
],
51+
]);
52+
}
53+
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
namespace SetPropertyHookParameter;
4+
5+
class Foo
6+
{
7+
8+
public int $ok {
9+
set (int $v) {
10+
11+
}
12+
}
13+
14+
/** @var positive-int */
15+
public int $ok2 {
16+
set (int $v) {
17+
18+
}
19+
}
20+
21+
/** @var positive-int */
22+
public int $ok3 {
23+
/** @param positive-int|array<string> */
24+
set (int|array $v) {
25+
26+
}
27+
}
28+
29+
public $ok4 {
30+
set ($v) {
31+
32+
}
33+
}
34+
35+
}
36+
37+
class Bar
38+
{
39+
40+
public $a {
41+
set (int $v) {
42+
43+
}
44+
}
45+
46+
public int $b {
47+
set ($v) {
48+
49+
}
50+
}
51+
52+
public int $c {
53+
set (string $v) {
54+
55+
}
56+
}
57+
58+
public int|string $d {
59+
set (string $v) {
60+
61+
}
62+
}
63+
64+
public int $e {
65+
/** @param positive-int $v */
66+
set (int $v) {
67+
68+
}
69+
}
70+
71+
public int $f {
72+
/** @param positive-int|array<string> $v */
73+
set (int|array $v) {
74+
75+
}
76+
}
77+
78+
}

0 commit comments

Comments
 (0)