Skip to content

Commit 8900a43

Browse files
authored
Implement @require-extends rules
1 parent 8b6260c commit 8900a43

19 files changed

+1188
-9
lines changed

conf/config.level2.neon

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ rules:
4242
- PHPStan\Rules\PhpDoc\IncompatiblePropertyPhpDocTypeRule
4343
- PHPStan\Rules\PhpDoc\InvalidThrowsPhpDocValueRule
4444
- PHPStan\Rules\Properties\AccessPrivatePropertyThroughStaticRule
45+
- PHPStan\Rules\Classes\RequireImplementsRule
46+
- PHPStan\Rules\Classes\RequireExtendsRule
47+
- PHPStan\Rules\PhpDoc\RequireImplementsDefinitionClassRule
48+
- PHPStan\Rules\PhpDoc\RequireExtendsDefinitionClassRule
49+
- PHPStan\Rules\PhpDoc\RequireExtendsDefinitionTraitRule
4550

4651
conditionalTags:
4752
PHPStan\Rules\Functions\IncompatibleArrowFunctionDefaultParameterTypeRule:
@@ -64,6 +69,19 @@ services:
6469
checkClassCaseSensitivity: %checkClassCaseSensitivity%
6570
tags:
6671
- phpstan.rules.rule
72+
73+
-
74+
class: PHPStan\Rules\PhpDoc\RequireExtendsCheck
75+
arguments:
76+
checkClassCaseSensitivity: %checkClassCaseSensitivity%
77+
78+
-
79+
class: PHPStan\Rules\PhpDoc\RequireImplementsDefinitionTraitRule
80+
arguments:
81+
checkClassCaseSensitivity: %checkClassCaseSensitivity%
82+
tags:
83+
- phpstan.rules.rule
84+
6785
-
6886
class: PHPStan\Rules\Functions\IncompatibleArrowFunctionDefaultParameterTypeRule
6987
-

phpstan-baseline.neon

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,16 @@ parameters:
448448
count: 2
449449
path: src/Rules/Classes/ImpossibleInstanceOfRule.php
450450

451+
-
452+
message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#"
453+
count: 2
454+
path: src/Rules/Classes/RequireExtendsRule.php
455+
456+
-
457+
message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#"
458+
count: 1
459+
path: src/Rules/Classes/RequireImplementsRule.php
460+
451461
-
452462
message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#"
453463
count: 6
@@ -631,6 +641,16 @@ parameters:
631641
count: 1
632642
path: src/Rules/Methods/StaticMethodCallCheck.php
633643

644+
-
645+
message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#"
646+
count: 1
647+
path: src/Rules/PhpDoc/RequireExtendsCheck.php
648+
649+
-
650+
message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#"
651+
count: 1
652+
path: src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php
653+
634654
-
635655
message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericObjectType is error\\-prone and deprecated\\.$#"
636656
count: 1

src/PhpDoc/PhpDocNodeResolver.php

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@
2727
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode;
2828
use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode;
2929
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
30-
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode;
31-
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode;
3230
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
3331
use PHPStan\Reflection\PassedByReference;
3432
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
@@ -418,19 +416,35 @@ public function resolveMixinTags(PhpDocNode $phpDocNode, NameScope $nameScope):
418416
*/
419417
public function resolveRequireExtendsTags(PhpDocNode $phpDocNode, NameScope $nameScope): array
420418
{
421-
return array_map(fn (RequireExtendsTagValueNode $requireExtendsTagValueNode): RequireExtendsTag => new RequireExtendsTag(
422-
$this->typeNodeResolver->resolve($requireExtendsTagValueNode->type, $nameScope),
423-
), $phpDocNode->getRequireExtendsTagValues());
419+
$resolved = [];
420+
421+
foreach (['@psalm-require-extends', '@phpstan-require-extends'] as $tagName) {
422+
foreach ($phpDocNode->getRequireExtendsTagValues($tagName) as $tagValue) {
423+
$resolved[] = new RequireExtendsTag(
424+
$this->typeNodeResolver->resolve($tagValue->type, $nameScope),
425+
);
426+
}
427+
}
428+
429+
return $resolved;
424430
}
425431

426432
/**
427433
* @return array<RequireImplementsTag>
428434
*/
429435
public function resolveRequireImplementsTags(PhpDocNode $phpDocNode, NameScope $nameScope): array
430436
{
431-
return array_map(fn (RequireImplementsTagValueNode $requireImplementsTagValueNode): RequireImplementsTag => new RequireImplementsTag(
432-
$this->typeNodeResolver->resolve($requireImplementsTagValueNode->type, $nameScope),
433-
), $phpDocNode->getRequireImplementsTagValues());
437+
$resolved = [];
438+
439+
foreach (['@psalm-require-implements', '@phpstan-require-implements'] as $tagName) {
440+
foreach ($phpDocNode->getRequireImplementsTagValues($tagName) as $tagValue) {
441+
$resolved[] = new RequireImplementsTag(
442+
$this->typeNodeResolver->resolve($tagValue->type, $nameScope),
443+
);
444+
}
445+
}
446+
447+
return $resolved;
434448
}
435449

436450
/**

src/Reflection/ClassReflection.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -924,7 +924,7 @@ public function getTraits(bool $recursive = false): array
924924
}
925925

926926
/**
927-
* @return string[]
927+
* @return list<class-string>
928928
*/
929929
public function getParentClassesNames(): array
930930
{
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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+
use PHPStan\Rules\RuleErrorBuilder;
10+
use PHPStan\Type\ObjectType;
11+
use PHPStan\Type\VerbosityLevel;
12+
use function sprintf;
13+
14+
/**
15+
* @implements Rule<InClassNode>
16+
*/
17+
class RequireExtendsRule implements Rule
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+
$classReflection = $node->getClassReflection();
28+
29+
$errors = [];
30+
foreach ($classReflection->getImmediateInterfaces() as $interface) {
31+
$extendsTags = $interface->getRequireExtendsTags();
32+
foreach ($extendsTags as $extendsTag) {
33+
$type = $extendsTag->getType();
34+
if (!$type instanceof ObjectType) {
35+
continue;
36+
}
37+
38+
if ($classReflection->isSubclassOf($type->getClassName())) {
39+
continue;
40+
}
41+
42+
$errors[] = RuleErrorBuilder::message(
43+
sprintf(
44+
'Interface %s requires implementing class to extend %s, but %s does not.',
45+
$interface->getDisplayName(),
46+
$type->describe(VerbosityLevel::typeOnly()),
47+
$classReflection->getDisplayName(),
48+
),
49+
)->build();
50+
}
51+
}
52+
53+
foreach ($classReflection->getTraits() as $trait) {
54+
$extendsTags = $trait->getRequireExtendsTags();
55+
foreach ($extendsTags as $extendsTag) {
56+
$type = $extendsTag->getType();
57+
if (!$type instanceof ObjectType) {
58+
continue;
59+
}
60+
61+
if ($classReflection->isSubclassOf($type->getClassName())) {
62+
continue;
63+
}
64+
65+
$errors[] = RuleErrorBuilder::message(
66+
sprintf(
67+
'Trait %s requires using class to extend %s, but %s does not.',
68+
$trait->getDisplayName(),
69+
$type->describe(VerbosityLevel::typeOnly()),
70+
$classReflection->getDisplayName(),
71+
),
72+
)->build();
73+
}
74+
}
75+
76+
return $errors;
77+
}
78+
79+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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+
use PHPStan\Rules\RuleErrorBuilder;
10+
use PHPStan\Type\ObjectType;
11+
use PHPStan\Type\VerbosityLevel;
12+
use function sprintf;
13+
14+
/**
15+
* @implements Rule<InClassNode>
16+
*/
17+
class RequireImplementsRule implements Rule
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+
$classReflection = $node->getClassReflection();
28+
29+
$errors = [];
30+
foreach ($classReflection->getTraits() as $trait) {
31+
$implementsTags = $trait->getRequireImplementsTags();
32+
foreach ($implementsTags as $implementsTag) {
33+
$type = $implementsTag->getType();
34+
if (!$type instanceof ObjectType) {
35+
continue;
36+
}
37+
38+
if ($classReflection->implementsInterface($type->getClassName())) {
39+
continue;
40+
}
41+
42+
$errors[] = RuleErrorBuilder::message(
43+
sprintf(
44+
'Trait %s requires using class to implement %s, but %s does not.',
45+
$trait->getDisplayName(),
46+
$type->describe(VerbosityLevel::typeOnly()),
47+
$classReflection->getDisplayName(),
48+
),
49+
)->build();
50+
}
51+
}
52+
53+
return $errors;
54+
}
55+
56+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PhpDoc;
4+
5+
use PhpParser\Node;
6+
use PHPStan\PhpDoc\Tag\RequireExtendsTag;
7+
use PHPStan\Rules\ClassCaseSensitivityCheck;
8+
use PHPStan\Rules\ClassNameNodePair;
9+
use PHPStan\Rules\RuleError;
10+
use PHPStan\Rules\RuleErrorBuilder;
11+
use PHPStan\Type\ObjectType;
12+
use PHPStan\Type\VerbosityLevel;
13+
use function array_merge;
14+
use function count;
15+
use function sprintf;
16+
17+
final class RequireExtendsCheck
18+
{
19+
20+
public function __construct(
21+
private ClassCaseSensitivityCheck $classCaseSensitivityCheck,
22+
private bool $checkClassCaseSensitivity,
23+
)
24+
{
25+
}
26+
27+
/**
28+
* @param array<RequireExtendsTag> $extendsTags
29+
* @return RuleError[]
30+
*/
31+
public function checkExtendsTags(Node $node, array $extendsTags): array
32+
{
33+
$errors = [];
34+
35+
if (count($extendsTags) > 1) {
36+
$errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends can only be used once.'))->build();
37+
}
38+
39+
foreach ($extendsTags as $extendsTag) {
40+
$type = $extendsTag->getType();
41+
if (!$type instanceof ObjectType) {
42+
$errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly())))->build();
43+
continue;
44+
}
45+
46+
$class = $type->getClassName();
47+
$referencedClassReflection = $type->getClassReflection();
48+
49+
if ($referencedClassReflection === null) {
50+
$errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends contains unknown class %s.', $class))->discoveringSymbolsTip()->build();
51+
continue;
52+
}
53+
54+
if (!$referencedClassReflection->isClass()) {
55+
$errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends cannot contain non-class type %s.', $class))->build();
56+
} elseif ($referencedClassReflection->isFinal()) {
57+
$errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends cannot contain final class %s.', $class))->build();
58+
} elseif ($this->checkClassCaseSensitivity) {
59+
$errors = array_merge(
60+
$errors,
61+
$this->classCaseSensitivityCheck->checkClassNames([
62+
new ClassNameNodePair($class, $node),
63+
]),
64+
);
65+
}
66+
}
67+
68+
return $errors;
69+
}
70+
71+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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\InClassNode;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Rules\RuleErrorBuilder;
10+
use function count;
11+
12+
/**
13+
* @implements Rule<InClassNode>
14+
*/
15+
class RequireExtendsDefinitionClassRule implements Rule
16+
{
17+
18+
public function __construct(
19+
private RequireExtendsCheck $requireExtendsCheck,
20+
)
21+
{
22+
}
23+
24+
public function getNodeType(): string
25+
{
26+
return InClassNode::class;
27+
}
28+
29+
public function processNode(Node $node, Scope $scope): array
30+
{
31+
$classReflection = $node->getClassReflection();
32+
$extendsTags = $classReflection->getRequireExtendsTags();
33+
34+
if (count($extendsTags) === 0) {
35+
return [];
36+
}
37+
38+
if (!$classReflection->isInterface()) {
39+
return [
40+
RuleErrorBuilder::message('PHPDoc tag @phpstan-require-extends is only valid on trait or interface.')->build(),
41+
];
42+
}
43+
44+
return $this->requireExtendsCheck->checkExtendsTags($node, $extendsTags);
45+
}
46+
47+
}

0 commit comments

Comments
 (0)