Skip to content

Commit ef83213

Browse files
committed
ExistingClassesInPropertyHookTypehintsRule - level 0
1 parent 92e9d43 commit ef83213

File tree

6 files changed

+189
-1
lines changed

6 files changed

+189
-1
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ lint:
9393
--exclude tests/PHPStan/Rules/Classes/data/invalid-hooked-properties.php \
9494
--exclude tests/PHPStan/Parser/data/cleaning-property-hooks-before.php \
9595
--exclude tests/PHPStan/Parser/data/cleaning-property-hooks-after.php \
96+
--exclude tests/PHPStan/Rules/Properties/data/existing-classes-property-hooks.php \
9697
src tests
9798

9899
cs:

conf/config.level0.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ rules:
9292
- PHPStan\Rules\Operators\InvalidIncDecOperationRule
9393
- PHPStan\Rules\Properties\AccessPropertiesInAssignRule
9494
- PHPStan\Rules\Properties\AccessStaticPropertiesInAssignRule
95+
- PHPStan\Rules\Properties\ExistingClassesInPropertyHookTypehintsRule
9596
- PHPStan\Rules\Properties\InvalidCallablePropertyTypeRule
9697
- PHPStan\Rules\Properties\MissingReadOnlyPropertyAssignRule
9798
- PHPStan\Rules\Properties\MissingReadOnlyByPhpDocPropertyAssignRule

src/Rules/FunctionDefinitionCheck.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ public function checkAnonymousFunction(
245245
*/
246246
public function checkClassMethod(
247247
PhpMethodFromParserNodeReflection $methodReflection,
248-
ClassMethod $methodNode,
248+
ClassMethod|Node\PropertyHook $methodNode,
249249
string $parameterMessage,
250250
string $returnMessage,
251251
string $unionTypesMessage,
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Properties;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\Variable;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Internal\SprintfHelper;
9+
use PHPStan\Node\InPropertyHookNode;
10+
use PHPStan\Rules\FunctionDefinitionCheck;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\ShouldNotHappenException;
13+
use function sprintf;
14+
use function ucfirst;
15+
16+
/**
17+
* @implements Rule<InPropertyHookNode>
18+
*/
19+
final class ExistingClassesInPropertyHookTypehintsRule implements Rule
20+
{
21+
22+
public function __construct(private FunctionDefinitionCheck $check)
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+
$hookReflection = $node->getHookReflection();
34+
if (!$hookReflection->isPropertyHook()) {
35+
throw new ShouldNotHappenException();
36+
}
37+
$className = SprintfHelper::escapeFormatString($node->getClassReflection()->getDisplayName());
38+
$hookName = $hookReflection->getPropertyHookName();
39+
$propertyName = SprintfHelper::escapeFormatString($hookReflection->getHookedPropertyName());
40+
41+
$originalHookNode = $node->getOriginalNode();
42+
if ($hookReflection->getPropertyHookName() === 'set' && $originalHookNode->params === []) {
43+
$originalHookNode = clone $originalHookNode;
44+
$originalHookNode->params = [
45+
new Node\Param(new Variable('value'), null, null),
46+
];
47+
}
48+
49+
return $this->check->checkClassMethod(
50+
$hookReflection,
51+
$originalHookNode,
52+
sprintf(
53+
'Parameter $%%s of %s hook for property %s::$%s has invalid type %%s.',
54+
$hookName,
55+
$className,
56+
$propertyName,
57+
),
58+
sprintf(
59+
'%s hook for property %s::$%s has invalid return type %%s.',
60+
ucfirst($hookName),
61+
$className,
62+
$propertyName,
63+
),
64+
sprintf('%s hook for property %s::$%s uses native union types but they\'re supported only on PHP 8.0 and later.', $hookName, $className, $propertyName),
65+
sprintf('Template type %%s of %s hook for property %s::$%s is not referenced in a parameter.', $hookName, $className, $propertyName),
66+
sprintf(
67+
'Parameter $%%s of %s hook for property %s::$%s has unresolvable native type.',
68+
$hookName,
69+
$className,
70+
$propertyName,
71+
),
72+
sprintf(
73+
'%s hook for property %s::$%s has unresolvable native return type.',
74+
ucfirst($hookName),
75+
$className,
76+
$propertyName,
77+
),
78+
sprintf(
79+
'%s hook for property %s::$%s has invalid @phpstan-self-out type %%s.',
80+
ucfirst($hookName),
81+
$className,
82+
$propertyName,
83+
),
84+
);
85+
}
86+
87+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Properties;
4+
5+
use PHPStan\Php\PhpVersion;
6+
use PHPStan\Rules\ClassCaseSensitivityCheck;
7+
use PHPStan\Rules\ClassForbiddenNameCheck;
8+
use PHPStan\Rules\ClassNameCheck;
9+
use PHPStan\Rules\FunctionDefinitionCheck;
10+
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Testing\RuleTestCase;
13+
use const PHP_VERSION_ID;
14+
15+
/**
16+
* @extends RuleTestCase<ExistingClassesInPropertyHookTypehintsRule>
17+
*/
18+
class ExistingClassesInPropertyHookTypehintsRuleTest extends RuleTestCase
19+
{
20+
21+
protected function getRule(): Rule
22+
{
23+
$reflectionProvider = $this->createReflectionProvider();
24+
return new ExistingClassesInPropertyHookTypehintsRule(
25+
new FunctionDefinitionCheck(
26+
$reflectionProvider,
27+
new ClassNameCheck(
28+
new ClassCaseSensitivityCheck($reflectionProvider, true),
29+
new ClassForbiddenNameCheck(self::getContainer()),
30+
),
31+
new UnresolvableTypeHelper(),
32+
new PhpVersion(PHP_VERSION_ID),
33+
true,
34+
false,
35+
),
36+
);
37+
}
38+
39+
public function testRule(): void
40+
{
41+
if (PHP_VERSION_ID < 80400) {
42+
$this->markTestSkipped('Test requires PHP 8.4.');
43+
}
44+
45+
$this->analyse([__DIR__ . '/data/existing-classes-property-hooks.php'], [
46+
[
47+
'Parameter $v of set hook for property ExistingClassesPropertyHooks\Foo::$i has invalid type ExistingClassesPropertyHooks\Nonexistent.',
48+
9,
49+
],
50+
[
51+
'Parameter $v of set hook for property ExistingClassesPropertyHooks\Foo::$j has unresolvable native type.',
52+
15,
53+
],
54+
[
55+
'Get hook for property ExistingClassesPropertyHooks\Foo::$k has invalid return type ExistingClassesPropertyHooks\Undefined.',
56+
22,
57+
],
58+
[
59+
'Parameter $value of set hook for property ExistingClassesPropertyHooks\Foo::$l has invalid type ExistingClassesPropertyHooks\Undefined.',
60+
29,
61+
],
62+
]);
63+
}
64+
65+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php // lint >= 8.4
2+
3+
namespace ExistingClassesPropertyHooks;
4+
5+
class Foo
6+
{
7+
8+
public int $i {
9+
set (Nonexistent $v) {
10+
11+
}
12+
}
13+
14+
public \stdClass $j {
15+
set (\stdClass&\Exception $v) {
16+
17+
}
18+
}
19+
20+
/** @var Undefined */
21+
public $k {
22+
get {
23+
24+
}
25+
}
26+
27+
/** @var Undefined */
28+
public $l {
29+
set {
30+
31+
}
32+
}
33+
34+
}

0 commit comments

Comments
 (0)