Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/Dependency/DependencyResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,15 @@ private function addClassToDependencies(string $className, array &$dependenciesR
}
}

foreach ($classReflection->getSealedTags() as $sealedTag) {
foreach ($sealedTag->getType()->getReferencedClasses() as $referencedClass) {
if (!$this->reflectionProvider->hasClass($referencedClass)) {
continue;
}
$dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass);
}
}

foreach ($classReflection->getTemplateTags() as $templateTag) {
foreach ($templateTag->getBound()->getReferencedClasses() as $referencedClass) {
if (!$this->reflectionProvider->hasClass($referencedClass)) {
Expand Down
19 changes: 19 additions & 0 deletions src/PhpDoc/PhpDocNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use PHPStan\PhpDoc\Tag\RequireExtendsTag;
use PHPStan\PhpDoc\Tag\RequireImplementsTag;
use PHPStan\PhpDoc\Tag\ReturnTag;
use PHPStan\PhpDoc\Tag\SealedTypeTag;
use PHPStan\PhpDoc\Tag\SelfOutTypeTag;
use PHPStan\PhpDoc\Tag\TemplateTag;
use PHPStan\PhpDoc\Tag\ThrowsTag;
Expand Down Expand Up @@ -524,6 +525,24 @@ public function resolveRequireImplementsTags(PhpDocNode $phpDocNode, NameScope $
return $resolved;
}

/**
* @return array<SealedTypeTag>
*/
public function resolveSealedTags(PhpDocNode $phpDocNode, NameScope $nameScope): array
{
$resolved = [];

foreach (['@psalm-inheritors', '@phpstan-sealed'] as $tagName) {
foreach ($phpDocNode->getSealedTagValues($tagName) as $tagValue) {
$resolved[] = new SealedTypeTag(
$this->typeNodeResolver->resolve($tagValue->type, $nameScope),
);
}
}

return $resolved;
}

/**
* @return array<string, TypeAliasTag>
*/
Expand Down
21 changes: 21 additions & 0 deletions src/PhpDoc/ResolvedPhpDocBlock.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use PHPStan\PhpDoc\Tag\RequireExtendsTag;
use PHPStan\PhpDoc\Tag\RequireImplementsTag;
use PHPStan\PhpDoc\Tag\ReturnTag;
use PHPStan\PhpDoc\Tag\SealedTypeTag;
use PHPStan\PhpDoc\Tag\SelfOutTypeTag;
use PHPStan\PhpDoc\Tag\TemplateTag;
use PHPStan\PhpDoc\Tag\ThrowsTag;
Expand Down Expand Up @@ -111,6 +112,9 @@ final class ResolvedPhpDocBlock
/** @var array<RequireImplementsTag>|false */
private array|false $requireImplementsTags = false;

/** @var array<SealedTypeTag>|false */
private array|false $sealedTypeTags = false;

/** @var array<TypeAliasTag>|false */
private array|false $typeAliasTags = false;

Expand Down Expand Up @@ -218,6 +222,7 @@ public static function createEmpty(): self
$self->mixinTags = [];
$self->requireExtendsTags = [];
$self->requireImplementsTags = [];
$self->sealedTypeTags = [];
$self->typeAliasTags = [];
$self->typeAliasImportTags = [];
$self->assertTags = [];
Expand Down Expand Up @@ -282,6 +287,7 @@ public function merge(array $parents, array $parentPhpDocBlocks): self
$result->mixinTags = $this->getMixinTags();
$result->requireExtendsTags = $this->getRequireExtendsTags();
$result->requireImplementsTags = $this->getRequireImplementsTags();
$result->sealedTypeTags = $this->getSealedTags();
$result->typeAliasTags = $this->getTypeAliasTags();
$result->typeAliasImportTags = $this->getTypeAliasImportTags();
$result->assertTags = self::mergeAssertTags($this->getAssertTags(), $parents, $parentPhpDocBlocks);
Expand Down Expand Up @@ -663,6 +669,21 @@ public function getRequireImplementsTags(): array
return $this->requireImplementsTags;
}

/**
* @return array<SealedTypeTag>
*/
public function getSealedTags(): array
{
if ($this->sealedTypeTags === false) {
$this->sealedTypeTags = $this->phpDocNodeResolver->resolveSealedTags(
$this->phpDocNode,
$this->getNameScope(),
);
}

return $this->sealedTypeTags;
}

/**
* @return array<TypeAliasTag>
*/
Expand Down
27 changes: 27 additions & 0 deletions src/PhpDoc/Tag/SealedTypeTag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types = 1);

namespace PHPStan\PhpDoc\Tag;

use PHPStan\Type\Type;

/**
* @api
*/
final class SealedTypeTag implements TypedTag
{

public function __construct(private Type $type)
{
}

public function getType(): Type
{
return $this->type;
}

public function withType(Type $type): self
{
return new self($type);
}

}
14 changes: 14 additions & 0 deletions src/Reflection/ClassReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use PHPStan\PhpDoc\Tag\PropertyTag;
use PHPStan\PhpDoc\Tag\RequireExtendsTag;
use PHPStan\PhpDoc\Tag\RequireImplementsTag;
use PHPStan\PhpDoc\Tag\SealedTypeTag;
use PHPStan\PhpDoc\Tag\TemplateTag;
use PHPStan\PhpDoc\Tag\TypeAliasImportTag;
use PHPStan\PhpDoc\Tag\TypeAliasTag;
Expand Down Expand Up @@ -1887,6 +1888,19 @@ public function getRequireImplementsTags(): array
return $resolvedPhpDoc->getRequireImplementsTags();
}

/**
* @return array<SealedTypeTag>
*/
public function getSealedTags(): array
{
$resolvedPhpDoc = $this->getResolvedPhpDoc();
if ($resolvedPhpDoc === null) {
return [];
}

return $resolvedPhpDoc->getSealedTags();
}

/**
* @return array<string, PropertyTag>
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php declare(strict_types = 1);

namespace PHPStan\Reflection\Php;

use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\AllowedSubTypesClassReflectionExtension;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Type\UnionType;
use function count;

#[AutowiredService]
final class SealedAllowedSubTypesClassReflectionExtension implements AllowedSubTypesClassReflectionExtension
{

public function supports(ClassReflection $classReflection): bool
{
return count($classReflection->getSealedTags()) > 0;
}

public function getAllowedSubTypes(ClassReflection $classReflection): array
{
$types = [];

foreach ($classReflection->getSealedTags() as $sealedTag) {
$type = $sealedTag->getType();
if ($type instanceof UnionType) {
$types = $type->getTypes();
} else {
$types = [$type];
}
}

return $types;
}

}
3 changes: 3 additions & 0 deletions src/Rules/ClassNameUsageLocation.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ final class ClassNameUsageLocation
public const PHPDOC_TAG_PROPERTY = 'propertyTag';
public const PHPDOC_TAG_REQUIRE_EXTENDS = 'requireExtends';
public const PHPDOC_TAG_REQUIRE_IMPLEMENTS = 'requireImplements';
public const PHPDOC_TAG_SEALED = 'sealed';
public const STATIC_METHOD_CALL = 'staticMethod';
public const PHPDOC_TAG_TEMPLATE_BOUND = 'templateBound';
public const PHPDOC_TAG_TEMPLATE_DEFAULT = 'templateDefault';
Expand Down Expand Up @@ -255,6 +256,8 @@ public function createMessage(string $part): string
return sprintf('PHPDoc tag @phpstan-require-extends references %s.', $part);
case self::PHPDOC_TAG_REQUIRE_IMPLEMENTS:
return sprintf('PHPDoc tag @phpstan-require-implements references %s.', $part);
case self::PHPDOC_TAG_SEALED:
return sprintf('PHPDoc tag @phpstan-sealed references %s.', $part);
case self::STATIC_METHOD_CALL:
$method = $this->getMethod();
if ($method !== null) {
Expand Down
1 change: 1 addition & 0 deletions src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ final class InvalidPHPStanDocTagRule implements Rule
'@phpstan-readonly-allow-private-mutation',
'@phpstan-require-extends',
'@phpstan-require-implements',
'@phpstan-sealed',
'@phpstan-param-immediately-invoked-callable',
'@phpstan-param-later-invoked-callable',
'@phpstan-param-closure-this',
Expand Down
100 changes: 100 additions & 0 deletions src/Rules/PhpDoc/SealedDefinitionClassRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\PhpDoc;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Node\InClassNode;
use PHPStan\Rules\ClassNameCheck;
use PHPStan\Rules\ClassNameNodePair;
use PHPStan\Rules\ClassNameUsageLocation;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\VerbosityLevel;
use function array_column;
use function array_map;
use function array_merge;
use function count;
use function sprintf;

/**
* @implements Rule<InClassNode>
*/
#[RegisteredRule(level: 0)]
final class SealedDefinitionClassRule implements Rule
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be level 2. That's where PHPDocs are being checked.

{

public function __construct(
private ClassNameCheck $classCheck,
#[AutowiredParameter]
private bool $checkClassCaseSensitivity,
#[AutowiredParameter(ref: '%tips.discoveringSymbols%')]
private bool $discoveringSymbolsTip,
)
{
}

public function getNodeType(): string
{
return InClassNode::class;
}

public function processNode(Node $node, Scope $scope): array
{
$classReflection = $node->getClassReflection();
$sealedTags = $classReflection->getSealedTags();

if (count($sealedTags) === 0) {
return [];
}

if ($classReflection->isEnum()) {
return [
RuleErrorBuilder::message('PHPDoc tag @phpstan-sealed is only valid on class or interface.')
->identifier('sealed.onEnum')
->build(),
];
}

$errors = [];
foreach ($sealedTags as $sealedTag) {
$type = $sealedTag->getType();
$classNames = $type->getObjectClassNames();
if (count($classNames) === 0) {
$errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-sealed contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly())))
->identifier('sealed.nonObject')
->build();
continue;
}

$referencedClassReflections = array_map(static fn ($reflection) => [$reflection, $reflection->getName()], $type->getObjectClassReflections());
$referencedClassReflectionsMap = array_column($referencedClassReflections, 0, 1);
foreach ($classNames as $class) {
$referencedClassReflection = $referencedClassReflectionsMap[$class] ?? null;
if ($referencedClassReflection === null) {
$errorBuilder = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-sealed contains unknown class %s.', $class))
->identifier('class.notFound');

if ($this->discoveringSymbolsTip) {
$errorBuilder->discoveringSymbolsTip();
}

$errors[] = $errorBuilder->build();
continue;
}

$errors = array_merge(
$errors,
$this->classCheck->checkClassNames($scope, [
new ClassNameNodePair($class, $node),
], ClassNameUsageLocation::from(ClassNameUsageLocation::PHPDOC_TAG_SEALED), $this->checkClassCaseSensitivity),
);
}
}

return $errors;
}

}
54 changes: 54 additions & 0 deletions src/Rules/PhpDoc/SealedDefinitionTraitRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\PhpDoc;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use function count;

/**
* @implements Rule<Node\Stmt\Trait_>
*/
#[RegisteredRule(level: 0)]
final class SealedDefinitionTraitRule implements Rule
{

public function __construct(
private ReflectionProvider $reflectionProvider,
)
{
}

public function getNodeType(): string
{
return Node\Stmt\Trait_::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (
$node->namespacedName === null
|| !$this->reflectionProvider->hasClass($node->namespacedName->toString())
) {
return [];
}

$traitReflection = $this->reflectionProvider->getClass($node->namespacedName->toString());
$sealedTags = $traitReflection->getSealedTags();

if (count($sealedTags) === 0) {
return [];
}

return [
RuleErrorBuilder::message('PHPDoc tag @phpstan-sealed is only valid on class or interface.')
->identifier('sealed.onTrait')
->build(),
];
}

}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/class-name-usage-location.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use function PHPStan\Testing\assertType;

function (ClassNameUsageLocation $location): void {
assertType("'assert.test'|'attribute.test'|'catch.test'|'class.extendsTest'|'class.implementsTest'|'classConstant.test'|'enum.implementsTest'|'generics.testBound'|'generics.testDefault'|'instanceof.test'|'interface.extendsTest'|'methodTag.test'|'mixin.test'|'new.test'|'parameter.test'|'property.test'|'propertyTag.test'|'requireExtends.test'|'requireImplements.test'|'return.test'|'selfOut.test'|'staticMethod.test'|'staticProperty.test'|'traitUse.test'|'typeAlias.test'|'varTag.test'", $location->createIdentifier('test'));
assertType("'assert.test'|'attribute.test'|'catch.test'|'class.extendsTest'|'class.implementsTest'|'classConstant.test'|'enum.implementsTest'|'generics.testBound'|'generics.testDefault'|'instanceof.test'|'interface.extendsTest'|'methodTag.test'|'mixin.test'|'new.test'|'parameter.test'|'property.test'|'propertyTag.test'|'requireExtends.test'|'requireImplements.test'|'return.test'|'sealed.test'|'selfOut.test'|'staticMethod.test'|'staticProperty.test'|'traitUse.test'|'typeAlias.test'|'varTag.test'", $location->createIdentifier('test'));

if ($location->value === ClassNameUsageLocation::INSTANTIATION || $location->value === ClassNameUsageLocation::PROPERTY_TYPE) {
assertType("'new.test'|'property.test'", $location->createIdentifier('test'));
Expand Down
Loading
Loading