diff --git a/src/ReflectionAttribute.php b/src/ReflectionAttribute.php index 85299b6..f48e97e 100644 --- a/src/ReflectionAttribute.php +++ b/src/ReflectionAttribute.php @@ -39,7 +39,7 @@ class ReflectionAttribute extends BaseReflectionAttribute implements NodeAwareIn /** * @param class-string $attributeName - * @param array $arguments + * @param array $arguments */ public function __construct( string $attributeName, @@ -83,7 +83,12 @@ public function getNode(): Node\Attribute $arguments = []; foreach ($attr->args as $arg) { $nodeExpressionResolver->process($arg->value); - $arguments[] = $nodeExpressionResolver->getValue(); + // Named arguments are keyed by their name, exactly like the native reflection does + if (isset($arg->name)) { + $arguments[$arg->name->toString()] = $nodeExpressionResolver->getValue(); + } else { + $arguments[] = $nodeExpressionResolver->getValue(); + } } if ($arguments !== $this->arguments) { @@ -105,7 +110,7 @@ public function isRepeated(): bool /** * {@inheritDoc} * - * @return array + * @return array */ public function getArguments(): array { @@ -120,6 +125,44 @@ public function getName(): string return $this->attributeName; } + /** + * Returns the attribute class name without the namespace prefix. + * + * Available in the native reflection since PHP 8.6, but the internal implementation operates on + * the internal attribute structure that is never initialized for the parsed reflection, therefore + * it is always resolved from the attribute name itself. + */ + public function getShortName(): string + { + $nameParts = explode('\\', $this->getName()); + + return (string) array_pop($nameParts); + } + + /** + * Returns the namespace of the attribute class or an empty string for the global namespace. + * + * @see self::getShortName() for the reason why this method is not inherited from the native one + */ + public function getNamespaceName(): string + { + $nameParts = explode('\\', $this->getName()); + // Removes the last part with the short class name itself + array_pop($nameParts); + + return implode('\\', $nameParts); + } + + /** + * Checks if the attribute class is defined in a namespace. + * + * @see self::getShortName() for the reason why this method is not inherited from the native one + */ + public function inNamespace(): bool + { + return $this->getNamespaceName() !== ''; + } + /** * {@inheritDoc} */ diff --git a/src/ReflectionClassConstant.php b/src/ReflectionClassConstant.php index 51e9f3c..3e707be 100644 --- a/src/ReflectionClassConstant.php +++ b/src/ReflectionClassConstant.php @@ -320,7 +320,11 @@ public function __toString(): string $valueType = $this->type ?? new ReflectionType('mixed', false); } - return sprintf( + // Native reflection prints the doc comment of the constant right before its declaration + $docComment = $this->getDocComment(); + $prefix = $docComment !== false ? $docComment . "\n" : ''; + + return $prefix . sprintf( "Constant [ %s %s %s ] { %s }\n", implode(' ', Reflection::getModifierNames($this->getModifiers())), ReflectionType::convertToDisplayType($valueType), diff --git a/src/ReflectionConstant.php b/src/ReflectionConstant.php index deaf698..2707476 100644 --- a/src/ReflectionConstant.php +++ b/src/ReflectionConstant.php @@ -126,6 +126,16 @@ public function getNamespaceName(): string return implode('\\', $namespaceParts); } + /** + * Checks if the constant is defined in a namespace + * + * Mirrors the \ReflectionConstant::inNamespace() method that was added in PHP 8.6 + */ + public function inNamespace(): bool + { + return $this->getNamespaceName() !== ''; + } + /** * Returns the value of the constant, evaluated at the pure AST level */ @@ -179,7 +189,12 @@ public function getAttributes(?string $name = null, int $flags = 0): array $arguments = []; foreach ($attr->args as $arg) { $nodeExpressionResolver->process($arg->value); - $arguments[] = $nodeExpressionResolver->getValue(); + // Named arguments are keyed by their name, exactly like the native reflection does + if (isset($arg->name)) { + $arguments[$arg->name->toString()] = $nodeExpressionResolver->getValue(); + } else { + $arguments[] = $nodeExpressionResolver->getValue(); + } } $isRepeated = self::isAttributeRepeated($resolvedAttrName, $this->declarationNode->attrGroups); @@ -292,7 +307,7 @@ private static function isAttributeRepeated(string $attributeName, array $attrGr * argument of ReflectionAttribute does not accept a constant reflection. * * @param class-string $attributeName - * @param array $arguments + * @param array $arguments */ private static function createAttributeReflection( Attribute $attributeNode, @@ -303,7 +318,7 @@ private static function createAttributeReflection( return new class ($attributeNode, $attributeName, $arguments, $isRepeated) extends ReflectionAttribute { /** * @param class-string $attributeClassName - * @param array $attributeArguments + * @param array $attributeArguments */ public function __construct( private Attribute $attributeNode, @@ -323,7 +338,7 @@ public function getName(): string } /** - * @return array + * @return array */ public function getArguments(): array { diff --git a/src/Traits/AttributeResolverTrait.php b/src/Traits/AttributeResolverTrait.php index 2a5a95f..dda5114 100644 --- a/src/Traits/AttributeResolverTrait.php +++ b/src/Traits/AttributeResolverTrait.php @@ -61,7 +61,12 @@ public function getAttributes(?string $name = null, int $flags = 0): array $arguments = []; foreach ($attr->args as $arg) { $nodeExpressionResolver->process($arg->value); - $arguments[] = $nodeExpressionResolver->getValue(); + // Named arguments are keyed by their name, exactly like the native reflection does + if (isset($arg->name)) { + $arguments[$arg->name->toString()] = $nodeExpressionResolver->getValue(); + } else { + $arguments[] = $nodeExpressionResolver->getValue(); + } } $attributeNameNode = $attr->name; diff --git a/tests/Php86MiscParityTest.php b/tests/Php86MiscParityTest.php new file mode 100644 index 0000000..7b4cf55 --- /dev/null +++ b/tests/Php86MiscParityTest.php @@ -0,0 +1,365 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\ParserReflection; + +use Go\ParserReflection\Locator\CallableLocator; +use Go\ParserReflection\Locator\ComposerLocator; +use PHPUnit\Framework\TestCase; + +/** + * Tests for the smaller PHP 8.6 reflection items: + * + * - the #[\Override] attribute that is allowed on class, interface and enum constants; + * - the __debugInfo() magic method that is allowed on enums (Debuggable Enums RFC); + * - the attribute API additions of the native \ReflectionAttribute class. + * + * Both stub files can be parsed by any runtime, because the engine analyzes sources without loading + * them, but they can only be included by a PHP 8.6+ one. Therefore every assertion that needs the + * stub to be loaded (native reflection parity) is guarded with a PHP_VERSION_ID >= 80600 check. + */ +class Php86MiscParityTest extends TestCase +{ + private const STUB_NAMESPACE = 'Go\ParserReflection\Stub'; + + private const OVERRIDE_STUB_FILE = __DIR__ . '/Stub/FileWithOverrideConstants86.php'; + + private const ENUM_STUB_FILE = __DIR__ . '/Stub/FileWithDebuggableEnums86.php'; + + private const BASE_CONTRACT = self::STUB_NAMESPACE . '\BaseContractWithConstants86'; + + private const EXTENDED_CONTRACT = self::STUB_NAMESPACE . '\ExtendedContractWithConstants86'; + + private const OVERRIDING_CLASS = self::STUB_NAMESPACE . '\ClassWithOverriddenConstants86'; + + private const OVERRIDING_ENUM = self::STUB_NAMESPACE . '\EnumWithOverriddenConstants86'; + + private const CONSTANT_MARKER = self::STUB_NAMESPACE . '\ClassConstantMarker86'; + + private const PURE_ENUM = self::STUB_NAMESPACE . '\PureDebuggableEnum86'; + + private const BACKED_ENUM = self::STUB_NAMESPACE . '\BackedDebuggableEnum86'; + + private const PLAIN_ENUM = self::STUB_NAMESPACE . '\PlainEnumWithoutDebugInfo86'; + + /** + * Expected attribute names for every constant of the stub classes, keyed by "Class::CONSTANT" + * + * @var array> + */ + private const EXPECTED_CONSTANT_ATTRIBUTES = [ + self::EXTENDED_CONTRACT . '::LEVEL' => [\Override::class], + self::OVERRIDING_CLASS . '::PLAIN' => [], + self::OVERRIDING_CLASS . '::TAG' => [\Override::class], + self::OVERRIDING_CLASS . '::WEIGHT' => [\Override::class, self::CONSTANT_MARKER], + self::OVERRIDING_CLASS . '::KIND' => [\Override::class, self::CONSTANT_MARKER, self::CONSTANT_MARKER], + self::OVERRIDING_ENUM . '::LEVEL' => [\Override::class], + self::OVERRIDING_ENUM . '::KIND' => [\Override::class, self::CONSTANT_MARKER], + self::OVERRIDING_ENUM . '::OWN' => [], + ]; + + /** + * Maps every class of the stub files to the file that declares it, for the locator below + * + * @var array + */ + private const STUB_CLASS_MAP = [ + self::CONSTANT_MARKER => self::OVERRIDE_STUB_FILE, + self::BASE_CONTRACT => self::OVERRIDE_STUB_FILE, + self::EXTENDED_CONTRACT => self::OVERRIDE_STUB_FILE, + self::STUB_NAMESPACE . '\AbstractClassWithConstants86' => self::OVERRIDE_STUB_FILE, + self::OVERRIDING_CLASS => self::OVERRIDE_STUB_FILE, + self::OVERRIDING_ENUM => self::OVERRIDE_STUB_FILE, + self::STUB_NAMESPACE . '\DebuggableEnumContract86' => self::ENUM_STUB_FILE, + self::PURE_ENUM => self::ENUM_STUB_FILE, + self::BACKED_ENUM => self::ENUM_STUB_FILE, + self::PLAIN_ENUM => self::ENUM_STUB_FILE, + ]; + + protected function setUp(): void + { + // Both stub files hold several classes at once, so they can not be resolved by the PSR-4 rules + ReflectionEngine::init(new CallableLocator( + static fn(string $className): false|string => self::STUB_CLASS_MAP[$className] ?? false + )); + } + + protected function tearDown(): void + { + // Restores the default locator for the following tests, because this one replaces it + ReflectionEngine::init(new ComposerLocator()); + } + + /** + * The #[\Override] attribute on class, interface and enum constants is resolved from the AST alone + */ + public function testOverrideAttributeOnConstantsIsResolvedWithoutLoading(): void + { + foreach (self::EXPECTED_CONSTANT_ATTRIBUTES as $constantReference => $expectedAttributeNames) { + [$className, $constantName] = explode('::', $constantReference); + + $parsedConstant = new ReflectionClassConstant($className, $constantName); + $attributes = $parsedConstant->getAttributes(); + + $this->assertSame( + $expectedAttributeNames, + array_map(static fn(ReflectionAttribute $attribute): string => $attribute->getName(), $attributes), + 'Attributes of ' . $constantReference . ' should be resolved from the AST' + ); + } + } + + /** + * The #[\Override] attribute can be combined with the userland ones, in any attribute group + */ + public function testOverrideAttributeCombinedWithOtherAttributes(): void + { + $singleGroupConstant = new ReflectionClassConstant(self::OVERRIDING_CLASS, 'WEIGHT'); + $overrideAttributes = $singleGroupConstant->getAttributes(\Override::class); + $markerAttributes = $singleGroupConstant->getAttributes(self::CONSTANT_MARKER); + + $this->assertCount(1, $overrideAttributes); + $this->assertSame([], $overrideAttributes[0]->getArguments()); + $this->assertFalse($overrideAttributes[0]->isRepeated()); + + $this->assertCount(1, $markerAttributes); + $this->assertSame(['tag' => 'weight', 'priority' => 5], $markerAttributes[0]->getArguments()); + $this->assertFalse($markerAttributes[0]->isRepeated()); + + $multiGroupConstant = new ReflectionClassConstant(self::OVERRIDING_CLASS, 'KIND'); + $repeatedAttributes = $multiGroupConstant->getAttributes(self::CONSTANT_MARKER); + + $this->assertCount(1, $multiGroupConstant->getAttributes(\Override::class)); + $this->assertFalse($multiGroupConstant->getAttributes(\Override::class)[0]->isRepeated()); + $this->assertCount(2, $repeatedAttributes); + $this->assertSame(['first'], $repeatedAttributes[0]->getArguments()); + $this->assertSame(['second', 'priority' => 2], $repeatedAttributes[1]->getArguments()); + $this->assertTrue($repeatedAttributes[0]->isRepeated()); + $this->assertTrue($repeatedAttributes[1]->isRepeated()); + + // Every attribute keeps the AST node it was built from + foreach ([...$overrideAttributes, ...$markerAttributes, ...$repeatedAttributes] as $attribute) { + $this->assertInstanceOf(\PhpParser\Node\Attribute::class, $attribute->getNode()); + } + } + + /** + * Filtering by the attribute name should work for the constants of interfaces and enums too + */ + public function testOverrideAttributeFilteringOnInterfaceAndEnumConstants(): void + { + $interfaceConstant = new ReflectionClassConstant(self::EXTENDED_CONTRACT, 'LEVEL'); + $this->assertCount(1, $interfaceConstant->getAttributes(\Override::class)); + $this->assertSame('extended', $interfaceConstant->getValue()); + $this->assertSame(self::EXTENDED_CONTRACT, $interfaceConstant->getDeclaringClass()->getName()); + + $enumConstant = new ReflectionClassConstant(self::OVERRIDING_ENUM, 'KIND'); + $this->assertCount(1, $enumConstant->getAttributes(\Override::class)); + $this->assertCount(1, $enumConstant->getAttributes(self::CONSTANT_MARKER)); + $this->assertCount(0, $enumConstant->getAttributes(\Deprecated::class)); + $this->assertSame('enum-kind', $enumConstant->getValue()); + + // Enum cases are reported as class constants, but they carry no attributes here + $enumCase = new ReflectionClassConstant(self::OVERRIDING_ENUM, 'First'); + $this->assertTrue($enumCase->isEnumCase()); + $this->assertSame([], $enumCase->getAttributes()); + + $this->assertFalse( + class_exists(self::OVERRIDING_ENUM, false), + 'Enum with overridden constants should not be loaded by the static analysis' + ); + } + + /** + * Enums may declare the __debugInfo() magic method since PHP 8.6, it is an ordinary method for reflection + */ + public function testDebugInfoMethodOnEnumsIsResolvedWithoutLoading(): void + { + foreach ([self::PURE_ENUM, self::BACKED_ENUM] as $enumName) { + $parsedEnum = new ReflectionClass($enumName); + + $this->assertTrue($parsedEnum->hasMethod('__debugInfo'), $enumName . ' should have __debugInfo()'); + $this->assertContains( + '__debugInfo', + array_map(static fn(\ReflectionMethod $method): string => $method->getName(), $parsedEnum->getMethods()), + $enumName . '::getMethods() should list __debugInfo()' + ); + + $parsedMethod = $parsedEnum->getMethod('__debugInfo'); + $this->assertTrue($parsedMethod->isPublic()); + $this->assertFalse($parsedMethod->isStatic()); + $this->assertSame(0, $parsedMethod->getNumberOfParameters()); + $this->assertSame('array', (string) $parsedMethod->getReturnType()); + $this->assertSame($enumName, $parsedMethod->getDeclaringClass()->getName()); + } + + $this->assertFalse((new ReflectionClass(self::PLAIN_ENUM))->hasMethod('__debugInfo')); + $this->assertFalse( + enum_exists(self::PURE_ENUM, false), + 'Enum with __debugInfo() should not be loaded by the static analysis' + ); + } + + /** + * The dedicated ReflectionEnum should report the __debugInfo() method next to the enum specifics + */ + public function testDebugInfoMethodIsReportedByReflectionEnum(): void + { + $parsedEnum = new ReflectionEnum(self::BACKED_ENUM); + + $this->assertTrue($parsedEnum->isBacked()); + $this->assertSame('int', (string) $parsedEnum->getBackingType()); + $this->assertSame( + ['Low', 'High'], + array_map(static fn(\ReflectionEnumUnitCase $case): string => $case->getName(), $parsedEnum->getCases()) + ); + $this->assertTrue($parsedEnum->hasMethod('__debugInfo')); + $this->assertTrue($parsedEnum->hasMethod('describe')); + $this->assertTrue($parsedEnum->getMethod('describe')->isStatic()); + $this->assertContains(self::STUB_NAMESPACE . '\DebuggableEnumContract86', $parsedEnum->getInterfaceNames()); + } + + /** + * PHP 8.6 adds the name-related methods to the native \ReflectionAttribute, they operate on the internal + * attribute structure that is never initialized for the parsed reflection, hence the own implementation + */ + public function testAttributeNameMethodsAreImplementedForParsedAttributes(): void + { + $overrideAttribute = (new ReflectionClassConstant(self::OVERRIDING_CLASS, 'TAG'))->getAttributes()[0]; + + $this->assertSame(\Override::class, $overrideAttribute->getName()); + $this->assertSame('Override', $overrideAttribute->getShortName()); + $this->assertSame('', $overrideAttribute->getNamespaceName()); + $this->assertFalse($overrideAttribute->inNamespace()); + + $markerAttribute = (new ReflectionClassConstant(self::OVERRIDING_CLASS, 'WEIGHT')) + ->getAttributes(self::CONSTANT_MARKER)[0]; + + $this->assertSame(self::CONSTANT_MARKER, $markerAttribute->getName()); + $this->assertSame('ClassConstantMarker86', $markerAttribute->getShortName()); + $this->assertSame(self::STUB_NAMESPACE, $markerAttribute->getNamespaceName()); + $this->assertTrue($markerAttribute->inNamespace()); + } + + /** + * Audit of the attribute API additions: \ReflectionAttribute::getCurrent() was still in voting when the + * 8.6 beta was released, so there is nothing to emulate for it yet + */ + public function testAttributeApiAdditionsAreEitherImplementedOrAbsent(): void + { + if (method_exists(\ReflectionAttribute::class, 'getCurrent')) { + $this->markTestIncomplete( + 'ReflectionAttribute::getCurrent() has landed in this runtime and needs a parsed counterpart' + ); + } + + $reflectionAttribute = new \ReflectionClass(ReflectionAttribute::class); + foreach (get_class_methods(\ReflectionAttribute::class) as $nativeMethodName) { + if ($nativeMethodName === '__toString') { + // Not implemented for the parsed attributes yet, tracked separately from the 8.6 support + continue; + } + $this->assertSame( + ReflectionAttribute::class, + $reflectionAttribute->getMethod($nativeMethodName)->getDeclaringClass()->getName(), + 'Method ' . $nativeMethodName . '() should be implemented for the parsed attributes' + ); + } + } + + /** + * Static results for the #[\Override] constants should be equal to the native ones on PHP 8.6 + */ + public function testOverrideConstantsMatchNativeReflectionOnPhp86(): void + { + $this->skipWithoutPhp86(); + include_once self::OVERRIDE_STUB_FILE; + + foreach (array_keys(self::EXPECTED_CONSTANT_ATTRIBUTES) as $constantReference) { + [$className, $constantName] = explode('::', $constantReference); + + $parsedConstant = new ReflectionClassConstant($className, $constantName); + $nativeConstant = new \ReflectionClassConstant($className, $constantName); + + $parsedAttributes = $parsedConstant->getAttributes(); + $nativeAttributes = $nativeConstant->getAttributes(); + + $this->assertSame( + array_map(static fn(\ReflectionAttribute $attribute): string => $attribute->getName(), $nativeAttributes), + array_map(static fn(ReflectionAttribute $attribute): string => $attribute->getName(), $parsedAttributes), + 'Attribute names of ' . $constantReference . ' should be equal to the native ones' + ); + + foreach ($nativeAttributes as $index => $nativeAttribute) { + $parsedAttribute = $parsedAttributes[$index]; + + $this->assertSame($nativeAttribute->getArguments(), $parsedAttribute->getArguments(), $constantReference); + $this->assertSame($nativeAttribute->isRepeated(), $parsedAttribute->isRepeated(), $constantReference); + $this->assertSame($nativeAttribute->getShortName(), $parsedAttribute->getShortName(), $constantReference); + $this->assertSame($nativeAttribute->getNamespaceName(), $parsedAttribute->getNamespaceName(), $constantReference); + $this->assertSame($nativeAttribute->inNamespace(), $parsedAttribute->inNamespace(), $constantReference); + } + + $this->assertSame($nativeConstant->getValue(), $parsedConstant->getValue(), $constantReference); + $this->assertSame( + $nativeConstant->getDeclaringClass()->getName(), + $parsedConstant->getDeclaringClass()->getName(), + $constantReference + ); + $this->assertSame($nativeConstant->__toString(), $parsedConstant->__toString(), $constantReference); + } + } + + /** + * Static results for the debuggable enums should be equal to the native ones on PHP 8.6 + */ + public function testDebuggableEnumsMatchNativeReflectionOnPhp86(): void + { + $this->skipWithoutPhp86(); + include_once self::ENUM_STUB_FILE; + + foreach ([self::PURE_ENUM, self::BACKED_ENUM, self::PLAIN_ENUM] as $enumName) { + $parsedEnum = new ReflectionEnum($enumName); + $nativeEnum = new \ReflectionEnum($enumName); + + $this->assertSame($nativeEnum->hasMethod('__debugInfo'), $parsedEnum->hasMethod('__debugInfo'), $enumName); + $this->assertSame( + array_map(static fn(\ReflectionMethod $method): string => $method->getName(), $nativeEnum->getMethods()), + array_map(static fn(\ReflectionMethod $method): string => $method->getName(), $parsedEnum->getMethods()), + 'Methods of ' . $enumName . ' should be equal to the native ones' + ); + $this->assertSame( + array_map(static fn(\ReflectionEnumUnitCase $case): string => $case->getName(), $nativeEnum->getCases()), + array_map(static fn(\ReflectionEnumUnitCase $case): string => $case->getName(), $parsedEnum->getCases()), + $enumName + ); + + if (!$nativeEnum->hasMethod('__debugInfo')) { + continue; + } + + $parsedMethod = $parsedEnum->getMethod('__debugInfo'); + $nativeMethod = $nativeEnum->getMethod('__debugInfo'); + + $this->assertSame($nativeMethod->isPublic(), $parsedMethod->isPublic(), $enumName); + $this->assertSame($nativeMethod->isStatic(), $parsedMethod->isStatic(), $enumName); + $this->assertSame((string) $nativeMethod->getReturnType(), (string) $parsedMethod->getReturnType(), $enumName); + $this->assertSame($nativeMethod->__toString(), $parsedMethod->__toString(), $enumName); + } + } + + private function skipWithoutPhp86(): void + { + if (PHP_VERSION_ID < 80600) { + $this->markTestSkipped('Native reflection parity for this stub requires the PHP 8.6 runtime'); + } + } +} diff --git a/tests/ReflectionConstantTest.php b/tests/ReflectionConstantTest.php index 2ec2289..fbab2da 100644 --- a/tests/ReflectionConstantTest.php +++ b/tests/ReflectionConstantTest.php @@ -73,6 +73,7 @@ public function testConstantsAreResolvedWithoutLoadingFile(): void $this->assertSame(self::STUB_NAMESPACE . '\\' . $shortName, $parsedConstant->name); $this->assertSame($shortName, $parsedConstant->getShortName()); $this->assertSame(self::STUB_NAMESPACE, $parsedConstant->getNamespaceName()); + $this->assertTrue($parsedConstant->inNamespace()); $this->assertSame($expectedValue, $parsedConstant->getValue()); $this->assertFalse($parsedConstant->isDeprecated()); $this->assertSame([], $parsedConstant->getAttributes()); @@ -188,6 +189,10 @@ public function testConstantsMatchNativeReflection(): void $this->assertSame($nativeConstant->getName(), $parsedConstant->getName(), $constantName); $this->assertSame($nativeConstant->getShortName(), $parsedConstant->getShortName(), $constantName); $this->assertSame($nativeConstant->getNamespaceName(), $parsedConstant->getNamespaceName(), $constantName); + if (PHP_VERSION_ID >= 80600) { + // \ReflectionConstant::inNamespace() is available since PHP 8.6 only + $this->assertSame($nativeConstant->inNamespace(), $parsedConstant->inNamespace(), $constantName); + } $this->assertSame($nativeConstant->getValue(), $parsedConstant->getValue(), $constantName); $this->assertSame($nativeConstant->isDeprecated(), $parsedConstant->isDeprecated(), $constantName); $this->assertSame($nativeConstant->__toString(), $parsedConstant->__toString(), $constantName); @@ -211,7 +216,11 @@ public function testAttributesOnConstantsAreResolvedStatically(): void $this->assertCount(1, $attributes); $this->assertInstanceOf(ReflectionAttribute::class, $attributes[0]); $this->assertSame(Deprecated::class, $attributes[0]->getName()); - $this->assertSame(['Use ATTRIBUTED_CONSTANT_MODERN instead', '8.5'], $attributes[0]->getArguments()); + // Named arguments keep their names, just like the native reflection reports them + $this->assertSame( + ['message' => 'Use ATTRIBUTED_CONSTANT_MODERN instead', 'since' => '8.5'], + $attributes[0]->getArguments() + ); $this->assertFalse($attributes[0]->isRepeated()); $this->assertInstanceOf(Attribute::class, $attributes[0]->getNode()); @@ -229,7 +238,7 @@ public function testAttributesOnConstantsAreResolvedStatically(): void $markedAttributes = $markedConstant->getAttributes(); $this->assertCount(1, $markedAttributes); $this->assertSame(self::STUB_NAMESPACE . '\ConstantMarker', $markedAttributes[0]->getName()); - $this->assertSame(['marked'], $markedAttributes[0]->getArguments()); + $this->assertSame(['tag' => 'marked'], $markedAttributes[0]->getArguments()); $this->assertSame([], $markedConstant->getAttributes(Deprecated::class)); $modernConstant = $parsedNamespace->getReflectionConstant('ATTRIBUTED_CONSTANT_MODERN'); @@ -256,6 +265,7 @@ public function testConstantInGlobalNamespace(): void $this->assertSame('ATTRIBUTED_GLOBAL_CONSTANT', $globalConstant->getName()); $this->assertSame('ATTRIBUTED_GLOBAL_CONSTANT', $globalConstant->getShortName()); $this->assertSame('', $globalConstant->getNamespaceName()); + $this->assertFalse($globalConstant->inNamespace()); $this->assertSame('global', $globalConstant->getValue()); $this->assertTrue($globalConstant->isDeprecated()); $this->assertFalse( diff --git a/tests/Stub/FileWithDebuggableEnums86.php b/tests/Stub/FileWithDebuggableEnums86.php new file mode 100644 index 0000000..d812982 --- /dev/null +++ b/tests/Stub/FileWithDebuggableEnums86.php @@ -0,0 +1,74 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + * + * The __debugInfo() magic method is allowed on enums since PHP 8.6 only (Debuggable Enums RFC), + * therefore this file can be parsed by any runtime, but it may only be included by a PHP 8.6+ one. + */ + +namespace Go\ParserReflection\Stub; + +interface DebuggableEnumContract86 +{ + public function label(): string; +} + +enum PureDebuggableEnum86 implements DebuggableEnumContract86 +{ + case Alpha; + + case Beta; + + public function label(): string + { + return strtolower($this->name); + } + + /** + * Magic method that is allowed for enums since PHP 8.6 + * + * @return array + */ + public function __debugInfo(): array + { + return ['label' => $this->label()]; + } +} + +enum BackedDebuggableEnum86: int implements DebuggableEnumContract86 +{ + case Low = 1; + + case High = 10; + + public function label(): string + { + return $this->name . ':' . $this->value; + } + + /** + * Magic method with an explicit visibility modifier and a static helper next to it + * + * @return array + */ + public function __debugInfo(): array + { + return ['name' => $this->name, 'value' => $this->value, 'label' => self::describe($this)]; + } + + public static function describe(self $case): string + { + return $case->label(); + } +} + +enum PlainEnumWithoutDebugInfo86: string +{ + case Only = 'only'; +} diff --git a/tests/Stub/FileWithOverrideConstants86.php b/tests/Stub/FileWithOverrideConstants86.php new file mode 100644 index 0000000..2b4d33c --- /dev/null +++ b/tests/Stub/FileWithOverrideConstants86.php @@ -0,0 +1,89 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + * + * The #[\Override] attribute is allowed on class constants since PHP 8.6 only, therefore this file + * can be parsed by any runtime, but it may only be included by a PHP 8.6+ one. + */ + +namespace Go\ParserReflection\Stub; + +/** + * Attribute that is applied to the class constants together with the #[\Override] one + */ +#[\Attribute(\Attribute::TARGET_CLASS_CONSTANT | \Attribute::IS_REPEATABLE)] +final class ClassConstantMarker86 +{ + public function __construct(public readonly string $tag = '', public readonly int $priority = 0) + { + } +} + +interface BaseContractWithConstants86 +{ + const string LEVEL = 'base'; + + const string KIND = 'contract'; +} + +interface ExtendedContractWithConstants86 extends BaseContractWithConstants86 +{ + #[\Override] + const string LEVEL = 'extended'; +} + +abstract class AbstractClassWithConstants86 +{ + const string TAG = 'abstract-tag'; + + const int WEIGHT = 1; +} + +final class ClassWithOverriddenConstants86 extends AbstractClassWithConstants86 implements BaseContractWithConstants86 +{ + /** + * Plain constant without any attribute at all + */ + const string PLAIN = 'plain'; + + /** + * Simplest case: single #[\Override] attribute on the inherited class constant + */ + #[\Override] + const string TAG = 'class-tag'; + + /** + * #[\Override] combined with an userland attribute inside the very same attribute group + */ + #[\Override, ClassConstantMarker86(tag: 'weight', priority: 5)] + const int WEIGHT = 42; + + /** + * #[\Override] for an interface constant, mixed with repeated userland attributes + */ + #[\Override] + #[ClassConstantMarker86('first')] + #[ClassConstantMarker86('second', priority: 2)] + const string KIND = 'class-kind'; +} + +enum EnumWithOverriddenConstants86: string implements BaseContractWithConstants86 +{ + #[\Override] + const string LEVEL = 'enum'; + + #[\Override, ClassConstantMarker86(tag: 'enum-kind')] + const string KIND = 'enum-kind'; + + const string OWN = 'own'; + + case First = 'first'; + + case Second = 'second'; +}