diff --git a/phpunit/src/EnumCaseValuePropertyDefaultTest.php b/phpunit/src/EnumCaseValuePropertyDefaultTest.php new file mode 100644 index 00000000..dd96cae0 --- /dev/null +++ b/phpunit/src/EnumCaseValuePropertyDefaultTest.php @@ -0,0 +1,116 @@ +value` and `->name` from enum + * cases (php-src "Fetch properties of enums in constant expressions"). The + * stub generator must fold those fetches into scalar property defaults the + * same way the declaration evaluator does; every other property fetch in a + * constant expression stays rejected. + * @internal + * @coversNothing + */ +final class EnumCaseValuePropertyDefaultTest extends PHPUnit\Framework\TestCase +{ + private string $testRoot; + + protected function setUp(): void + { + $this->testRoot = sys_get_temp_dir() . '/typephp-enum-case-propdefault-' . bin2hex(random_bytes(8)); + mkdir($this->testRoot, 0777, true); + } + + protected function tearDown(): void + { + if (!is_dir($this->testRoot)) { + return; + } + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($this->testRoot, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST, + ); + foreach ($iterator as $entry) { + $entry->isDir() ? rmdir($entry->getPathname()) : unlink($entry->getPathname()); + } + rmdir($this->testRoot); + } + + /** @dataProvider foldedDefaultProvider */ + public function testEnumCaseFetchFoldsIntoPropertyDefault(string $source, string $expectedInitCode): void + { + $compiler = $this->compilerFor($source); + $file = $this->testRoot . '/program.php'; + $compiler->prepareFile($file); + $compiler->convertFile($file); + + $header = file_get_contents($compiler->getArgInfoHeaderFile($file)); + self::assertIsString($header); + self::assertStringContainsString($expectedInitCode, $header); + } + + public static function foldedDefaultProvider(): iterable + { + yield 'int-backed ->value folds to negative backing value' => [ + <<<'PHP' +value; } +function main(): void {} +PHP, + 'ZVAL_LONG(&property_role_default_value, -1);', + ]; + yield 'string-backed ->value folds to backing string' => [ + <<<'PHP' +value; } +function main(): void {} +PHP, + 'zend_string_init_interned("guest", sizeof("guest") - 1, 1);', + ]; + yield '->name folds to the case name string' => [ + <<<'PHP' +name; } +function main(): void {} +PHP, + 'zend_string_init_interned("Hearts", sizeof("Hearts") - 1, 1);', + ]; + } + + public function testNonEnumPropertyFetchRemainsRejected(): void + { + $compiler = $this->compilerFor(<<<'PHP' + 1]; } +class Broken { public int $value = Container::DATA->key; } +function main(): void {} +PHP); + $file = $this->testRoot . '/program.php'; + $compiler->prepareFile($file); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('must be a global, non-magic constant, Expr_PropertyFetch given'); + $compiler->convertFile($file); + } + + private function compilerFor(string $source): CompilerTest + { + $file = $this->testRoot . '/program.php'; + file_put_contents($file, $source); + + global $translator; + $compiler = CompilerTest::create($this->testRoot); + $translator = $compiler; + $compiler->addFiles([$file]); + return $compiler; + } +} diff --git a/src/Resolver/ClassConstantValueTrait.php b/src/Resolver/ClassConstantValueTrait.php index ce5b2252..8dd47da1 100644 --- a/src/Resolver/ClassConstantValueTrait.php +++ b/src/Resolver/ClassConstantValueTrait.php @@ -595,7 +595,7 @@ private function resolveCompileTimeClassName( return '\\' . $this->getNamespacedClassName($raw, $scope->namespace); } - private function resolveCompileTimeEnumCaseBackingValue(NodeAbstract $origin, EnumCaseRef $case): int|string + public function resolveCompileTimeEnumCaseBackingValue(NodeAbstract $origin, EnumCaseRef $case): int|string { if ($this->isInternalClass($case->enumClass)) { $value = constant(ltrim($case->enumClass, '\\') . '::' . $case->caseName); diff --git a/src/gen_stub.php b/src/gen_stub.php index f40a3a77..649baaec 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -2680,6 +2680,31 @@ static function (Expr $expr) use ( }; } + // PHP 8.2+ constant expressions fetch `->value`/`->name` from + // enum cases. Mirror the declaration evaluator: fold the two + // backed properties; any other property fetch falls through + // to the shared diagnostic below. + if ($expr instanceof Expr\PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) { + $object = $evaluator->instance->evaluateDirectly($expr->var); + if ($object === null && $expr instanceof Expr\NullsafePropertyFetch) { + return null; + } + if ($object instanceof \TypePhp\Entity\EnumCaseRef) { + $property = $expr->name instanceof Node\Identifier + ? $expr->name->toString() + : $evaluator->instance->evaluateDirectly($expr->name); + if ($property === 'name') { + return $object->caseName; + } + if ($property === 'value') { + return getTranslator()->resolveCompileTimeEnumCaseBackingValue($expr, $object); + } + throw new Exception( + "Undefined enum case property `{$object->enumClass}::{$object->caseName}->{$property}`" + ); + } + } + // $expr is a ConstFetch with a name of a C macro here if (!($expr instanceof Expr\ConstFetch) and !($expr instanceof Expr\ClassConstFetch)) { _error: diff --git a/tests/compiler/object_property/enum-case-value-property-default.phpt b/tests/compiler/object_property/enum-case-value-property-default.phpt new file mode 100644 index 00000000..fe4da808 --- /dev/null +++ b/tests/compiler/object_property/enum-case-value-property-default.phpt @@ -0,0 +1,43 @@ +--TEST-- +Enum case ->value and ->name fold into property defaults (PHP 8.2+) +--FILE-- +value; + public string $level = Level::Guest->value; + public string $caseName = Suit::Hearts->name; + public TextRole $defaultRole = TextRole::User; +} + +function main(): void +{ + $card = new RoleCard(); + var_dump($card->role); + var_dump($card->level); + var_dump($card->caseName); + var_dump($card->defaultRole === TextRole::User); +} +?> +--EXPECT-- +int(-1) +string(5) "guest" +string(6) "Hearts" +bool(true)