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.
178 lines
7.8 KiB
178 lines
7.8 KiB
<?php
|
|
/**
|
|
* This file is part of TypePHP(AOT).
|
|
*
|
|
* @link https://www.swoole.com/aot/
|
|
* @contact service@swoole.com
|
|
*/
|
|
|
|
use TypePhp\CompilerTest;
|
|
use TypePhp\Exception\TestError;
|
|
|
|
/**
|
|
* @internal
|
|
* @coversNothing
|
|
*/
|
|
final class TraitMemberValueCompatibilityTest extends PHPUnit\Framework\TestCase
|
|
{
|
|
private string $testRoot;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->testRoot = sys_get_temp_dir() . '/typephp-trait-values-' . 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 compatibleMemberProvider */
|
|
public function testEquivalentMemberValuesAreAccepted(string $declarations): void
|
|
{
|
|
[$compiler, $file] = $this->compilerFor("<?php\n{$declarations}\nfunction main(): void {}\n");
|
|
$compiler->prepareFile($file);
|
|
$compiler->composeTraitDeclarations([$file]);
|
|
$compiler->convertFile($file);
|
|
|
|
self::assertFileExists($compiler->getCppFile($file));
|
|
}
|
|
|
|
public static function compatibleMemberProvider(): iterable
|
|
{
|
|
yield 'equivalent constant expressions' => [
|
|
'trait A { public const X = 1 + 1; } trait B { public const X = 2; } class C { use A, B; }',
|
|
];
|
|
yield 'equivalent property arrays' => [
|
|
'trait A { public array $value = [1, 2]; } trait B { public array $value = array(1, 2); } class C { use A, B; }',
|
|
];
|
|
yield 'integer coerces to declared float' => [
|
|
'trait A { public float $value = 1; public const float X = 1; } trait B { public float $value = 1.0; public const float X = 1.0; } class C { use A, B; }',
|
|
];
|
|
yield 'same enum case identity' => [
|
|
"enum Status: string { case Active = 'active'; case Disabled = 'disabled'; } trait A { public const X = Status::Active; } trait B { public const X = Status::Active; } class C { use A, B; }",
|
|
];
|
|
yield 'same enum case nested in array' => [
|
|
"enum Status: string { case Active = 'active'; } trait A { public const X = ['case' => Status::Active]; } trait B { public const X = array('case' => Status::Active); } class C { use A, B; }",
|
|
];
|
|
yield 'indirect enum case identity' => [
|
|
"enum Status: string { case Active = 'active'; } const CURRENT = Status::Active; trait A { public const X = CURRENT; } trait B { public const X = Status::Active; } class C { use A, B; }",
|
|
];
|
|
yield 'inherited constant retaining enum identity' => [
|
|
"enum Status: string { case Active = 'active'; } class Base { public const X = Status::Active; } class Values extends Base {} trait A { public const X = Values::X; } trait B { public const X = Status::Active; } class C { use A, B; }",
|
|
];
|
|
yield 'enum case property defaults' => [
|
|
"enum Status: string { case Active = 'active'; } trait A { public Status \$value = Status::Active; } trait B { public Status \$value = Status::Active; } class C { use A, B; }",
|
|
];
|
|
yield 'trait self constant reference' => [
|
|
'trait A { public const BASE = 1; public const X = self::BASE + 1; } trait B { public const BASE = 1; public const X = 2; } class C { use A, B; }',
|
|
];
|
|
yield 'class declaration and trait expression are equivalent' => [
|
|
'trait A { public const X = 1 + 1; public array $value = [1, 2]; } class C { use A; public const X = 2; public array $value = array(1, 2); }',
|
|
];
|
|
}
|
|
|
|
public function testClassMagicConstantIsComparedInConsumerScope(): void
|
|
{
|
|
[$compiler, $file] = $this->compilerFor(<<<'PHP'
|
|
<?php
|
|
trait A { public const X = __CLASS__; }
|
|
trait B { public const X = __CLASS__; }
|
|
class C { use A, B; }
|
|
function main(): void {}
|
|
PHP);
|
|
$compiler->prepareFile($file);
|
|
$compiler->composeTraitDeclarations([$file]);
|
|
|
|
// gen_stub.php does not currently lower __CLASS__ in a class constant;
|
|
// this assertion deliberately protects only Trait compatibility.
|
|
self::addToAssertionCount(1);
|
|
}
|
|
|
|
public function testLexicalImportsAreUsedWhileComparingTraitConstants(): void
|
|
{
|
|
[$compiler, $file] = $this->compilerFor(<<<'PHP'
|
|
<?php
|
|
namespace Domain;
|
|
enum Status: string { case Active = 'active'; }
|
|
namespace App;
|
|
use Domain\Status as State;
|
|
use Domain\Status as Current;
|
|
trait A { public const X = State::Active; }
|
|
trait B { public const X = Current::Active; }
|
|
class C { use A, B; }
|
|
function main(): void {}
|
|
PHP);
|
|
$compiler->prepareFile($file);
|
|
$compiler->composeTraitDeclarations([$file]);
|
|
|
|
// Stub registration still owns the separate lowering of imported
|
|
// names; this test isolates compatibility evaluation.
|
|
self::addToAssertionCount(1);
|
|
}
|
|
|
|
/** @dataProvider incompatibleMemberProvider */
|
|
public function testDifferentMemberValuesAreRejected(string $declarations): void
|
|
{
|
|
[$compiler, $file] = $this->compilerFor("<?php\n{$declarations}\nfunction main(): void {}\n");
|
|
$compiler->prepareFile($file);
|
|
|
|
try {
|
|
$compiler->composeTraitDeclarations([$file]);
|
|
$compiler->convertFile($file);
|
|
self::fail('Compilation unexpectedly succeeded');
|
|
} catch (TestError $error) {
|
|
self::assertMatchesRegularExpression('/conflict|already exists/i', $error->getMessage());
|
|
}
|
|
}
|
|
|
|
public static function incompatibleMemberProvider(): iterable
|
|
{
|
|
yield 'different enum cases' => [
|
|
"enum Status: string { case Active = 'active'; case Disabled = 'disabled'; } trait A { public const X = Status::Active; } trait B { public const X = Status::Disabled; } class C { use A, B; }",
|
|
];
|
|
yield 'different enums with equal backing scalars' => [
|
|
"enum First: string { case Active = 'active'; } enum Second: string { case Active = 'active'; } trait A { public const X = First::Active; } trait B { public const X = Second::Active; } class C { use A, B; }",
|
|
];
|
|
yield 'different enum case inside array' => [
|
|
"enum Status: string { case Active = 'active'; case Disabled = 'disabled'; } trait A { public const X = [Status::Active]; } trait B { public const X = [Status::Disabled]; } class C { use A, B; }",
|
|
];
|
|
yield 'different enum property defaults' => [
|
|
"enum Status: string { case Active = 'active'; case Disabled = 'disabled'; } trait A { public Status \$value = Status::Active; } trait B { public Status \$value = Status::Disabled; } class C { use A, B; }",
|
|
];
|
|
yield '__TRAIT__ remains lexical' => [
|
|
'trait A { public const X = __TRAIT__; } trait B { public const X = __TRAIT__; } class C { use A, B; }',
|
|
];
|
|
yield 'class property and trait property differ' => [
|
|
'trait A { public int $value = 1; } class C { use A; public int $value = 2; }',
|
|
];
|
|
yield 'class constant and trait constant differ' => [
|
|
'trait A { public const X = 1; } class C { use A; public const X = 2; }',
|
|
];
|
|
}
|
|
|
|
/** @return array{CompilerTest, string} */
|
|
private function compilerFor(string $source): array
|
|
{
|
|
$file = $this->testRoot . '/program.php';
|
|
file_put_contents($file, $source);
|
|
|
|
global $translator;
|
|
$compiler = CompilerTest::create($this->testRoot);
|
|
$translator = $compiler;
|
|
$compiler->addFiles([$file]);
|
|
|
|
return [$compiler, $file];
|
|
}
|
|
}
|
|
|