Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions conf/config.level0.neon
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ rules:
- PHPStan\Rules\Arrays\EmptyArrayItemRule
- PHPStan\Rules\Arrays\OffsetAccessWithoutDimForReadingRule
- PHPStan\Rules\Cast\UnsetCastRule
- PHPStan\Rules\Classes\AllowedSubTypesRule
- PHPStan\Rules\Classes\ClassAttributesRule
- PHPStan\Rules\Classes\ClassConstantAttributesRule
- PHPStan\Rules\Classes\ClassConstantRule
Expand Down
5 changes: 5 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,11 @@ services:
tags:
- phpstan.broker.methodsClassReflectionExtension

-
class: PHPStan\Reflection\Php\EnumAllowedSubTypesClassReflectionExtension
tags:
- phpstan.broker.allowedSubTypesClassReflectionExtension

-
class: PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension
tags:
Expand Down
4 changes: 2 additions & 2 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ parameters:
path: src/Analyser/MutatingScope.php

-
message: "#^Parameter \\#10 \\$reflection of class PHPStan\\\\Reflection\\\\ClassReflection constructor expects PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionClass\\|PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum, object given\\.$#"
message: "#^Parameter \\#11 \\$reflection of class PHPStan\\\\Reflection\\\\ClassReflection constructor expects PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionClass\\|PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum, object given\\.$#"
count: 1
path: src/Analyser/NodeScopeResolver.php

Expand Down Expand Up @@ -192,7 +192,7 @@ parameters:
path: src/Reflection/BetterReflection/BetterReflectionProvider.php

-
message: "#^Parameter \\#10 \\$reflection of class PHPStan\\\\Reflection\\\\ClassReflection constructor expects PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionClass\\|PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum, object given\\.$#"
message: "#^Parameter \\#11 \\$reflection of class PHPStan\\\\Reflection\\\\ClassReflection constructor expects PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionClass\\|PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum, object given\\.$#"
count: 1
path: src/Reflection/BetterReflection/BetterReflectionProvider.php

Expand Down
1 change: 1 addition & 0 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -1524,6 +1524,7 @@ private function createAstClassReflection(Node\Stmt\ClassLike $stmt, string $cla
$this->phpVersion,
$this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(),
$this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(),
$this->classReflectionExtensionRegistryProvider->getRegistry()->getAllowedSubTypesClassReflectionExtensions(),
$betterReflectionClass->getName(),
$betterReflectionClass instanceof ReflectionEnum && PHP_VERSION_ID >= 80000 ? new $enumAdapter($betterReflectionClass) : new ReflectionClass($betterReflectionClass),
null,
Expand Down
1 change: 1 addition & 0 deletions src/Broker/BrokerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class BrokerFactory

public const PROPERTIES_CLASS_REFLECTION_EXTENSION_TAG = 'phpstan.broker.propertiesClassReflectionExtension';
public const METHODS_CLASS_REFLECTION_EXTENSION_TAG = 'phpstan.broker.methodsClassReflectionExtension';
public const ALLOWED_SUB_TYPES_CLASS_REFLECTION_EXTENSION_TAG = 'phpstan.broker.allowedSubTypesClassReflectionExtension';
public const DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG = 'phpstan.broker.dynamicMethodReturnTypeExtension';
public const DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG = 'phpstan.broker.dynamicStaticMethodReturnTypeExtension';
public const DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG = 'phpstan.broker.dynamicFunctionReturnTypeExtension';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\DependencyInjection\Reflection;

use PHPStan\Broker\Broker;
use PHPStan\Reflection\AllowedSubTypesClassReflectionExtension;
use PHPStan\Reflection\ClassReflectionExtensionRegistry;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use PHPStan\Reflection\PropertiesClassReflectionExtension;
Expand All @@ -18,10 +19,12 @@ class DirectClassReflectionExtensionRegistryProvider implements ClassReflectionE
/**
* @param PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions
* @param MethodsClassReflectionExtension[] $methodsClassReflectionExtensions
* @param AllowedSubTypesClassReflectionExtension[] $allowedSubTypesClassReflectionExtensions
*/
public function __construct(
private array $propertiesClassReflectionExtensions,
private array $methodsClassReflectionExtensions,
private array $allowedSubTypesClassReflectionExtensions,
)
{
}
Expand All @@ -41,12 +44,18 @@ public function addMethodsClassReflectionExtension(MethodsClassReflectionExtensi
$this->methodsClassReflectionExtensions[] = $extension;
}

public function addAllowedSubTypesClassReflectionExtension(AllowedSubTypesClassReflectionExtension $extension): void
{
$this->allowedSubTypesClassReflectionExtensions[] = $extension;
}

public function getRegistry(): ClassReflectionExtensionRegistry
{
return new ClassReflectionExtensionRegistry(
$this->broker,
$this->propertiesClassReflectionExtensions,
$this->methodsClassReflectionExtensions,
$this->allowedSubTypesClassReflectionExtensions,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public function getRegistry(): ClassReflectionExtensionRegistry
$this->container->getByType(Broker::class),
array_merge([$phpClassReflectionExtension], $this->container->getServicesByTag(BrokerFactory::PROPERTIES_CLASS_REFLECTION_EXTENSION_TAG), [$annotationsPropertiesClassReflectionExtension]),
array_merge([$phpClassReflectionExtension], $this->container->getServicesByTag(BrokerFactory::METHODS_CLASS_REFLECTION_EXTENSION_TAG), [$annotationsMethodsClassReflectionExtension]),
$this->container->getServicesByTag(BrokerFactory::ALLOWED_SUB_TYPES_CLASS_REFLECTION_EXTENSION_TAG),
);
}

Expand Down
18 changes: 18 additions & 0 deletions src/Reflection/AllowedSubTypesClassReflectionExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php declare(strict_types = 1);

namespace PHPStan\Reflection;

use PHPStan\Type\Type;

/** @api */
interface AllowedSubTypesClassReflectionExtension
{

public function supports(ClassReflection $classReflection): bool;

/**
* @return array<Type>
*/
public function getAllowedSubTypes(ClassReflection $classReflection): array;

}
2 changes: 2 additions & 0 deletions src/Reflection/BetterReflection/BetterReflectionProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ public function getClass(string $className): ClassReflection
$this->phpVersion,
$this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(),
$this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(),
$this->classReflectionExtensionRegistryProvider->getRegistry()->getAllowedSubTypesClassReflectionExtensions(),
$reflectionClass->getName(),
$reflectionClass instanceof ReflectionEnum && PHP_VERSION_ID >= 80000 ? new $enumAdapter($reflectionClass) : new ReflectionClass($reflectionClass),
null,
Expand Down Expand Up @@ -211,6 +212,7 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $
$this->phpVersion,
$this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(),
$this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(),
$this->classReflectionExtensionRegistryProvider->getRegistry()->getAllowedSubTypesClassReflectionExtensions(),
sprintf('class@anonymous/%s:%s', $filename, $classNode->getLine()),
new ReflectionClass($reflectionClass),
$scopeFile,
Expand Down
17 changes: 17 additions & 0 deletions src/Reflection/ClassReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ class ClassReflection
/**
* @param PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions
* @param MethodsClassReflectionExtension[] $methodsClassReflectionExtensions
* @param AllowedSubTypesClassReflectionExtension[] $allowedSubTypesClassReflectionExtensions
*/
public function __construct(
private ReflectionProvider $reflectionProvider,
Expand All @@ -130,6 +131,7 @@ public function __construct(
private PhpVersion $phpVersion,
private array $propertiesClassReflectionExtensions,
private array $methodsClassReflectionExtensions,
private array $allowedSubTypesClassReflectionExtensions,
private string $displayName,
private ReflectionClass|ReflectionEnum $reflection,
private ?string $anonymousFilename,
Expand Down Expand Up @@ -1292,6 +1294,7 @@ public function withTypes(array $types): self
$this->phpVersion,
$this->propertiesClassReflectionExtensions,
$this->methodsClassReflectionExtensions,
$this->allowedSubTypesClassReflectionExtensions,
$this->displayName,
$this->reflection,
$this->anonymousFilename,
Expand Down Expand Up @@ -1491,4 +1494,18 @@ public function getResolvedMixinTypes(): array
return $types;
}

/**
* @return array<Type>|null
*/
public function getAllowedSubTypes(): ?array
{
foreach ($this->allowedSubTypesClassReflectionExtensions as $allowedSubTypesClassReflectionExtension) {
if ($allowedSubTypesClassReflectionExtension->supports($this)) {
return $allowedSubTypesClassReflectionExtension->getAllowedSubTypes($this);
}
}

return null;
}

}
12 changes: 11 additions & 1 deletion src/Reflection/ClassReflectionExtensionRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ class ClassReflectionExtensionRegistry
/**
* @param PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions
* @param MethodsClassReflectionExtension[] $methodsClassReflectionExtensions
* @param AllowedSubTypesClassReflectionExtension[] $allowedSubTypesClassReflectionExtensions
*/
public function __construct(
Broker $broker,
private array $propertiesClassReflectionExtensions,
private array $methodsClassReflectionExtensions,
private array $allowedSubTypesClassReflectionExtensions,
)
{
foreach (array_merge($propertiesClassReflectionExtensions, $methodsClassReflectionExtensions) as $extension) {
foreach (array_merge($propertiesClassReflectionExtensions, $methodsClassReflectionExtensions, $allowedSubTypesClassReflectionExtensions) as $extension) {
if (!($extension instanceof BrokerAwareExtension)) {
continue;
}
Expand All @@ -43,4 +45,12 @@ public function getMethodsClassReflectionExtensions(): array
return $this->methodsClassReflectionExtensions;
}

/**
* @return AllowedSubTypesClassReflectionExtension[]
*/
public function getAllowedSubTypesClassReflectionExtensions(): array
{
return $this->allowedSubTypesClassReflectionExtensions;
}

}
28 changes: 28 additions & 0 deletions src/Reflection/Php/EnumAllowedSubTypesClassReflectionExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php declare(strict_types = 1);

namespace PHPStan\Reflection\Php;

use PHPStan\Reflection\AllowedSubTypesClassReflectionExtension;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Type\Enum\EnumCaseObjectType;
use function array_keys;

class EnumAllowedSubTypesClassReflectionExtension implements AllowedSubTypesClassReflectionExtension
{

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

public function getAllowedSubTypes(ClassReflection $classReflection): array
{
$cases = [];
foreach (array_keys($classReflection->getEnumCases()) as $name) {
$cases[] = new EnumCaseObjectType($classReflection->getName(), $name);
}

return $cases;
}

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

namespace PHPStan\Rules\Classes;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\ObjectType;
use function array_values;
use function sprintf;

/**
* @implements Rule<InClassNode>
*/
class AllowedSubTypesRule 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.

This rule isn't registered. Level 0 would be fine 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops, added, thanks.

{

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

/**
* @param InClassNode $node
*/
public function processNode(Node $node, Scope $scope): array
{
$classReflection = $node->getClassReflection();
$className = $classReflection->getName();

$parents = array_values($classReflection->getImmediateInterfaces());
$parentClass = $classReflection->getParentClass();
if ($parentClass !== null) {
$parents[] = $parentClass;
}

$messages = [];

foreach ($parents as $parentReflection) {
$allowedSubTypes = $parentReflection->getAllowedSubTypes();
if ($allowedSubTypes === null) {
continue;
}

foreach ($allowedSubTypes as $allowedSubType) {
if (!$allowedSubType instanceof ObjectType) {
continue;
}

if ($className === $allowedSubType->getClassName()) {
continue 2;
}
}

$messages[] = RuleErrorBuilder::message(sprintf(
'Type %s is not allowed to be a subtype of %s.',
$className,
$parentReflection->getName(),
))->build();
}

return $messages;
}

}
Loading