Skip to content

Commit 92e9d43

Browse files
committed
IncompatiblePropertyHookPhpDocTypeRule - level 2
1 parent 50ff7bc commit 92e9d43

File tree

4 files changed

+241
-0
lines changed

4 files changed

+241
-0
lines changed

conf/config.level2.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ rules:
5252
- PHPStan\Rules\PhpDoc\IncompatibleSelfOutTypeRule
5353
- PHPStan\Rules\PhpDoc\IncompatibleClassConstantPhpDocTypeRule
5454
- PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeRule
55+
- PHPStan\Rules\PhpDoc\IncompatiblePropertyHookPhpDocTypeRule
5556
- PHPStan\Rules\PhpDoc\IncompatiblePropertyPhpDocTypeRule
5657
- PHPStan\Rules\PhpDoc\InvalidThrowsPhpDocValueRule
5758
- PHPStan\Rules\PhpDoc\IncompatibleParamImmediatelyInvokedCallableRule
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PhpDoc;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\InPropertyHookNode;
8+
use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
9+
use PHPStan\Rules\Rule;
10+
use PHPStan\Type\FileTypeMapper;
11+
use PHPStan\Type\Type;
12+
13+
/**
14+
* @implements Rule<InPropertyHookNode>
15+
*/
16+
final class IncompatiblePropertyHookPhpDocTypeRule implements Rule
17+
{
18+
19+
public function __construct(
20+
private FileTypeMapper $fileTypeMapper,
21+
private IncompatiblePhpDocTypeCheck $check,
22+
)
23+
{
24+
}
25+
26+
public function getNodeType(): string
27+
{
28+
return InPropertyHookNode::class;
29+
}
30+
31+
public function processNode(Node $node, Scope $scope): array
32+
{
33+
$docComment = $node->getDocComment();
34+
if ($docComment === null) {
35+
return [];
36+
}
37+
38+
$hookReflection = $node->getHookReflection();
39+
40+
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
41+
$scope->getFile(),
42+
$node->getClassReflection()->getName(),
43+
$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
44+
$hookReflection->getName(),
45+
$docComment->getText(),
46+
);
47+
48+
return $this->check->check(
49+
$scope,
50+
$node,
51+
$resolvedPhpDoc,
52+
$hookReflection->getName(),
53+
$this->getNativeParameterTypes($hookReflection),
54+
$this->getByRefParameters($hookReflection),
55+
$hookReflection->getNativeReturnType(),
56+
);
57+
}
58+
59+
/**
60+
* @return array<string, Type>
61+
*/
62+
private function getNativeParameterTypes(PhpMethodFromParserNodeReflection $node): array
63+
{
64+
$parameters = [];
65+
foreach ($node->getParameters() as $parameter) {
66+
$parameters[$parameter->getName()] = $parameter->getNativeType();
67+
}
68+
69+
return $parameters;
70+
}
71+
72+
/**
73+
* @return array<string, false>
74+
*/
75+
private function getByRefParameters(PhpMethodFromParserNodeReflection $node): array
76+
{
77+
$parameters = [];
78+
foreach ($node->getParameters() as $parameter) {
79+
$parameters[$parameter->getName()] = false;
80+
}
81+
82+
return $parameters;
83+
}
84+
85+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PhpDoc;
4+
5+
use PHPStan\Rules\ClassCaseSensitivityCheck;
6+
use PHPStan\Rules\ClassForbiddenNameCheck;
7+
use PHPStan\Rules\ClassNameCheck;
8+
use PHPStan\Rules\Generics\GenericObjectTypeCheck;
9+
use PHPStan\Rules\Generics\TemplateTypeCheck;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Testing\RuleTestCase;
12+
use PHPStan\Type\FileTypeMapper;
13+
use const PHP_VERSION_ID;
14+
15+
/**
16+
* @extends RuleTestCase<IncompatiblePropertyHookPhpDocTypeRule>
17+
*/
18+
class IncompatiblePropertyHookPhpDocTypeRuleTest extends RuleTestCase
19+
{
20+
21+
protected function getRule(): Rule
22+
{
23+
$reflectionProvider = $this->createReflectionProvider();
24+
$typeAliasResolver = $this->createTypeAliasResolver([], $reflectionProvider);
25+
26+
return new IncompatiblePropertyHookPhpDocTypeRule(
27+
self::getContainer()->getByType(FileTypeMapper::class),
28+
new IncompatiblePhpDocTypeCheck(
29+
new GenericObjectTypeCheck(),
30+
new UnresolvableTypeHelper(),
31+
new GenericCallableRuleHelper(
32+
new TemplateTypeCheck(
33+
$reflectionProvider,
34+
new ClassNameCheck(
35+
new ClassCaseSensitivityCheck($reflectionProvider, true),
36+
new ClassForbiddenNameCheck(self::getContainer()),
37+
),
38+
new GenericObjectTypeCheck(),
39+
$typeAliasResolver,
40+
true,
41+
),
42+
),
43+
),
44+
);
45+
}
46+
47+
public function testRule(): void
48+
{
49+
if (PHP_VERSION_ID < 80400) {
50+
$this->markTestSkipped('Test requires PHP 8.4.');
51+
}
52+
53+
$this->analyse([__DIR__ . '/data/incompatible-property-hook-phpdoc-types.php'], [
54+
[
55+
'PHPDoc tag @return with type string is incompatible with native type int.',
56+
10,
57+
],
58+
[
59+
'PHPDoc tag @return with type string is incompatible with native type void.',
60+
17,
61+
],
62+
[
63+
'PHPDoc tag @param for parameter $value with type string is incompatible with native type int.',
64+
27,
65+
],
66+
[
67+
'Parameter $value for PHPDoc tag @param-out is not passed by reference.',
68+
27,
69+
],
70+
[
71+
'PHPDoc tag @param for parameter $value contains unresolvable type.',
72+
34,
73+
],
74+
[
75+
'PHPDoc tag @param for parameter $value contains generic type Exception<int> but class Exception is not generic.',
76+
41,
77+
],
78+
[
79+
'PHPDoc tag @param for parameter $value template T of callable<T of mixed>(T): T shadows @template T for class IncompatiblePropertyHookPhpDocTypes\GenericFoo.',
80+
54,
81+
],
82+
[
83+
'PHPDoc tag @param for parameter $value template of callable<\stdClass of mixed>(T): T cannot have existing class \stdClass as its name.',
84+
61,
85+
],
86+
]);
87+
}
88+
89+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php // lint >= 8.4
2+
3+
namespace IncompatiblePropertyHookPhpDocTypes;
4+
5+
class Foo
6+
{
7+
8+
public int $i {
9+
/** @return string */
10+
get {
11+
return $this->i;
12+
}
13+
}
14+
15+
public int $j {
16+
/** @return string */
17+
set {
18+
$this->j = 1;
19+
}
20+
}
21+
22+
public int $k {
23+
/**
24+
* @param string $value
25+
* @param-out int $value
26+
*/
27+
set {
28+
$this->k = 1;
29+
}
30+
}
31+
32+
public int $l {
33+
/** @param \stdClass&\Exception $value */
34+
set {
35+
36+
}
37+
}
38+
39+
public \Exception $m {
40+
/** @param \Exception<int> $value */
41+
set {
42+
43+
}
44+
}
45+
46+
}
47+
48+
/** @template T */
49+
class GenericFoo
50+
{
51+
52+
public int $n {
53+
/** @param int|callable<T>(T): T $value */
54+
set (int|callable $value) {
55+
56+
}
57+
}
58+
59+
public int $o {
60+
/** @param int|callable<\stdClass>(T): T $value */
61+
set (int|callable $value) {
62+
63+
}
64+
}
65+
66+
}

0 commit comments

Comments
 (0)