Skip to content

Commit 7e2ab56

Browse files
Add phpstan-sealed support
1 parent 5ab9acc commit 7e2ab56

16 files changed

+511
-1
lines changed

src/Dependency/DependencyResolver.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,15 @@ private function addClassToDependencies(string $className, array &$dependenciesR
536536
}
537537
}
538538

539+
foreach ($classReflection->getSealedTags() as $sealedTag) {
540+
foreach ($sealedTag->getType()->getReferencedClasses() as $referencedClass) {
541+
if (!$this->reflectionProvider->hasClass($referencedClass)) {
542+
continue;
543+
}
544+
$dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass);
545+
}
546+
}
547+
539548
foreach ($classReflection->getTemplateTags() as $templateTag) {
540549
foreach ($templateTag->getBound()->getReferencedClasses() as $referencedClass) {
541550
if (!$this->reflectionProvider->hasClass($referencedClass)) {

src/PhpDoc/PhpDocNodeResolver.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use PHPStan\PhpDoc\Tag\RequireExtendsTag;
2020
use PHPStan\PhpDoc\Tag\RequireImplementsTag;
2121
use PHPStan\PhpDoc\Tag\ReturnTag;
22+
use PHPStan\PhpDoc\Tag\SealedTypeTag;
2223
use PHPStan\PhpDoc\Tag\SelfOutTypeTag;
2324
use PHPStan\PhpDoc\Tag\TemplateTag;
2425
use PHPStan\PhpDoc\Tag\ThrowsTag;
@@ -524,6 +525,24 @@ public function resolveRequireImplementsTags(PhpDocNode $phpDocNode, NameScope $
524525
return $resolved;
525526
}
526527

528+
/**
529+
* @return array<SealedTypeTag>
530+
*/
531+
public function resolveSealedTags(PhpDocNode $phpDocNode, NameScope $nameScope): array
532+
{
533+
$resolved = [];
534+
535+
foreach (['@psalm-inheritors', '@phpstan-sealed'] as $tagName) {
536+
foreach ($phpDocNode->getSealedTagValues($tagName) as $tagValue) {
537+
$resolved[] = new SealedTypeTag(
538+
$this->typeNodeResolver->resolve($tagValue->type, $nameScope),
539+
);
540+
}
541+
}
542+
543+
return $resolved;
544+
}
545+
527546
/**
528547
* @return array<string, TypeAliasTag>
529548
*/

src/PhpDoc/ResolvedPhpDocBlock.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use PHPStan\PhpDoc\Tag\RequireExtendsTag;
1717
use PHPStan\PhpDoc\Tag\RequireImplementsTag;
1818
use PHPStan\PhpDoc\Tag\ReturnTag;
19+
use PHPStan\PhpDoc\Tag\SealedTypeTag;
1920
use PHPStan\PhpDoc\Tag\SelfOutTypeTag;
2021
use PHPStan\PhpDoc\Tag\TemplateTag;
2122
use PHPStan\PhpDoc\Tag\ThrowsTag;
@@ -111,6 +112,9 @@ final class ResolvedPhpDocBlock
111112
/** @var array<RequireImplementsTag>|false */
112113
private array|false $requireImplementsTags = false;
113114

115+
/** @var array<SealedTypeTag>|false */
116+
private array|false $sealedTypeTags = false;
117+
114118
/** @var array<TypeAliasTag>|false */
115119
private array|false $typeAliasTags = false;
116120

@@ -218,6 +222,7 @@ public static function createEmpty(): self
218222
$self->mixinTags = [];
219223
$self->requireExtendsTags = [];
220224
$self->requireImplementsTags = [];
225+
$self->sealedTypeTags = [];
221226
$self->typeAliasTags = [];
222227
$self->typeAliasImportTags = [];
223228
$self->assertTags = [];
@@ -282,6 +287,7 @@ public function merge(array $parents, array $parentPhpDocBlocks): self
282287
$result->mixinTags = $this->getMixinTags();
283288
$result->requireExtendsTags = $this->getRequireExtendsTags();
284289
$result->requireImplementsTags = $this->getRequireImplementsTags();
290+
$result->sealedTypeTags = $this->getSealedTags();
285291
$result->typeAliasTags = $this->getTypeAliasTags();
286292
$result->typeAliasImportTags = $this->getTypeAliasImportTags();
287293
$result->assertTags = self::mergeAssertTags($this->getAssertTags(), $parents, $parentPhpDocBlocks);
@@ -663,6 +669,21 @@ public function getRequireImplementsTags(): array
663669
return $this->requireImplementsTags;
664670
}
665671

672+
/**
673+
* @return array<SealedTypeTag>
674+
*/
675+
public function getSealedTags(): array
676+
{
677+
if ($this->sealedTypeTags === false) {
678+
$this->sealedTypeTags = $this->phpDocNodeResolver->resolveSealedTags(
679+
$this->phpDocNode,
680+
$this->getNameScope(),
681+
);
682+
}
683+
684+
return $this->sealedTypeTags;
685+
}
686+
666687
/**
667688
* @return array<TypeAliasTag>
668689
*/

src/PhpDoc/Tag/SealedTypeTag.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDoc\Tag;
4+
5+
use PHPStan\Type\Type;
6+
7+
/**
8+
* @api
9+
*/
10+
final class SealedTypeTag implements TypedTag
11+
{
12+
13+
public function __construct(private Type $type)
14+
{
15+
}
16+
17+
public function getType(): Type
18+
{
19+
return $this->type;
20+
}
21+
22+
public function withType(Type $type): self
23+
{
24+
return new self($type);
25+
}
26+
27+
}

src/Reflection/ClassReflection.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use PHPStan\PhpDoc\Tag\PropertyTag;
2424
use PHPStan\PhpDoc\Tag\RequireExtendsTag;
2525
use PHPStan\PhpDoc\Tag\RequireImplementsTag;
26+
use PHPStan\PhpDoc\Tag\SealedTypeTag;
2627
use PHPStan\PhpDoc\Tag\TemplateTag;
2728
use PHPStan\PhpDoc\Tag\TypeAliasImportTag;
2829
use PHPStan\PhpDoc\Tag\TypeAliasTag;
@@ -1887,6 +1888,19 @@ public function getRequireImplementsTags(): array
18871888
return $resolvedPhpDoc->getRequireImplementsTags();
18881889
}
18891890

1891+
/**
1892+
* @return array<SealedTypeTag>
1893+
*/
1894+
public function getSealedTags(): array
1895+
{
1896+
$resolvedPhpDoc = $this->getResolvedPhpDoc();
1897+
if ($resolvedPhpDoc === null) {
1898+
return [];
1899+
}
1900+
1901+
return $resolvedPhpDoc->getSealedTags();
1902+
}
1903+
18901904
/**
18911905
* @return array<string, PropertyTag>
18921906
*/

src/Rules/ClassNameUsageLocation.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ final class ClassNameUsageLocation
3838
public const PHPDOC_TAG_PROPERTY = 'propertyTag';
3939
public const PHPDOC_TAG_REQUIRE_EXTENDS = 'requireExtends';
4040
public const PHPDOC_TAG_REQUIRE_IMPLEMENTS = 'requireImplements';
41+
public const PHPDOC_TAG_SEALED = 'sealed';
4142
public const STATIC_METHOD_CALL = 'staticMethod';
4243
public const PHPDOC_TAG_TEMPLATE_BOUND = 'templateBound';
4344
public const PHPDOC_TAG_TEMPLATE_DEFAULT = 'templateDefault';
@@ -255,6 +256,8 @@ public function createMessage(string $part): string
255256
return sprintf('PHPDoc tag @phpstan-require-extends references %s.', $part);
256257
case self::PHPDOC_TAG_REQUIRE_IMPLEMENTS:
257258
return sprintf('PHPDoc tag @phpstan-require-implements references %s.', $part);
259+
case self::PHPDOC_TAG_SEALED:
260+
return sprintf('PHPDoc tag @phpstan-sealed references %s.', $part);
258261
case self::STATIC_METHOD_CALL:
259262
$method = $this->getMethod();
260263
if ($method !== null) {

src/Rules/Classes/SealedRule.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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\DependencyInjection\RegisteredRule;
8+
use PHPStan\Node\InClassNode;
9+
use PHPStan\Rules\Rule;
10+
use PHPStan\Rules\RuleErrorBuilder;
11+
use PHPStan\Type\ObjectType;
12+
use PHPStan\Type\VerbosityLevel;
13+
use function array_values;
14+
use function sprintf;
15+
16+
/**
17+
* @implements Rule<InClassNode>
18+
*/
19+
#[RegisteredRule(level: 0)]
20+
final class SealedRule implements Rule
21+
{
22+
23+
public function getNodeType(): string
24+
{
25+
return InClassNode::class;
26+
}
27+
28+
public function processNode(Node $node, Scope $scope): array
29+
{
30+
$classReflection = $node->getClassReflection();
31+
if ($classReflection->isEnum()) {
32+
return [];
33+
}
34+
35+
$className = $classReflection->getName();
36+
37+
$parents = array_values($classReflection->getImmediateInterfaces());
38+
$parentClass = $classReflection->getParentClass();
39+
if ($parentClass !== null) {
40+
$parents[] = $parentClass;
41+
}
42+
43+
$errors = [];
44+
foreach ($parents as $parent) {
45+
$sealedTags = $parent->getSealedTags();
46+
foreach ($sealedTags as $sealedTag) {
47+
$type = $sealedTag->getType();
48+
if ($type->isSuperTypeOf(new ObjectType($className))->yes()) {
49+
continue;
50+
}
51+
52+
$errors[] = RuleErrorBuilder::message(
53+
sprintf(
54+
'%s %s is sealed and only permits %s as subtypes, %s given.',
55+
$parent->isInterface() ? 'Interface' : 'Class',
56+
$parent->getDisplayName(),
57+
$type->describe(VerbosityLevel::typeOnly()),
58+
$classReflection->getDisplayName(),
59+
),
60+
)
61+
->identifier('class.sealed')
62+
->build();
63+
}
64+
}
65+
66+
return $errors;
67+
}
68+
69+
}

src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ final class InvalidPHPStanDocTagRule implements Rule
5858
'@phpstan-readonly-allow-private-mutation',
5959
'@phpstan-require-extends',
6060
'@phpstan-require-implements',
61+
'@phpstan-sealed',
6162
'@phpstan-param-immediately-invoked-callable',
6263
'@phpstan-param-later-invoked-callable',
6364
'@phpstan-param-closure-this',
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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\DependencyInjection\AutowiredParameter;
8+
use PHPStan\DependencyInjection\RegisteredRule;
9+
use PHPStan\Node\InClassNode;
10+
use PHPStan\Rules\ClassNameCheck;
11+
use PHPStan\Rules\ClassNameNodePair;
12+
use PHPStan\Rules\ClassNameUsageLocation;
13+
use PHPStan\Rules\Rule;
14+
use PHPStan\Rules\RuleErrorBuilder;
15+
use PHPStan\Type\VerbosityLevel;
16+
use function array_column;
17+
use function array_map;
18+
use function array_merge;
19+
use function count;
20+
use function sprintf;
21+
22+
/**
23+
* @implements Rule<InClassNode>
24+
*/
25+
#[RegisteredRule(level: 0)]
26+
final class SealedDefinitionClassRule implements Rule
27+
{
28+
29+
public function __construct(
30+
private ClassNameCheck $classCheck,
31+
#[AutowiredParameter]
32+
private bool $checkClassCaseSensitivity,
33+
#[AutowiredParameter(ref: '%tips.discoveringSymbols%')]
34+
private bool $discoveringSymbolsTip,
35+
)
36+
{
37+
}
38+
39+
public function getNodeType(): string
40+
{
41+
return InClassNode::class;
42+
}
43+
44+
public function processNode(Node $node, Scope $scope): array
45+
{
46+
$classReflection = $node->getClassReflection();
47+
$sealedTags = $classReflection->getSealedTags();
48+
49+
if (count($sealedTags) === 0) {
50+
return [];
51+
}
52+
53+
if ($classReflection->isEnum()) {
54+
return [
55+
RuleErrorBuilder::message('PHPDoc tag @phpstan-sealed is only valid on class or interface.')
56+
->identifier('sealed.onEnum')
57+
->build(),
58+
];
59+
}
60+
61+
$errors = [];
62+
foreach ($sealedTags as $sealedTag) {
63+
$type = $sealedTag->getType();
64+
$classNames = $type->getObjectClassNames();
65+
if (count($classNames) === 0) {
66+
$errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-sealed contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly())))
67+
->identifier('sealed.nonObject')
68+
->build();
69+
continue;
70+
}
71+
72+
$referencedClassReflections = array_map(static fn ($reflection) => [$reflection, $reflection->getName()], $type->getObjectClassReflections());
73+
$referencedClassReflectionsMap = array_column($referencedClassReflections, 0, 1);
74+
foreach ($classNames as $class) {
75+
$referencedClassReflection = $referencedClassReflectionsMap[$class] ?? null;
76+
if ($referencedClassReflection === null) {
77+
$errorBuilder = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-sealed contains unknown class %s.', $class))
78+
->identifier('class.notFound');
79+
80+
if ($this->discoveringSymbolsTip) {
81+
$errorBuilder->discoveringSymbolsTip();
82+
}
83+
84+
$errors[] = $errorBuilder->build();
85+
continue;
86+
}
87+
88+
$errors = array_merge(
89+
$errors,
90+
$this->classCheck->checkClassNames($scope, [
91+
new ClassNameNodePair($class, $node),
92+
], ClassNameUsageLocation::from(ClassNameUsageLocation::PHPDOC_TAG_SEALED), $this->checkClassCaseSensitivity),
93+
);
94+
}
95+
}
96+
97+
return $errors;
98+
}
99+
100+
}

0 commit comments

Comments
 (0)