diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 5ab7a9b38..d7ecd58b0 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -24,6 +24,7 @@ jobs: php-version: - "8.2" - "8.3" + - "8.4" operating-system: - "ubuntu-latest" - "windows-latest" @@ -67,6 +68,7 @@ jobs: php-version: - "8.2" - "8.3" + - "8.4" operating-system: - "ubuntu-latest" @@ -107,6 +109,7 @@ jobs: dependencies: - "locked" php-version: + # Throws deprecated errors on 8.4 - "8.3" operating-system: - "ubuntu-latest" @@ -148,6 +151,7 @@ jobs: dependencies: - "locked" php-version: + # Does not work on 8.4 - "8.3" operating-system: - "ubuntu-latest" @@ -192,7 +196,7 @@ jobs: dependencies: - "locked" php-version: - - "8.3" + - "8.4" operating-system: - "ubuntu-latest" - "windows-latest" @@ -234,7 +238,7 @@ jobs: dependencies: - "locked" php-version: - - "8.3" + - "8.4" operating-system: - "ubuntu-latest" @@ -268,7 +272,7 @@ jobs: dependencies: - "locked" php-version: - - "8.3" + - "8.4" operating-system: - "ubuntu-latest" - "windows-latest" @@ -347,7 +351,7 @@ jobs: dependencies: - "locked" php-version: - - "8.3" + - "8.4" operating-system: - "ubuntu-latest" diff --git a/composer.json b/composer.json index cec0cb8d0..7e4f742aa 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "Better Reflection - an improved code reflection API", "license": "MIT", "require": { - "php": "~8.2.0 || ~8.3.2", + "php": "~8.2.0 || ~8.3.2 || ~8.4.1", "ext-json": "*", "jetbrains/phpstorm-stubs": "2024.2", "nikic/php-parser": "^5.3.1" diff --git a/composer.lock b/composer.lock index 8d3a66ae3..e2c064551 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "51f69226dac86a1c51783cb2205bf72e", + "content-hash": "b4ed745f3892bdbce0cbfeb5e100340a", "packages": [ { "name": "jetbrains/phpstorm-stubs", @@ -3220,7 +3220,7 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "~8.2.0 || ~8.3.2", + "php": "~8.2.0 || ~8.3.2 || ~8.4.1", "ext-json": "*" }, "platform-dev": {}, diff --git a/phpstan.neon b/phpstan.neon index e94b5da0e..a269daf53 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -25,6 +25,10 @@ parameters: - phar://%currentWorkingDirectory%/test/unit/Fixture/autoload.phar/vendor/autoload.php ignoreErrors: + - + identifier: class.notFound + message: '#(unknown class|invalid type) PropertyHookType#' + - identifier: missingType.generics - diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 5785c6199..7796eec1a 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -144,11 +144,15 @@ + + + + @@ -201,14 +205,19 @@ + + + + + diff --git a/src/Reflection/Adapter/ReflectionClass.php b/src/Reflection/Adapter/ReflectionClass.php index 64bbe2ed5..6778e8511 100644 --- a/src/Reflection/Adapter/ReflectionClass.php +++ b/src/Reflection/Adapter/ReflectionClass.php @@ -33,6 +33,12 @@ */ final class ReflectionClass extends CoreReflectionClass { + /** @internal */ + public const SKIP_INITIALIZATION_ON_SERIALIZE_COMPATIBILITY = 8; + + /** @internal */ + public const SKIP_DESTRUCTOR_COMPATIBILITY = 16; + public function __construct(private BetterReflectionClass|BetterReflectionEnum $betterReflectionClass) { unset($this->name); @@ -450,6 +456,50 @@ public function newInstanceArgs(array|null $args = null): object throw new Exception\NotImplemented('Not implemented'); } + /** @param int-mask-of $options */ + public function newLazyGhost(callable $initializer, int $options = 0): object + { + throw new Exception\NotImplemented('Not implemented'); + } + + /** @param int-mask-of $options */ + public function newLazyProxy(callable $factory, int $options = 0): object + { + throw new Exception\NotImplemented('Not implemented'); + } + + public function markLazyObjectAsInitialized(object $object): object + { + throw new Exception\NotImplemented('Not implemented'); + } + + public function getLazyInitializer(object $object): callable|null + { + throw new Exception\NotImplemented('Not implemented'); + } + + public function initializeLazyObject(object $object): object + { + throw new Exception\NotImplemented('Not implemented'); + } + + public function isUninitializedLazyObject(object $object): bool + { + throw new Exception\NotImplemented('Not implemented'); + } + + /** @param int-mask-of $options */ + public function resetAsLazyGhost(object $object, callable $initializer, int $options = 0): void + { + throw new Exception\NotImplemented('Not implemented'); + } + + /** @param int-mask-of $options */ + public function resetAsLazyProxy(object $object, callable $factory, int $options = 0): void + { + throw new Exception\NotImplemented('Not implemented'); + } + /** @psalm-mutation-free */ public function getParentClass(): ReflectionClass|false { diff --git a/src/Reflection/Adapter/ReflectionClassConstant.php b/src/Reflection/Adapter/ReflectionClassConstant.php index 90432db64..3ddde192a 100644 --- a/src/Reflection/Adapter/ReflectionClassConstant.php +++ b/src/Reflection/Adapter/ReflectionClassConstant.php @@ -160,6 +160,11 @@ public function isEnumCase(): bool return $this->betterClassConstantOrEnumCase instanceof BetterReflectionEnumCase; } + public function isDeprecated(): bool + { + return $this->betterClassConstantOrEnumCase->isDeprecated(); + } + public function __get(string $name): mixed { if ($name === 'name') { diff --git a/src/Reflection/Adapter/ReflectionEnum.php b/src/Reflection/Adapter/ReflectionEnum.php index 4b475ec79..58bfb4fd4 100644 --- a/src/Reflection/Adapter/ReflectionEnum.php +++ b/src/Reflection/Adapter/ReflectionEnum.php @@ -376,6 +376,50 @@ public function newInstanceArgs(array|null $args = null): object throw new Exception\NotImplemented('Not implemented'); } + /** @param int-mask-of $options */ + public function newLazyGhost(callable $initializer, int $options = 0): object + { + throw new Exception\NotImplemented('Not implemented'); + } + + /** @param int-mask-of $options */ + public function newLazyProxy(callable $factory, int $options = 0): object + { + throw new Exception\NotImplemented('Not implemented'); + } + + public function markLazyObjectAsInitialized(object $object): object + { + throw new Exception\NotImplemented('Not implemented'); + } + + public function getLazyInitializer(object $object): callable|null + { + throw new Exception\NotImplemented('Not implemented'); + } + + public function initializeLazyObject(object $object): object + { + throw new Exception\NotImplemented('Not implemented'); + } + + public function isUninitializedLazyObject(object $object): bool + { + throw new Exception\NotImplemented('Not implemented'); + } + + /** @param int-mask-of $options */ + public function resetAsLazyGhost(object $object, callable $initializer, int $options = 0): void + { + throw new Exception\NotImplemented('Not implemented'); + } + + /** @param int-mask-of $options */ + public function resetAsLazyProxy(object $object, callable $factory, int $options = 0): void + { + throw new Exception\NotImplemented('Not implemented'); + } + public function getParentClass(): ReflectionClass|false { return false; diff --git a/src/Reflection/Adapter/ReflectionEnumBackedCase.php b/src/Reflection/Adapter/ReflectionEnumBackedCase.php index 47e3aebcd..f4391baec 100644 --- a/src/Reflection/Adapter/ReflectionEnumBackedCase.php +++ b/src/Reflection/Adapter/ReflectionEnumBackedCase.php @@ -129,6 +129,11 @@ public function getBackingValue(): int|string return $this->betterReflectionEnumCase->getValue(); } + public function isDeprecated(): bool + { + return $this->betterReflectionEnumCase->isDeprecated(); + } + public function __get(string $name): mixed { if ($name === 'name') { diff --git a/src/Reflection/Adapter/ReflectionEnumUnitCase.php b/src/Reflection/Adapter/ReflectionEnumUnitCase.php index f905812fc..1521a6273 100644 --- a/src/Reflection/Adapter/ReflectionEnumUnitCase.php +++ b/src/Reflection/Adapter/ReflectionEnumUnitCase.php @@ -124,6 +124,11 @@ public function getEnum(): ReflectionEnum return new ReflectionEnum($this->betterReflectionEnumCase->getDeclaringEnum()); } + public function isDeprecated(): bool + { + return $this->betterReflectionEnumCase->isDeprecated(); + } + public function __get(string $name): mixed { if ($name === 'name') { diff --git a/src/Reflection/Adapter/ReflectionNamedType.php b/src/Reflection/Adapter/ReflectionNamedType.php index 80d97e3de..b83111f5e 100644 --- a/src/Reflection/Adapter/ReflectionNamedType.php +++ b/src/Reflection/Adapter/ReflectionNamedType.php @@ -12,29 +12,48 @@ /** @psalm-immutable */ final class ReflectionNamedType extends CoreReflectionNamedType { - public function __construct(private BetterReflectionNamedType $betterReflectionType, private bool $allowsNull) + /** @var non-empty-string */ + private string $nameType; + + private bool $isBuiltin; + + /** @var non-empty-string */ + private string $toString; + + /** @param \Roave\BetterReflection\Reflection\ReflectionNamedType|non-empty-string $type */ + public function __construct(BetterReflectionNamedType|string $type, private bool $allowsNull) { + if ($type instanceof BetterReflectionNamedType) { + $nameType = $type->getName(); + $this->nameType = $nameType; + $this->isBuiltin = $this->computeIsBuiltin($nameType, $type->isBuiltin()); + $this->toString = $type->__toString(); + } else { + $this->nameType = $type; + $this->isBuiltin = true; + $this->toString = $type; + } } public function getName(): string { - return $this->betterReflectionType->getName(); + return $this->nameType; } /** @return non-empty-string */ public function __toString(): string { - $type = strtolower($this->betterReflectionType->getName()); + $normalizedType = strtolower($this->nameType); if ( ! $this->allowsNull - || $type === 'mixed' - || $type === 'null' + || $normalizedType === 'mixed' + || $normalizedType === 'null' ) { - return $this->betterReflectionType->__toString(); + return $this->toString; } - return '?' . $this->betterReflectionType->__toString(); + return '?' . $this->toString; } public function allowsNull(): bool @@ -44,12 +63,17 @@ public function allowsNull(): bool public function isBuiltin(): bool { - $type = strtolower($this->betterReflectionType->getName()); + return $this->isBuiltin; + } + + private function computeIsBuiltin(string $namedType, bool $isBuiltin): bool + { + $normalizedType = strtolower($namedType); - if ($type === 'self' || $type === 'parent' || $type === 'static') { + if ($normalizedType === 'self' || $normalizedType === 'parent' || $normalizedType === 'static') { return false; } - return $this->betterReflectionType->isBuiltin(); + return $isBuiltin; } } diff --git a/src/Reflection/Adapter/ReflectionObject.php b/src/Reflection/Adapter/ReflectionObject.php index 6221d013c..88f0efca4 100644 --- a/src/Reflection/Adapter/ReflectionObject.php +++ b/src/Reflection/Adapter/ReflectionObject.php @@ -396,6 +396,50 @@ public function newInstanceArgs(array|null $args = null): ReflectionObject throw new Exception\NotImplemented('Not implemented'); } + /** @param int-mask-of $options */ + public function newLazyGhost(callable $initializer, int $options = 0): object + { + throw new Exception\NotImplemented('Not implemented'); + } + + /** @param int-mask-of $options */ + public function newLazyProxy(callable $factory, int $options = 0): object + { + throw new Exception\NotImplemented('Not implemented'); + } + + public function markLazyObjectAsInitialized(object $object): object + { + throw new Exception\NotImplemented('Not implemented'); + } + + public function getLazyInitializer(object $object): callable|null + { + throw new Exception\NotImplemented('Not implemented'); + } + + public function initializeLazyObject(object $object): object + { + throw new Exception\NotImplemented('Not implemented'); + } + + public function isUninitializedLazyObject(object $object): bool + { + throw new Exception\NotImplemented('Not implemented'); + } + + /** @param int-mask-of $options */ + public function resetAsLazyGhost(object $object, callable $initializer, int $options = 0): void + { + throw new Exception\NotImplemented('Not implemented'); + } + + /** @param int-mask-of $options */ + public function resetAsLazyProxy(object $object, callable $factory, int $options = 0): void + { + throw new Exception\NotImplemented('Not implemented'); + } + /** @psalm-mutation-free */ public function getParentClass(): ReflectionClass|false { diff --git a/src/Reflection/Adapter/ReflectionProperty.php b/src/Reflection/Adapter/ReflectionProperty.php index 6f009056e..fe7112055 100644 --- a/src/Reflection/Adapter/ReflectionProperty.php +++ b/src/Reflection/Adapter/ReflectionProperty.php @@ -6,12 +6,16 @@ use ArgumentCountError; use OutOfBoundsException; +use PropertyHookType; use ReflectionException as CoreReflectionException; +use ReflectionMethod as CoreReflectionMethod; use ReflectionProperty as CoreReflectionProperty; use Roave\BetterReflection\Reflection\Exception\NoObjectProvided; use Roave\BetterReflection\Reflection\Exception\NotAnObject; use Roave\BetterReflection\Reflection\ReflectionAttribute as BetterReflectionAttribute; +use Roave\BetterReflection\Reflection\ReflectionMethod as BetterReflectionMethod; use Roave\BetterReflection\Reflection\ReflectionProperty as BetterReflectionProperty; +use Roave\BetterReflection\Reflection\ReflectionPropertyHookType as BetterReflectionPropertyHookType; use Throwable; use TypeError; use ValueError; @@ -23,6 +27,21 @@ /** @psalm-suppress PropertyNotSetInConstructor */ final class ReflectionProperty extends CoreReflectionProperty { + /** @internal */ + public const IS_FINAL_COMPATIBILITY = 32; + + /** @internal */ + public const IS_ABSTRACT_COMPATIBILITY = 64; + + /** @internal */ + public const IS_VIRTUAL_COMPATIBILITY = 512; + + /** @internal */ + public const IS_PROTECTED_SET_COMPATIBILITY = 2048; + + /** @internal */ + public const IS_PRIVATE_SET_COMPATIBILITY = 4096; + public function __construct(private BetterReflectionProperty $betterReflectionProperty) { unset($this->name); @@ -66,6 +85,21 @@ public function setValue(mixed $objectOrValue, mixed $value = null): void } } + public function setRawValueWithoutLazyInitialization(object $object, mixed $value): void + { + throw new Exception\NotImplemented('Not implemented'); + } + + public function isLazy(object $object): bool + { + throw new Exception\NotImplemented('Not implemented'); + } + + public function skipLazyInitialization(object $object): void + { + throw new Exception\NotImplemented('Not implemented'); + } + /** @psalm-mutation-free */ public function hasType(): bool { @@ -90,24 +124,54 @@ public function isPrivate(): bool return $this->betterReflectionProperty->isPrivate(); } + /** @psalm-mutation-free */ + public function isPrivateSet(): bool + { + return $this->betterReflectionProperty->isPrivateSet(); + } + /** @psalm-mutation-free */ public function isProtected(): bool { return $this->betterReflectionProperty->isProtected(); } + /** @psalm-mutation-free */ + public function isProtectedSet(): bool + { + return $this->betterReflectionProperty->isProtectedSet(); + } + /** @psalm-mutation-free */ public function isStatic(): bool { return $this->betterReflectionProperty->isStatic(); } + /** @psalm-mutation-free */ + public function isFinal(): bool + { + return $this->betterReflectionProperty->isFinal(); + } + + /** @psalm-mutation-free */ + public function isAbstract(): bool + { + return $this->betterReflectionProperty->isAbstract(); + } + /** @psalm-mutation-free */ public function isDefault(): bool { return $this->betterReflectionProperty->isDefault(); } + /** @psalm-mutation-free */ + public function isDynamic(): bool + { + return $this->betterReflectionProperty->isDynamic(); + } + /** @psalm-mutation-free */ public function getModifiers(): int { @@ -190,6 +254,71 @@ public function isReadOnly(): bool return $this->betterReflectionProperty->isReadOnly(); } + /** @psalm-mutation-free */ + public function isVirtual(): bool + { + return $this->betterReflectionProperty->isVirtual(); + } + + public function hasHooks(): bool + { + return $this->betterReflectionProperty->hasHooks(); + } + + /** @psalm-suppress UndefinedClass */ + public function hasHook(PropertyHookType $hookType): bool + { + return $this->betterReflectionProperty->hasHook(BetterReflectionPropertyHookType::fromCoreReflectionPropertyHookType($hookType)); + } + + /** @psalm-suppress UndefinedClass */ + public function getHook(PropertyHookType $hookType): ReflectionMethod|null + { + $hook = $this->betterReflectionProperty->getHook(BetterReflectionPropertyHookType::fromCoreReflectionPropertyHookType($hookType)); + if ($hook === null) { + return null; + } + + return new ReflectionMethod($hook); + } + + /** @return array{get?: ReflectionMethod, set?: ReflectionMethod} */ + public function getHooks(): array + { + return array_map( + static fn (BetterReflectionMethod $betterReflectionMethod): CoreReflectionMethod => new ReflectionMethod($betterReflectionMethod), + $this->betterReflectionProperty->getHooks(), + ); + } + + public function getSettableType(): ReflectionUnionType|ReflectionNamedType|ReflectionIntersectionType|null + { + $setHook = $this->betterReflectionProperty->getHook(BetterReflectionPropertyHookType::Set); + if ($setHook !== null) { + return ReflectionType::fromTypeOrNull($setHook->getParameters()[0]->getType()); + } + + if ($this->isVirtual()) { + return new ReflectionNamedType('never', false); + } + + return $this->getType(); + } + + public function getRawValue(object $object): mixed + { + throw new Exception\NotImplemented('Not implemented'); + } + + public function setRawValue(object $object, mixed $value): void + { + if ($this->hasHooks()) { + throw new Exception\NotImplemented('Not implemented'); + } + + $this->setValue($object, $value); + } + public function __get(string $name): mixed { if ($name === 'name') { diff --git a/src/Reflection/Deprecated/DeprecatedHelper.php b/src/Reflection/Deprecated/DeprecatedHelper.php new file mode 100644 index 000000000..85e57f022 --- /dev/null +++ b/src/Reflection/Deprecated/DeprecatedHelper.php @@ -0,0 +1,29 @@ +getAttributes(), 'Deprecated') !== []) { + return true; + } + + return AnnotationHelper::isDeprecated($reflection->getDocComment()); + } +} diff --git a/src/Reflection/ReflectionClass.php b/src/Reflection/ReflectionClass.php index 111a26b51..10939789a 100644 --- a/src/Reflection/ReflectionClass.php +++ b/src/Reflection/ReflectionClass.php @@ -21,8 +21,8 @@ use Roave\BetterReflection\Reflection\Adapter\ReflectionClassConstant as ReflectionClassConstantAdapter; use Roave\BetterReflection\Reflection\Adapter\ReflectionMethod as ReflectionMethodAdapter; use Roave\BetterReflection\Reflection\Adapter\ReflectionProperty as ReflectionPropertyAdapter; -use Roave\BetterReflection\Reflection\Annotation\AnnotationHelper; use Roave\BetterReflection\Reflection\Attribute\ReflectionAttributeHelper; +use Roave\BetterReflection\Reflection\Deprecated\DeprecatedHelper; use Roave\BetterReflection\Reflection\Exception\CircularReference; use Roave\BetterReflection\Reflection\Exception\ClassDoesNotExist; use Roave\BetterReflection\Reflection\Exception\NoObjectProvided; @@ -571,6 +571,7 @@ private function createImmediateMethods(ClassNode|InterfaceNode|TraitNode|EnumNo $reflector, $methodNode, $this->locatedSource, + $methodNode->name->name, $this->getNamespaceName(), $this, $this, @@ -596,6 +597,8 @@ private function addEnumMethods(EnumNode $node, array $methods): array { $internalLocatedSource = new InternalLocatedSource('', $this->getName(), 'Core'); $createMethod = function (string $name, array $params, Node\Identifier|Node\NullableType $returnType) use ($internalLocatedSource): ReflectionMethod { + assert($name !== ''); + /** @var array{flags: int, params: Node\Param[], returnType: Node\Identifier|Node\NullableType} $classMethodSubnodes */ $classMethodSubnodes = [ 'flags' => Modifiers::PUBLIC | Modifiers::STATIC, @@ -610,6 +613,7 @@ private function addEnumMethods(EnumNode $node, array $methods): array $classMethodSubnodes, ), $internalLocatedSource, + $name, $this->getNamespaceName(), $this, $this, @@ -1191,7 +1195,7 @@ public function isUserDefined(): bool public function isDeprecated(): bool { - return AnnotationHelper::isDeprecated($this->docComment); + return DeprecatedHelper::isDeprecated($this); } /** diff --git a/src/Reflection/ReflectionClassConstant.php b/src/Reflection/ReflectionClassConstant.php index 33b09acaf..5c1223f08 100644 --- a/src/Reflection/ReflectionClassConstant.php +++ b/src/Reflection/ReflectionClassConstant.php @@ -11,8 +11,8 @@ use Roave\BetterReflection\NodeCompiler\CompileNodeToValue; use Roave\BetterReflection\NodeCompiler\CompilerContext; use Roave\BetterReflection\Reflection\Adapter\ReflectionClassConstant as ReflectionClassConstantAdapter; -use Roave\BetterReflection\Reflection\Annotation\AnnotationHelper; use Roave\BetterReflection\Reflection\Attribute\ReflectionAttributeHelper; +use Roave\BetterReflection\Reflection\Deprecated\DeprecatedHelper; use Roave\BetterReflection\Reflection\StringCast\ReflectionClassConstantStringCast; use Roave\BetterReflection\Reflector\Reflector; use Roave\BetterReflection\Util\CalculateReflectionColumn; @@ -265,7 +265,7 @@ public function getDocComment(): string|null public function isDeprecated(): bool { - return AnnotationHelper::isDeprecated($this->getDocComment()); + return DeprecatedHelper::isDeprecated($this); } /** @return non-empty-string */ diff --git a/src/Reflection/ReflectionEnumCase.php b/src/Reflection/ReflectionEnumCase.php index 1b45d139a..9bd1ac361 100644 --- a/src/Reflection/ReflectionEnumCase.php +++ b/src/Reflection/ReflectionEnumCase.php @@ -10,8 +10,8 @@ use Roave\BetterReflection\NodeCompiler\CompiledValue; use Roave\BetterReflection\NodeCompiler\CompileNodeToValue; use Roave\BetterReflection\NodeCompiler\CompilerContext; -use Roave\BetterReflection\Reflection\Annotation\AnnotationHelper; use Roave\BetterReflection\Reflection\Attribute\ReflectionAttributeHelper; +use Roave\BetterReflection\Reflection\Deprecated\DeprecatedHelper; use Roave\BetterReflection\Reflection\StringCast\ReflectionEnumCaseStringCast; use Roave\BetterReflection\Reflector\Reflector; use Roave\BetterReflection\Util\CalculateReflectionColumn; @@ -172,7 +172,7 @@ public function getDocComment(): string|null public function isDeprecated(): bool { - return AnnotationHelper::isDeprecated($this->docComment); + return DeprecatedHelper::isDeprecated($this); } /** @return list */ diff --git a/src/Reflection/ReflectionFunctionAbstract.php b/src/Reflection/ReflectionFunctionAbstract.php index ddfe5e68c..bb44a14e0 100644 --- a/src/Reflection/ReflectionFunctionAbstract.php +++ b/src/Reflection/ReflectionFunctionAbstract.php @@ -13,6 +13,7 @@ use PhpParser\NodeVisitor\FindingVisitor; use Roave\BetterReflection\Reflection\Annotation\AnnotationHelper; use Roave\BetterReflection\Reflection\Attribute\ReflectionAttributeHelper; +use Roave\BetterReflection\Reflection\Deprecated\DeprecatedHelper; use Roave\BetterReflection\Reflection\Exception\CodeLocationMissing; use Roave\BetterReflection\SourceLocator\Located\LocatedSource; use Roave\BetterReflection\Util\CalculateReflectionColumn; @@ -97,7 +98,7 @@ abstract public function __toString(): string; abstract public function getShortName(): string; /** @psalm-external-mutation-free */ - private function fillFromNode(MethodNode|Node\Stmt\Function_|Node\Expr\Closure|Node\Expr\ArrowFunction $node): void + private function fillFromNode(MethodNode|Node\PropertyHook|Node\Stmt\Function_|Node\Expr\Closure|Node\Expr\ArrowFunction $node): void { $this->parameters = $this->createParameters($node); $this->returnsReference = $node->returnsByRef(); @@ -135,7 +136,7 @@ private function fillFromNode(MethodNode|Node\Stmt\Function_|Node\Expr\Closure|N } /** @return array */ - private function createParameters(Node\Stmt\ClassMethod|Node\Stmt\Function_|Node\Expr\Closure|Node\Expr\ArrowFunction $node): array + private function createParameters(Node\Stmt\ClassMethod|Node\PropertyHook|Node\Stmt\Function_|Node\Expr\Closure|Node\Expr\ArrowFunction $node): array { $parameters = []; @@ -283,7 +284,7 @@ public function isClosure(): bool public function isDeprecated(): bool { - return AnnotationHelper::isDeprecated($this->docComment); + return DeprecatedHelper::isDeprecated($this); } public function isInternal(): bool @@ -326,7 +327,7 @@ public function couldThrow(): bool return $this->couldThrow; } - private function computeCouldThrow(MethodNode|Node\Stmt\Function_|Node\Expr\Closure|Node\Expr\ArrowFunction $node): bool + private function computeCouldThrow(MethodNode|Node\PropertyHook|Node\Stmt\Function_|Node\Expr\Closure|Node\Expr\ArrowFunction $node): bool { $statements = $node->getStmts(); @@ -497,7 +498,7 @@ public function getTentativeReturnType(): ReflectionNamedType|ReflectionUnionTyp return $this->returnType; } - private function createReturnType(MethodNode|Node\Stmt\Function_|Node\Expr\Closure|Node\Expr\ArrowFunction $node): ReflectionNamedType|ReflectionUnionType|ReflectionIntersectionType|null + private function createReturnType(MethodNode|Node\PropertyHook|Node\Stmt\Function_|Node\Expr\Closure|Node\Expr\ArrowFunction $node): ReflectionNamedType|ReflectionUnionType|ReflectionIntersectionType|null { $returnType = $node->getReturnType(); diff --git a/src/Reflection/ReflectionMethod.php b/src/Reflection/ReflectionMethod.php index 19ac618ed..dd2259cdb 100644 --- a/src/Reflection/ReflectionMethod.php +++ b/src/Reflection/ReflectionMethod.php @@ -34,23 +34,25 @@ class ReflectionMethod private int $modifiers; /** + * @param non-empty-string $name * @param non-empty-string|null $aliasName * @param non-empty-string|null $namespace */ private function __construct( private Reflector $reflector, - MethodNode|Node\Stmt\Function_|Node\Expr\Closure|Node\Expr\ArrowFunction $node, + MethodNode|Node\PropertyHook|Node\Stmt\Function_|Node\Expr\Closure|Node\Expr\ArrowFunction $node, private LocatedSource $locatedSource, + string $name, private string|null $namespace, private ReflectionClass $declaringClass, private ReflectionClass $implementingClass, private ReflectionClass $currentClass, private string|null $aliasName, ) { - assert($node instanceof MethodNode); + assert($node instanceof MethodNode || $node instanceof Node\PropertyHook); - $this->name = $node->name->name; - $this->modifiers = $this->computeModifiers($node); + $this->name = $name; + $this->modifiers = $node instanceof MethodNode ? $this->computeModifiers($node) : 0; $this->fillFromNode($node); } @@ -58,13 +60,15 @@ private function __construct( /** * @internal * + * @param non-empty-string $name * @param non-empty-string|null $aliasName * @param non-empty-string|null $namespace */ public static function createFromNode( Reflector $reflector, - MethodNode $node, + MethodNode|Node\PropertyHook $node, LocatedSource $locatedSource, + string $name, string|null $namespace, ReflectionClass $declaringClass, ReflectionClass $implementingClass, @@ -75,6 +79,7 @@ public static function createFromNode( $reflector, $node, $locatedSource, + $name, $namespace, $declaringClass, $implementingClass, diff --git a/src/Reflection/ReflectionProperty.php b/src/Reflection/ReflectionProperty.php index dd56af161..28e9ff110 100644 --- a/src/Reflection/ReflectionProperty.php +++ b/src/Reflection/ReflectionProperty.php @@ -7,16 +7,19 @@ use Closure; use Error; use OutOfBoundsException; +use PhpParser\Modifiers; use PhpParser\Node; use PhpParser\Node\Stmt\Property as PropertyNode; +use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor\FindingVisitor; use ReflectionException; use ReflectionProperty as CoreReflectionProperty; use Roave\BetterReflection\NodeCompiler\CompiledValue; use Roave\BetterReflection\NodeCompiler\CompileNodeToValue; use Roave\BetterReflection\NodeCompiler\CompilerContext; use Roave\BetterReflection\Reflection\Adapter\ReflectionProperty as ReflectionPropertyAdapter; -use Roave\BetterReflection\Reflection\Annotation\AnnotationHelper; use Roave\BetterReflection\Reflection\Attribute\ReflectionAttributeHelper; +use Roave\BetterReflection\Reflection\Deprecated\DeprecatedHelper; use Roave\BetterReflection\Reflection\Exception\ClassDoesNotExist; use Roave\BetterReflection\Reflection\Exception\CodeLocationMissing; use Roave\BetterReflection\Reflection\Exception\NoObjectProvided; @@ -68,6 +71,20 @@ class ReflectionProperty /** @var positive-int|null */ private int|null $endColumn; + private bool $immediateVirtual; + + /** @var array{get?: ReflectionMethod, set?: ReflectionMethod} */ + private array $immediateHooks; + + /** + * @var array{get?: ReflectionMethod, set?: ReflectionMethod}|null + * @psalm-allow-private-mutation + */ + private array|null $cachedHooks = null; + + /** @psalm-allow-private-mutation */ + private bool|null $cachedVirtual = null; + /** @psalm-allow-private-mutation */ private CompiledValue|null $compiledDefaultValue = null; @@ -80,12 +97,14 @@ private function __construct( private bool $isPromoted, private bool $declaredAtCompileTime, ) { - $this->name = $propertyNode->name->name; - $this->modifiers = $this->computeModifiers($node); - $this->type = $this->createType($node); - $this->default = $propertyNode->default; - $this->docComment = GetLastDocComment::forNode($node); - $this->attributes = ReflectionAttributeHelper::createAttributes($reflector, $this, $node->attrGroups); + $this->name = $propertyNode->name->name; + $this->modifiers = $this->computeModifiers($node); + $this->type = $this->createType($node); + $this->default = $propertyNode->default; + $this->docComment = GetLastDocComment::forNode($node); + $this->attributes = ReflectionAttributeHelper::createAttributes($reflector, $this, $node->attrGroups); + $this->immediateVirtual = $this->computeImmediateVirtual($node); + $this->immediateHooks = $this->createImmediateHooks($node); $startLine = $node->getStartLine(); if ($startLine === -1) { @@ -213,6 +232,11 @@ public function isDefault(): bool return $this->declaredAtCompileTime; } + public function isDynamic(): bool + { + return ! $this->isDefault(); + } + /** * Get the core-reflection-compatible modifier values. * @@ -220,7 +244,11 @@ public function isDefault(): bool */ public function getModifiers(): int { - return $this->modifiers; + /** @var int-mask-of $modifiers */ + $modifiers = $this->modifiers + + ($this->isVirtual() ? ReflectionPropertyAdapter::IS_VIRTUAL_COMPATIBILITY : 0); + + return $modifiers; } /** @@ -241,6 +269,11 @@ public function isPrivate(): bool return ($this->modifiers & CoreReflectionProperty::IS_PRIVATE) === CoreReflectionProperty::IS_PRIVATE; } + public function isPrivateSet(): bool + { + return ($this->modifiers & ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY) === ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY; + } + /** * Is the property protected? */ @@ -249,6 +282,11 @@ public function isProtected(): bool return ($this->modifiers & CoreReflectionProperty::IS_PROTECTED) === CoreReflectionProperty::IS_PROTECTED; } + public function isProtectedSet(): bool + { + return ($this->modifiers & ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY) === ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY; + } + /** * Is the property public? */ @@ -265,6 +303,16 @@ public function isStatic(): bool return ($this->modifiers & CoreReflectionProperty::IS_STATIC) === CoreReflectionProperty::IS_STATIC; } + public function isFinal(): bool + { + return ($this->modifiers & ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY) === ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY; + } + + public function isAbstract(): bool + { + return ($this->modifiers & ReflectionPropertyAdapter::IS_ABSTRACT_COMPATIBILITY) === ReflectionPropertyAdapter::IS_ABSTRACT_COMPATIBILITY; + } + public function isPromoted(): bool { return $this->isPromoted; @@ -353,7 +401,7 @@ public function getDefaultValue(): string|int|float|bool|array|null public function isDeprecated(): bool { - return AnnotationHelper::isDeprecated($this->getDocComment()); + return DeprecatedHelper::isDeprecated($this); } /** @@ -548,6 +596,36 @@ public function hasType(): bool return $this->type !== null; } + public function isVirtual(): bool + { + $this->cachedVirtual ??= $this->createCachedVirtual(); + + return $this->cachedVirtual; + } + + public function hasHooks(): bool + { + return $this->getHooks() !== []; + } + + public function hasHook(ReflectionPropertyHookType $hookType): bool + { + return isset($this->getHooks()[$hookType->value]); + } + + public function getHook(ReflectionPropertyHookType $hookType): ReflectionMethod|null + { + return $this->getHooks()[$hookType->value] ?? null; + } + + /** @return array{get?: ReflectionMethod, set?: ReflectionMethod} */ + public function getHooks(): array + { + $this->cachedHooks ??= $this->createCachedHooks(); + + return $this->cachedHooks; + } + /** * @param class-string $className * @@ -592,9 +670,118 @@ private function computeModifiers(PropertyNode $node): int $modifiers = $node->isReadonly() ? ReflectionPropertyAdapter::IS_READONLY : 0; $modifiers += $node->isStatic() ? CoreReflectionProperty::IS_STATIC : 0; $modifiers += $node->isPrivate() ? CoreReflectionProperty::IS_PRIVATE : 0; + $modifiers += $node->isPrivateSet() ? ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY : 0; $modifiers += $node->isProtected() ? CoreReflectionProperty::IS_PROTECTED : 0; + $modifiers += $node->isProtectedSet() ? ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY : 0; $modifiers += $node->isPublic() ? CoreReflectionProperty::IS_PUBLIC : 0; + $modifiers += ($node->flags & ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY) === ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY ? ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY : 0; + $modifiers += ($node->flags & Modifiers::ABSTRACT) === Modifiers::ABSTRACT ? ReflectionPropertyAdapter::IS_ABSTRACT_COMPATIBILITY : 0; + /** @phpstan-ignore return.type */ return $modifiers; } + + private function computeImmediateVirtual(PropertyNode $node): bool + { + if ($node->hooks === []) { + return false; + } + + $setHook = null; + + foreach ($node->hooks as $hook) { + if ($hook->name->name === 'set') { + $setHook = $hook; + break; + } + } + + if ($setHook === null) { + return true; + } + + $setHookBody = $setHook->getStmts(); + assert($setHookBody !== null); + + $visitor = new FindingVisitor(static fn (Node $node): bool => $node instanceof Node\Expr\Assign); + $traverser = new NodeTraverser($visitor); + $traverser->traverse($setHookBody); + + foreach ($visitor->getFoundNodes() as $assigNode) { + assert($assigNode instanceof Node\Expr\Assign); + $variableToAssign = $assigNode->var; + + if ( + $variableToAssign instanceof Node\Expr\PropertyFetch + && $variableToAssign->var instanceof Node\Expr\Variable + && $variableToAssign->var->name === 'this' + && $variableToAssign->name instanceof Node\Identifier + && $variableToAssign->name->name === $this->name + ) { + return false; + } + } + + return true; + } + + /** @return array{get?: ReflectionMethod, set?: ReflectionMethod} */ + private function createImmediateHooks(PropertyNode $node): array + { + $hooks = []; + + foreach ($node->hooks as $hook) { + $hookName = $hook->name->name; + assert($hookName === 'get' || $hookName === 'set'); + + $hooks[$hookName] = ReflectionMethod::createFromNode( + $this->reflector, + $hook, + $this->getDeclaringClass()->getLocatedSource(), + sprintf('$%s::%s', $this->name, $hookName), + null, + $this->getDeclaringClass(), + $this->getImplementingClass(), + $this->getDeclaringClass(), + ); + } + + return $hooks; + } + + private function createCachedVirtual(): bool + { + if (! $this->immediateVirtual) { + return false; + } + + return $this->getParentProperty()?->isVirtual() ?? $this->immediateVirtual; + } + + /** @return array{get?: ReflectionMethod, set?: ReflectionMethod} */ + private function createCachedHooks(): array + { + $hooks = $this->immediateHooks; + + if (isset($hooks['get'], $hooks['set'])) { + return $hooks; + } + + $parentHooks = $this->getParentProperty()?->getHooks() ?? []; + + foreach ($parentHooks as $hookName => $parentHook) { + if (isset($hooks[$hookName])) { + continue; + } + + $hooks[$hookName] = $parentHook; + } + + return $hooks; + } + + private function getParentProperty(): ReflectionProperty|null + { + return $this->getDeclaringClass()->getParentClass()?->getProperty($this->name); + } } diff --git a/src/Reflection/ReflectionPropertyHookType.php b/src/Reflection/ReflectionPropertyHookType.php new file mode 100644 index 000000000..856ec9de2 --- /dev/null +++ b/src/Reflection/ReflectionPropertyHookType.php @@ -0,0 +1,23 @@ + self::Get, + CoreReflectionPropertyHookType::Set => self::Set, + }; + } +} diff --git a/src/SourceLocator/Located/InternalLocatedSource.php b/src/SourceLocator/Located/InternalLocatedSource.php index 2018697ca..5ad730585 100644 --- a/src/SourceLocator/Located/InternalLocatedSource.php +++ b/src/SourceLocator/Located/InternalLocatedSource.php @@ -12,7 +12,7 @@ class InternalLocatedSource extends LocatedSource { /** @param non-empty-string $extensionName */ - public function __construct(string $source, string $name, private string $extensionName) + public function __construct(string $source, string $name, private string $extensionName, private string|null $aliasName = null) { parent::__construct($source, $name); } @@ -27,4 +27,9 @@ public function getExtensionName(): string|null { return $this->extensionName; } + + public function getAliasName(): string|null + { + return $this->aliasName; + } } diff --git a/src/SourceLocator/Type/PhpInternalSourceLocator.php b/src/SourceLocator/Type/PhpInternalSourceLocator.php index c789685ac..0fc6a41f5 100644 --- a/src/SourceLocator/Type/PhpInternalSourceLocator.php +++ b/src/SourceLocator/Type/PhpInternalSourceLocator.php @@ -5,6 +5,7 @@ namespace Roave\BetterReflection\SourceLocator\Type; use InvalidArgumentException; +use ReflectionClass as CoreReflectionClass; use Roave\BetterReflection\Identifier\Identifier; use Roave\BetterReflection\SourceLocator\Ast\Locator; use Roave\BetterReflection\SourceLocator\Exception\InvalidFileLocation; @@ -13,6 +14,9 @@ use Roave\BetterReflection\SourceLocator\SourceStubber\SourceStubber; use Roave\BetterReflection\SourceLocator\SourceStubber\StubData; +use function class_exists; +use function strtolower; + final class PhpInternalSourceLocator extends AbstractSourceLocator { public function __construct(Locator $astLocator, private SourceStubber $stubber) @@ -41,8 +45,19 @@ private function getClassSource(Identifier $identifier): InternalLocatedSource|n /** @psalm-var class-string|trait-string $className */ $className = $identifier->getName(); + $aliasName = null; + + if (class_exists($className, false)) { + $reflectionClass = new CoreReflectionClass($className); + + if (strtolower($reflectionClass->getName()) !== strtolower($className)) { + $aliasName = $className; + $className = $reflectionClass->getName(); + $identifier = new Identifier($className, $identifier->getType()); + } + } - return $this->createLocatedSourceFromStubData($identifier, $this->stubber->generateClassStub($className)); + return $this->createLocatedSourceFromStubData($identifier, $this->stubber->generateClassStub($className), $aliasName); } private function getFunctionSource(Identifier $identifier): InternalLocatedSource|null @@ -63,7 +78,7 @@ private function getConstantSource(Identifier $identifier): InternalLocatedSourc return $this->createLocatedSourceFromStubData($identifier, $this->stubber->generateConstantStub($identifier->getName())); } - private function createLocatedSourceFromStubData(Identifier $identifier, StubData|null $stubData): InternalLocatedSource|null + private function createLocatedSourceFromStubData(Identifier $identifier, StubData|null $stubData, string|null $aliasName = null): InternalLocatedSource|null { if ($stubData === null) { return null; @@ -80,6 +95,7 @@ private function createLocatedSourceFromStubData(Identifier $identifier, StubDat $stubData->getStub(), $identifier->getName(), $extensionName, + $aliasName, ); } } diff --git a/test/unit/Fixture/AsymetricVisibilityClass.php b/test/unit/Fixture/AsymetricVisibilityClass.php new file mode 100644 index 000000000..4e7bb1a0b --- /dev/null +++ b/test/unit/Fixture/AsymetricVisibilityClass.php @@ -0,0 +1,25 @@ + class Roave\BetterReflectionTest\Fixture\AsymetricVisibilityClass ] { + @@ %s/Fixture/AsymetricVisibilityClass.php 5-24 + + - Constants [0] { + } + + - Static properties [0] { + } + + - Static methods [0] { + } + + - Properties [12] { + Property [ public string $publicPublicSet ] + Property [ public string $publicProtectedSet ] + Property [ public string $publicPrivateSet ] + Property [ protected int $protectedProtectedSet ] + Property [ protected int $protectedPrivateSet ] + Property [ private bool $privatePrivateSet ] + Property [ public string $promotedPublicPublicSet ] + Property [ public string $promotedPublicProtectedSet ] + Property [ public string $promotedPublicPrivateSet ] + Property [ protected int $promotedProtectedProtectedSet ] + Property [ protected int $promotedProtectedPrivateSet ] + Property [ private bool $promotedPrivatePrivateSet ] + } + + - Methods [1] { + Method [ public method __construct ] { + @@ %s/Fixture/AsymetricVisibilityClass.php 14 - 23 + + - Parameters [6] { + Parameter #0 [ string $promotedPublicPublicSet ] + Parameter #1 [ string $promotedPublicProtectedSet ] + Parameter #2 [ string $promotedPublicPrivateSet ] + Parameter #3 [ int $promotedProtectedProtectedSet ] + Parameter #4 [ int $promotedProtectedPrivateSet ] + Parameter #5 [ bool $promotedPrivatePrivateSet ] + } + } + } +} diff --git a/test/unit/Fixture/ExampleClass.php b/test/unit/Fixture/ExampleClass.php index 3178c6653..5db6c8717 100644 --- a/test/unit/Fixture/ExampleClass.php +++ b/test/unit/Fixture/ExampleClass.php @@ -43,6 +43,8 @@ class ExampleClass public readonly int $readOnlyProperty; + final public int $finalPublicProperty = 123; + public static $publicStaticProperty; protected static $protectedStaticProperty; diff --git a/test/unit/Fixture/ExampleClassExport.txt b/test/unit/Fixture/ExampleClassExport.txt index c5bd4a3e4..236a1f251 100644 --- a/test/unit/Fixture/ExampleClassExport.txt +++ b/test/unit/Fixture/ExampleClassExport.txt @@ -1,5 +1,5 @@ Class [ class Roave\BetterReflectionTest\Fixture\ExampleClass ] { - @@ %s/test/unit/Fixture/ExampleClass.php 10-59 + @@ %s/test/unit/Fixture/ExampleClass.php 10-61 - Constants [8] { Constant [ public integer MY_CONST_1 ] { 123 } @@ -21,18 +21,19 @@ Class [ class Roave\BetterReflectionTest\Fixture\ExampleClass ] { - Static methods [0] { } - - Properties [6] { + - Properties [7] { Property [ private $privateProperty ] Property [ protected $protectedProperty ] Property [ public $publicProperty ] Property [ public readonly int $readOnlyProperty ] + Property [ public int $finalPublicProperty ] Property [ private ?int $promotedProperty ] Property [ private $promotedProperty2 ] } - Methods [2] { Method [ public method __construct ] { - @@ %s/test/unit/Fixture/ExampleClass.php 52 - 54 + @@ %s/test/unit/Fixture/ExampleClass.php 54 - 56 - Parameters [3] { Parameter #0 [ ?int $promotedProperty = 123 ] @@ -42,7 +43,7 @@ Class [ class Roave\BetterReflectionTest\Fixture\ExampleClass ] { } Method [ public method someMethod ] { - @@ %s/test/unit/Fixture/ExampleClass.php 56 - 58 + @@ %s/test/unit/Fixture/ExampleClass.php 58 - 60 } } } diff --git a/test/unit/Fixture/PropertyHooks.php b/test/unit/Fixture/PropertyHooks.php new file mode 100644 index 000000000..0729b3e4e --- /dev/null +++ b/test/unit/Fixture/PropertyHooks.php @@ -0,0 +1,72 @@ +writeOnlyHook = $value; + } + } + + public string $readAndWriteHook { + get { + $this->readAndWriteHook; + } + set (string $value) { + $this->readAndWriteHook = $value; + } + } + + public string $virtualBecauseOfStrangeSetHook { + set (string $value) { + $this->differentProperty = $value; + } + } +} + +abstract class AbstractPropertyHooks +{ + abstract public string $hook { get; } +} + +class GetPropertyHook extends AbstractPropertyHooks +{ + public string $hook { + get { + return 'hook'; + } + } +} + +class GetAndSetPropertyHook extends GetPropertyHook +{ + public string $hook { + set (string $value) { + $this->hook = $value; + } + } +} + +trait PropertyHookTrait +{ + public string $hook { + get { + return 'hook'; + } + } +} + +class UsePropertyHookFromTrait +{ + use PropertyHookTrait; +} diff --git a/test/unit/Reflection/Adapter/ReflectionClassConstantTest.php b/test/unit/Reflection/Adapter/ReflectionClassConstantTest.php index 5a09ec314..e83487f6a 100644 --- a/test/unit/Reflection/Adapter/ReflectionClassConstantTest.php +++ b/test/unit/Reflection/Adapter/ReflectionClassConstantTest.php @@ -61,6 +61,7 @@ public static function methodExpectationProvider(): array ['getDocComment', null, null, []], ['getAttributes', null, [], []], ['isFinal', null, true, []], + ['isDeprecated', null, true, []], ]; } diff --git a/test/unit/Reflection/Adapter/ReflectionClassTest.php b/test/unit/Reflection/Adapter/ReflectionClassTest.php index 75e0f2db9..08abadc02 100644 --- a/test/unit/Reflection/Adapter/ReflectionClassTest.php +++ b/test/unit/Reflection/Adapter/ReflectionClassTest.php @@ -1286,4 +1286,92 @@ public function testGetTraits(): void self::assertArrayHasKey($traitOneClassName, $traits); self::assertArrayHasKey($traitTwoClassName, $traits); } + + public function testNewLazyGhost(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionClass = $this->createMock(BetterReflectionClass::class); + + $reflectionClassAdapter = new ReflectionClassAdapter($betterReflectionClass); + $reflectionClassAdapter->newLazyGhost(static fn () => null); + } + + public function testNewLazyProxy(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionClass = $this->createMock(BetterReflectionClass::class); + + $reflectionClassAdapter = new ReflectionClassAdapter($betterReflectionClass); + $reflectionClassAdapter->newLazyProxy(static fn () => null); + } + + public function testMarkLazyObjectAsInitialized(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionClass = $this->createMock(BetterReflectionClass::class); + + $reflectionClassAdapter = new ReflectionClassAdapter($betterReflectionClass); + $reflectionClassAdapter->markLazyObjectAsInitialized(new stdClass()); + } + + public function testGetLazyInitializer(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionClass = $this->createMock(BetterReflectionClass::class); + + $reflectionClassAdapter = new ReflectionClassAdapter($betterReflectionClass); + $reflectionClassAdapter->getLazyInitializer(new stdClass()); + } + + public function testInitializeLazyObject(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionClass = $this->createMock(BetterReflectionClass::class); + + $reflectionClassAdapter = new ReflectionClassAdapter($betterReflectionClass); + $reflectionClassAdapter->initializeLazyObject(new stdClass()); + } + + public function testIsUninitializedLazyObject(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionClass = $this->createMock(BetterReflectionClass::class); + + $reflectionClassAdapter = new ReflectionClassAdapter($betterReflectionClass); + $reflectionClassAdapter->isUninitializedLazyObject(new stdClass()); + } + + public function testResetAsLazyGhost(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionClass = $this->createMock(BetterReflectionClass::class); + + $reflectionClassAdapter = new ReflectionClassAdapter($betterReflectionClass); + $reflectionClassAdapter->resetAsLazyGhost(new stdClass(), static fn () => null); + } + + public function testResetAsLazyProxy(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionClass = $this->createMock(BetterReflectionClass::class); + + $reflectionClassAdapter = new ReflectionClassAdapter($betterReflectionClass); + $reflectionClassAdapter->resetAsLazyProxy(new stdClass(), static fn () => null); + } } diff --git a/test/unit/Reflection/Adapter/ReflectionEnumBackedCaseTest.php b/test/unit/Reflection/Adapter/ReflectionEnumBackedCaseTest.php index deaab9481..620342a81 100644 --- a/test/unit/Reflection/Adapter/ReflectionEnumBackedCaseTest.php +++ b/test/unit/Reflection/Adapter/ReflectionEnumBackedCaseTest.php @@ -54,6 +54,7 @@ public static function methodExpectationProvider(): array ['getValue', NotImplemented::class, null, []], ['getDocComment', null, null, []], ['getAttributes', null, [], []], + ['isDeprecated', null, true, []], ]; } diff --git a/test/unit/Reflection/Adapter/ReflectionEnumTest.php b/test/unit/Reflection/Adapter/ReflectionEnumTest.php index 4f0f2f3a1..821c61cd8 100644 --- a/test/unit/Reflection/Adapter/ReflectionEnumTest.php +++ b/test/unit/Reflection/Adapter/ReflectionEnumTest.php @@ -1140,4 +1140,92 @@ public function testGetPropertiesWithFilter(): void self::assertCount(1, $reflectionEnumAdapter->getProperties(CoreReflectionProperty::IS_PRIVATE)); self::assertCount(1, $reflectionEnumAdapter->getProperties(CoreReflectionProperty::IS_PROTECTED)); } + + public function testNewLazyGhost(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionEnum = $this->createMock(BetterReflectionEnum::class); + + $reflectionEnumAdapter = new ReflectionEnumAdapter($betterReflectionEnum); + $reflectionEnumAdapter->newLazyGhost(static fn () => null); + } + + public function testNewLazyProxy(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionEnum = $this->createMock(BetterReflectionEnum::class); + + $reflectionEnumAdapter = new ReflectionEnumAdapter($betterReflectionEnum); + $reflectionEnumAdapter->newLazyProxy(static fn () => null); + } + + public function testMarkLazyObjectAsInitialized(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionEnum = $this->createMock(BetterReflectionEnum::class); + + $reflectionEnumAdapter = new ReflectionEnumAdapter($betterReflectionEnum); + $reflectionEnumAdapter->markLazyObjectAsInitialized(new stdClass()); + } + + public function testGetLazyInitializer(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionEnum = $this->createMock(BetterReflectionEnum::class); + + $reflectionEnumAdapter = new ReflectionEnumAdapter($betterReflectionEnum); + $reflectionEnumAdapter->getLazyInitializer(new stdClass()); + } + + public function testInitializeLazyObject(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionEnum = $this->createMock(BetterReflectionEnum::class); + + $reflectionEnumAdapter = new ReflectionEnumAdapter($betterReflectionEnum); + $reflectionEnumAdapter->initializeLazyObject(new stdClass()); + } + + public function testIsUninitializedLazyObject(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionEnum = $this->createMock(BetterReflectionEnum::class); + + $reflectionEnumAdapter = new ReflectionEnumAdapter($betterReflectionEnum); + $reflectionEnumAdapter->isUninitializedLazyObject(new stdClass()); + } + + public function testResetAsLazyGhost(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionEnum = $this->createMock(BetterReflectionEnum::class); + + $reflectionEnumAdapter = new ReflectionEnumAdapter($betterReflectionEnum); + $reflectionEnumAdapter->resetAsLazyGhost(new stdClass(), static fn () => null); + } + + public function testResetAsLazyProxy(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionEnum = $this->createMock(BetterReflectionEnum::class); + + $reflectionEnumAdapter = new ReflectionEnumAdapter($betterReflectionEnum); + $reflectionEnumAdapter->resetAsLazyProxy(new stdClass(), static fn () => null); + } } diff --git a/test/unit/Reflection/Adapter/ReflectionEnumUnitCaseTest.php b/test/unit/Reflection/Adapter/ReflectionEnumUnitCaseTest.php index 103b5a344..b07e1d964 100644 --- a/test/unit/Reflection/Adapter/ReflectionEnumUnitCaseTest.php +++ b/test/unit/Reflection/Adapter/ReflectionEnumUnitCaseTest.php @@ -54,6 +54,7 @@ public static function methodExpectationProvider(): array ['getValue', NotImplemented::class, null, []], ['getDocComment', null, null, []], ['getAttributes', null, [], []], + ['isDeprecated', null, true, []], ]; } diff --git a/test/unit/Reflection/Adapter/ReflectionObjectTest.php b/test/unit/Reflection/Adapter/ReflectionObjectTest.php index aa130483c..8cfa8a622 100644 --- a/test/unit/Reflection/Adapter/ReflectionObjectTest.php +++ b/test/unit/Reflection/Adapter/ReflectionObjectTest.php @@ -1239,4 +1239,92 @@ public function testGetPropertiesWithFilter(): void self::assertCount(1, $reflectionObjectAdapter->getProperties(CoreReflectionProperty::IS_PRIVATE)); self::assertCount(1, $reflectionObjectAdapter->getProperties(CoreReflectionProperty::IS_PROTECTED)); } + + public function testNewLazyGhost(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionObject = $this->createMock(BetterReflectionObject::class); + + $reflectionObjectAdapter = new ReflectionObjectAdapter($betterReflectionObject); + $reflectionObjectAdapter->newLazyGhost(static fn () => null); + } + + public function testNewLazyProxy(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionObject = $this->createMock(BetterReflectionObject::class); + + $reflectionObjectAdapter = new ReflectionObjectAdapter($betterReflectionObject); + $reflectionObjectAdapter->newLazyProxy(static fn () => null); + } + + public function testMarkLazyObjectAsInitialized(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionObject = $this->createMock(BetterReflectionObject::class); + + $reflectionObjectAdapter = new ReflectionObjectAdapter($betterReflectionObject); + $reflectionObjectAdapter->markLazyObjectAsInitialized(new stdClass()); + } + + public function testGetLazyInitializer(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionObject = $this->createMock(BetterReflectionObject::class); + + $reflectionObjectAdapter = new ReflectionObjectAdapter($betterReflectionObject); + $reflectionObjectAdapter->getLazyInitializer(new stdClass()); + } + + public function testInitializeLazyObject(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionObject = $this->createMock(BetterReflectionObject::class); + + $reflectionObjectAdapter = new ReflectionObjectAdapter($betterReflectionObject); + $reflectionObjectAdapter->initializeLazyObject(new stdClass()); + } + + public function testIsUninitializedLazyObject(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionObject = $this->createMock(BetterReflectionObject::class); + + $reflectionObjectAdapter = new ReflectionObjectAdapter($betterReflectionObject); + $reflectionObjectAdapter->isUninitializedLazyObject(new stdClass()); + } + + public function testResetAsLazyGhost(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionObject = $this->createMock(BetterReflectionObject::class); + + $reflectionObjectAdapter = new ReflectionObjectAdapter($betterReflectionObject); + $reflectionObjectAdapter->resetAsLazyGhost(new stdClass(), static fn () => null); + } + + public function testResetAsLazyProxy(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionObject = $this->createMock(BetterReflectionObject::class); + + $reflectionObjectAdapter = new ReflectionObjectAdapter($betterReflectionObject); + $reflectionObjectAdapter->resetAsLazyProxy(new stdClass(), static fn () => null); + } } diff --git a/test/unit/Reflection/Adapter/ReflectionPropertyTest.php b/test/unit/Reflection/Adapter/ReflectionPropertyTest.php index f35f4a0f0..217a2ef1b 100644 --- a/test/unit/Reflection/Adapter/ReflectionPropertyTest.php +++ b/test/unit/Reflection/Adapter/ReflectionPropertyTest.php @@ -8,10 +8,13 @@ use OutOfBoundsException; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; use PHPUnit\Framework\TestCase; +use PropertyHookType; use ReflectionClass as CoreReflectionClass; use ReflectionException as CoreReflectionException; use ReflectionProperty as CoreReflectionProperty; +use Roave\BetterReflection\Reflection\Adapter\Exception\NotImplemented; use Roave\BetterReflection\Reflection\Adapter\ReflectionAttribute as ReflectionAttributeAdapter; use Roave\BetterReflection\Reflection\Adapter\ReflectionClass as ReflectionClassAdapter; use Roave\BetterReflection\Reflection\Adapter\ReflectionNamedType as ReflectionNamedTypeAdapter; @@ -21,7 +24,9 @@ use Roave\BetterReflection\Reflection\Exception\ObjectNotInstanceOfClass; use Roave\BetterReflection\Reflection\ReflectionAttribute as BetterReflectionAttribute; use Roave\BetterReflection\Reflection\ReflectionClass as BetterReflectionClass; +use Roave\BetterReflection\Reflection\ReflectionMethod as BetterReflectionMethod; use Roave\BetterReflection\Reflection\ReflectionNamedType as BetterReflectionNamedType; +use Roave\BetterReflection\Reflection\ReflectionParameter as BetterReflectionParameter; use Roave\BetterReflection\Reflection\ReflectionProperty as BetterReflectionProperty; use stdClass; use TypeError; @@ -60,9 +65,14 @@ public static function methodExpectationProvider(): array ['getName', [], 'name', null, 'name', null], ['isPublic', [], true, null, true, null], ['isPrivate', [], true, null, true, null], + ['isPrivateSet', [], true, null, true, null], ['isProtected', [], true, null, true, null], + ['isProtectedSet', [], true, null, true, null], ['isStatic', [], true, null, true, null], + ['isFinal', [], true, null, true, null], + ['isAbstract', [], true, null, true, null], ['isDefault', [], true, null, true, null], + ['isDynamic', [], false, null, false, null], ['getModifiers', [], 123, null, 123, null], ['getDocComment', [], null, null, false, null], ['hasType', [], true, null, true, null], @@ -70,6 +80,9 @@ public static function methodExpectationProvider(): array ['getDefaultValue', [], null, null, null, null], ['isPromoted', [], true, null, true, null], ['isReadOnly', [], true, null, true, null], + ['isVirtual', [], true, null, true, null], + ['hasHooks', [], false, null, false, null], + ['getHooks', [], [], null, [], null], ]; } @@ -460,4 +473,166 @@ public function testUnknownProperty(): void /** @phpstan-ignore property.notFound, expr.resultUnused */ $reflectionPropertyAdapter->foo; } + + public function testSetRawValueWithoutLazyInitialization(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionProperty = $this->createMock(BetterReflectionProperty::class); + $reflectionPropertyAdapter = new ReflectionPropertyAdapter($betterReflectionProperty); + $reflectionPropertyAdapter->setRawValueWithoutLazyInitialization(new stdClass(), null); + } + + public function testIsLazy(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionProperty = $this->createMock(BetterReflectionProperty::class); + $reflectionPropertyAdapter = new ReflectionPropertyAdapter($betterReflectionProperty); + $reflectionPropertyAdapter->isLazy(new stdClass()); + } + + public function testSkipLazyInitialization(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionProperty = $this->createMock(BetterReflectionProperty::class); + $reflectionPropertyAdapter = new ReflectionPropertyAdapter($betterReflectionProperty); + $reflectionPropertyAdapter->skipLazyInitialization(new stdClass()); + } + + #[RequiresPhp('8.4')] + public function testHasAndGetHookWhenNoHooks(): void + { + $betterReflectionProperty = $this->createMock(BetterReflectionProperty::class); + $betterReflectionProperty + ->method('hasHook') + ->willReturn(false); + $betterReflectionProperty + ->method('getHook') + ->willReturn(null); + + $reflectionPropertyAdapter = new ReflectionPropertyAdapter($betterReflectionProperty); + self::assertFalse($reflectionPropertyAdapter->hasHook(PropertyHookType::Get)); + self::assertNull($reflectionPropertyAdapter->getHook(PropertyHookType::Get)); + } + + #[RequiresPhp('8.4')] + public function testHasAndGetHook(): void + { + $betterReflectionProperty = $this->createMock(BetterReflectionProperty::class); + $betterReflectionProperty + ->method('hasHook') + ->willReturn(true); + $betterReflectionProperty + ->method('getHook') + ->willReturn($this->createMock(BetterReflectionMethod::class)); + + $reflectionPropertyAdapter = new ReflectionPropertyAdapter($betterReflectionProperty); + self::assertTrue($reflectionPropertyAdapter->hasHook(PropertyHookType::Get)); + self::assertNotNull($reflectionPropertyAdapter->getHook(PropertyHookType::Get)); + } + + public function testGetSettableType(): void + { + $setHookParameterType = $this->createMock(BetterReflectionNamedType::class); + $setHookParameterType + ->method('getName') + ->willReturn('int'); + + $setHookParameter = $this->createMock(BetterReflectionParameter::class); + $setHookParameter + ->method('getType') + ->willReturn($setHookParameterType); + + $setHook = $this->createMock(BetterReflectionMethod::class); + $setHook + ->method('getParameters') + ->willReturn([$setHookParameter]); + + $betterReflectionProperty = $this->createMock(BetterReflectionProperty::class); + $betterReflectionProperty + ->method('getHook') + ->willReturn($setHook); + + $reflectionPropertyAdapter = new ReflectionPropertyAdapter($betterReflectionProperty); + self::assertInstanceOf(ReflectionNamedTypeAdapter::class, $reflectionPropertyAdapter->getSettableType()); + self::assertSame('int', $reflectionPropertyAdapter->getSettableType()->getName()); + } + + public function testGetSettableTypeWhenVirtual(): void + { + $betterReflectionProperty = $this->createMock(BetterReflectionProperty::class); + $betterReflectionProperty + ->method('getHook') + ->willReturn(null); + $betterReflectionProperty + ->method('isVirtual') + ->willReturn(true); + + $reflectionPropertyAdapter = new ReflectionPropertyAdapter($betterReflectionProperty); + self::assertInstanceOf(ReflectionNamedTypeAdapter::class, $reflectionPropertyAdapter->getSettableType()); + self::assertSame('never', $reflectionPropertyAdapter->getSettableType()->getName()); + } + + public function testGetSettableTypeWhenNoHooks(): void + { + $betterReflectionProperty = $this->createMock(BetterReflectionProperty::class); + $betterReflectionProperty + ->method('getHook') + ->willReturn(null); + $betterReflectionProperty + ->method('isVirtual') + ->willReturn(false); + $betterReflectionProperty + ->method('getType') + ->willReturn($this->createMock(BetterReflectionNamedType::class)); + + $reflectionPropertyAdapter = new ReflectionPropertyAdapter($betterReflectionProperty); + self::assertInstanceOf(ReflectionNamedTypeAdapter::class, $reflectionPropertyAdapter->getSettableType()); + } + + public function testGetSettableTypeWhenNoHooksAndNoType(): void + { + $betterReflectionProperty = $this->createMock(BetterReflectionProperty::class); + $betterReflectionProperty + ->method('getHook') + ->willReturn(null); + $betterReflectionProperty + ->method('isVirtual') + ->willReturn(false); + $betterReflectionProperty + ->method('getType') + ->willReturn(null); + + $reflectionPropertyAdapter = new ReflectionPropertyAdapter($betterReflectionProperty); + self::assertNull($reflectionPropertyAdapter->getSettableType()); + } + + public function testSetRawValueWhenNoHooks(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionProperty = $this->createMock(BetterReflectionProperty::class); + $betterReflectionProperty + ->method('hasHooks') + ->willReturn(true); + + $reflectionPropertyAdapter = new ReflectionPropertyAdapter($betterReflectionProperty); + $reflectionPropertyAdapter->setRawValue(new stdClass(), null); + } + + public function testGetRawValue(): void + { + self::expectException(NotImplemented::class); + self::expectExceptionMessage('Not implemented'); + + $betterReflectionProperty = $this->createMock(BetterReflectionProperty::class); + $reflectionPropertyAdapter = new ReflectionPropertyAdapter($betterReflectionProperty); + $reflectionPropertyAdapter->getRawValue(new stdClass()); + } } diff --git a/test/unit/Reflection/Deprecated/DeprecatedHelperTest.php b/test/unit/Reflection/Deprecated/DeprecatedHelperTest.php new file mode 100644 index 000000000..86f58e0fb --- /dev/null +++ b/test/unit/Reflection/Deprecated/DeprecatedHelperTest.php @@ -0,0 +1,100 @@ + */ + public static function deprecatedAttributeProvider(): array + { + return [ + [ + '', + false, + ], + [ + '#[SomeAttribute]', + false, + ], + [ + '#[Deprecated]', + true, + ], + [ + '#[Deprecated(since: "8.0.0")]', + true, + ], + [ + '#[SomeAttribute] #[Deprecated]', + true, + ], + ]; + } + + #[DataProvider('deprecatedAttributeProvider')] + public function testIsDeprecatedByAttribute(string $deprecatedCode, bool $isDeprecated): void + { + $php = sprintf('astLocator())); + $reflection = $reflector->reflectClass('Foo'); + + self::assertSame($isDeprecated, DeprecatedHelper::isDeprecated($reflection)); + } + + /** @return list */ + public static function deprecatedDocCommentProvider(): array + { + return [ + [null, false], + ['', false], + [ + '/** + * @return string + */', + false, + ], + [ + '/** + * @deprecatedPolicy + */', + false, + ], + ['/** @deprecated */', true], + ['/**@deprecated*/', true], + [ + '/** + * @deprecated since 8.0.0 + */', + true, + ], + ]; + } + + #[DataProvider('deprecatedDocCommentProvider')] + public function testIsDeprecatedByDocComment(string|null $docComment, bool $isDeprecated): void + { + $reflection = $this->createMock(ReflectionClass::class); + $reflection + ->method('getDocComment') + ->willReturn($docComment); + + self::assertSame($isDeprecated, DeprecatedHelper::isDeprecated($reflection)); + } +} diff --git a/test/unit/Reflection/ReflectionClassConstantTest.php b/test/unit/Reflection/ReflectionClassConstantTest.php index a244c3ce7..2513baa7f 100644 --- a/test/unit/Reflection/ReflectionClassConstantTest.php +++ b/test/unit/Reflection/ReflectionClassConstantTest.php @@ -236,9 +236,13 @@ public function testGetDeclaringAndImplementingClass(string $constantName, strin } /** @return list */ - public static function deprecatedDocCommentProvider(): array + public static function deprecatedProvider(): array { return [ + [ + '#[Deprecated]', + true, + ], [ '/** * @deprecated since 8.0 @@ -258,14 +262,14 @@ public static function deprecatedDocCommentProvider(): array ]; } - #[DataProvider('deprecatedDocCommentProvider')] - public function testIsDeprecated(string $docComment, bool $isDeprecated): void + #[DataProvider('deprecatedProvider')] + public function testIsDeprecated(string $deprecatedCode, bool $isDeprecated): void { $php = sprintf('astLocator())); $classReflection = $reflector->reflectClass('Foo'); diff --git a/test/unit/Reflection/ReflectionClassTest.php b/test/unit/Reflection/ReflectionClassTest.php index 4dea2ba1d..7d14faf9b 100644 --- a/test/unit/Reflection/ReflectionClassTest.php +++ b/test/unit/Reflection/ReflectionClassTest.php @@ -24,6 +24,7 @@ use ReflectionMethod as CoreReflectionMethod; use ReflectionProperty as CoreReflectionProperty; use Roave\BetterReflection\Reflection\Adapter\ReflectionClassConstant as ReflectionClassConstantAdapter; +use Roave\BetterReflection\Reflection\Adapter\ReflectionProperty as ReflectionPropertyAdapter; use Roave\BetterReflection\Reflection\Exception\CircularReference; use Roave\BetterReflection\Reflection\Exception\NotAClassReflection; use Roave\BetterReflection\Reflection\Exception\NotAnInterfaceReflection; @@ -615,7 +616,7 @@ public function testGetProperties(): void $properties = $classInfo->getProperties(); self::assertContainsOnlyInstancesOf(ReflectionProperty::class, $properties); - self::assertCount(9, $properties); + self::assertCount(10, $properties); } public function testGetPropertiesForPureEnum(): void @@ -637,6 +638,7 @@ public function testGetPropertiesForPureEnum(): void self::assertTrue($property->isReadOnly()); self::assertFalse($property->isPromoted()); self::assertTrue($property->isDefault()); + self::assertFalse($property->isDynamic()); // No value property for pure enum self::assertArrayNotHasKey('value', $properties); @@ -695,6 +697,7 @@ public function testGetPropertiesForBackedEnum(string $className, array $propert self::assertTrue($property->isReadOnly(), $fullPropertyName); self::assertFalse($property->isPromoted(), $fullPropertyName); self::assertTrue($property->isDefault(), $fullPropertyName); + self::assertFalse($property->isDynamic(), $fullPropertyName); self::assertSame($propertyType, $property->getType()->__toString(), $fullPropertyName); } } @@ -724,20 +727,24 @@ class Foo self::assertSame($expectedPropertiesNames, array_keys($properties)); } - /** @return list, 1: int}> */ + /** @return list, 1: int}> */ public static function getPropertiesWithFilterDataProvider(): array { return [ [CoreReflectionProperty::IS_STATIC, 3], - [CoreReflectionProperty::IS_PUBLIC, 3], + [ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY, 1], + [ReflectionPropertyAdapter::IS_ABSTRACT_COMPATIBILITY, 0], + [CoreReflectionProperty::IS_PUBLIC, 4], [CoreReflectionProperty::IS_PROTECTED, 2], [CoreReflectionProperty::IS_PRIVATE, 4], [ CoreReflectionProperty::IS_STATIC | + ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY | + ReflectionPropertyAdapter::IS_ABSTRACT_COMPATIBILITY | CoreReflectionProperty::IS_PUBLIC | CoreReflectionProperty::IS_PROTECTED | CoreReflectionProperty::IS_PRIVATE, - 9, + 10, ], ]; } @@ -753,6 +760,54 @@ public function testGetPropertiesWithFilter(int $filter, int $count): void self::assertCount($count, $classInfo->getImmediateProperties($filter)); } + /** @return list, 1: int}> */ + public static function getPropertiesWithAsymetricVisibilityFilterDataProvider(): array + { + return [ + [CoreReflectionProperty::IS_PUBLIC, 6], + [CoreReflectionProperty::IS_PROTECTED, 4], + [CoreReflectionProperty::IS_PRIVATE, 2], + [ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY, 4], + [ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY, 6], + [ + CoreReflectionProperty::IS_PUBLIC | + ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY, + 10, + ], + ]; + } + + /** @param int-mask-of $filter */ + #[DataProvider('getPropertiesWithAsymetricVisibilityFilterDataProvider')] + public function testGetPropertiesWithAsymetricVisibilityFilter(int $filter, int $count): void + { + $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/AsymetricVisibilityClass.php', $this->astLocator)); + $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\AsymetricVisibilityClass'); + + self::assertCount($count, $classInfo->getProperties($filter)); + self::assertCount($count, $classInfo->getImmediateProperties($filter)); + } + + /** @return list, 1: int}> */ + public static function getPropertiesWithVirtualFilterDataProvider(): array + { + return [ + [0, 4], + [ReflectionPropertyAdapter::IS_VIRTUAL_COMPATIBILITY, 2], + ]; + } + + /** @param int-mask-of $filter */ + #[DataProvider('getPropertiesWithVirtualFilterDataProvider')] + public function testGetPropertiesWithVirtualFilter(int $filter, int $count): void + { + $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/PropertyHooks.php', $this->astLocator)); + $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\PropertyHooks'); + + self::assertCount($count, $classInfo->getProperties($filter)); + self::assertCount($count, $classInfo->getImmediateProperties($filter)); + } + public function testGetPropertiesReturnsInheritedProperties(): void { $classInfo = (new DefaultReflector(new SingleFileSourceLocator( @@ -2253,6 +2308,17 @@ public function testToString(): void ); } + public function testToStringWithAsymetricVisibility(): void + { + $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/AsymetricVisibilityClass.php', $this->astLocator)); + $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\AsymetricVisibilityClass'); + + self::assertStringMatchesFormat( + file_get_contents(__DIR__ . '/../Fixture/AsymetricVisibilityClassExport.txt'), + $classInfo->__toString(), + ); + } + public function testGetStaticProperties(): void { $staticPropertiesFixtureFile = __DIR__ . '/../Fixture/StaticProperties.php'; @@ -2793,9 +2859,13 @@ public function __toString(): string } /** @return list */ - public static function deprecatedDocCommentProvider(): array + public static function deprecatedProvider(): array { return [ + [ + '#[Deprecated]', + true, + ], [ '/** * @deprecated since 8.0 @@ -2815,12 +2885,12 @@ public static function deprecatedDocCommentProvider(): array ]; } - #[DataProvider('deprecatedDocCommentProvider')] - public function testIsDeprecated(string $docComment, bool $isDeprecated): void + #[DataProvider('deprecatedProvider')] + public function testIsDeprecated(string $deprecatedCode, bool $isDeprecated): void { $php = sprintf('astLocator)); $classReflection = $reflector->reflectClass('Foo'); diff --git a/test/unit/Reflection/ReflectionFunctionAbstractTest.php b/test/unit/Reflection/ReflectionFunctionAbstractTest.php index 89fee0763..ca50541d7 100644 --- a/test/unit/Reflection/ReflectionFunctionAbstractTest.php +++ b/test/unit/Reflection/ReflectionFunctionAbstractTest.php @@ -136,32 +136,46 @@ public function testIsClosureWithArrowFunction(): void self::assertTrue($function->isClosure()); } - #[DataProvider('nonDeprecatedProvider')] - public function testIsDeprecated(string $comment): void - { - $php = sprintf('astLocator)); - $function = $reflector->reflectFunction('foo'); - - self::assertFalse($function->isDeprecated()); - } - - /** @return list */ - public static function nonDeprecatedProvider(): array + /** @return list */ + public static function deprecatedProvider(): array { return [ - [''], + [ + '#[Deprecated]', + true, + ], [ '/** - * @deprecatedPolicy + * @deprecated since 8.0 */', + true, + ], + [ + '/** + * @deprecated + */', + true, + ], + [ + '', + false, ], ]; } + #[DataProvider('deprecatedProvider')] + public function testIsDeprecated(string $deprecatedCode, bool $isDeprecated): void + { + $php = sprintf('astLocator)); + $function = $reflector->reflectFunction('foo'); + + self::assertSame($isDeprecated, $function->isDeprecated()); + } + public function testIsInternal(): void { $php = 'isReadOnly()); } + public function testIsFinal(): void + { + $classInfo = $this->reflector->reflectClass(ExampleClass::class); + + $notReadOnlyProperty = $classInfo->getProperty('publicProperty'); + self::assertFalse($notReadOnlyProperty->isFinal()); + + $finalPublicProperty = $classInfo->getProperty('finalPublicProperty'); + self::assertTrue($finalPublicProperty->isFinal()); + self::assertTrue($finalPublicProperty->isPublic()); + } + + public function testIsNotAbstract(): void + { + $classInfo = $this->reflector->reflectClass(ExampleClass::class); + + $notAbstractProperty = $classInfo->getProperty('publicProperty'); + self::assertFalse($notAbstractProperty->isAbstract()); + } + public function testIsReadOnlyInReadOnlyClass(): void { $reflector = new DefaultReflector(new SingleFileSourceLocator( @@ -221,7 +244,7 @@ public function testGetDocCommentReturnsNullWithNoComment(): void self::assertNull($property->getDocComment()); } - /** @return list */ + /** @return list}> */ public static function modifierProvider(): array { return [ @@ -230,6 +253,7 @@ public static function modifierProvider(): array ['privateProperty', CoreReflectionProperty::IS_PRIVATE], ['publicStaticProperty', CoreReflectionProperty::IS_PUBLIC | CoreReflectionProperty::IS_STATIC], ['readOnlyProperty', CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_READONLY], + ['finalPublicProperty', CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY], ]; } @@ -255,22 +279,29 @@ public function testIsPromoted(): void self::assertSame('int|null', $promotedProperty->getType()->__toString()); self::assertFalse($promotedProperty->hasDefaultValue()); self::assertNull($promotedProperty->getDefaultValue()); - self::assertSame(52, $promotedProperty->getStartLine()); - self::assertSame(52, $promotedProperty->getEndLine()); + self::assertSame(54, $promotedProperty->getStartLine()); + self::assertSame(54, $promotedProperty->getEndLine()); self::assertSame(60, $promotedProperty->getStartColumn()); self::assertSame(95, $promotedProperty->getEndColumn()); self::assertSame('/** Some doccomment */', $promotedProperty->getDocComment()); } - public function testIsDefault(): void + public function testIsDefaultAndIsDynamic(): void { $classInfo = $this->reflector->reflectClass(ExampleClass::class); - self::assertTrue($classInfo->getProperty('publicProperty')->isDefault()); - self::assertTrue($classInfo->getProperty('publicStaticProperty')->isDefault()); + $publicProperty = $classInfo->getProperty('publicProperty'); + + self::assertTrue($publicProperty->isDefault()); + self::assertFalse($publicProperty->isDynamic()); + + $publicStaticProperty = $classInfo->getProperty('publicStaticProperty'); + + self::assertTrue($publicStaticProperty->isDefault()); + self::assertFalse($publicStaticProperty->isDynamic()); } - public function testIsDefaultWithRuntimeDeclaredProperty(): void + public function testIsDefaultAndIsDynamicWithRuntimeDeclaredProperty(): void { $classInfo = $this->reflector->reflectClass(ExampleClass::class); $propertyPropertyNode = new PropertyItem('foo'); @@ -285,6 +316,7 @@ public function testIsDefaultWithRuntimeDeclaredProperty(): void ); self::assertFalse($propertyNode->isDefault()); + self::assertTrue($propertyNode->isDynamic()); } public function testToString(): void @@ -861,4 +893,190 @@ public function testWithImplementingClass(): void self::assertCount(2, $cloneAttributes); self::assertNotSame($attributes[0], $cloneAttributes[0]); } + + /** @return list}> */ + public static function asymetricVisibilityModifierProvider(): array + { + return [ + ['publicPublicSet', CoreReflectionProperty::IS_PUBLIC], + ['publicProtectedSet', CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY], + ['publicPrivateSet', CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY], + ['protectedProtectedSet', CoreReflectionProperty::IS_PROTECTED | ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY], + ['protectedPrivateSet', CoreReflectionProperty::IS_PROTECTED | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY], + ['privatePrivateSet', CoreReflectionProperty::IS_PRIVATE | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY], + ['promotedPublicPublicSet', CoreReflectionProperty::IS_PUBLIC], + ['promotedPublicProtectedSet', CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY], + ['promotedPublicPrivateSet', CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY], + ['promotedProtectedProtectedSet', CoreReflectionProperty::IS_PROTECTED | ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY], + ['promotedProtectedPrivateSet', CoreReflectionProperty::IS_PROTECTED | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY], + ['promotedPrivatePrivateSet', CoreReflectionProperty::IS_PRIVATE | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY], + ]; + } + + /** @param non-empty-string $propertyName */ + #[DataProvider('asymetricVisibilityModifierProvider')] + public function testGetAsymetricVisibilityModifiers(string $propertyName, int $expectedModifier): void + { + $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/AsymetricVisibilityClass.php', $this->astLocator)); + $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\AsymetricVisibilityClass'); + $property = $classInfo->getProperty($propertyName); + + self::assertSame($expectedModifier, $property->getModifiers()); + } + + public function testIsAbstract(): void + { + $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/PropertyHooks.php', $this->astLocator)); + $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\AbstractPropertyHooks'); + + $hookProperty = $classInfo->getProperty('hook'); + self::assertTrue($hookProperty->isAbstract()); + } + + public function testNoHooks(): void + { + $classInfo = $this->reflector->reflectClass(ExampleClass::class); + $property = $classInfo->getProperty('publicProperty'); + + self::assertFalse($property->hasHooks()); + self::assertCount(0, $property->getHooks()); + self::assertFalse($property->hasHook(ReflectionPropertyHookType::Get)); + self::assertNull($property->getHook(ReflectionPropertyHookType::Set)); + self::assertFalse($property->hasHook(ReflectionPropertyHookType::Get)); + self::assertNull($property->getHook(ReflectionPropertyHookType::Set)); + } + + public function testReadOnlyHook(): void + { + $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/PropertyHooks.php', $this->astLocator)); + $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\PropertyHooks'); + + $hookProperty = $classInfo->getProperty('readOnlyHook'); + self::assertTrue($hookProperty->isDefault()); + self::assertTrue($hookProperty->isVirtual()); + self::assertTrue($hookProperty->hasHooks()); + + self::assertTrue($hookProperty->hasHook(ReflectionPropertyHookType::Get)); + self::assertFalse($hookProperty->hasHook(ReflectionPropertyHookType::Set)); + + $hooks = $hookProperty->getHooks(); + self::assertCount(1, $hooks); + self::assertArrayHasKey('get', $hooks); + self::assertInstanceOf(ReflectionMethod::class, $hooks['get']); + self::assertSame('$readOnlyHook::get', $hooks['get']->getName()); + self::assertSame($hooks['get'], $hookProperty->getHook(ReflectionPropertyHookType::Get)); + } + + public function testWriteOnlyHook(): void + { + $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/PropertyHooks.php', $this->astLocator)); + $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\PropertyHooks'); + + $hookProperty = $classInfo->getProperty('writeOnlyHook'); + self::assertTrue($hookProperty->isDefault()); + self::assertFalse($hookProperty->isVirtual()); + self::assertTrue($hookProperty->hasHooks()); + + self::assertFalse($hookProperty->hasHook(ReflectionPropertyHookType::Get)); + self::assertTrue($hookProperty->hasHook(ReflectionPropertyHookType::Set)); + + $hooks = $hookProperty->getHooks(); + self::assertCount(1, $hooks); + self::assertArrayHasKey('set', $hooks); + self::assertInstanceOf(ReflectionMethod::class, $hooks['set']); + self::assertSame('$writeOnlyHook::set', $hooks['set']->getName()); + self::assertSame($hooks['set'], $hookProperty->getHook(ReflectionPropertyHookType::Set)); + } + + public function testBothReadAndWriteHooks(): void + { + $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/PropertyHooks.php', $this->astLocator)); + $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\PropertyHooks'); + + $hookProperty = $classInfo->getProperty('readAndWriteHook'); + self::assertTrue($hookProperty->isDefault()); + self::assertFalse($hookProperty->isVirtual()); + self::assertTrue($hookProperty->hasHooks()); + + self::assertTrue($hookProperty->hasHook(ReflectionPropertyHookType::Get)); + self::assertTrue($hookProperty->hasHook(ReflectionPropertyHookType::Set)); + + $hooks = $hookProperty->getHooks(); + self::assertCount(2, $hooks); + + self::assertArrayHasKey('get', $hooks); + self::assertInstanceOf(ReflectionMethod::class, $hooks['get']); + self::assertSame('$readAndWriteHook::get', $hooks['get']->getName()); + self::assertSame($hooks['get'], $hookProperty->getHook(ReflectionPropertyHookType::Get)); + + self::assertArrayHasKey('set', $hooks); + self::assertInstanceOf(ReflectionMethod::class, $hooks['set']); + self::assertSame('$readAndWriteHook::set', $hooks['set']->getName()); + self::assertSame($hooks['set'], $hookProperty->getHook(ReflectionPropertyHookType::Set)); + } + + public function testHooksForAbstractProperty(): void + { + $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/PropertyHooks.php', $this->astLocator)); + $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\AbstractPropertyHooks'); + + $hookProperty = $classInfo->getProperty('hook'); + + self::assertTrue($hookProperty->isDefault()); + self::assertTrue($hookProperty->isVirtual()); + self::assertTrue($hookProperty->hasHooks()); + + $hooks = $hookProperty->getHooks(); + self::assertCount(1, $hooks); + + self::assertArrayHasKey('get', $hooks); + self::assertInstanceOf(ReflectionMethod::class, $hooks['get']); + self::assertSame('$hook::get', $hooks['get']->getName()); + self::assertSame($hooks['get'], $hookProperty->getHook(ReflectionPropertyHookType::Get)); + } + + public function testIsVirtualWhenSetHookIsStrange(): void + { + $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/PropertyHooks.php', $this->astLocator)); + $classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\PropertyHooks'); + + $hookProperty = $classInfo->getProperty('virtualBecauseOfStrangeSetHook'); + self::assertTrue($hookProperty->isVirtual()); + } + + public function testExtendingHooks(): void + { + $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/PropertyHooks.php', $this->astLocator)); + $getClassInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\GetPropertyHook'); + + $getHookProperty = $getClassInfo->getProperty('hook'); + self::assertTrue($getHookProperty->isVirtual()); + self::assertCount(1, $getHookProperty->getHooks()); + self::assertTrue($getHookProperty->hasHook(ReflectionPropertyHookType::Get)); + self::assertFalse($getHookProperty->hasHook(ReflectionPropertyHookType::Set)); + self::assertSame('Roave\BetterReflectionTest\Fixture\GetPropertyHook', $getHookProperty->getHook(ReflectionPropertyHookType::Get)->getDeclaringClass()->getName()); + + $getAndSetClassInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\GetAndSetPropertyHook'); + + $getAndSetHookProperty = $getAndSetClassInfo->getProperty('hook'); + self::assertFalse($getAndSetHookProperty->isVirtual()); + self::assertCount(2, $getAndSetHookProperty->getHooks()); + self::assertTrue($getAndSetHookProperty->hasHook(ReflectionPropertyHookType::Get)); + self::assertTrue($getAndSetHookProperty->hasHook(ReflectionPropertyHookType::Set)); + self::assertSame('Roave\BetterReflectionTest\Fixture\GetPropertyHook', $getAndSetHookProperty->getHook(ReflectionPropertyHookType::Get)->getDeclaringClass()->getName()); + self::assertSame('Roave\BetterReflectionTest\Fixture\GetAndSetPropertyHook', $getAndSetHookProperty->getHook(ReflectionPropertyHookType::Set)->getDeclaringClass()->getName()); + } + + public function testUseHookFromTrait(): void + { + $reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/PropertyHooks.php', $this->astLocator)); + $getClassInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\UsePropertyHookFromTrait'); + + $hookProperty = $getClassInfo->getProperty('hook'); + self::assertTrue($hookProperty->isVirtual()); + self::assertCount(1, $hookProperty->getHooks()); + self::assertTrue($hookProperty->hasHook(ReflectionPropertyHookType::Get)); + self::assertFalse($hookProperty->hasHook(ReflectionPropertyHookType::Set)); + self::assertSame('Roave\BetterReflectionTest\Fixture\PropertyHookTrait', $hookProperty->getHook(ReflectionPropertyHookType::Get)->getDeclaringClass()->getName()); + } } diff --git a/test/unit/SourceLocator/SourceStubber/PhpStormStubsSourceStubberTest.php b/test/unit/SourceLocator/SourceStubber/PhpStormStubsSourceStubberTest.php index 664594c15..9ed9284db 100644 --- a/test/unit/SourceLocator/SourceStubber/PhpStormStubsSourceStubberTest.php +++ b/test/unit/SourceLocator/SourceStubber/PhpStormStubsSourceStubberTest.php @@ -131,6 +131,20 @@ static function (string $className): bool { return false; } + // Missing in JetBrains/phpstorm-stubs + if ( + PHP_VERSION_ID >= 80400 + && in_array($className, [ + 'Deprecated', + 'Generator', + 'RequestParseBodyException', + 'RoundingMode', + 'StreamBucket', + ], true) + ) { + return false; + } + // Check only always enabled extensions return in_array($reflection->getExtensionName(), self::EXTENSIONS, true); }, @@ -153,6 +167,11 @@ public function testInternalClasses(string $className): void self::assertSame($internalReflection->isInterface(), $class->isInterface()); self::assertSame($internalReflection->isTrait(), $class->isTrait()); + if (PHP_VERSION_ID >= 80400 && $className === 'SplObjectStorage') { + // Needs fixes in JetBrains/phpstorm-stubs + return; + } + self::assertSameClassAttributes($internalReflection, $class); } @@ -284,6 +303,25 @@ public static function internalFunctionsProvider(): array static function (string $functionName): bool { $reflection = new CoreReflectionFunction($functionName); + // Missing in JetBrains/phpstorm-stubs + if ( + PHP_VERSION_ID >= 80400 + && in_array($functionName, [ + 'array_all', + 'array_any', + 'array_find', + 'array_find_key', + 'die', + 'exit', + 'fpow', + 'http_clear_last_response_headers', + 'http_get_last_response_headers', + 'request_parse_body', + ], true) + ) { + return false; + } + // Check only always enabled extensions return in_array($reflection->getExtensionName(), self::EXTENSIONS, true); }, @@ -344,6 +382,17 @@ public static function internalConstantsProvider(): array } foreach ($extensionConstants as $constantName => $constantValue) { + // Missing in JetBrains/phpstorm-stubs + if ( + PHP_VERSION_ID >= 80400 + && in_array($constantName, [ + 'PHP_OUTPUT_HANDLER_PROCESSED', + 'PHP_SBINDIR', + ], true) + ) { + continue; + } + $provider[] = [$constantName, $constantValue, $extensionName]; } } diff --git a/test/unit/SourceLocator/SourceStubber/ReflectionSourceStubberTest.php b/test/unit/SourceLocator/SourceStubber/ReflectionSourceStubberTest.php index bc386299c..378bbc37b 100644 --- a/test/unit/SourceLocator/SourceStubber/ReflectionSourceStubberTest.php +++ b/test/unit/SourceLocator/SourceStubber/ReflectionSourceStubberTest.php @@ -10,14 +10,16 @@ use PHPUnit\Framework\Attributes\RequiresPhp; use PHPUnit\Framework\TestCase; use ReflectionClass as CoreReflectionClass; +use ReflectionEnum as CoreReflectionEnum; +use ReflectionEnumBackedCase as CoreReflectionEnumBackedCase; use ReflectionException; use ReflectionFunction as CoreReflectionFunction; use ReflectionMethod as CoreReflectionMethod; use ReflectionParameter as CoreReflectionParameter; use Roave\BetterReflection\Reflection\Adapter\ReflectionType; use Roave\BetterReflection\Reflection\ReflectionClass; -use Roave\BetterReflection\Reflection\ReflectionClassConstant; use Roave\BetterReflection\Reflection\ReflectionConstant; +use Roave\BetterReflection\Reflection\ReflectionEnum; use Roave\BetterReflection\Reflection\ReflectionMethod; use Roave\BetterReflection\Reflection\ReflectionParameter; use Roave\BetterReflection\Reflector\DefaultReflector; @@ -41,6 +43,7 @@ use function array_filter; use function array_map; use function array_merge; +use function enum_exists; use function get_declared_classes; use function get_declared_interfaces; use function get_declared_traits; @@ -278,7 +281,9 @@ public function testInternalClasses(string $className): void self::assertTrue($class->isInternal()); self::assertFalse($class->isUserDefined()); - $internalReflection = new CoreReflectionClass($className); + $internalReflection = enum_exists($className, false) + ? new CoreReflectionEnum($className) + : new CoreReflectionClass($className); self::assertSame($internalReflection->isInterface(), $class->isInterface()); self::assertSame($internalReflection->isTrait(), $class->isTrait()); @@ -308,7 +313,7 @@ private function assertSameInterfaces(CoreReflectionClass $original, ReflectionC self::assertSame($originalInterfacesNames, $stubbedInterfacesNames); } - private function assertSameClassAttributes(CoreReflectionClass $original, ReflectionClass $stubbed): void + private function assertSameClassAttributes(CoreReflectionClass|CoreReflectionEnum $original, ReflectionClass|ReflectionEnum $stubbed): void { self::assertSame($original->getName(), $stubbed->getName()); @@ -320,16 +325,23 @@ private function assertSameClassAttributes(CoreReflectionClass $original, Reflec } $this->assertSameClassConstants($original, $stubbed); + + if (! ($original instanceof CoreReflectionEnum && $stubbed instanceof ReflectionEnum)) { + return; + } + + $this->assertSameEnumCases($original, $stubbed); } private function assertSameClassConstants(CoreReflectionClass $original, ReflectionClass $stubbed): void { - self::assertEquals( - $original->getConstants(), - array_map(static fn (ReflectionClassConstant $classConstant) => $classConstant->getValue(), $stubbed->getConstants()), - ); + // We don't check getConstants() method because native reflection returns constants and enum cases together foreach ($original->getReflectionConstants() as $originalConstant) { + if ($originalConstant->isEnumCase()) { + continue; + } + if ( ! method_exists($originalConstant, 'hasType') || ! method_exists($originalConstant, 'getType') @@ -339,6 +351,7 @@ private function assertSameClassConstants(CoreReflectionClass $original, Reflect $stubbedConstant = $stubbed->getConstant($originalConstant->getName()); + self::assertNotNull($stubbedConstant); self::assertSame($originalConstant->hasType(), $stubbedConstant->hasType()); self::assertSame( (string) $originalConstant->getType(), @@ -348,6 +361,25 @@ private function assertSameClassConstants(CoreReflectionClass $original, Reflect } } + private function assertSameEnumCases(CoreReflectionEnum $original, ReflectionEnum $stubbed): void + { + foreach ($original->getCases() as $originalEnumCase) { + $stubbedEnumCase = $stubbed->getCase($originalEnumCase->getName()); + + self::assertNotNull($stubbedEnumCase); + + if (! ($originalEnumCase instanceof CoreReflectionEnumBackedCase)) { + continue; + } + + self::assertSame( + $originalEnumCase->getBackingValue(), + $stubbedEnumCase->getValue(), + $original->getName() . '::' . $originalEnumCase->getName(), + ); + } + } + private function assertSameMethodAttributes(CoreReflectionMethod $original, ReflectionMethod $stubbed): void { $originalParameterNames = array_map( @@ -423,7 +455,7 @@ public static function internalFunctionsProvider(): array static function (string $functionName): bool { $reflection = new CoreReflectionFunction($functionName); - return $reflection->isInternal(); + return $reflection->isInternal() && ! $reflection->isDeprecated(); }, ), );