Skip to content

Commit 55ea2ae

Browse files
committed
Bleeding edge - check type in @property tags
1 parent 030acbb commit 55ea2ae

10 files changed

+553
-1
lines changed

conf/config.level2.neon

+10
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ rules:
4949
- PHPStan\Rules\PhpDoc\RequireExtendsDefinitionTraitRule
5050

5151
conditionalTags:
52+
PHPStan\Rules\Classes\PropertyTagRule:
53+
phpstan.rules.rule: %featureToggles.absentTypeChecks%
54+
PHPStan\Rules\Classes\PropertyTagTraitRule:
55+
phpstan.rules.rule: %featureToggles.absentTypeChecks%
5256
PHPStan\Rules\Functions\IncompatibleArrowFunctionDefaultParameterTypeRule:
5357
phpstan.rules.rule: %featureToggles.closureDefaultParameterTypeRule%
5458
PHPStan\Rules\Functions\IncompatibleClosureDefaultParameterTypeRule:
@@ -75,6 +79,12 @@ services:
7579
tags:
7680
- phpstan.rules.rule
7781

82+
-
83+
class: PHPStan\Rules\Classes\PropertyTagRule
84+
85+
-
86+
class: PHPStan\Rules\Classes\PropertyTagTraitRule
87+
7888
-
7989
class: PHPStan\Rules\PhpDoc\RequireExtendsCheck
8090
arguments:

conf/config.neon

+5
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,11 @@ services:
917917
checkClassCaseSensitivity: %checkClassCaseSensitivity%
918918
absentTypeChecks: %featureToggles.absentTypeChecks%
919919

920+
-
921+
class: PHPStan\Rules\Classes\PropertyTagCheck
922+
arguments:
923+
checkClassCaseSensitivity: %checkClassCaseSensitivity%
924+
920925
-
921926
class: PHPStan\Rules\Comparison\ConstantConditionRuleHelper
922927
arguments:

src/Reflection/ClassReflection.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -1732,7 +1732,7 @@ public function getRequireImplementsTags(): array
17321732
}
17331733

17341734
/**
1735-
* @return array<PropertyTag>
1735+
* @return array<string, PropertyTag>
17361736
*/
17371737
public function getPropertyTags(): array
17381738
{
+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Classes;
4+
5+
use PhpParser\Node\Stmt\ClassLike;
6+
use PHPStan\Internal\SprintfHelper;
7+
use PHPStan\Reflection\ClassReflection;
8+
use PHPStan\Reflection\ReflectionProvider;
9+
use PHPStan\Rules\ClassNameCheck;
10+
use PHPStan\Rules\ClassNameNodePair;
11+
use PHPStan\Rules\Generics\GenericObjectTypeCheck;
12+
use PHPStan\Rules\IdentifierRuleError;
13+
use PHPStan\Rules\MissingTypehintCheck;
14+
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
15+
use PHPStan\Rules\RuleErrorBuilder;
16+
use PHPStan\ShouldNotHappenException;
17+
use PHPStan\Type\Type;
18+
use PHPStan\Type\VerbosityLevel;
19+
use function array_merge;
20+
use function implode;
21+
use function sprintf;
22+
23+
final class PropertyTagCheck
24+
{
25+
26+
public function __construct(
27+
private ReflectionProvider $reflectionProvider,
28+
private ClassNameCheck $classCheck,
29+
private GenericObjectTypeCheck $genericObjectTypeCheck,
30+
private MissingTypehintCheck $missingTypehintCheck,
31+
private UnresolvableTypeHelper $unresolvableTypeHelper,
32+
private bool $checkClassCaseSensitivity,
33+
)
34+
{
35+
}
36+
37+
/**
38+
* @return list<IdentifierRuleError>
39+
*/
40+
public function check(
41+
ClassReflection $classReflection,
42+
ClassLike $node,
43+
): array
44+
{
45+
$errors = [];
46+
foreach ($classReflection->getPropertyTags() as $propertyName => $propertyTag) {
47+
$readableType = $propertyTag->getReadableType();
48+
$writableType = $propertyTag->getWritableType();
49+
50+
$types = [];
51+
$tagName = '@property';
52+
if ($readableType !== null) {
53+
if ($writableType !== null) {
54+
if ($writableType->equals($readableType)) {
55+
$types[] = $readableType;
56+
} else {
57+
$types[] = $readableType;
58+
$types[] = $writableType;
59+
}
60+
} else {
61+
$tagName = '@property-read';
62+
$types[] = $readableType;
63+
}
64+
} elseif ($writableType !== null) {
65+
$tagName = '@property-write';
66+
$types[] = $writableType;
67+
} else {
68+
throw new ShouldNotHappenException();
69+
}
70+
71+
foreach ($types as $type) {
72+
foreach ($this->checkPropertyType($classReflection, $propertyName, $tagName, $type, $node) as $error) {
73+
$errors[] = $error;
74+
}
75+
}
76+
}
77+
78+
return $errors;
79+
}
80+
81+
/**
82+
* @return list<IdentifierRuleError>
83+
*/
84+
private function checkPropertyType(ClassReflection $classReflection, string $propertyName, string $tagName, Type $type, ClassLike $node): array
85+
{
86+
if ($this->unresolvableTypeHelper->containsUnresolvableType($type)) {
87+
return [
88+
RuleErrorBuilder::message(sprintf(
89+
'PHPDoc tag %s for property %s::$%s contains unresolvable type.',
90+
$tagName,
91+
$classReflection->getDisplayName(),
92+
$propertyName,
93+
))->identifier('propertyTag.unresolvableType')
94+
->build(),
95+
];
96+
}
97+
98+
$escapedClassName = SprintfHelper::escapeFormatString($classReflection->getDisplayName());
99+
$escapedPropertyName = SprintfHelper::escapeFormatString($propertyName);
100+
$escapedTagName = SprintfHelper::escapeFormatString($tagName);
101+
102+
$errors = $this->genericObjectTypeCheck->check(
103+
$type,
104+
sprintf('PHPDoc tag %s for property %s::$%s contains generic type %%s but %%s %%s is not generic.', $escapedTagName, $escapedClassName, $escapedPropertyName),
105+
sprintf('Generic type %%s in PHPDoc tag %s for property %s::$%s does not specify all template types of %%s %%s: %%s', $escapedTagName, $escapedClassName, $escapedPropertyName),
106+
sprintf('Generic type %%s in PHPDoc tag %s for property %s::$%s specifies %%d template types, but %%s %%s supports only %%d: %%s', $escapedTagName, $escapedClassName, $escapedPropertyName),
107+
sprintf('Type %%s in generic type %%s in PHPDoc tag %s for property %s::$%s is not subtype of template type %%s of %%s %%s.', $escapedTagName, $escapedClassName, $escapedPropertyName),
108+
sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag %s for property %s::$%s is in conflict with %%s template type %%s of %%s %%s.', $escapedTagName, $escapedClassName, $escapedPropertyName),
109+
sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag %s for property %s::$%s is redundant, template type %%s of %%s %%s has the same variance.', $escapedTagName, $escapedClassName, $escapedPropertyName),
110+
);
111+
112+
foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($type) as [$innerName, $genericTypeNames]) {
113+
$errors[] = RuleErrorBuilder::message(sprintf(
114+
'PHPDoc tag %s for property %s::$%s contains generic %s but does not specify its types: %s',
115+
$tagName,
116+
$classReflection->getDisplayName(),
117+
$propertyName,
118+
$innerName,
119+
implode(', ', $genericTypeNames),
120+
))
121+
->identifier('missingType.generics')
122+
->build();
123+
}
124+
125+
foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($type) as $iterableType) {
126+
$iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly());
127+
$errors[] = RuleErrorBuilder::message(sprintf(
128+
'%s %s has PHPDoc tag %s for property $%s with no value type specified in iterable type %s.',
129+
$classReflection->getClassTypeDescription(),
130+
$classReflection->getDisplayName(),
131+
$tagName,
132+
$propertyName,
133+
$iterableTypeDescription,
134+
))
135+
->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP)
136+
->identifier('missingType.iterableValue')
137+
->build();
138+
}
139+
140+
foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($type) as $callableType) {
141+
$errors[] = RuleErrorBuilder::message(sprintf(
142+
'%s %s has PHPDoc tag %s for property $%s with no signature specified for %s.',
143+
$classReflection->getClassTypeDescription(),
144+
$classReflection->getDisplayName(),
145+
$tagName,
146+
$propertyName,
147+
$callableType->describe(VerbosityLevel::typeOnly()),
148+
))->identifier('missingType.callable')->build();
149+
}
150+
151+
foreach ($type->getReferencedClasses() as $class) {
152+
if (!$this->reflectionProvider->hasClass($class)) {
153+
$errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag %s for property %s::$%s contains unknown class %s.', $tagName, $classReflection->getDisplayName(), $propertyName, $class))
154+
->identifier('class.notFound')
155+
->discoveringSymbolsTip()
156+
->build();
157+
} elseif ($this->reflectionProvider->getClass($class)->isTrait()) {
158+
$errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag %s for property %s::$%s contains invalid type %s.', $tagName, $classReflection->getDisplayName(), $propertyName, $class))
159+
->identifier('propertyTag.trait')
160+
->build();
161+
} else {
162+
$errors = array_merge(
163+
$errors,
164+
$this->classCheck->checkClassNames([
165+
new ClassNameNodePair($class, $node),
166+
], $this->checkClassCaseSensitivity),
167+
);
168+
}
169+
}
170+
171+
return $errors;
172+
}
173+
174+
}

src/Rules/Classes/PropertyTagRule.php

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Classes;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\InClassNode;
8+
use PHPStan\Rules\Rule;
9+
10+
/**
11+
* @implements Rule<InClassNode>
12+
*/
13+
final class PropertyTagRule implements Rule
14+
{
15+
16+
public function __construct(private PropertyTagCheck $check)
17+
{
18+
}
19+
20+
public function getNodeType(): string
21+
{
22+
return InClassNode::class;
23+
}
24+
25+
public function processNode(Node $node, Scope $scope): array
26+
{
27+
return $this->check->check($node->getClassReflection(), $node->getOriginalNode());
28+
}
29+
30+
}
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Classes;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\ReflectionProvider;
8+
use PHPStan\Rules\Rule;
9+
10+
/**
11+
* @implements Rule<Node\Stmt\Trait_>
12+
*/
13+
final class PropertyTagTraitRule implements Rule
14+
{
15+
16+
public function __construct(private PropertyTagCheck $check, private ReflectionProvider $reflectionProvider)
17+
{
18+
}
19+
20+
public function getNodeType(): string
21+
{
22+
return Node\Stmt\Trait_::class;
23+
}
24+
25+
public function processNode(Node $node, Scope $scope): array
26+
{
27+
$traitName = $node->namespacedName;
28+
if ($traitName === null) {
29+
return [];
30+
}
31+
32+
if (!$this->reflectionProvider->hasClass($traitName->toString())) {
33+
return [];
34+
}
35+
36+
return $this->check->check($this->reflectionProvider->getClass($traitName->toString()), $node);
37+
}
38+
39+
}

0 commit comments

Comments
 (0)