parent
1db9423312
commit
c5ba5afe65
9 changed files with 728 additions and 43 deletions
@ -0,0 +1,144 @@ |
||||
<?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 TraitAdaptationValidationTest extends PHPUnit\Framework\TestCase |
||||
{ |
||||
private string $testRoot; |
||||
|
||||
protected function setUp(): void |
||||
{ |
||||
$this->testRoot = sys_get_temp_dir() . '/typephp-trait-adaptation-' . 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 invalidAdaptationProvider */ |
||||
public function testInvalidAdaptationIsRejectedDuringComposition(string $body, string $message): void |
||||
{ |
||||
[$compiler, $file] = $this->compilerFor("<?php\n{$body}\nfunction main(): void {}\n");
|
||||
$compiler->prepareFile($file); |
||||
|
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage($message); |
||||
$compiler->composeTraitDeclarations([$file]); |
||||
} |
||||
|
||||
public static function invalidAdaptationProvider(): iterable |
||||
{ |
||||
yield 'unqualified alias names a missing method' => [ |
||||
'trait A { public function f(): void {} } class C { use A { missing as renamed; } }', |
||||
'alias was defined for method `missing()`', |
||||
]; |
||||
yield 'trait declared after consumer is still validated' => [ |
||||
'class C { use A { missing as renamed; } } trait A { public function f(): void {} }', |
||||
'alias was defined for method `missing()`', |
||||
]; |
||||
yield 'qualified alias names a missing method' => [ |
||||
'trait A { public function f(): void {} } class C { use A { A::missing as renamed; } }', |
||||
'alias was defined for method `A::missing()`', |
||||
]; |
||||
yield 'alias names a trait not used by the class' => [ |
||||
'trait A { public function f(): void {} } trait B { public function f(): void {} } class C { use A { B::f as renamed; } }', |
||||
"Required Trait `B` wasn't added to `C`", |
||||
]; |
||||
yield 'precedence winner has no method' => [ |
||||
'trait A {} trait B { public function f(): void {} } class C { use A, B { A::f insteadof B; } }', |
||||
'precedence rule was defined for `A::f()`', |
||||
]; |
||||
yield 'precedence winner trait is not used' => [ |
||||
'trait A { public function f(): void {} } trait B { public function f(): void {} } class C { use B { A::f insteadof B; } }', |
||||
"Required Trait `A` wasn't added to `C`", |
||||
]; |
||||
yield 'precedence loser trait is not used' => [ |
||||
'trait A { public function f(): void {} } trait B { public function f(): void {} } class C { use A { A::f insteadof B; } }', |
||||
"Required Trait `B` wasn't added to `C`", |
||||
]; |
||||
yield 'same method is excluded twice' => [ |
||||
'trait A { public function f(): void {} } trait B { public function f(): void {} } class C { use A, B { A::f insteadof B; A::f insteadof B; } }', |
||||
'was excluded multiple times', |
||||
]; |
||||
yield 'unqualified alias is ambiguous' => [ |
||||
'trait A { public function f(): void {} } trait B { public function f(): void {} } class C { use A, B { f as renamed; A::f insteadof B; } }', |
||||
'exists in multiple traits', |
||||
]; |
||||
} |
||||
|
||||
public function testValidAdaptationsRemainSupported(): void |
||||
{ |
||||
[$compiler, $file] = $this->compilerFor(<<<'PHP' |
||||
<?php |
||||
trait A { public function f(): void {} } |
||||
trait B {} |
||||
trait D { public function f(): void {} } |
||||
class C { |
||||
use A; |
||||
use B, D { |
||||
A::f insteadof B, D; |
||||
D::f as other; |
||||
} |
||||
} |
||||
function main(): void {} |
||||
PHP); |
||||
$compiler->prepareFile($file); |
||||
$compiler->composeTraitDeclarations([$file]); |
||||
$compiler->convertFile($file); |
||||
|
||||
self::assertFileExists($compiler->getCppFile($file)); |
||||
} |
||||
|
||||
public function testAliasCanTargetMethodFromNestedTrait(): void |
||||
{ |
||||
[$compiler, $file] = $this->compilerFor(<<<'PHP' |
||||
<?php |
||||
trait Inner { public function f(): void {} } |
||||
trait Outer { use Inner; } |
||||
class C { use Outer { f as renamed; } } |
||||
function main(): void {} |
||||
PHP); |
||||
$compiler->prepareFile($file); |
||||
$compiler->composeTraitDeclarations([$file]); |
||||
$compiler->convertFile($file); |
||||
|
||||
self::assertFileExists($compiler->getCppFile($file)); |
||||
} |
||||
|
||||
/** @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]; |
||||
} |
||||
} |
||||
@ -0,0 +1,178 @@ |
||||
<?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]; |
||||
} |
||||
} |
||||
@ -0,0 +1,45 @@ |
||||
--TEST-- |
||||
Trait members compare evaluated values instead of source spelling |
||||
--FILE-- |
||||
<?php |
||||
enum Status: string |
||||
{ |
||||
case Active = 'active'; |
||||
} |
||||
|
||||
trait FirstValues |
||||
{ |
||||
public const int SCORE = 1 + 1; |
||||
public array $items = [1, 2]; |
||||
public Status $status = Status::Active; |
||||
} |
||||
|
||||
trait SecondValues |
||||
{ |
||||
public const int SCORE = 2; |
||||
public array $items = array(1, 2); |
||||
public Status $status = Status::Active; |
||||
} |
||||
|
||||
class Values |
||||
{ |
||||
use FirstValues, SecondValues; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$values = new Values(); |
||||
var_dump(Values::SCORE); |
||||
var_dump($values->items); |
||||
var_dump($values->status === Status::Active); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(2) |
||||
array(2) { |
||||
[0]=> |
||||
int(1) |
||||
[1]=> |
||||
int(2) |
||||
} |
||||
bool(true) |
||||
Loading…
Reference in new issue