TypePHP 编译器
https://swoole.com/aot/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
209 lines
6.5 KiB
209 lines
6.5 KiB
<?php
|
|
/**
|
|
* This file is part of TypePHP(AOT).
|
|
*
|
|
* @link https://www.swoole.com/aot/
|
|
* @contact service@swoole.com
|
|
*/
|
|
|
|
use PhpParser\Modifiers;
|
|
use TypePhp\CompilerTest;
|
|
use TypePhp\Exception\TestError;
|
|
|
|
/**
|
|
* @internal
|
|
* @coversNothing
|
|
*/
|
|
final class EnumDeclarationRulesTest extends PHPUnit\Framework\TestCase
|
|
{
|
|
private string $testRoot;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->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'
|
|
<?php
|
|
enum Suit { case Hearts; case Hearts; }
|
|
function main(): void {}
|
|
PHP);
|
|
|
|
$this->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("<?php\nenum Suit { {$members} }\nfunction main(): void {}\n");
|
|
|
|
$this->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(<<<PHP
|
|
<?php
|
|
{$declarations}
|
|
{$enum}
|
|
function main(): void {}
|
|
PHP);
|
|
$file = $this->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'
|
|
<?php
|
|
enum Code: string { case One = '1'; case ZeroOne = '01'; }
|
|
function main(): void {}
|
|
PHP);
|
|
$file = $this->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'
|
|
<?php
|
|
enum Suit { case Hearts; case hearts; }
|
|
function main(): void {}
|
|
PHP);
|
|
$file = $this->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'
|
|
<?php
|
|
enum Suit { case Hearts; }
|
|
function main(): void {}
|
|
PHP);
|
|
$compiler->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'
|
|
<?php
|
|
enum Suit { case Hearts; }
|
|
class InvalidSuit extends Suit {}
|
|
function main(): void {}
|
|
PHP);
|
|
$file = $this->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;
|
|
}
|
|
}
|
|
|