Skip to content

Commit 9ff1100

Browse files
committed
ExistingClassesInPropertyHookTypehintsRule - level 0
1 parent 7db6363 commit 9ff1100

File tree

5 files changed

+157
-1
lines changed

5 files changed

+157
-1
lines changed

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: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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\Internal\SprintfHelper;
8+
use PHPStan\Node\InPropertyHookNode;
9+
use PHPStan\Rules\FunctionDefinitionCheck;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\ShouldNotHappenException;
12+
use function sprintf;
13+
use function ucfirst;
14+
15+
/**
16+
* @implements Rule<InPropertyHookNode>
17+
*/
18+
final class ExistingClassesInPropertyHookTypehintsRule implements Rule
19+
{
20+
21+
public function __construct(private FunctionDefinitionCheck $check)
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+
throw new ShouldNotHappenException();
35+
}
36+
$className = SprintfHelper::escapeFormatString($node->getClassReflection()->getDisplayName());
37+
$hookName = $hookReflection->getPropertyHookName();
38+
$propertyName = SprintfHelper::escapeFormatString($hookReflection->getHookedPropertyName());
39+
40+
return $this->check->checkClassMethod(
41+
$hookReflection,
42+
$node->getOriginalNode(),
43+
sprintf(
44+
'Parameter $%%s of %s hook for property %s::$%s has invalid type %%s.',
45+
$hookName,
46+
$className,
47+
$propertyName,
48+
),
49+
sprintf(
50+
'%s hook for property %s::$%s has invalid return type %%s.',
51+
ucfirst($hookName),
52+
$className,
53+
$propertyName,
54+
),
55+
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),
56+
sprintf('Template type %%s of %s hook for property %s::$%s is not referenced in a parameter.', $hookName, $className, $propertyName),
57+
sprintf(
58+
'Parameter $%%s of %s hook for property %s::$%s has unresolvable native type.',
59+
$hookName,
60+
$className,
61+
$propertyName,
62+
),
63+
sprintf(
64+
'%s hook for property %s::$%s has unresolvable native return type.',
65+
ucfirst($hookName),
66+
$className,
67+
$propertyName,
68+
),
69+
sprintf(
70+
'%s hook for property %s::$%s has invalid @phpstan-self-out type %%s.',
71+
ucfirst($hookName),
72+
$className,
73+
$propertyName,
74+
),
75+
);
76+
}
77+
78+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
}
56+
57+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
}

0 commit comments

Comments
 (0)