-
Notifications
You must be signed in to change notification settings - Fork 516
Add phpstan-sealed support #4095
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||
} | ||
|
||
} |
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; | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Classes; | ||
|
||
use PhpParser\Node; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\DependencyInjection\RegisteredRule; | ||
use PHPStan\Node\InClassNode; | ||
use PHPStan\Rules\Rule; | ||
use PHPStan\Rules\RuleErrorBuilder; | ||
use PHPStan\Type\ObjectType; | ||
use PHPStan\Type\VerbosityLevel; | ||
use function array_values; | ||
use function sprintf; | ||
|
||
/** | ||
* @implements Rule<InClassNode> | ||
*/ | ||
#[RegisteredRule(level: 0)] | ||
final class SealedRule implements Rule | ||
{ | ||
|
||
public function getNodeType(): string | ||
{ | ||
return InClassNode::class; | ||
} | ||
|
||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
$classReflection = $node->getClassReflection(); | ||
if ($classReflection->isEnum()) { | ||
return []; | ||
} | ||
|
||
$className = $classReflection->getName(); | ||
|
||
$parents = array_values($classReflection->getImmediateInterfaces()); | ||
$parentClass = $classReflection->getParentClass(); | ||
if ($parentClass !== null) { | ||
$parents[] = $parentClass; | ||
} | ||
|
||
$errors = []; | ||
foreach ($parents as $parent) { | ||
$sealedTags = $parent->getSealedTags(); | ||
foreach ($sealedTags as $sealedTag) { | ||
$type = $sealedTag->getType(); | ||
if ($type->isSuperTypeOf(new ObjectType($className))->yes()) { | ||
continue; | ||
} | ||
|
||
$errors[] = RuleErrorBuilder::message( | ||
sprintf( | ||
'%s %s is sealed and only permits %s as subtypes, %s given.', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of this rule, you should implement this extension https://apiref.phpstan.org/2.1.x/PHPStan.Reflection.AllowedSubTypesClassReflectionExtension.html. Which actually make this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With SealedAllowedSubTypesClassReflectionExtension implemented, this rule can be deleted. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My bad, dunno why I forget to remove it ; it's done. |
||
$parent->isInterface() ? 'Interface' : 'Class', | ||
$parent->getDisplayName(), | ||
$type->describe(VerbosityLevel::typeOnly()), | ||
$classReflection->getDisplayName(), | ||
), | ||
) | ||
->identifier('class.sealed') | ||
->build(); | ||
} | ||
} | ||
|
||
return $errors; | ||
} | ||
|
||
} |
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)] | ||
ondrejmirtes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
final class SealedDefinitionClassRule implements Rule | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
|
||
} |
Uh oh!
There was an error while loading. Please reload this page.