testRoot = sys_get_temp_dir() . '/typephp-enum-rules-' . 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); } public function testDuplicateCaseIsRejectedDuringPrepare(): void { $compiler = $this->compilerFor(<<<'PHP' expectException(TestError::class); $this->expectExceptionMessage('Cannot redefine class constant Suit::Hearts'); $compiler->prepareFile($this->testRoot . '/program.php'); } /** @dataProvider caseConstantClashProvider */ public function testCaseAndClassConstantCannotShareAName(string $members): void { $compiler = $this->compilerFor("expectException(TestError::class); $this->expectExceptionMessage('Cannot redefine class constant Suit::Hearts'); $compiler->prepareFile($this->testRoot . '/program.php'); } public static function caseConstantClashProvider(): iterable { yield 'case then constant' => ['case Hearts; public const Hearts = 1;']; yield 'constant then case' => ['public const Hearts = 1; case Hearts;']; } /** @dataProvider duplicateBackingValueProvider */ public function testDuplicateBackingValueIsRejectedAfterConstantEvaluation( string $declarations, string $enum, string $expectedCases, ): void { $compiler = $this->compilerFor(<<testRoot . '/program.php'; $compiler->prepareFile($file); try { $compiler->convertFile($file); self::fail('Compilation unexpectedly succeeded'); } catch (TestError $error) { self::assertStringContainsString( "Duplicate value in enum Code for cases {$expectedCases}", $error->getMessage(), ); } self::assertFileDoesNotExist($compiler->getCppFile($file)); } public static function duplicateBackingValueProvider(): iterable { yield 'integer literals' => [ '', 'enum Code: int { case First = 1; case Second = 1; }', 'First and Second', ]; yield 'integer constant expression' => [ '', 'enum Code: int { case First = 1 + 1; case Second = 2; }', 'First and Second', ]; yield 'negative zero folds to zero' => [ '', 'enum Code: int { case First = -0; case Second = 0; }', 'First and Second', ]; yield 'string constant expression' => [ '', "enum Code: string { case First = 'type' . 'php'; case Second = 'typephp'; }", 'First and Second', ]; yield 'global and class constants' => [ 'const VALUE = 4; class Provider { public const VALUE = VALUE; }', 'enum Code: int { case First = VALUE; case Second = Provider::VALUE; }', 'First and Second', ]; yield 'enum case value reference' => [ '', 'enum Code: int { case First = 5; case Second = self::First->value; }', 'First and Second', ]; yield 'forward reference retains source order' => [ '', 'enum Code: int { case First = self::Third->value + 1; case Second = 7; case Third = 6; }', 'First and Second', ]; } public function testDistinctNumericStringsRemainDistinct(): void { $compiler = $this->compilerFor(<<<'PHP' testRoot . '/program.php'; $compiler->prepareFile($file); $compiler->convertFile($file); self::assertSame( ['One' => '1', 'ZeroOne' => '01'], $compiler->getClassDef('Code')?->enumCases, ); } public function testCaseNamesRemainCaseSensitive(): void { $compiler = $this->compilerFor(<<<'PHP' testRoot . '/program.php'; $compiler->prepareFile($file); $compiler->convertFile($file); self::assertSame(['Hearts' => null, 'hearts' => null], $compiler->getClassDef('Suit')?->enumCases); } public function testEnumIsImplicitlyFinal(): void { $compiler = $this->compilerFor(<<<'PHP' prepareFile($this->testRoot . '/program.php'); $enum = $compiler->getClassDef('Suit'); self::assertNotNull($enum); self::assertNotSame(0, $enum->flags & Modifiers::FINAL); } public function testClassCannotExtendEnum(): void { $compiler = $this->compilerFor(<<<'PHP' testRoot . '/program.php'; $compiler->prepareFile($file); $this->expectException(TestError::class); $this->expectExceptionMessage('cannot extend final class `Suit`'); $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; } }