Skip to content

Commit 780a54c

Browse files
committed
Bleeding edge - IncompatibleClassConstantPhpDocTypeRule
1 parent 4cb02d1 commit 780a54c

8 files changed

+212
-5
lines changed

conf/bleedingEdge.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ parameters:
2424
validateOverridingMethodsInStubs: true
2525
crossCheckInterfaces: true
2626
finalByPhpDocTag: true
27+
classConstants: true
2728
stubFiles:
2829
- ../stubs/arrayFunctions.stub

conf/config.level2.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ parameters:
66
checkThisOnly: false
77
checkPhpDocMissingReturn: true
88

9+
conditionalTags:
10+
PHPStan\Rules\PhpDoc\IncompatibleClassConstantPhpDocTypeRule:
11+
phpstan.rules.rule: %featureToggles.classConstants%
12+
913
rules:
1014
- PHPStan\Rules\Cast\EchoRule
1115
- PHPStan\Rules\Cast\InvalidCastRule
@@ -50,6 +54,8 @@ services:
5054
crossCheckInterfaces: %featureToggles.crossCheckInterfaces%
5155
tags:
5256
- phpstan.rules.rule
57+
-
58+
class: PHPStan\Rules\PhpDoc\IncompatibleClassConstantPhpDocTypeRule
5359
-
5460
class: PHPStan\Rules\Generics\InterfaceAncestorsRule
5561
arguments:

conf/config.neon

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ parameters:
4848
validateOverridingMethodsInStubs: false
4949
crossCheckInterfaces: false
5050
finalByPhpDocTag: false
51+
classConstants: false
5152
fileExtensions:
5253
- php
5354
checkAlwaysTrueCheckTypeFunctionCall: false
@@ -225,7 +226,8 @@ parametersSchema:
225226
neverInGenericReturnType: bool(),
226227
validateOverridingMethodsInStubs: bool(),
227228
crossCheckInterfaces: bool(),
228-
finalByPhpDocTag: bool()
229+
finalByPhpDocTag: bool(),
230+
classConstants: bool()
229231
])
230232
fileExtensions: listOf(string())
231233
checkAlwaysTrueCheckTypeFunctionCall: bool()
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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\Reflection\ClassConstantReflection;
8+
use PHPStan\Reflection\ClassReflection;
9+
use PHPStan\Rules\Generics\GenericObjectTypeCheck;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleError;
12+
use PHPStan\Rules\RuleErrorBuilder;
13+
use PHPStan\Type\ConstantTypeHelper;
14+
use PHPStan\Type\VerbosityLevel;
15+
16+
/**
17+
* @implements \PHPStan\Rules\Rule<Node\Stmt\ClassConst>
18+
*/
19+
class IncompatibleClassConstantPhpDocTypeRule implements Rule
20+
{
21+
22+
private \PHPStan\Rules\Generics\GenericObjectTypeCheck $genericObjectTypeCheck;
23+
24+
private UnresolvableTypeHelper $unresolvableTypeHelper;
25+
26+
public function __construct(
27+
GenericObjectTypeCheck $genericObjectTypeCheck,
28+
UnresolvableTypeHelper $unresolvableTypeHelper
29+
)
30+
{
31+
$this->genericObjectTypeCheck = $genericObjectTypeCheck;
32+
$this->unresolvableTypeHelper = $unresolvableTypeHelper;
33+
}
34+
35+
public function getNodeType(): string
36+
{
37+
return Node\Stmt\ClassConst::class;
38+
}
39+
40+
public function processNode(Node $node, Scope $scope): array
41+
{
42+
if (!$scope->isInClass()) {
43+
throw new \PHPStan\ShouldNotHappenException();
44+
}
45+
46+
$errors = [];
47+
foreach ($node->consts as $const) {
48+
$constantName = $const->name->toString();
49+
$errors = array_merge($errors, $this->processSingleConstant($scope->getClassReflection(), $constantName));
50+
}
51+
52+
return $errors;
53+
}
54+
55+
/**
56+
* @param string $constantName
57+
* @return RuleError[]
58+
*/
59+
private function processSingleConstant(ClassReflection $classReflection, string $constantName): array
60+
{
61+
$constantReflection = $classReflection->getConstant($constantName);
62+
if (!$constantReflection instanceof ClassConstantReflection) {
63+
return [];
64+
}
65+
66+
if (!$constantReflection->hasPhpDocType()) {
67+
return [];
68+
}
69+
70+
$phpDocType = $constantReflection->getValueType();
71+
72+
$errors = [];
73+
if (
74+
$this->unresolvableTypeHelper->containsUnresolvableType($phpDocType)
75+
) {
76+
$errors[] = RuleErrorBuilder::message(sprintf(
77+
'PHPDoc tag @var for constant %s::%s contains unresolvable type.',
78+
$constantReflection->getDeclaringClass()->getName(),
79+
$constantName
80+
))->build();
81+
} else {
82+
$nativeType = ConstantTypeHelper::getTypeFromValue($constantReflection->getValue());
83+
$isSuperType = $phpDocType->isSuperTypeOf($nativeType);
84+
$verbosity = VerbosityLevel::getRecommendedLevelByType($phpDocType, $nativeType);
85+
if ($isSuperType->no()) {
86+
$errors[] = RuleErrorBuilder::message(sprintf(
87+
'PHPDoc tag @var for constant %s::%s with type %s is incompatible with value %s.',
88+
$constantReflection->getDeclaringClass()->getDisplayName(),
89+
$constantName,
90+
$phpDocType->describe($verbosity),
91+
$nativeType->describe(VerbosityLevel::value())
92+
))->build();
93+
94+
} elseif ($isSuperType->maybe()) {
95+
$errors[] = RuleErrorBuilder::message(sprintf(
96+
'PHPDoc tag @var for constant %s::%s with type %s is not subtype of value %s.',
97+
$constantReflection->getDeclaringClass()->getDisplayName(),
98+
$constantName,
99+
$phpDocType->describe($verbosity),
100+
$nativeType->describe(VerbosityLevel::value())
101+
))->build();
102+
}
103+
}
104+
105+
return array_merge($errors, $this->genericObjectTypeCheck->check(
106+
$phpDocType,
107+
sprintf(
108+
'PHPDoc tag @var for constant %s::%s contains generic type %%s but class %%s is not generic.',
109+
$constantReflection->getDeclaringClass()->getDisplayName(),
110+
$constantName
111+
),
112+
sprintf(
113+
'Generic type %%s in PHPDoc tag @var for constant %s::%s does not specify all template types of class %%s: %%s',
114+
$constantReflection->getDeclaringClass()->getDisplayName(),
115+
$constantName
116+
),
117+
sprintf(
118+
'Generic type %%s in PHPDoc tag @var for constant %s::%s specifies %%d template types, but class %%s supports only %%d: %%s',
119+
$constantReflection->getDeclaringClass()->getDisplayName(),
120+
$constantName
121+
),
122+
sprintf(
123+
'Type %%s in generic type %%s in PHPDoc tag @var for constant %s::%s is not subtype of template type %%s of class %%s.',
124+
$constantReflection->getDeclaringClass()->getDisplayName(),
125+
$constantName
126+
)
127+
));
128+
}
129+
130+
}

src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ public function processNode(Node $node, Scope $scope): array
6868
if (
6969
$node instanceof Node\Stmt\Property
7070
|| $node instanceof Node\Stmt\PropertyProperty
71+
|| $node instanceof Node\Stmt\ClassConst
72+
|| $node instanceof Node\Stmt\Const_
7173
) {
7274
return [];
7375
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PhpDoc;
4+
5+
use PHPStan\Rules\Generics\GenericObjectTypeCheck;
6+
use PHPStan\Rules\Rule;
7+
use PHPStan\Testing\RuleTestCase;
8+
9+
/**
10+
* @extends RuleTestCase<IncompatibleClassConstantPhpDocTypeRule>
11+
*/
12+
class IncompatibleClassConstantPhpDocTypeRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new IncompatibleClassConstantPhpDocTypeRule(new GenericObjectTypeCheck(), new UnresolvableTypeHelper(true));
18+
}
19+
20+
public function testRule(): void
21+
{
22+
$this->analyse([__DIR__ . '/data/incompatible-class-constant-phpdoc.php'], [
23+
[
24+
'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Foo::FOO contains unresolvable type.',
25+
9,
26+
],
27+
[
28+
'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Foo::BAZ with type string is incompatible with value 1.',
29+
17,
30+
],
31+
[
32+
'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Foo::DOLOR with type IncompatibleClassConstantPhpDoc\Foo<int> is incompatible with value 1.',
33+
26,
34+
],
35+
[
36+
'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Foo::DOLOR contains generic type IncompatibleClassConstantPhpDoc\Foo<int> but class IncompatibleClassConstantPhpDoc\Foo is not generic.',
37+
26,
38+
],
39+
]);
40+
}
41+
42+
}

tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,6 @@ public function testRule(): void
9797
67,
9898
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
9999
],
100-
[
101-
'PHPDoc tag @var contains unresolvable type.',
102-
90,
103-
],
104100
]);
105101
}
106102

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace IncompatibleClassConstantPhpDoc;
4+
5+
class Foo
6+
{
7+
8+
/** @var self&\stdClass */
9+
const FOO = 1;
10+
11+
/** @var int */
12+
const BAR = 1;
13+
14+
const NO_TYPE = 'string';
15+
16+
/** @var string */
17+
const BAZ = 1;
18+
19+
/** @var string|int */
20+
const LOREM = 1;
21+
22+
/** @var int */
23+
const IPSUM = self::LOREM; // resolved to 1, I'd prefer string|int
24+
25+
/** @var self<int> */
26+
const DOLOR = 1;
27+
28+
}

0 commit comments

Comments
 (0)