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